The status screens (service-unavailable, maintenance, channel-subscription,
blacklist, account-deleted) all shared a generic flat look — an icon in a grey
circle, a title, a subtitle, three raw pulsing dots — that didn't match the
app's premium dark-glass aesthetic.
Introduce one shared BlockingShell that all five compose: an opaque themed
canvas with a self-contained accent glow, a centered glass card
(rounded-[--bento-radius] border bg-dark-900/80 backdrop-blur + inset
highlight), a gradient-ringed icon medallion (warning/error/info accent)
instead of a flat grey circle, font-display typography, the canonical Button
for every action, and a subtle framer-motion scale/slide entrance. Accent per
screen: warning (maintenance, account-deleted, service-unavailable), error
(blacklist), info (channel).
Behavior is preserved 1:1 — every i18n key, the channel list + open buttons,
the telegram deep-link, retry/check/close, the recovery poll, focus trap and
aria all carried over verbatim; only the visuals change.
A personal glyph-by-glyph re-review of the migration (the "steering wheel"
guard) found two wrong matches:
- MaintenanceScreen: a wrench/maintenance svg had been mapped to AdjustmentsIcon
(sliders) → use a proper WrenchIcon (PiWrench).
- InstallationGuide tutorial button: an open-book svg had been mapped to
DocumentIcon (file) → use BookOpenIcon (PiBookOpen).
Add WrenchIcon and BookOpenIcon to the central barrel. All other replacements
were verified correct (paper-plane→Send, server→HardDrives, warning-triangle→
Warning, etc.).
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.
Map text/bg/border-gray-N → dark-N across the four blocking screens (Maintenance,
Blacklisted, ChannelSubscription, AccountDeleted) so muted text + decorative surfaces
follow the design tokens. BroadcastPreview's email mockup and MessageMediaGrid's
fullscreen overlay intentionally keep their literal gray/white (mock chrome).
Sweep all amber-N color classes to warning-N (uniformly pending/warning semantics).
Yellow and orange left for per-file review (mixed Stars brand + semantic uses).
Telegram CloudStorage token recovery (resilience against WebView localStorage wipes):
- mirror the refresh token to per-user CloudStorage on login, remove it on logout
- restoreRefreshTokenFromCloud() recovers the refresh token in initialize() when
localStorage is empty, then the normal refresh flow re-establishes the session
- move the initialize() bootstrap call from auth.ts module-load into main.tsx after
the Telegram SDK init(), since launch params + CloudStorage are unavailable before init
- best-effort: no-ops outside Telegram and on any error -> falls back to prior behavior
Accessibility:
- role=alertdialog + aria-modal + aria-labelledby + focus management (useFocusTrap)
on Maintenance, Blacklisted, ChannelSubscription and AccountDeleted blocking screens
- aria-label on ColorPicker Hue/Saturation/Lightness range inputs
Replace all bare useAuthStore(), useBlockingStore(), and
useSuccessNotification() calls with individual field selectors.
This prevents components from re-rendering when unrelated store
fields change.
- 21 useAuthStore usages across 20 files: individual selectors
for 1-2 fields, useShallow for 3+ fields
- 5 useBlockingStore usages: individual selectors
- 2 useSuccessNotification usages: individual selectors