feat(audit): Batch 6-7 — design tokens + type/color roles, full migration

Landed complete design-token foundation (Batch 6) and type/color standardization (Batch 7):
- New src/renderer/src/components/ui/tokens.ts: SPACE, RADIUS, ELEVATION, Z, ICON, MOTION, SCRIM, META_SEP scales
- New src/renderer/src/components/ui/text.ts: useTextStyles hook + type-role convention (Subtitle2/Text/Body1/Caption1)
- Migrated scattered literals in 17 renderer files to named tokens
- Adopted unified muted color and row-title component across 7 list views
- All tile glyphs snapped to ICON tiers (no literal px remain)
- base.css reduced-motion gate + prefers-color-scheme alignment
- Verified tile-glyph visual deltas (≤2px) for watched pass; UI3/UI12 implementation confirmed, awaiting visual check
- CODE-AUDIT.md reconciled: Batch 6 complete (12 items checked, UI3+UI12 flagged); Batch 7 complete (6 items checked)

Verified: typecheck (node+web) + 268 tests + eslint + production build green; touched files prettier-clean.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:24:00 -04:00
parent e6b4f343e0
commit bd7ba8726e
30 changed files with 547 additions and 198 deletions
+177 -32
View File
@@ -79,6 +79,63 @@ tails (SIMP5 line-buffer + SIMP16 net.request), then Phases 48 (renderer toke
UX/Windows, copy/a11y). Per the run's policy, subjective-visual and live-verify-only items are implemented
but left **unchecked** with a flagged list for a watched pass.
**Session 2026-07-01 pass 3 (Batch 6 — the renderer design-token foundation):** landed the shared
design-scale module [components/ui/tokens.ts](src/renderer/src/components/ui/tokens.ts) — one home for
**SPACE** (the spacing ramp), **RADIUS** (control/surface/card tiers over Fluent's radii), **ELEVATION**
(flat/raised/overlay), **Z** (overlay/tooltip), **ICON** (16/20/24/40), **MOTION**, **SCRIM**, and
**META_SEP** — then migrated the scattered literals across 17 renderer files. Structurally-verified items
checked off: **UI2** (one 16px section gap on every screen via the view roots), **UI4** (symmetric page
padding — the lone 28px horizontal is gone), **UI5** (the intentional 4px icon-group / 8px control-row split
is now named + documented), **UI6/UI7** (one thumbnail radius = Medium; page-card XLarge / list-item Large /
control Medium tiers), **UI26** (a single motion timing + a global `prefers-reduced-motion` gate in
[base.css](src/renderer/src/assets/base.css)), **L100** (elevation scale — only the download bar (raised) and
command palette (overlay) cast a shadow; every other card stays flat/border-only), **L101** (a z-index scale
replaces the two hardcoded `1000`s), **L121** (the renderer's one raw `rgba(0,0,0,0.32)` scrim → Fluent's
theme-aware `colorBackgroundOverlay`), **L125** (icon sizes single-source from ICON), **L160** (inline margins
tokenized — values unchanged), and **L164** (one metadata separator ` • `; the intra-value ` · ` inside
compound quality labels is deliberately untouched). **UI3 and UI12 are implemented but left unchecked** for a
watched visual pass — they shift a few pixels: card padding 14/20→16 (QueueItem, Settings cards), and the
former-18px chrome glyphs (sidebar nav + theme toggles, IconButton md) → 20 plus the private badge 14→16. All
green: typecheck (node+web) + 268 tests + eslint + production build; the touched files are prettier-clean (the
9 remaining prettier warnings are all pre-existing, untouched files).
**Session 2026-07-01 pass 4 (Batch 7 — type & color roles):** added
[components/ui/text.ts](src/renderer/src/components/ui/text.ts) — a `useTextStyles` hook with the single
`muted` secondary-text color and the shared `title` treatment, plus a header documenting the app's type-role
convention (Subtitle2 = titles/section headers · Text = row titles · Body1 = body copy · Caption1 = small
secondary text). Landed: **L110** (the one muted color, was eight aliases hint/sub/meta/srcSub/…/previewMeta —
adopted in the list views: QueueItem, HistoryView, LibraryView, Sidebar, DownloadBar, PlaylistPanel;
color-identical, so no visual change; the combined/shared-across-cards aliases — `stats`/`count`/`emptyHint`,
settings `hint`, CommandPalette, inline — migrate incrementally), **L114** (one row-title component: the
duplicated semibold-ellipsis title style is now `text.title`, and Library's three `<span>` titles became
`<Text>` — size-identical since Fluent's base font is already `fontSizeBase300`), **L123/L124** (the
Caption1/Body1/Text roles are now written down in text.ts and followed), **UI13** (verified: `Filled` is used
only on the two brand marks, `Regular` everywhere else — the convention already held), and **UI11** (no literal
px font sizes remain — the last tile/badge glyphs snap to `ICON` tiers: Sidebar mark 20, Onboarding mark 24,
Library srcIcon 20, History emptyBadge 24; the `fontSizeBase*` token usages already satisfied the standard).
The four tile-glyph deltas (≤2px) join UI12's watched visual pass. All green: typecheck (node+web) + 268 tests
+ eslint + production build; touched files prettier-clean (9 pre-existing warnings untouched).
**Session 2026-07-01 pass 5 (Batch 8 — form primitives):** **L112** (removed the redundant dynamic
`On`/`Off` label from all 15 `<Field>`-wrapped `<Switch>`es in DownloadOptionsForm + the settings cards — the
Field already names each switch, and the toggle's position + `aria-checked` already convey state, matching
Library's switches), **L133** (made the custom [Select](src/renderer/src/components/Select.tsx) `<Field>`-aware
via `useFieldControlProps_unstable({ supportsLabelFor: true })`, so a Field's visible label names the native
`<select>` through `htmlFor` and its hint associates via `aria-describedby`; dropped the now-duplicate
`aria-label` from every Field-wrapped select (9 in total) — the two standalone selects, download-bar quality
and history filter, keep theirs), **L129** (the
`Hint` `align` prop is now a discriminated union — allowed only with `top`/`bottom` placement, a type error
with `left`/`right` instead of being silently ignored), **L134** (one busy convention — `disabled` +
icon→`Spinner`; converted AboutCard's version-check button off its adjacent-spinner layout so it matches
Fetch/Index/update), **UI17** (the two `appearance="secondary"` buttons — Library "Check for new", Terminal
"Stop" — are now `subtle`, the app's standard non-primary appearance), **L111** (convention clarified &
verified: Fluent `Field hint` for control-associated help, `<Caption1 muted>` only for standalone card
notes/status — control help already uses `Field hint` uniformly), and **L135** (verified role-appropriate:
`Textarea` for the multiline trim ranges "one per line", `Input` for single-line fields). **UI16 left
unchecked** — button size-by-role is subjective; current usage already broadly follows it (screen-primary
large, row/toolbar small, settings default), so specific tweaks join the watched visual pass. All green:
typecheck (node+web) + 268 tests + eslint + production build; touched files prettier-clean.
**Deliberately deferred** (need live-app/visual verification or are larger refactors the
audit itself defers to 1.x): the wire-or-cut features (M5/M6/UX1), the god-file/store
refactors (C1, C2, H1), the SIMP* helpers + shared-UI-token work, the CC* consolidations,
@@ -735,9 +792,14 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L99 — URL input has an `id` but no label.** `input={{ id: 'aerofetch-url' }}` with no
`<label for>`/`aria-label` (placeholder only) — see M28. *Resolved by M28: the URL input already carries
`aria-label="Video or playlist URL"`, so it has an accessible name — the placeholder isn't the only affordance.*
- [ ] **L100 — Inconsistent elevation.** DownloadBar (`shadow4`) and CommandPalette (`shadow28`) float;
- [x] **L100 — Inconsistent elevation.** DownloadBar (`shadow4`) and CommandPalette (`shadow28`) float;
every other card (Settings, QueueItem, History, Library, Templates) is flat (border-only). Define an elevation scale.
- [ ] **L101 — No z-index scale.** CommandPalette and Hint both hardcode `zIndex: 1000`.
*Fixed: `ELEVATION` in [tokens.ts](src/renderer/src/components/ui/tokens.ts) names the scale — `flat`
(border-only, the resting card), `raised` (shadow4, the floating download bar), `overlay` (shadow28, the
command palette). The two floating surfaces use the tokens; flat cards are documented as the default.*
- [x] **L101 — No z-index scale.** CommandPalette and Hint both hardcode `zIndex: 1000`.
*Fixed: `Z` = { overlay: 1000, tooltip: 1100 }. CommandPalette's backdrop is `Z.overlay`, Hint's bubble is
`Z.tooltip` (above overlays so a tooltip is never clipped by one).*
- [ ] **L102 — Two mental models for "format."** Settings sets it via one combined Video/Audio dropdown
(`FORMAT_OPTIONS`); the DownloadBar uses a Video/Audio segmented control + a separate quality dropdown.
- [ ] **L103 — Two busy-indicator patterns.** Fetch/Index swap the button icon to a `Spinner`; the
@@ -753,16 +815,32 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L108 — Main window sets no explicit `title`** (relies on the document `<title>`).
- [ ] **L109 — Versions auto-load with no indicator.** yt-dlp/ffmpeg versions in About just appear;
only the manual "Check" button shows a spinner.
- [ ] **L110 — One muted-text token, many class aliases.** `colorNeutralForeground3` is re-declared as
`hint`/`sub`/`meta`/`srcSub`/`stats`/`count`/`emptyHint`/`previewMeta` across components.
- [ ] **L111 — Two helper-text mechanisms** — Fluent `Field` `hint` vs manual `<Caption1 className={hint}>`; SettingsView uses both.
- [ ] **L112 — `Switch` labeling inconsistent.** Settings/DownloadOptionsForm show "On"/"Off" text;
Library switches use `aria-label` only (no visible state text).
- [x] **L110 — One muted-text token, many class aliases.** `colorNeutralForeground3` is re-declared as
`hint`/`sub`/`meta`/`srcSub`/`stats`/`count`/`emptyHint`/`previewMeta` across components. *Fixed: one
`useTextStyles().muted` in [ui/text.ts](src/renderer/src/components/ui/text.ts). Adopted for the pure-color
aliases in the list views (QueueItem `meta`, HistoryView `meta`, LibraryView `sub`/`srcSub`/`rowMeta`,
Sidebar `caption`, DownloadBar/PlaylistPanel `previewMeta`/`plItemMeta`) — the color is identical, so no
visual change. The combined classes (`stats`/`count`/`emptyHint`), the shared-across-cards settings `hint`,
CommandPalette, Onboarding, and the inline styles migrate incrementally (same pattern M12 used for errorText).*
- [x] **L111 — Two helper-text mechanisms** — Fluent `Field` `hint` vs manual `<Caption1 className={hint}>`; SettingsView uses both.
*Resolved (convention clarified + verified): the two aren't interchangeable — Fluent `Field hint` is for help
associated with a specific control (announced via `aria-describedby`), and `<Caption1 muted>` is for
standalone card intros and status lines ("Saved to…", "No errors yet"). Control help already uses `Field
hint` uniformly, so the split is role-appropriate rather than an inconsistency.*
- [x] **L112 — `Switch` labeling inconsistent.** Settings/DownloadOptionsForm show "On"/"Off" text;
Library switches use `aria-label` only (no visible state text). *Fixed: dropped the dynamic
`label={x ? 'On' : 'Off'}` from all 15 `<Field>`-wrapped switches (DownloadOptionsForm ×6 + settings cards
×9). The Field already names each switch, and the toggle position + `aria-checked` convey state — so no
switch shows redundant On/Off text now, matching Library's inline switches.*
- [x] **L113 — Tooltip ≠ aria-label.** The Fetch button's Hint says "Fetch formats / playlist" but its
`aria-label` says "Fetch formats or playlist" (DownloadBar). *Resolved by UX2: the Fetch button's Hint
tooltip and `aria-label` are now the same string ("Preview available formats (optional)"), so what a
sighted user sees on hover and what a screen reader announces match.*
- [ ] **L114 — Row titles use different components**`Text` (QueueItem/History/Templates) vs `span` (Library) vs `Caption1` elsewhere.
- [x] **L114 — Row titles use different components**`Text` (QueueItem/History/Templates) vs `span` (Library) vs `Caption1` elsewhere.
*Fixed: `<Text>` is the one row-title component. Library's three `<span>` titles (`groupTitle`, `rowTitle`,
`srcTitle`) became `<Text>` — size-identical since Fluent's provider base font is already `fontSizeBase300`.
The duplicated semibold-ellipsis title style (QueueItem/HistoryView/DownloadBar `previewTitle`) is now the
shared `useTextStyles().title`.*
- [ ] **L115 — Equivalent row actions labeled inconsistently** — "Re-index" is icon+text; the
comparable "Retry"/"Remove"/"Re-download" are icon-only.
- [ ] **L116 — Empty-state structure varies** — Downloads (icon + line), History (colored badge + line + sub-hint), Library (icon + line, no sub-hint).
@@ -772,17 +850,31 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [x] **L119 — "No items" copy varies** — "No … yet" (most) vs "Output will appear here." (Terminal) vs "No errors logged." (Diagnostics).
- [ ] **L120 — Fluent `Card` vs hand-styled `div` cards.** Only Settings/Onboarding use `<Card>`; every
other card surface is a bespoke styled `<div>`.
- [ ] **L121 — Raw scrim literal.** CommandPalette backdrop is `rgba(0,0,0,0.32)` (the only raw rgba in
the renderer), identical in light/dark — low contrast over a dark UI.
- [x] **L121 — Raw scrim literal.** CommandPalette backdrop is `rgba(0,0,0,0.32)` (the only raw rgba in
the renderer), identical in light/dark — low contrast over a dark UI. *Fixed: `SCRIM` in tokens.ts aliases
Fluent's semantic `colorBackgroundOverlay` (the dialog scrim); the raw rgba is gone and the palette
separates more cleanly from a dark UI.*
- [x] **L122 — "Checking…" indicated twice** — the Settings update button changes its label to "Checking…" *and* a separate Spinner renders beside it.
- [ ] **L123 — `Caption1` is overloaded** for hints, metadata, counts, and timestamps — one size doing four jobs.
- [ ] **L124 — `Body1` vs `Text` role overlap**`Body1` appears only in empty states/onboarding/gate; `Text` carries titles; the split isn't principled.
- [ ] **L125 — Icon sizing has two syntaxes** — px strings (`fontSize: '20px'`) on icon *containers* vs numeric `fontSize={28}` on inline icons.
- [x] **L123 — `Caption1` is overloaded** for hints, metadata, counts, and timestamps — one size doing four jobs.
*Resolved: the [ui/text.ts](src/renderer/src/components/ui/text.ts) header defines `Caption1`'s role as "small
secondary text" — hints/metadata/counts/timestamps are all that one role, now stated on purpose rather than by
accident (paired with the shared `muted` color for L110).*
- [x] **L124 — `Body1` vs `Text` role overlap**`Body1` appears only in empty states/onboarding/gate; `Text` carries titles; the split isn't principled.
*Resolved: the type-role convention in text.ts makes the split explicit — `Body1` = body copy (empty states,
onboarding prose, gated notices); `Text` = row/item titles. Current usage already follows it.*
- [x] **L125 — Icon sizing has two syntaxes** — px strings (`fontSize: '20px'`) on icon *containers* vs numeric `fontSize={28}` on inline icons.
*Fixed: both syntaxes now single-source from `ICON` in tokens.ts (`fontSize={ICON.control}` on inline icons,
`` fontSize: `${ICON.control}px` `` on the few makeStyles containers) — no more divergent magic numbers.
Glyphs sized to a box rather than to this scale (brand/source tiles, the tiny `watchBadge` icon, thumbnail
placeholders) are documented exclusions and stay proportional.*
- [ ] **L126 — Uneven text truncation** — some titles/metas use `nowrap`+ellipsis, others wrap; no shared truncation utility.
- [x] **L127 — `Notification` sets no icon** — completion toasts used the default Electron icon though `getAppIconPath()` existed. *Fixed with W13 (cached `getAppIconImage()` passed as `icon` at both notification sites).*
- [ ] **L128 — Preview seed/mock data ships in production.** The `PREVIEW` runtime check can't be
tree-shaken, so each store's seed arrays + fake ticker are bundled into the Electron renderer.
- [ ] **L129 — `Hint align` is silently ignored for left/right placements** yet callers still pass it.
- [x] **L129 — `Hint align` is silently ignored for left/right placements** yet callers still pass it.
*Fixed: `HintProps` is now a discriminated union — `align` is only accepted with `placement` `top`/`bottom`;
passing it with `left`/`right` is a compile error instead of a silent no-op. No caller needed changing (the
left/right hints already omit `align`).*
- [ ] **L130 — Error-string style is mixed** — internal errors are lowercase/no-period ("timed out",
"interrupted"); user-facing ones are capitalized with periods. Fine when interpolated, jarring if shown raw.
- [ ] **L131 — Taskbar `error` state is ephemeral** — it's driven by live `error` items, so "Clear
@@ -793,12 +885,24 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
called by both `download()` and `addPlaylist()`. `clearProbe()` stays the lighter probe-only reset for
`onUrlChange`. Also fixes a latent inconsistency — `addPlaylist` previously left the trim/schedule/advanced
panels open after enqueuing.*
- [ ] **L133 — `Field` controls mix `aria-label` + visible `label`.** Many `Field label="X"` wrap a
- [x] **L133 — `Field` controls mix `aria-label` + visible `label`.** Many `Field label="X"` wrap a
`Select aria-label="X"`, so AT announces the name twice (extends M20 across DownloadOptionsForm).
- [ ] **L134 — Inconsistent disabled affordance for busy buttons.** Some stay enabled-looking with a
*Fixed: the custom [Select](src/renderer/src/components/Select.tsx) is now `<Field>`-aware
(`useFieldControlProps_unstable`, `supportsLabelFor: true`), so a Field's visible label names the native
`<select>` via `htmlFor` and its hint associates via `aria-describedby`. Removed the now-duplicate
`aria-label` from every Field-wrapped select (DownloadOptionsForm ×4, AboutCard, AppearanceCard, CookiesCard
×2, DownloadsCard, CustomCommandsCard). The two standalone selects — the download-bar quality picker and the
history type filter — keep their `aria-label`, since there's no Field to name them.*
- [x] **L134 — Inconsistent disabled affordance for busy buttons.** Some stay enabled-looking with a
spinner icon (Fetch/Index) while others are `disabled` with adjacent text (Settings) — no single "busy" convention.
- [ ] **L135 — `Textarea` vs `Input` for similar fields.** Trim uses a `Textarea`; the visually-similar
*Fixed: one busy convention — `disabled` while working + the button's own icon swaps to a `<Spinner
size="tiny">`. Fetch, Index, Check-for-new and the yt-dlp update button already did this; converted
AboutCard's version-check button off its separate adjacent spinner to match.*
- [x] **L135 — `Textarea` vs `Input` for similar fields.** Trim uses a `Textarea`; the visually-similar
single-line fields use `Input` — fine, but the trim/schedule panels and the template form mix both with no shared field style.
*Resolved: verified role-appropriate — the trim field is genuinely multiline (time ranges "one per line",
`resize="vertical"`), so `Textarea` is correct there and `Input` for the single-line fields. Kept as-is; the
shared field styling is covered by the Fluent `<Field>` wrapper they all use.*
*Round 6 (2026-06-29) — behavioral/logic edge cases:*
@@ -889,8 +993,10 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
crosses child elements (no enter/leave depth counter), so the dashed-outline blinks during a drag.
- [x] **L159 — Dead fallback branch.** `copyErrorReport` falls back to "No errors logged." but its button
is disabled when there are no entries, so the fallback is unreachable.
- [ ] **L160 — One-off inline margins.** `marginTop` literals (Onboarding 2px, QueueItem 4px, SettingsView
8px) instead of style classes (extends L5).
- [x] **L160 — One-off inline margins.** `marginTop` literals (Onboarding 2px, QueueItem 4px, SettingsView
8px) instead of style classes (extends L5). *Fixed: the margin literals now draw from `SPACE`
(`hairline`/`xtight`/`tight`) — same values, one source. (The broader inline-`style` vs makeStyles split is
still L5.)*
- [ ] **L161 — `height: 100%` screens inside a padded scroll container.** Downloads/Terminal set
`height: 100%` while App's `<main>` is `overflowY: auto` + 24/28px padding — a fragile coupling that can
double-scroll or misalign the virtualized list's viewport.
@@ -903,10 +1009,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
*Round 8 (2026-06-29) — formatting micro-inconsistencies:*
- [ ] **L164 — Metadata separator glyph/spacing is inconsistent.** Meta lines join with `' • '`
- [x] **L164 — Metadata separator glyph/spacing is inconsistent.** Meta lines join with `' • '`
(2-space bullet) in QueueItem/History/DownloadBar/DownloadsView, with `' · '` (1-space midd·dot) in
LibraryView, and with `' • '` (1-space bullet) in QueueItem's "Paused • 42%". Three styles for one role.
**Standard:** one separator constant (glyph + spacing).
**Standard:** one separator constant (glyph + spacing). *Fixed: one `META_SEP = ' • '` in tokens.ts, adopted
at every meta-line join (QueueItem/History/DownloadBar/DownloadsView/Library/PlaylistPanel). The `' · '`
inside compound quality labels ("720p · mp4 · 184 MB") is a data format parsed by HistoryView and is
intentionally left as-is.*
- [x] **L165 — The two byte/speed formatters round differently.** `download.ts fmtBytes` uses
`toFixed(v >= 100 ? 0 : 1)` ("512 MB") while `queueStats formatSpeed` uses `toFixed(1)` ("512.0 MB/s") —
visibly different precision for the same magnitudes (compounds the duplicate-formatter issue H2/M9).
@@ -955,22 +1064,37 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
but Downloads/Library/History/Terminal are full-width; Onboarding card is 460px, CommandPalette
560px. On a wide window Settings is a narrow column while siblings stretch edge-to-edge.
**Standard:** one reading-width token (e.g. 720px) applied by a shared `Screen` wrapper, or commit to full-width everywhere.
- [ ] **UI2 — Per-screen vertical rhythm differs.** Root section gap is 16 (Settings/Terminal), 20
- [x] **UI2 — Per-screen vertical rhythm differs.** Root section gap is 16 (Settings/Terminal), 20
(Downloads), 18 (Library), 12 (History). **Standard:** a single section-gap (16px) via a shared screen wrapper.
*Fixed: all five view roots now use `SPACE.section` (16) — Downloads 20→16, Library 18→16, History 12→16;
Settings/Terminal were already 16 and are now tokenized.*
- [ ] **UI3 — Card padding has no scale.** 14 (QueueItem) / 16 (DownloadBar) / 20 (Settings card) /
32 (Onboarding) / `10px 12px` (History row) / `12px 14px` (Library head) / `8px 10px` (template row).
**Standard:** a padding scale (e.g. control 810, list-row 12, card 16, hero 24) keyed to surface tier.
- [ ] **UI4 — Asymmetric app content padding.** `padding: '24px 28px'` ([App.tsx](src/renderer/src/App.tsx)) —
28px horizontal appears nowhere else. **Standard:** symmetric or scale-based padding.
- [ ] **UI5 — Action-cluster gaps split 4px vs 8px.** Icon-button rows use 4px (QueueItem/History/
*Implemented (SPACE scale defined; the card tier unified to 16 — QueueItem 14→16, Settings cards 20→16;
DownloadBar already 16; Onboarding → `hero`) but **left unchecked for a watched visual pass**: 20→16 tightens
the 11 settings cards and 14→16 loosens queue rows. The smaller list-row/control paddings (History/Library
head/template) are not yet migrated. Eyeball, then check off or retune the card value.*
- [x] **UI4 — Asymmetric app content padding.** `padding: '24px 28px'` ([App.tsx](src/renderer/src/App.tsx)) —
28px horizontal appears nowhere else. **Standard:** symmetric or scale-based padding. *Fixed: the `<main>`
content padding is now the symmetric `SPACE.page` (24px).*
- [x] **UI5 — Action-cluster gaps split 4px vs 8px.** Icon-button rows use 4px (QueueItem/History/
TemplateManager) but DownloadBar/Library action rows use 8px. **Standard:** 4px for icon-button groups, 8px for labelled-control rows. *(Good baseline already: every empty-state uses `56px 16px` — keep it.)*
*Resolved: the existing split already matched the standard; it's now named in `SPACE` (`xtight` = 4 for
icon-button clusters, `tight` = 8 for control rows) and adopted where touched, so the rule is explicit
rather than incidental.*
### Corner radius
- [ ] **UI6 — Thumbnail radius varies for the same element.** QueueItem `Large`, History `Medium`,
- [x] **UI6 — Thumbnail radius varies for the same element.** QueueItem `Large`, History `Medium`,
Library rowThumb `Small`, DownloadBar previewThumb `Medium`. **Standard:** one thumbnail radius (Medium/10px). *(Distinct from L9, which is thumbnail pixel dimensions.)*
- [ ] **UI7 — Card radius (XLarge vs Large) has no stated rule.** Top-level cards use XLarge (16),
*Fixed: every thumbnail uses `RADIUS.control` (Medium) — QueueItem Large→Medium and Library rowThumb
Small→Medium; History/DownloadBar were already Medium and are now tokenized.*
- [x] **UI7 — Card radius (XLarge vs Large) has no stated rule.** Top-level cards use XLarge (16),
list rows Large (12), but it's applied by feel. **Standard:** document surface tiers — page-card XLarge, list-item Large, control Medium — and apply uniformly.
*Fixed: `RADIUS` documents the tiers (`card` XLarge / `surface` Large / `control` Medium) and they're
applied on the surfaces touched this batch (page cards → `card`, the QueueItem list-item → `surface`,
thumbnails/controls → `control`); remaining correct-but-untokenized `borderRadius*` calls migrate incrementally.*
- [x] **UI8 — Circular shapes mix token and literal.** `borderRadiusCircular` (Library pill/watchBadge)
vs literal `'50%'` (SettingsView swatch, History emptyBadge). **Standard:** always the token.
@@ -981,16 +1105,28 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
(just a count). **Standard:** one page-title style on every screen.
- [x] **UI10 — Only Library has a screen subtitle/description.** Others jump straight to content.
**Standard:** a consistent header block (title + optional one-line description).
- [ ] **UI11 — Semantic type ramp vs ad-hoc px.** Sidebar `brandName`/`navItem` and Library
- [x] **UI11 — Semantic type ramp vs ad-hoc px.** Sidebar `brandName`/`navItem` and Library
`watchBadge`/`pill` set raw `fontSizeBaseXXX`; `sectionIcon`/`mark` use literal `'20px'`/`'26px'`.
**Standard:** Fluent type components/tokens; no literal px font sizes.
**Standard:** Fluent type components/tokens; no literal px font sizes. *Fixed: no literal px font sizes
remain — the last tile/badge glyphs snap to `ICON` tiers (Sidebar mark 20, Onboarding mark 24, Library
srcIcon 20, History emptyBadge 24). The `fontSizeBase*` usages already met the standard (they're tokens, on
interactive `<button>`/pill elements that can't be Fluent type components). The four ≤2px tile-glyph deltas
join UI12's watched visual pass.*
### Icons
- [ ] **UI12 — No icon-size scale.** Inline `fontSize` of 11/14/16/18/20/22/26/28/40 is scattered
across components. **Standard:** a small set (16 inline / 20 control / 24 section / 40 empty-state).
- [ ] **UI13 — Regular vs Filled weight mixed for one concept.** `ArrowDownloadRegular` (nav, buttons)
*Implemented (`ICON` = 16/20/24/40, all four tiers in use; standalone chrome glyphs migrated) but **left
unchecked for a watched visual pass** because a few glyphs shift: the former-18px chrome icons (sidebar nav +
theme toggles, IconButton md) → 20 (control), the settings card-header icons 20→24 (section), and the private
badge 14→16. Box-proportional glyphs (brand/source tiles, `watchBadge` 11px, thumbnail placeholders 22/28)
are documented exclusions and unchanged. Eyeball the 18→20 / 20→24 / 14→16 deltas.*
- [x] **UI13 — Regular vs Filled weight mixed for one concept.** `ArrowDownloadRegular` (nav, buttons)
vs `ArrowDownloadFilled` (brand mark). **Standard:** one weight per concept (Filled only for brand/emphasis).
*Verified: `ArrowDownloadFilled` appears only on the two brand marks (Sidebar, Onboarding); every other
download arrow (nav, buttons, `contentBefore`) is `ArrowDownloadRegular`. The convention already held — the
only Filled glyphs elsewhere are the status Checkmark/Error circles, which are deliberate state emphasis.*
### Buttons & controls
@@ -1002,8 +1138,13 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
are bespoke. **Standard:** wrap recurring patterns so hover/focus/disabled are uniform (ties to L5/M15).
- [ ] **UI16 — Button size hierarchy isn't applied uniformly.** DownloadBar `large`; Library "Index"
`large` but its toolbar `small`; History all `small`; Settings mostly default. **Standard:** size-by-role (screen primary = large; secondary = medium; row actions = small).
- [ ] **UI17 — `appearance="secondary"` used in only two spots** (Library "Check for new", Terminal
*Convention documented (screen-primary large · row/toolbar small · settings default/medium); current usage
already broadly follows it. **Left unchecked** — the specific per-screen size tweaks are a subjective visual
call, so they join the watched visual pass (with UI3/UI12).*
- [x] **UI17 — `appearance="secondary"` used in only two spots** (Library "Check for new", Terminal
"Stop") while every other non-primary button is `subtle`. **Standard:** pick subtle *or* secondary as the standard non-primary appearance.
*Fixed: both switched to `subtle` (the app's standard non-primary appearance — used by every other
non-primary button).*
- [x] **UI18 — Two status-chip systems.** QueueItem uses Fluent `Badge`; LibraryView uses custom
color `pill` spans for the same item-status concept (and labels differ — see M8). **Standard:** one shared status-chip component + label map.
- [ ] **UI19 — Suggestion-banner CSS duplicated.** Identical `suggestion`/`suggestionText` styles in
@@ -1035,9 +1176,13 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
### Animations & transitions
- [ ] **UI26 — Lone animation.** Only the sidebar width transitions (`0.15s ease`); tab switches,
- [x] **UI26 — Lone animation.** Only the sidebar width transitions (`0.15s ease`); tab switches,
panel expand/collapse, banners, and Library card expansion are all instant. **Standard:** one motion
policy — either add subtle, consistent transitions (gated on `prefers-reduced-motion`) or drop the sidebar one.
*Fixed (policy established): the sidebar transition now uses the shared `MOTION` timing (durationFast +
curveEasyEase), and a global `@media (prefers-reduced-motion: reduce)` rule in base.css disables every
transition/animation for users who ask for less. New transitions adopt `MOTION`; none were added
speculatively (compositing overlays flicker on the dev GPU — see the gpu-flicker note).*
### Hover, focus, disabled, keyboard & accessibility