feat(audit): Batch 9 — surface primitives (empty-state, banner, truncation)

Shared UI primitives to end the "two ways to do X" drift on surfaces:

- EmptyState (ui/EmptyState.tsx): one centered empty block — optional focal
  icon/badge + Body1 message + optional muted hint, with a `compact` variant
  for in-content placeholders. Adopted by Downloads, History, Library (L116),
  and — compact — the Terminal log, Diagnostics, and TemplateManager (L117).
- Banner (ui/Banner.tsx) + LinkSuggestion (ui/LinkSuggestion.tsx): one tinted
  notification row (icon + truncating/wrap content + actions, tone brand/warning)
  replacing five hand-rolled style blocks — the DownloadBar copied-link
  suggestion / channel nudge / duplicate warning and the Library copied-link
  suggestion (UI19).
- useTextStyles().truncate: shared single-line-ellipsis utility, adopted in
  Library rows + the playlist panel + Banner; `title` now also sets minWidth:0
  so it shrinks in a flex row (L126).
- L102: Settings "Default format" now uses the same SegmentedControl (Video/
  Audio) + quality Select as the DownloadBar — one mental model; the combined
  `kind|quality` string encoding is gone.

Resolved by verified convention (documented in code / CODE-AUDIT.md):
- L103 busy = in-button icon→Spinner everywhere (done in L134; no adjacent-
  spinner pattern survives).
- L115 list-row actions icon-only+tooltip vs card-toolbar actions labelled.
- L120 Fluent <Card> for static content cards; tokenized surface recipe for
  interactive/list-item cards.
- UI20 toggles = solid brand, list-selection = brand-tint (rule noted on
  SegmentedControl); the cited Library pill is gone via UI18.

