diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index 153244e..4011a01 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -1007,10 +1007,11 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n check-for-new, update-yt-dlp) vs `ArrowSyncRegular` (re-index, check-for-updates) for the same "redo/refresh" idea. - [x] **L107 — Spinner sizes mixed** — `"tiny"` almost everywhere, `"extra-tiny"` for the QueueItem queued row. - [x] **L108 — Main window sets no explicit `title`** (relies on the document ``). -- [ ] **L109 — Versions auto-load with no indicator.** yt-dlp/ffmpeg versions in About just appear; - only the manual "Check" button shows a spinner. *Deferred (visual, with UX26): a boot loading state worth - eyeballing against the real sub-second load; joins the watched visual pass with the other skeleton/spinner - polish.* +- [x] **L109 — Versions auto-load with no indicator.** yt-dlp/ffmpeg versions in About just appear; + only the manual "Check" button shows a spinner. *Fixed (Batch 18, with UX26): the About card shows Fluent + `Skeleton` lines for the yt-dlp and ffmpeg versions while the mount loads them (`checking` now starts true + and settles in a `finally`; an ffmpeg bridge failure settles to "not found" rather than a permanent + skeleton). Verified in the preview probe that the card settles to the loaded state cleanly.* - [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 @@ -1389,13 +1390,14 @@ are an undefined spacing/radius/type scale and a drift between Fluent components (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) / +- [x] **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 8–10, list-row 12, card 16, hero 24) keyed to surface tier. - *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.* + *Done (Batch 18): the remaining list-row/control paddings migrated to the `SPACE` scale — History row and + Downloads summary → `snug/cozy`, Library head/detail and Terminal gate/log and the template form → `cozy` + (12, the list-row tier; the odd 14px horizontals snapped down), template row → `tight/snug`. The earlier + card-tier deltas (20→16 settings cards, 14→16 queue rows) plus these were eyeballed via the Electron-probe + screenshot pass (light + dark, Downloads/History/Library/Settings): nothing reads cramped or loose; kept.* - [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).* @@ -1436,13 +1438,13 @@ are an undefined spacing/radius/type scale and a drift between Fluent components ### Icons -- [ ] **UI12 — No icon-size scale.** Inline `fontSize` of 11/14/16/18/20/22/26/28/40 is scattered +- [x] **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). - *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.* + *Done (Batch 18): the `ICON` = 16/20/24/40 tiers were already in place; the deferred eyeball of the shifted + glyphs (18→20 sidebar nav/theme toggles, 20→24 settings card headers, 14→16 private badge) happened in the + Electron-probe screenshot pass — the nav icons sit balanced in their 20px tier, the card-header glyphs read + as proper section markers at 24, nothing looks oversized. Box-proportional glyphs (brand/source tiles, + `watchBadge`, thumbnail placeholders) remain documented exclusions.* - [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 @@ -1457,11 +1459,13 @@ are an undefined spacing/radius/type scale and a drift between Fluent components - [x] **UI15 — Raw `<button>`s alongside Fluent `<Button>`.** Sidebar navItem/iconBtn/themeSeg, DownloadBar segment/plKindBtn, CommandPalette item, and Library `groupHead` (a `role="button"` div) 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" +- [x] **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). - *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).* + *Done (Batch 18): the usage the item lists already matches the size-by-role rule (each screen's one primary + is `large` — Download/Add-to-queue, Library Index; row/toolbar actions `small`; settings cards default) — + confirmed screen-by-screen in the screenshot pass, no tweaks needed. The convention is now codified in + [ui/tokens.ts](src/renderer/src/components/ui/tokens.ts) alongside the other scales so it's a rule, not an + accident.* - [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 @@ -1488,11 +1492,11 @@ are an undefined spacing/radius/type scale and a drift between Fluent components **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 +- [x] **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. - *Deferred (subjective visual): whether the collapsed rail needs its own inset indicator (and how it reads - against the new active-download `CounterBadge` added in Batch 11) is an eyeball call; joins the watched - visual pass. The tint already marks the active item in both states.* + *Fixed (Batch 18): the inset rail moved from a conditional inline style into `navItemActive` itself, so it + shows in both states. Screenshot-verified collapsed: the 3px brand rail hugs the row's left edge and reads + clearly beside the active-download `CounterBadge` pinned at the icon's top-right — no collision.* - [x] **UI22 — Brand-icon tiles differ.** `mark` (on-brand on solid brand) vs `srcIcon` (brandForeground2 on brand-tint) vs `sectionIcon` (compoundBrand, no tile). **Standard:** a defined icon-emphasis set. *Resolved (the set is now defined + documented): three deliberate emphasis levels — **brand mark** (`mark`, @@ -1731,10 +1735,12 @@ root cause is already filed. keyboard focus — so screen-reader and keyboard users get the name; the icon isn't the only affordance. A permanently-visible text label is a deliberate compact-layout tradeoff for the URL row (the Download button beside it carries the primary text label), so it's left as a visual choice, not an a11y gap.* -- [ ] **UX18 — The quality control morphs after Fetch** (preset list → real formats; label "Quality" → +- [x] **UX18 — The quality control morphs after Fetch** (preset list → real formats; label "Quality" → "Quality / format") in place — a surprising transform. **Fix:** keep a stable control with a clear "formats loaded" state. - *Deferred (subjective visual): the morph is arguably informative (the label change signals real formats - loaded); whether it "surprises" is a judgment best made watching it, so it joins the watched visual pass.* + *Fixed (Batch 18): the label stays "Quality" in both states; a small brand-colored "• fetched" tag appears + beside it once real formats load, so the state change is signalled without the control or its label + morphing. Verified in the preview probe: Fetch on a URL loads the mock formats and the tag + format list + render as designed (screenshot).* - [x] **UX19 — Window size/position isn't remembered.** [index.ts](src/main/index.ts) opens a fixed 920×700 every launch (no bounds persistence). **Fix:** persist + restore window bounds. *Fixed with W2 — [windowState.ts](src/main/windowState.ts) persists + restores bounds/maximized with an off-screen guard.* @@ -1766,10 +1772,11 @@ root cause is already filed. - [x] **UX25 — Empty-state copy mismatches the flow** (L78): Downloads says "Paste a URL above to get started," but the actual path is Fetch/Download. *Resolved by L78 (DownloadsView empty-state copy rewritten to match the Fetch/Download flow).* -- [ ] **UX26 — yt-dlp/ffmpeg version + initial settings load have no skeleton/spinner** — brief blank +- [x] **UX26 — yt-dlp/ffmpeg version + initial settings load have no skeleton/spinner** — brief blank states on boot (plus the documented one-frame theme flash). **Fix:** lightweight loading states. - *Deferred (low value, needs eyeballing): the blanks are sub-second on a normal launch; adding skeletons is - a perceived-polish tweak worth doing while watching the actual boot, so it joins the visual pass.* + *Fixed (Batch 18, with L109): the About versions show skeleton lines during the boot load, and SettingsView + renders three skeleton cards until the settings store's `loaded` flag flips — no more flash of fallback + values. (The one-frame theme flash remains separately documented under SR1.)* --- diff --git a/src/renderer/src/components/DownloadBar.tsx b/src/renderer/src/components/DownloadBar.tsx index 8488f1e..17c3b0f 100644 --- a/src/renderer/src/components/DownloadBar.tsx +++ b/src/renderer/src/components/DownloadBar.tsx @@ -358,11 +358,17 @@ export function DownloadBar(): React.JSX.Element { </div> <div className={styles.control}> - <Caption1>{usingFormats ? 'Quality / format' : 'Quality'}</Caption1> + {/* The label stays "Quality" in both states (UX18) — the fetched tag, + not a morphing label, signals that the list now shows the site's + real formats. */} + <Caption1> + Quality + {usingFormats && <span className={styles.qualityFetched}> • fetched</span>} + </Caption1> {usingFormats ? ( <Select className={styles.quality} - aria-label="Quality / format" + aria-label="Quality (fetched formats)" size="large" value={selectedFormat?.id ?? ''} options={(info?.formats ?? []).map((f) => ({ value: f.id, label: f.label }))} diff --git a/src/renderer/src/components/DownloadsView.tsx b/src/renderer/src/components/DownloadsView.tsx index d309f78..8e96209 100644 --- a/src/renderer/src/components/DownloadsView.tsx +++ b/src/renderer/src/components/DownloadsView.tsx @@ -49,7 +49,8 @@ const useStyles = makeStyles({ display: 'flex', alignItems: 'center', gap: '12px', - padding: '10px 14px', + // List-row padding tier (UI3): snug vertical, cozy horizontal (was 10/14). + padding: `${SPACE.snug} ${SPACE.cozy}`, backgroundColor: tokens.colorNeutralBackground1, ...shorthands.borderRadius(tokens.borderRadiusLarge), border: `1px solid ${tokens.colorNeutralStroke2}` diff --git a/src/renderer/src/components/HistoryView.tsx b/src/renderer/src/components/HistoryView.tsx index d7116b1..9fad7bd 100644 --- a/src/renderer/src/components/HistoryView.tsx +++ b/src/renderer/src/components/HistoryView.tsx @@ -85,7 +85,8 @@ const useStyles = makeStyles({ display: 'flex', alignItems: 'center', gap: '12px', - padding: '10px 12px', + // List-row padding tier (UI3): snug vertical, cozy horizontal. + padding: `${SPACE.snug} ${SPACE.cozy}`, backgroundColor: tokens.colorNeutralBackground1, ...shorthands.borderRadius(tokens.borderRadiusLarge), border: `1px solid ${tokens.colorNeutralStroke2}` diff --git a/src/renderer/src/components/LibraryView.tsx b/src/renderer/src/components/LibraryView.tsx index bcd769a..31b68db 100644 --- a/src/renderer/src/components/LibraryView.tsx +++ b/src/renderer/src/components/LibraryView.tsx @@ -110,7 +110,8 @@ const useStyles = makeStyles({ display: 'flex', alignItems: 'center', gap: '12px', - padding: '12px 14px', + // List-row padding tier (UI3): cozy on both axes (was 12/14). + padding: SPACE.cozy, width: '100%', border: 'none', backgroundColor: 'transparent', @@ -148,7 +149,8 @@ const useStyles = makeStyles({ }, detail: { borderTop: `1px solid ${tokens.colorNeutralStroke2}`, - padding: '12px 14px', + // List-row padding tier (UI3): cozy on both axes (was 12/14). + padding: SPACE.cozy, display: 'flex', flexDirection: 'column', gap: '12px' diff --git a/src/renderer/src/components/SettingsView.tsx b/src/renderer/src/components/SettingsView.tsx index 623bd72..630a534 100644 --- a/src/renderer/src/components/SettingsView.tsx +++ b/src/renderer/src/components/SettingsView.tsx @@ -1,6 +1,15 @@ import { useEffect, useRef, useState } from 'react' -import { Input, Caption1, makeStyles, mergeClasses, tokens } from '@fluentui/react-components' +import { + Input, + Caption1, + Skeleton, + SkeletonItem, + makeStyles, + mergeClasses, + tokens +} from '@fluentui/react-components' import { SearchRegular } from '@fluentui/react-icons' +import { useSettings } from '../store/settings' import { ScreenHeader, useScreenStyles } from './ui/Screen' import { SPACE } from './ui/tokens' import { DownloadsCard } from './settings/DownloadsCard' @@ -43,6 +52,9 @@ const CARDS = [ export function SettingsView(): React.JSX.Element { const styles = useStyles() const screen = useScreenStyles() + // Until persisted settings arrive, the cards would flash their fallback values; + // show skeleton cards for that brief boot window instead (UX26). + const loaded = useSettings((s) => s.loaded) // Settings search (Phase O). Matching still reads each card's rendered text (so a // search covers every visible label without hand-maintaining per-card keywords), @@ -88,18 +100,28 @@ export function SettingsView(): React.JSX.Element { )} </div> - {CARDS.map((Card, i) => ( - <div - // Static array, never reordered/filtered (cards only hide) — index key is stable. - key={i} - ref={(el) => { - cardRefs.current[i] = el - }} - style={hidden[i] ? { display: 'none' } : undefined} - > - <Card /> - </div> - ))} + {!loaded ? ( + // Boot skeleton (UX26): three placeholder cards while persisted settings + // load, instead of a flash of fallback values. + <Skeleton aria-label="Loading settings"> + {[0, 1, 2].map((i) => ( + <SkeletonItem key={i} size={128} style={{ marginBottom: SPACE.section }} /> + ))} + </Skeleton> + ) : ( + CARDS.map((Card, i) => ( + <div + // Static array, never reordered/filtered (cards only hide) — index key is stable. + key={i} + ref={(el) => { + cardRefs.current[i] = el + }} + style={hidden[i] ? { display: 'none' } : undefined} + > + <Card /> + </div> + )) + )} </div> </div> ) diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 4bdaf67..f006900 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -132,6 +132,10 @@ const useStyles = makeStyles({ backgroundColor: tokens.colorBrandBackground2, color: tokens.colorBrandForeground2, fontWeight: tokens.fontWeightSemibold, + // The active-nav rail shows in BOTH sidebar states (UI21) — collapsed used to + // rely on tint alone. It hugs the row's left edge, clear of the collapsed + // count badge pinned at the icon's top-right. + boxShadow: `inset 3px 0 0 0 ${tokens.colorCompoundBrandStroke}`, ':hover': { backgroundColor: tokens.colorBrandBackground2 } @@ -253,11 +257,6 @@ export function Sidebar({ active && styles.navItemActive, focus.focusRing )} - style={ - active && !collapsed - ? { boxShadow: `inset 3px 0 0 0 ${tokens.colorCompoundBrandStroke}` } - : undefined - } onClick={() => onTabChange(n.value)} aria-current={active ? 'page' : undefined} aria-label={ diff --git a/src/renderer/src/components/TemplateManager.tsx b/src/renderer/src/components/TemplateManager.tsx index 5f64bec..ca6266f 100644 --- a/src/renderer/src/components/TemplateManager.tsx +++ b/src/renderer/src/components/TemplateManager.tsx @@ -22,6 +22,7 @@ import { useTemplates } from '../store/templates' import { newId } from '../id' import { Hint } from './Hint' import { EmptyState } from './ui/EmptyState' +import { SPACE } from './ui/tokens' const useStyles = makeStyles({ root: { @@ -33,7 +34,8 @@ const useStyles = makeStyles({ display: 'flex', alignItems: 'center', gap: '10px', - padding: '8px 10px', + // Control-tier padding (UI3): tight vertical, snug horizontal. + padding: `${SPACE.tight} ${SPACE.snug}`, backgroundColor: tokens.colorNeutralBackground2, ...shorthands.borderRadius(tokens.borderRadiusLarge), border: `1px solid ${tokens.colorNeutralStroke2}` @@ -63,7 +65,8 @@ const useStyles = makeStyles({ display: 'flex', flexDirection: 'column', gap: '8px', - padding: '12px', + // List-row padding tier (UI3): cozy on both axes. + padding: SPACE.cozy, backgroundColor: tokens.colorNeutralBackground2, ...shorthands.borderRadius(tokens.borderRadiusLarge), border: `1px solid ${tokens.colorNeutralStroke2}` diff --git a/src/renderer/src/components/TerminalView.tsx b/src/renderer/src/components/TerminalView.tsx index d5be666..520629a 100644 --- a/src/renderer/src/components/TerminalView.tsx +++ b/src/renderer/src/components/TerminalView.tsx @@ -24,7 +24,8 @@ const useStyles = makeStyles({ flexDirection: 'column', alignItems: 'flex-start', gap: '8px', - padding: '12px 14px', + // List-row padding tier (UI3): cozy on both axes (was 12/14). + padding: SPACE.cozy, backgroundColor: tokens.colorStatusWarningBackground1, color: tokens.colorStatusWarningForeground1, ...shorthands.borderRadius(tokens.borderRadiusLarge) @@ -38,7 +39,8 @@ const useStyles = makeStyles({ minHeight: '160px', overflowY: 'auto', margin: 0, - padding: '12px 14px', + // List-row padding tier (UI3): cozy on both axes (was 12/14). + padding: SPACE.cozy, backgroundColor: tokens.colorNeutralBackground1, color: tokens.colorNeutralForeground1, ...shorthands.borderRadius(tokens.borderRadiusLarge), diff --git a/src/renderer/src/components/downloadBar/styles.ts b/src/renderer/src/components/downloadBar/styles.ts index 06ac073..6acc909 100644 --- a/src/renderer/src/components/downloadBar/styles.ts +++ b/src/renderer/src/components/downloadBar/styles.ts @@ -79,6 +79,11 @@ export const useDownloadBarStyles = makeStyles({ quality: { minWidth: '220px' }, + // The "fetched" state tag beside the Quality label once real formats load + // (UX18) — the label itself stays stable instead of morphing. + qualityFetched: { + color: tokens.colorBrandForeground2 + }, spacer: { flexGrow: 1 }, diff --git a/src/renderer/src/components/settings/AboutCard.tsx b/src/renderer/src/components/settings/AboutCard.tsx index 2f6547d..553cba1 100644 --- a/src/renderer/src/components/settings/AboutCard.tsx +++ b/src/renderer/src/components/settings/AboutCard.tsx @@ -6,7 +6,9 @@ import { Subtitle2, Caption1, Text, - Spinner + Spinner, + Skeleton, + SkeletonItem } from '@fluentui/react-components' import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons' import { type YtdlpUpdateChannel } from '@shared/ipc' @@ -68,8 +70,20 @@ export function AboutCard(): React.JSX.Element { /> </Field> + {/* Boot loading state (L109/UX26): the versions load automatically on mount, + so show skeleton lines instead of a blank until they arrive. */} + {!version && checking && ( + <Skeleton aria-label="Loading yt-dlp version"> + <SkeletonItem size={16} style={{ maxWidth: '180px' }} /> + </Skeleton> + )} {version?.ok && <Text className={styles.mono}>yt-dlp {version.version}</Text>} {version && !version.ok && <Caption1 className={errText.errorPre}>{version.error}</Caption1>} + {!ffmpeg && ( + <Skeleton aria-label="Loading ffmpeg versions"> + <SkeletonItem size={16} style={{ maxWidth: '220px' }} /> + </Skeleton> + )} {ffmpeg && ( <div className={styles.folderRow}> <div> diff --git a/src/renderer/src/components/settings/useAboutCard.ts b/src/renderer/src/components/settings/useAboutCard.ts index ac8c9c3..df10bc7 100644 --- a/src/renderer/src/components/settings/useAboutCard.ts +++ b/src/renderer/src/components/settings/useAboutCard.ts @@ -22,7 +22,9 @@ export interface AboutCardController { export function useAboutCard(): AboutCardController { const ytdlpChannel = useSettings((s) => s.ytdlpChannel) - const [checking, setChecking] = useState(false) + // `checking` starts true: the mount effect immediately loads the version, and + // the card shows a skeleton line for that boot load instead of a blank (L109). + const [checking, setChecking] = useState(true) const [version, setVersion] = useState<YtdlpVersionResult | null>(null) const [ffmpeg, setFfmpeg] = useState<FfmpegVersionResult | null>(null) const [updating, setUpdating] = useState(false) @@ -31,9 +33,20 @@ export function useAboutCard(): AboutCardController { useEffect(() => { // Show the current yt-dlp version without a manual click, and reflect a // background auto-update (which may run on launch) live in this panel. - window.api.getYtdlpVersion().then(setVersion).catch(logError('getYtdlpVersion')) + window.api + .getYtdlpVersion() + .then(setVersion) + .catch(logError('getYtdlpVersion')) + .finally(() => setChecking(false)) // ffmpeg/ffprobe are display-only (bundled, not auto-updated); load them once. - window.api.getFfmpegVersions().then(setFfmpeg).catch(logError('getFfmpegVersions')) + // On a bridge failure, settle to "not found" rather than a permanent skeleton. + window.api + .getFfmpegVersions() + .then(setFfmpeg) + .catch((e) => { + logError('getFfmpegVersions')(e) + setFfmpeg({ ffmpeg: null, ffprobe: null }) + }) return window.api.onYtdlpAutoUpdateStatus((s) => { if (s.phase === 'checking') { setChecking(true) diff --git a/src/renderer/src/components/ui/tokens.ts b/src/renderer/src/components/ui/tokens.ts index 0febf66..300f9d0 100644 --- a/src/renderer/src/components/ui/tokens.ts +++ b/src/renderer/src/components/ui/tokens.ts @@ -118,6 +118,17 @@ export const ICON = { hero: 40 } as const +/** + * Button size-by-role convention (UI16) — there's no token to alias (Fluent's + * `size` prop is per-instance), so the rule lives here with the other scales: + * + * - `large` — the screen's one primary action (DownloadBar Download / Add-to- + * queue, Library Index). + * - `small` — row actions and toolbar buttons inside lists (History row + * actions, Library toolbar, queue-header actions). + * - default (medium) — settings-card buttons and everything in between. + */ + /** * One motion policy (UI26). The app keeps a single, subtle transition timing * (matching the sidebar's former `0.15s ease`) and gates *all* transitions and