feat(audit): Batch 18 — watched visual pass (UI3, UI12, UI16, UI21, UX18, L109, UX26)
UI3: remaining list-row/control paddings on the SPACE scale (History row, Downloads summary, Library head/detail, Terminal gate/log, template rows). UI21: active-nav rail now shows collapsed too (moved into navItemActive). UX18: stable 'Quality' label + brand '• fetched' tag instead of the label/control morph after Fetch. L109/UX26: skeleton lines for the About yt-dlp/ffmpeg boot load; skeleton cards in Settings until the store's loaded flag flips. UI12/UI16: deferred eyeball deltas verified via Electron-probe screenshots (light+dark, collapsed sidebar, fetch flow); UI16 convention codified in ui/tokens.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+36
-29
@@ -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.
|
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] **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 `<title>`).
|
- [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;
|
- [x] **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
|
only the manual "Check" button shows a spinner. *Fixed (Batch 18, with UX26): the About card shows Fluent
|
||||||
eyeballing against the real sub-second load; joins the watched visual pass with the other skeleton/spinner
|
`Skeleton` lines for the yt-dlp and ffmpeg versions while the mount loads them (`checking` now starts true
|
||||||
polish.*
|
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
|
- [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
|
`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
|
`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.
|
(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;
|
*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.*
|
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).
|
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.
|
**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;
|
*Done (Batch 18): the remaining list-row/control paddings migrated to the `SPACE` scale — History row and
|
||||||
DownloadBar already 16; Onboarding → `hero`) but **left unchecked for a watched visual pass**: 20→16 tightens
|
Downloads summary → `snug/cozy`, Library head/detail and Terminal gate/log and the template form → `cozy`
|
||||||
the 11 settings cards and 14→16 loosens queue rows. The smaller list-row/control paddings (History/Library
|
(12, the list-row tier; the odd 14px horizontals snapped down), template row → `tight/snug`. The earlier
|
||||||
head/template) are not yet migrated. Eyeball, then check off or retune the card value.*
|
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)) —
|
- [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>`
|
28px horizontal appears nowhere else. **Standard:** symmetric or scale-based padding. *Fixed: the `<main>`
|
||||||
content padding is now the symmetric `SPACE.page` (24px).*
|
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
|
### 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).
|
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
|
*Done (Batch 18): the `ICON` = 16/20/24/40 tiers were already in place; the deferred eyeball of the shifted
|
||||||
unchecked for a watched visual pass** because a few glyphs shift: the former-18px chrome icons (sidebar nav +
|
glyphs (18→20 sidebar nav/theme toggles, 20→24 settings card headers, 14→16 private badge) happened in the
|
||||||
theme toggles, IconButton md) → 20 (control), the settings card-header icons 20→24 (section), and the private
|
Electron-probe screenshot pass — the nav icons sit balanced in their 20px tier, the card-header glyphs read
|
||||||
badge 14→16. Box-proportional glyphs (brand/source tiles, `watchBadge` 11px, thumbnail placeholders 22/28)
|
as proper section markers at 24, nothing looks oversized. Box-proportional glyphs (brand/source tiles,
|
||||||
are documented exclusions and unchanged. Eyeball the 18→20 / 20→24 / 14→16 deltas.*
|
`watchBadge`, thumbnail placeholders) remain documented exclusions.*
|
||||||
- [x] **UI13 — Regular vs Filled weight mixed for one concept.** `ArrowDownloadRegular` (nav, buttons)
|
- [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).
|
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
|
*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,
|
- [x] **UI15 — Raw `<button>`s alongside Fluent `<Button>`.** Sidebar navItem/iconBtn/themeSeg,
|
||||||
DownloadBar segment/plKindBtn, CommandPalette item, and Library `groupHead` (a `role="button"` div)
|
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).
|
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).
|
`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
|
*Done (Batch 18): the usage the item lists already matches the size-by-role rule (each screen's one primary
|
||||||
already broadly follows it. **Left unchecked** — the specific per-screen size tweaks are a subjective visual
|
is `large` — Download/Add-to-queue, Library Index; row/toolbar actions `small`; settings cards default) —
|
||||||
call, so they join the watched visual pass (with UI3/UI12).*
|
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
|
- [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.
|
"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
|
*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
|
**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
|
[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.*
|
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.
|
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
|
*Fixed (Batch 18): the inset rail moved from a conditional inline style into `navItemActive` itself, so it
|
||||||
against the new active-download `CounterBadge` added in Batch 11) is an eyeball call; joins the watched
|
shows in both states. Screenshot-verified collapsed: the 3px brand rail hugs the row's left edge and reads
|
||||||
visual pass. The tint already marks the active item in both states.*
|
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`
|
- [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.
|
(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`,
|
*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
|
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
|
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.*
|
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.
|
"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
|
*Fixed (Batch 18): the label stays "Quality" in both states; a small brand-colored "• fetched" tag appears
|
||||||
loaded); whether it "surprises" is a judgment best made watching it, so it joins the watched visual pass.*
|
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
|
- [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 —
|
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.*
|
[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
|
- [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
|
started," but the actual path is Fetch/Download. *Resolved by L78 (DownloadsView empty-state copy
|
||||||
rewritten to match the Fetch/Download flow).*
|
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.
|
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
|
*Fixed (Batch 18, with L109): the About versions show skeleton lines during the boot load, and SettingsView
|
||||||
a perceived-polish tweak worth doing while watching the actual boot, so it joins the visual pass.*
|
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.)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -358,11 +358,17 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.control}>
|
<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 ? (
|
{usingFormats ? (
|
||||||
<Select
|
<Select
|
||||||
className={styles.quality}
|
className={styles.quality}
|
||||||
aria-label="Quality / format"
|
aria-label="Quality (fetched formats)"
|
||||||
size="large"
|
size="large"
|
||||||
value={selectedFormat?.id ?? ''}
|
value={selectedFormat?.id ?? ''}
|
||||||
options={(info?.formats ?? []).map((f) => ({ value: f.id, label: f.label }))}
|
options={(info?.formats ?? []).map((f) => ({ value: f.id, label: f.label }))}
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ const useStyles = makeStyles({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '12px',
|
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,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
border: `1px solid ${tokens.colorNeutralStroke2}`
|
border: `1px solid ${tokens.colorNeutralStroke2}`
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ const useStyles = makeStyles({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
padding: '10px 12px',
|
// List-row padding tier (UI3): snug vertical, cozy horizontal.
|
||||||
|
padding: `${SPACE.snug} ${SPACE.cozy}`,
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
border: `1px solid ${tokens.colorNeutralStroke2}`
|
border: `1px solid ${tokens.colorNeutralStroke2}`
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ const useStyles = makeStyles({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
padding: '12px 14px',
|
// List-row padding tier (UI3): cozy on both axes (was 12/14).
|
||||||
|
padding: SPACE.cozy,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@@ -148,7 +149,8 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
detail: {
|
detail: {
|
||||||
borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
|
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',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '12px'
|
gap: '12px'
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
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 { SearchRegular } from '@fluentui/react-icons'
|
||||||
|
import { useSettings } from '../store/settings'
|
||||||
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
import { ScreenHeader, useScreenStyles } from './ui/Screen'
|
||||||
import { SPACE } from './ui/tokens'
|
import { SPACE } from './ui/tokens'
|
||||||
import { DownloadsCard } from './settings/DownloadsCard'
|
import { DownloadsCard } from './settings/DownloadsCard'
|
||||||
@@ -43,6 +52,9 @@ const CARDS = [
|
|||||||
export function SettingsView(): React.JSX.Element {
|
export function SettingsView(): React.JSX.Element {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const screen = useScreenStyles()
|
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
|
// 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),
|
// search covers every visible label without hand-maintaining per-card keywords),
|
||||||
@@ -88,18 +100,28 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{CARDS.map((Card, i) => (
|
{!loaded ? (
|
||||||
<div
|
// Boot skeleton (UX26): three placeholder cards while persisted settings
|
||||||
// Static array, never reordered/filtered (cards only hide) — index key is stable.
|
// load, instead of a flash of fallback values.
|
||||||
key={i}
|
<Skeleton aria-label="Loading settings">
|
||||||
ref={(el) => {
|
{[0, 1, 2].map((i) => (
|
||||||
cardRefs.current[i] = el
|
<SkeletonItem key={i} size={128} style={{ marginBottom: SPACE.section }} />
|
||||||
}}
|
))}
|
||||||
style={hidden[i] ? { display: 'none' } : undefined}
|
</Skeleton>
|
||||||
>
|
) : (
|
||||||
<Card />
|
CARDS.map((Card, i) => (
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ const useStyles = makeStyles({
|
|||||||
backgroundColor: tokens.colorBrandBackground2,
|
backgroundColor: tokens.colorBrandBackground2,
|
||||||
color: tokens.colorBrandForeground2,
|
color: tokens.colorBrandForeground2,
|
||||||
fontWeight: tokens.fontWeightSemibold,
|
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': {
|
':hover': {
|
||||||
backgroundColor: tokens.colorBrandBackground2
|
backgroundColor: tokens.colorBrandBackground2
|
||||||
}
|
}
|
||||||
@@ -253,11 +257,6 @@ export function Sidebar({
|
|||||||
active && styles.navItemActive,
|
active && styles.navItemActive,
|
||||||
focus.focusRing
|
focus.focusRing
|
||||||
)}
|
)}
|
||||||
style={
|
|
||||||
active && !collapsed
|
|
||||||
? { boxShadow: `inset 3px 0 0 0 ${tokens.colorCompoundBrandStroke}` }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onClick={() => onTabChange(n.value)}
|
onClick={() => onTabChange(n.value)}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
aria-label={
|
aria-label={
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useTemplates } from '../store/templates'
|
|||||||
import { newId } from '../id'
|
import { newId } from '../id'
|
||||||
import { Hint } from './Hint'
|
import { Hint } from './Hint'
|
||||||
import { EmptyState } from './ui/EmptyState'
|
import { EmptyState } from './ui/EmptyState'
|
||||||
|
import { SPACE } from './ui/tokens'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -33,7 +34,8 @@ const useStyles = makeStyles({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '10px',
|
gap: '10px',
|
||||||
padding: '8px 10px',
|
// Control-tier padding (UI3): tight vertical, snug horizontal.
|
||||||
|
padding: `${SPACE.tight} ${SPACE.snug}`,
|
||||||
backgroundColor: tokens.colorNeutralBackground2,
|
backgroundColor: tokens.colorNeutralBackground2,
|
||||||
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
border: `1px solid ${tokens.colorNeutralStroke2}`
|
border: `1px solid ${tokens.colorNeutralStroke2}`
|
||||||
@@ -63,7 +65,8 @@ const useStyles = makeStyles({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
padding: '12px',
|
// List-row padding tier (UI3): cozy on both axes.
|
||||||
|
padding: SPACE.cozy,
|
||||||
backgroundColor: tokens.colorNeutralBackground2,
|
backgroundColor: tokens.colorNeutralBackground2,
|
||||||
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
border: `1px solid ${tokens.colorNeutralStroke2}`
|
border: `1px solid ${tokens.colorNeutralStroke2}`
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const useStyles = makeStyles({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
padding: '12px 14px',
|
// List-row padding tier (UI3): cozy on both axes (was 12/14).
|
||||||
|
padding: SPACE.cozy,
|
||||||
backgroundColor: tokens.colorStatusWarningBackground1,
|
backgroundColor: tokens.colorStatusWarningBackground1,
|
||||||
color: tokens.colorStatusWarningForeground1,
|
color: tokens.colorStatusWarningForeground1,
|
||||||
...shorthands.borderRadius(tokens.borderRadiusLarge)
|
...shorthands.borderRadius(tokens.borderRadiusLarge)
|
||||||
@@ -38,7 +39,8 @@ const useStyles = makeStyles({
|
|||||||
minHeight: '160px',
|
minHeight: '160px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: '12px 14px',
|
// List-row padding tier (UI3): cozy on both axes (was 12/14).
|
||||||
|
padding: SPACE.cozy,
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
color: tokens.colorNeutralForeground1,
|
color: tokens.colorNeutralForeground1,
|
||||||
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
...shorthands.borderRadius(tokens.borderRadiusLarge),
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ export const useDownloadBarStyles = makeStyles({
|
|||||||
quality: {
|
quality: {
|
||||||
minWidth: '220px'
|
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: {
|
spacer: {
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
Subtitle2,
|
Subtitle2,
|
||||||
Caption1,
|
Caption1,
|
||||||
Text,
|
Text,
|
||||||
Spinner
|
Spinner,
|
||||||
|
Skeleton,
|
||||||
|
SkeletonItem
|
||||||
} from '@fluentui/react-components'
|
} from '@fluentui/react-components'
|
||||||
import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons'
|
import { InfoRegular, ArrowSyncRegular } from '@fluentui/react-icons'
|
||||||
import { type YtdlpUpdateChannel } from '@shared/ipc'
|
import { type YtdlpUpdateChannel } from '@shared/ipc'
|
||||||
@@ -68,8 +70,20 @@ export function AboutCard(): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</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?.ok && <Text className={styles.mono}>yt-dlp {version.version}</Text>}
|
||||||
{version && !version.ok && <Caption1 className={errText.errorPre}>{version.error}</Caption1>}
|
{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 && (
|
{ffmpeg && (
|
||||||
<div className={styles.folderRow}>
|
<div className={styles.folderRow}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export interface AboutCardController {
|
|||||||
export function useAboutCard(): AboutCardController {
|
export function useAboutCard(): AboutCardController {
|
||||||
const ytdlpChannel = useSettings((s) => s.ytdlpChannel)
|
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 [version, setVersion] = useState<YtdlpVersionResult | null>(null)
|
||||||
const [ffmpeg, setFfmpeg] = useState<FfmpegVersionResult | null>(null)
|
const [ffmpeg, setFfmpeg] = useState<FfmpegVersionResult | null>(null)
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
@@ -31,9 +33,20 @@ export function useAboutCard(): AboutCardController {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Show the current yt-dlp version without a manual click, and reflect a
|
// 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.
|
// 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.
|
// 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) => {
|
return window.api.onYtdlpAutoUpdateStatus((s) => {
|
||||||
if (s.phase === 'checking') {
|
if (s.phase === 'checking') {
|
||||||
setChecking(true)
|
setChecking(true)
|
||||||
|
|||||||
@@ -118,6 +118,17 @@ export const ICON = {
|
|||||||
hero: 40
|
hero: 40
|
||||||
} as const
|
} 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
|
* 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
|
* (matching the sidebar's former `0.15s ease`) and gates *all* transitions and
|
||||||
|
|||||||
Reference in New Issue
Block a user