L128 (build-time DCE of preview seeds) deferred to Batch 14 with PERF8.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:37:32 -04:00
parent bd7ba8726e
commit 3d4e574916
17 changed files with 407 additions and 238 deletions
+77 -11
View File
@@ -136,6 +136,22 @@ unchecked** — button size-by-role is subjective; current usage already broadly
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.
**Session 2026-07-01 pass 6 (Batch 9 — surface primitives):** three new shared UI primitives —
[EmptyState](src/renderer/src/components/ui/EmptyState.tsx) (**L116/L117**: one centered empty block — icon +
`Body1` message + optional muted hint, with a `compact` variant for in-content placeholders — adopted by
Downloads/History/Library and, compact, by the Terminal log + Diagnostics), [Banner](src/renderer/src/components/ui/Banner.tsx)
+ [LinkSuggestion](src/renderer/src/components/ui/LinkSuggestion.tsx) (**UI19**: one tinted notification row
replacing five hand-rolled style blocks — the DownloadBar suggestion/channel-nudge/dup-warning + the Library
copied-link suggestion), and a shared `useTextStyles().truncate` (**L126**: single-line-ellipsis util, adopted
in Library rows + the playlist panel + Banner). Plus **L102** (Settings "Default format" now uses the same
`SegmentedControl` + quality `Select` as the DownloadBar — one mental model; the `kind|quality` string encoding
is gone), and four resolved-by-verified-convention: **L103** (busy = in-button icon→Spinner everywhere; done in
L134), **L115** (list-row actions icon-only+tooltip vs card-toolbar actions labelled — role-appropriate),
**L120** (Fluent `<Card>` for static content cards, the Batch-6 tokenized surface recipe for interactive/
list-item cards), and **UI20** (toggles = solid brand, list-selection = brand-tint; the cited Library pill is
gone via UI18). **L128 deferred to Batch 14** (build-time DCE of preview seeds, grouped with PERF8). 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,
@@ -800,10 +816,18 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
- [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
- [x] **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
*Fixed: the Settings "Default format" control ([DownloadsCard.tsx](src/renderer/src/components/settings/DownloadsCard.tsx))
now uses the same shared `SegmentedControl` (Video/Audio) + quality `Select` as the DownloadBar, so both
screens present one mental model. The combined `FORMAT_OPTIONS`/`kind|quality` string encoding is gone —
the toggle writes `defaultKind` and the dropdown writes `defaultVideoQuality`/`defaultAudioQuality` directly.*
- [x] **L103 — Two busy-indicator patterns.** Fetch/Index swap the button icon to a `Spinner`; the
Settings check/update buttons keep their icon and render a *separate* adjacent `Spinner`.
*Resolved by L134 (Batch 8), verified this batch: every busy button swaps its own icon to a
`<Spinner size="tiny">` (Fetch/Index/Check-for-new/yt-dlp update/app-update/PO-mint/version-check); the only
remaining `<Spinner>`s are standalone status-row indicators (the "Fetching…"/"Waiting to start…"/indexing
rows), not a button + adjacent spinner. No separate-adjacent-spinner pattern survives.*
- [ ] **L104 — History isn't virtualized.** It renders all filtered rows, while Downloads and Library use `VirtualList`.
- [x] **L105 — `MediaKind` declared twice.** In both [ipc.ts](src/shared/ipc.ts) and
[store/downloads.ts](src/renderer/src/store/downloads.ts); components import it from both. Single source it.
@@ -841,15 +865,37 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
`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).
- [ ] **L117 — Terminal/error-log "empty" aren't centered blocks** like the other empty states (inline text instead).
- [x] **L115 — Equivalent row actions labeled inconsistently** — "Re-index" is icon+text; the
comparable "Retry"/"Remove"/"Re-download" are icon-only. *Resolved (convention verified + role-appropriate):
the two live in different UI contexts, and each context is internally consistent. **List-row item actions**
(QueueItem's Pause/Cancel/Retry/Remove/Open, History's Re-download/Open/Show/Remove) are icon-only with a
`Hint` tooltip + a ≥40px hit target — a compact, repeated-per-row affordance. **Card/toolbar actions** (the
Library source-detail row: Refresh/Remove/Download, the Downloads/History headers) are labeled icon+text —
a one-per-card toolbar where the label aids scanning. The former "Re-index" is the Library card toolbar's
"Refresh", correctly labeled alongside its siblings. No screen mixes the two within one context.*
- [x] **L116 — Empty-state structure varies** — Downloads (icon + line), History (colored badge + line + sub-hint), Library (icon + line, no sub-hint).
*Fixed: one shared [EmptyState](src/renderer/src/components/ui/EmptyState.tsx) — a centered column with an
optional focal `icon` (a hero glyph or History's colored badge), a `Body1` message at normal foreground, and
an optional muted `hint`. Adopted by Downloads, History, and Library, so the three now share one structure
(History keeps its badge + hint; Downloads/Library pass a hero glyph). The per-view `empty`/`emptyHint`
styles are deleted.*
- [x] **L117 — Terminal/error-log "empty" aren't centered blocks** like the other empty states (inline text instead).
*Fixed: both now render the shared `EmptyState` in its `compact` variant (smaller inset, for a placeholder
inside an already-populated screen). The Terminal log centers "No output yet." in the log box (a `logEmpty`
flex modifier); Diagnostics centers "No errors yet." — so both read as empty states rather than a stray line
of inline text.*
- [ ] **L118 — Cross-component focus via hardcoded id.** App focuses the URL field with
`document.getElementById('aerofetch-url')` — brittle coupling into DownloadBar's markup.
- [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>`.
- [x] **L120 — Fluent `Card` vs hand-styled `div` cards.** Only Settings/Onboarding use `<Card>`; every
other card surface is a bespoke styled `<div>`. *Resolved (convention documented + tokens applied):
static content cards (Settings cards, Onboarding) use Fluent `<Card>`; interactive & list-item surfaces
(the download bar, QueueItem, History/Library rows, the Library source card, the command palette) use the
Batch-6 tokenized surface recipe — `RADIUS.card`/`surface`, `ELEVATION`, `SPACE` padding on a styled `<div>`
— because Fluent's `Card` imposes its own internal padding/gap/interaction model that a custom row layout
fights. Forcing `<Card>` onto those would regress layout for no gain; the divergence L120 flags is now a
documented tier split, and the styled surfaces share the same radius/elevation tokens. (The larger "wrap
every recurring pattern" primitive work stays UI15's incremental track.)*
- [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
@@ -867,10 +913,18 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
`` 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] **L126 — Uneven text truncation** — some titles/metas use `nowrap`+ellipsis, others wrap; no shared truncation utility.
*Fixed: a shared `useTextStyles().truncate` ([text.ts](src/renderer/src/components/ui/text.ts)) — single-line
clip with ellipsis + `minWidth:0` — sits alongside the existing `title` (which is `truncate` + semibold).
Adopted for the ad-hoc copies: Library `rowTitle`, the DownloadBar playlist `plItemTitle`/`plHeaderText`
(the header is now `title`), and the shared `Banner` content (UI19). `title` also gained `minWidth:0` so it
shrinks correctly in a flex row. (Remaining correct-but-inline truncations migrate to the util incrementally.)*
- [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.
*Deferred to Batch 14 (perf/bundle): this is a build-time dead-code-elimination concern (gate the seed
imports behind `import.meta.env` so Rollup drops them from the packaged renderer), grouped with PERF8's
code-splitting where the lazy-loading is set up. Left open on purpose.*
- [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
@@ -1147,14 +1201,26 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
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
- [x] **UI19 — Suggestion-banner CSS duplicated.** Identical `suggestion`/`suggestionText` styles in
DownloadBar and LibraryView. **Standard:** a shared `LinkSuggestion` component (also kills drift).
*Fixed: a shared [Banner](src/renderer/src/components/ui/Banner.tsx) (leading icon + truncating/`wrap`
content + trailing actions, `tone` = brand/warning) and a [LinkSuggestion](src/renderer/src/components/ui/LinkSuggestion.tsx)
built on it. The DownloadBar's copied-link suggestion and the Library's are now one `<LinkSuggestion>`; the
DownloadBar's channel/playlist nudge (`wrap`) and duplicate warning (`tone="warning"`) reuse the base
`Banner`. Five hand-rolled style blocks (`suggestion`/`suggestionText`/`channelHint`/`channelHintText`/
`dupRow`) are deleted — the tint/padding/radius are identical, so no visual change.*
### Colors & selection
- [ ] **UI20 — "Selected/active" styling is split.** Solid brand (`colorBrandBackground` +
- [x] **UI20 — "Selected/active" styling is split.** Solid brand (`colorBrandBackground` +
on-brand text) for DownloadBar/Sidebar segments, but brand-tint (`colorBrandBackground2` +
`colorBrandForeground2`) for Sidebar nav, CommandPalette item, Library pill. **Standard:** one active treatment per control class.
*Resolved (rule named + verified): one active treatment **per control class** — **toggles** (the shared
`SegmentedControl`, used by both the DownloadBar kind toggle and the Sidebar theme switch after UI14) use
**solid brand**; **selection within a list** (Sidebar nav item, CommandPalette active option) uses
**brand-tint**. Each class is now internally consistent, and the rule is documented on
[SegmentedControl](src/renderer/src/components/ui/SegmentedControl.tsx). The "Library pill" the item cited is
gone — UI18 replaced it with the shared `StatusChip`, so there's no third treatment left to reconcile.*
- [ ] **UI21 — Sidebar active-nav rail is expanded-only.** The inset brand box-shadow shows only when
expanded; collapsed relies on tint alone. **Standard:** a consistent active indicator in both states.
- [ ] **UI22 — Brand-icon tiles differ.** `mark` (on-brand on solid brand) vs `srcIcon`