The panel referenced its own token names (--bg2/--tx/--tx-dim/--ac/--card)
that don't exist in the app, so it always fell back to hardcoded dark
values — mismatched on the light and Night-orange/green themes. Alias
those tokens to the app's theme tokens (--bg / --ink / --muted / --accent
/ --bg-soft-2) at the panel root, fix the active white-overlay fills that
vanished on light backgrounds (rows, input, toast), map the accept action
to --online, and soften the shadows. The panel now follows the active
theme; the game-piece colours (X/O, chess, Connect-4) stay as-is.
Games is now a "Jeux" tab in the footer alongside Accueil / Salons / Amis
— icon over label, accent when its panel is open, an alert dot when it's
your move or a challenge is pending — a native bottom-nav home for it. The
panel still opens from the persistent 'overlay' slot, and the new-game /
challenge ("invite to game") flow lives inside it (plus the profile Défier
action). Drops the now-unused topbar-plugin-in-overflow machinery; the
clock stays in the desktop header.
The clock is a passive HH:MM display (a sandboxed .sbx-slot), so it doesn't
belong in the action menu — and it's redundant on phones, where the OS
status bar already shows the time. Drop sandboxed topbar plugins from the
⋮ overflow; the bar copy is already hidden on mobile, so the clock is now
gone on phones while staying in the desktop header.
A topbar plugin's popover was rendered as a child of its launcher button,
so the button had to stay mounted for the popover to survive — which is
why the games button couldn't move off the crowded mobile header. Add a
persistent 'overlay' UI slot at the app root and render the games panel
there, decoupling it from the button. The launcher is now stateless, so
on mobile it drops into the ⋮ overflow as a labelled "Jeux" row (freeing
the header for the channel name + topic) while its panel stays anchored.
Desktop keeps the icon in the bar.
The games panel is a fixed overlay rendered as a child of its button, so
the button has to stay mounted for the panel to survive — it can't move
into the ephemeral overflow menu, where it would unmount (and the panel
vanish) the moment the menu closes. Now that search / manage / leave live
in the overflow, the bar has room, so show the games plugin button inline
on mobile instead of hiding it. Reachable again, and the title and topic
still fit.
On phones the channel header crammed every button into one row: the title
sat behind the games-plugin button and the topic was pushed off screen.
Collapse the one-shot actions (search, manage, leave) into a "⋮" overflow
menu, hide topbar plugin items on mobile, and keep only the stateful notify
and pin toggles, the member count and the overflow inline. The channel
glyph, name and topic now have room to show as a proper two-line header.
The channel header's search, notify, pin, manage and close buttons — plus
the app-wide search-field magnifiers and the menu toggle — used OS emoji
glyphs. Extend the shared Icon component with search, close, menu, bell,
bell-off, pin and sliders, and use it across the topbar, the notify and
pin menus, and the explore / switcher / side-search fields. The icons
inherit each button's colour and get the same press and focus states as
the footer, so the whole chrome now reads as one icon family.
The account bar's tabs and settings button used OS emoji (home, compass,
people, gear), which render off-theme and differ across platforms. Add a
shared stroke-icon component — 24×24, currentColor, the same 1.9 stroke as
the composer glyphs — and use it for Accueil, Salons, Amis and Settings.
The icons inherit each button's colour, so the active tab now reads as a
soft accent pill, and the buttons gain a press dip and focus ring so the
bar feels native on desktop, not just touch.
All ten translations (~400 KB of JSON) were bundled into the entry
chunk, nearly all of it never used in a session. Ship only French — the
default and fallback — and fetch the other nine as separate chunks on
demand (language switch or a config-pinned default). The entry chunk
drops from 653 KB to 361 KB (200 KB to 118 KB gzipped), cutting the
parse/execute work that was delaying the largest paint.
Each IRC line arrives as its own WebSocket frame (text.ircv3.net), so
the store rendered once per line. Joining several busy channels at once
— NAMES plus a WHO reply per member — fired a render for every line and
stalled the main thread, which also inflated LCP. Buffer messages onto a
0ms timer so a whole burst drains in one render; under load the timers
back up and each drain swallows more lines. A timer rather than rAF keeps
unread counts and mention notifications live in a backgrounded tab.
Turnstile refreshed its callback ref in the render body; move it to an
effect. The gtag shim legitimately pushes the live arguments object, so
scope a disable to that line with a reason. Lint is now error-clean.
The nick field, channel +k/+l inputs and avatar hook re-synced from
props by calling setState inside an effect, which triggers an extra
render pass each time. Track the previous prop and adjust during
render instead — one pass, and the avatar now resets on account switch.
Set up an @/* -> src/* alias (tsconfig paths + vite resolve) and
convert every ../../-and-deeper import to it. Same-dir ./ and
one-level ../ stay relative. Build-time only, so the bundle is
unchanged; the deep imports were just hard to read and broke on
file moves.