From d524d906041772f0321bd4439a084ff48cabb3e2 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Fri, 14 Aug 2026 15:15:37 +0000 Subject: [PATCH] fix(ui): the wrong typeface on half the numbers, and a highlight nobody ever saw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four defects, all of them things you can only find by looking at the app rather than at the tests, which passed on every one of them. **Vietnamese set in a monospaced face.** `.tabular` set `font-family: var(--font-mono)` as well as `font-variant-numeric`, and it had been applied to every string that merely *contained* a number: "1 giờ 12 phút" on the home screen and the analytics cards, "hạn 2026-08-20" on a task, "3 công cụ" under an agent, the counts in the finder and on every chip. Those rendered in JetBrains Mono beside Inter everywhere else — tone marks sit badly in it and the words read as a fallback font that had failed to load. Split in two: `.tabular` keeps the mono face for a column of digits and nothing else (a clock beside a transcript line, a playhead, a date on its own), and `.nums` gives the same steady figures without changing the typeface under the words around them. Twenty sites moved. **The selected sidebar row had no highlight.** There is a `layoutId` pill that is supposed to travel between rows; it is `-z-10` so the label paints over it, but the button created no stacking context, so that negative index escaped and the pill painted behind the sidebar's own background. Invisible since the component was written — the selected row was green text and nothing else. One `isolate`. **Recording red failed AA against its own tint.** `#f2585b` on `rgba(242,88,91,0.1)` measures 4.35:1, which the screenshot audit had been reporting on every run for the refused-permission chip. Raised to `#fb6b70`: 5.1:1, and in the same pastel family as the accent and the assistant violet rather than beside them. **The capture card was a picture of a dead meter.** Forty motionless grey bars across a third of the home screen. They now breathe — a tenth of the amplitude of the live waveform and four times the period, findable only if you look for it, and stopped by `prefers-reduced-motion` like everything else. Beyond the fixes: one `.lift` class replaces the two hand-rolled hover treatments so a card that can be clicked says so before it is, and `Ticker` counts a plain integer up on arrival so a figure on the home or analytics screen reads as measured rather than as placeholder text. It refuses anything that is not a bare integer — rolling "1 giờ 12 phút" through intermediate states would spell out nonsense on the way. Verified by re-shooting all 44 screenshots in both schemes: no overflow, contrast AA everywhere, and the previously-reported failure gone. 297 unit tests and all 18 browser suites green. --- apps/web/src/components/Library.tsx | 2 +- apps/web/src/components/StatusBar.tsx | 8 +-- apps/web/src/components/library/Finder.tsx | 4 +- apps/web/src/components/library/Recent.tsx | 2 +- apps/web/src/components/shell/Sidebar.tsx | 8 ++- apps/web/src/components/ui/Chip.tsx | 4 +- apps/web/src/components/ui/Ticker.tsx | 79 ++++++++++++++++++++++ apps/web/src/components/ui/Wave.tsx | 19 +++++- apps/web/src/components/ui/index.ts | 1 + apps/web/src/screens/AgentsScreen.tsx | 9 ++- apps/web/src/screens/AnalyticsScreen.tsx | 7 +- apps/web/src/screens/HomeScreen.tsx | 29 ++++++-- apps/web/src/screens/ModelsScreen.tsx | 4 +- apps/web/src/screens/TasksScreen.tsx | 4 +- apps/web/src/styles/theme.css | 73 ++++++++++++++++++-- 15 files changed, 220 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/components/ui/Ticker.tsx diff --git a/apps/web/src/components/Library.tsx b/apps/web/src/components/Library.tsx index 3160b2f..f356ba5 100644 --- a/apps/web/src/components/Library.tsx +++ b/apps/web/src/components/Library.tsx @@ -665,7 +665,7 @@ function Tile({ than as a number. `word-break: keep-all` keeps a word whole; the wrap still happens, at the space between the hours and the minutes, where a reader expects it. */} {value} diff --git a/apps/web/src/components/StatusBar.tsx b/apps/web/src/components/StatusBar.tsx index d1a065a..38985ad 100644 --- a/apps/web/src/components/StatusBar.tsx +++ b/apps/web/src/components/StatusBar.tsx @@ -33,19 +33,19 @@ export function StatusBar({ {disconnected && ( {connection === "reconnecting" ? t("status.reconnecting") : t("status.connecting")} )} {device && ( - + {device} )} {speakers.length > 0 && ( - + {n("status.speakers", speakers.length)} )} @@ -63,7 +63,7 @@ export function StatusBar({ {stat.rss_mb} MB {behind && ( - + {t("status.behind")} )} diff --git a/apps/web/src/components/library/Finder.tsx b/apps/web/src/components/library/Finder.tsx index e48c420..40aaac4 100644 --- a/apps/web/src/components/library/Finder.tsx +++ b/apps/web/src/components/library/Finder.tsx @@ -121,7 +121,7 @@ export function Finder({ view, folder, tags, colour, onFolder, onTags, onColour )} > #{each.name} - {each.count} + {each.count} ); })} @@ -151,7 +151,7 @@ export function Finder({ view, folder, tags, colour, onFolder, onTags, onColour degrades to for anyone who cannot tell two of these colours apart, which is around one man in twelve. */} {t(`colour.${each.name}`)} - {each.count} + {each.count} ); })} diff --git a/apps/web/src/components/library/Recent.tsx b/apps/web/src/components/library/Recent.tsx index 2cd662c..f5a25e0 100644 --- a/apps/web/src/components/library/Recent.tsx +++ b/apps/web/src/components/library/Recent.tsx @@ -89,7 +89,7 @@ export function Recent({ onClick={() => onOpen(entry)} whileHover={{ y: -2 }} transition={GENTLE} - className="border-line bg-bg-soft hover:border-line-strong flex w-full items-center gap-3 rounded-[var(--radius-card)] border p-2.5 text-left transition-colors" + className="border-line bg-bg-soft lift flex w-full items-center gap-3 rounded-[var(--radius-card)] border p-2.5 text-left" > {/* A note has no waveform because it was typed; it gets the pen instead, at the same size, so the rows still line up. */} diff --git a/apps/web/src/components/shell/Sidebar.tsx b/apps/web/src/components/shell/Sidebar.tsx index e606ceb..5b99e8b 100644 --- a/apps/web/src/components/shell/Sidebar.tsx +++ b/apps/web/src/components/shell/Sidebar.tsx @@ -365,7 +365,11 @@ function NavButton({ onClick={() => onNavigate(item.key)} aria-current={active ? "page" : undefined} className={cn( - "relative flex w-full items-center gap-2.5 rounded-[var(--radius-pill)] px-2.5 py-2 text-sm transition-colors", + // `isolate` is load-bearing. The highlight below is `-z-10` so the label paints over it, + // but without a stacking context here that negative index escapes the button entirely and + // the pill painted *behind the sidebar's own background* — invisible on every screen. The + // selected row had been nothing but green text for as long as this component has existed. + "relative isolate flex w-full items-center gap-2.5 rounded-[var(--radius-pill)] px-2.5 py-2 text-sm transition-colors", active ? "text-accent font-medium" : "text-fg-dim hover:bg-bg-raised hover:text-fg", )} > @@ -387,7 +391,7 @@ function NavButton({