Оркестрация staggerContainer/staggerItem у framer-motion раздаёт детям
команду «показаться» один раз — при анимации родителя на mount. Дети,
смонтированные позже (когда resolve'ится запрос react-query), в
оркестрацию не попадают и навсегда остаются в initial (opacity: 0, y: 8).
Симптом: после жёсткой перезагрузки (F5) на странице виден только
заголовок — карточки есть в DOM, но прозрачны. При SPA-навигации кэш
react-query тёплый, дети монтируются вместе с родителем — поэтому баг
проявлялся только при холодном заходе и маскировался на проде (нет
StrictMode, same-origin API, тёплый кэш почти всегда выигрывают гонку
с ~300-мс окном анимации).
Два способа фикса в зависимости от формы страницы:
1. key={isLoading ? 'loading' : 'ready'} на контейнере — перемонтирование
после загрузки, stagger проигрывается заново уже со всеми детьми.
Для страниц, где весь контент асинхронный (каскад сохраняется):
- ConnectedAccounts — карточки провайдеров
- SavedCards — список сохранённых карт
2. Явные initial="initial" animate="animate" на условном staggerItem —
элемент анимирует себя сам, не дожидаясь оркестрации родителя. Для
страниц, где асинхронны только 1-2 опциональные карточки (не
перезапускает анимацию всей страницы на каждый resolve):
- Balance — «Способы пополнения» и ссылка «Сохранённые карты»
- Support — карточка «Написать нам» (support_type='both')
- Profile — реферальный виджет
Попутно: в Wheel у контейнера истории спинов стояли несуществующие
варианты initial="hidden"/animate="show" (у staggerContainer/staggerItem
ключи — initial/animate/exit), из-за чего stagger там молча не работал
вовсе; имена исправлены.
Проверено и не требует правок: TopUpAmount (early-return до загрузки
методов), TopUpMethodSelect и MergeAccounts (динамический контент —
обычный HTML внутри всегда смонтированных staggerItem), вложенный
контейнер транзакций в Balance (самоанимирующийся).
- useFeatureFlags: кэш флагов в localStorage — нижняя навигация больше
не прыгает на холодном старте (таб «Рефералы»/«Колесо» появлялся после
загрузки terms/config)
- applyThemeColors: частичный/битый ответ /branding/colors добивается
дефолтами вместо падения всего приложения в ErrorBoundary
- карточка «Связаться с поддержкой»: кнопка больше не дублирует заголовок
(«Написать», без переноса)
- «Мои подписки»: заголовок и «Купить ещё» в одну строку на 390px
- StatCard: подписи плиток переносятся на две строки вместо обрезки
многоточием («Бонус новому польз…»)
Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77
files with the central Phosphor (react-icons/pi) components from
@/components/icons, preserving each icon's size classes and colour (dynamic
stroke colours via the parent's currentColor, RefreshIcon's spinning state,
conditional rotate-180 chevrons).
Verified: tsc + vite build + eslint clean; an adversarial diff review of all
changed files found the replacements correct (70 files clean, 0 blockers).
Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate
non-icons (brand/provider logos, loading spinners & framer-motion animations,
charts/sparklines, background decoration) plus a few ambiguous glyphs.
Replace the cabinet's hand-written heroicons-style SVG icon components with
the Remnawave panel's own icon family — Phosphor via react-icons/pi (Duotone).
All icons now live in a single central barrel (src/components/icons:
index + extended-icons + editor-icons, 144 icons); every feature file imports
from it instead of redefining inline SVGs (~440 icon defs removed).
- Add react-icons dependency
- Desktop nav (AppShell) now uses the central Phosphor icons, removing a
stroke-vs-duotone inconsistency the partial migration introduced
- Icons with custom props (SortIcon's direction, expandable chevrons) kept as
thin Phosphor wrappers; the RemnawaveIcon brand logo and animated SVGs
(checkmarks, spinners) are intentionally left as-is
- Restore the original per-call-site icon sizes wherever the centralized
default differed (211 call sites)
After re-audit/critique cycle the deterministic detector (npx impeccable)
flagged 34 design-spec violations. Knock out 33 of them.
• 25 × pure-black-white: sed-sweep bg-black/X → bg-dark-950/X across
18+ files (modal scrims, photo viewer backdrop, code blocks). The
base resolves to rgba(10,15,26,X) — visually identical to true
black, satisfies the 'no #000' impeccable rule.
• 3 × bounce-easing: SuccessNotificationModal celebration icon and
SyncTab loading arrows used animate-bounce; replaced with
animate-pulse. Bounce easing reads dated; pulse conveys 'in
progress' without the cartoon feel.
• 3 × border-accent-on-rounded: TelegramCallback + VerifyEmail
spinners used 'border-b-2 border-accent-500' on rounded-full —
detector reads it as a side-stripe even though it's a ring loader.
Switch to canonical 'border-2 border-accent-500 border-t-transparent'
(3/4 ring colored). Same visual, no spec violation.
• 1 × ai-color-palette: AdminLandingStats had text-purple-400 on a
gift-stats heading; purple is not in the brand palette. Swap to
text-accent-400.
• 1 × layout-transition: TrafficProgressBar.tsx fill bar still used
transition: width 1.2s (slipped past the earlier optimize pass).
Convert to transform: scaleX with origin-left. Same gradient, same
duration, runs on the compositor.
Remaining: 1 finding in third-party Aceternity background-beams-collision
component (indigo-500 gradient on decorative WebGL background) — left
as-is, it's lifted decorative third-party code.
Detector: 34 → 1.
Four image sites that render thumbnails/previews inside lists or tab
panels now hint loading=lazy so the browser defers the network fetch
until the element is near the viewport:
- Support.tsx attachment preview grid
- AdminTickets.tsx reply attachment grid
- BroadcastPreview.tsx photo preview
- BrandingTab.tsx logo preview in admin settings
Top-of-page logos (AppHeader/AppShell/DesktopSidebar/Login/Telegram
redirect/DeepLink) intentionally stay eager — they're above the fold
and should paint on first frame. Payment-method icons (6x6 px) are
already negligible. Compose attachment previews use blob: URLs where
loading=lazy is a no-op.
Sweep 30 files: text/bg/border/ring/from/to/via/fill/stroke/shadow/divide/decoration/
outline/placeholder-red-N → -error-N. All red usages were semantically error/danger
(no brand red), so tokens now flow through the design-system CSS variables and respond
to palette overrides.
Add htmlFor/id pairs for the create-ticket subject and message fields so screen
readers announce the label and clicking the label focuses the control (WCAG 1.3.1).
- Add media_items array support to ticket messages (types, API clients)
- Create shared MessageMediaGrid component with photo grid, fullscreen
viewer, keyboard nav, video/document rendering
- Replace per-page AdminMessageMedia/MessageMedia with MessageMediaGrid
- Add linkifyText util (DOMPurify-sanitized) for auto-linking URLs
- Support multi-file upload (up to 10) in AdminTickets and Support pages
- Remove unused ticketsApi import from AdminUserDetail
- Add URL.revokeObjectURL on unmount and media removal in
AdminBroadcastCreate, AdminPinnedMessageCreate, Support
- Track blob URLs via refs to revoke before creating new ones
- Add clearCreateAttachment/clearReplyAttachment helpers in Support
to properly revoke blob URLs when clearing attachments
- Cap adminTraffic cache at 20 entries with FIFO eviction to prevent
unbounded memory growth from different filter combinations
Major changes:
- Redesign cabinet with Linear-style components and top navigation
- Replace detail modals with dedicated pages (users, broadcasts, email templates)
- Add email channel support for broadcasts
- Remove pull-to-refresh, improve drag-and-drop on touch devices
- Fix Telegram Mini App: fullscreen, back button, scroll restoration
- Unify admin pages color scheme to design system
- Mobile-first improvements: horizontal tabs for settings, better touch targets
- Removed VITE_APP_VERSION from Dockerfile and GitHub workflows.
- Simplified Vite configuration by eliminating the Git version retrieval function and global constant for app version.
- Updated ColorPicker and other components to enhance accessibility with aria-labels.
- Introduced unique identifiers for buttons in the AppEditorModal for better React key management.
- Improved error handling in the Login component to provide user-friendly messages without exposing sensitive data.