diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index 35b3faa..3ac8cbc 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -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 `` 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 + `` (Fetch/Index/Check-for-new/yt-dlp update/app-update/PO-mint/version-check); the only + remaining ``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 `` — 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 ``; every - other card surface is a bespoke styled `
`. +- [x] **L120 — Fluent `Card` vs hand-styled `div` cards.** Only Settings/Onboarding use ``; every + other card surface is a bespoke styled `
`. *Resolved (convention documented + tokens applied): + static content cards (Settings cards, Onboarding) use Fluent ``; 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 `
` + — because Fluent's `Card` imposes its own internal padding/gap/interaction model that a custom row layout + fights. Forcing `` 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 ``; 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` diff --git a/src/renderer/src/components/DownloadBar.tsx b/src/renderer/src/components/DownloadBar.tsx index 70722bf..81c7377 100644 --- a/src/renderer/src/components/DownloadBar.tsx +++ b/src/renderer/src/components/DownloadBar.tsx @@ -16,7 +16,6 @@ import { VideoClipRegular, MusicNote2Regular, ErrorCircleRegular, - LinkRegular, LibraryRegular, DismissRegular, CutRegular, @@ -30,6 +29,8 @@ import { QUALITY_OPTIONS } from '../qualityOptions' import { Select } from './Select' import { Hint } from './Hint' import { SegmentedControl } from './ui/SegmentedControl' +import { Banner } from './ui/Banner' +import { LinkSuggestion } from './ui/LinkSuggestion' import { useTextStyles } from './ui/text' import { META_SEP } from './ui/tokens' import { DownloadOptionsForm } from './DownloadOptionsForm' @@ -115,60 +116,62 @@ export function DownloadBar(): React.JSX.Element {
{suggestion && ( -
- - - {suggestionSource === 'external' ? 'Link received: ' : 'Use copied link? '} - {suggestion} - - -
+ )} {channelHint && ( -
- - + } + wrap + actions={ + <> + + -
+ )} {dup && ( -
- - Already in your queue: "{dup}". - -
+ } + actions={ + <> + +
{items.length === 0 ? ( -
- - Nothing queued yet. Paste a URL above, then click Download. -
+ } + message="Nothing queued yet. Paste a URL above, then click Download." + /> ) : ( -
-
- -
- No downloads yet. - Finished downloads will show up here. -
+ + +
+ } + message="No downloads yet." + hint="Finished downloads will show up here." + /> ) } diff --git a/src/renderer/src/components/LibraryView.tsx b/src/renderer/src/components/LibraryView.tsx index cb2f7db..7b3e790 100644 --- a/src/renderer/src/components/LibraryView.tsx +++ b/src/renderer/src/components/LibraryView.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useState } from 'react' import { - Body1, Caption1, Text, Input, @@ -23,9 +22,7 @@ import { AppsListRegular, VideoClipMultipleRegular, AlertRegular, - LibraryRegular, - LinkRegular, - DismissRegular + LibraryRegular } from '@fluentui/react-icons' import type { MediaItem, Source, MediaKind } from '@shared/ipc' import { isPreview as PREVIEW } from '../isPreview' @@ -42,6 +39,8 @@ import { VirtualList } from './VirtualList' import { ScreenHeader, useScreenStyles } from './ui/Screen' import { StatusChip } from './ui/StatusChip' import { SegmentedControl } from './ui/SegmentedControl' +import { EmptyState } from './ui/EmptyState' +import { LinkSuggestion } from './ui/LinkSuggestion' import { useFocusStyles } from './ui/focusRing' import { useTextStyles } from './ui/text' import { SPACE, RADIUS, ICON, META_SEP } from './ui/tokens' @@ -78,22 +77,6 @@ const useStyles = makeStyles({ root: { display: 'flex', flexDirection: 'column', gap: SPACE.section }, addRow: { display: 'flex', gap: '8px' }, addInput: { flexGrow: 1 }, - suggestion: { - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '8px 8px 8px 12px', - backgroundColor: tokens.colorBrandBackground2, - color: tokens.colorBrandForeground2, - ...shorthands.borderRadius(tokens.borderRadiusLarge) - }, - suggestionText: { - flexGrow: 1, - minWidth: 0, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' - }, toolbar: { display: 'flex', alignItems: 'center', @@ -116,15 +99,6 @@ const useStyles = makeStyles({ fontSize: tokens.fontSizeBase200 }, error: { color: tokens.colorPaletteRedForeground1 }, - empty: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '8px', - padding: '56px 16px', - color: tokens.colorNeutralForeground3, - textAlign: 'center' - }, list: { display: 'flex', flexDirection: 'column', gap: '10px' }, card: { border: `1px solid ${tokens.colorNeutralStroke2}`, @@ -218,13 +192,7 @@ const useStyles = makeStyles({ // One thumbnail radius app-wide (UI6): control tier / Medium. ...shorthands.borderRadius(RADIUS.control) }, - rowMain: { display: 'flex', flexDirection: 'column', minWidth: 0, flexGrow: 1 }, - rowTitle: { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - color: tokens.colorNeutralForeground1 - } + rowMain: { display: 'flex', flexDirection: 'column', minWidth: 0, flexGrow: 1 } }) /** Group items by playlist, sorted by index within a group; 'Uploads' sinks last. */ @@ -514,7 +482,7 @@ export function LibraryView(): React.JSX.Element { iconSize={16} />
- + {it.playlistIndex}. {it.title} {it.durationLabel && {it.durationLabel}} @@ -555,27 +523,16 @@ export function LibraryView(): React.JSX.Element {
{clip.suggestion && ( -
- - Use copied link? {clip.suggestion} - -
+ { + const link = clip.accept() + if (link) setUrl(link) + }} + onDismiss={clip.dismiss} + dismissLabel="Dismiss suggested link" + /> )}
@@ -620,10 +577,10 @@ export function LibraryView(): React.JSX.Element { {error && {error}} {sources.length === 0 && !indexing.active ? ( -
- - No channels or playlists yet. Paste one above to add it. -
+ } + message="No channels or playlists yet. Paste one above to add it." + /> ) : (
{sources.map((src) => ( diff --git a/src/renderer/src/components/TemplateManager.tsx b/src/renderer/src/components/TemplateManager.tsx index 8082afd..5f64bec 100644 --- a/src/renderer/src/components/TemplateManager.tsx +++ b/src/renderer/src/components/TemplateManager.tsx @@ -21,6 +21,7 @@ import type { CommandTemplate } from '@shared/ipc' import { useTemplates } from '../store/templates' import { newId } from '../id' import { Hint } from './Hint' +import { EmptyState } from './ui/EmptyState' const useStyles = makeStyles({ root: { @@ -71,9 +72,6 @@ const useStyles = makeStyles({ display: 'flex', gap: '8px', justifyContent: 'flex-end' - }, - empty: { - color: tokens.colorNeutralForeground3 } }) @@ -132,7 +130,7 @@ export function TemplateManager(): React.JSX.Element { return (
{templates.length === 0 && !draft && ( - No custom command templates yet. + )} {templates.map((t) => ( diff --git a/src/renderer/src/components/TerminalView.tsx b/src/renderer/src/components/TerminalView.tsx index 424151a..8afefb9 100644 --- a/src/renderer/src/components/TerminalView.tsx +++ b/src/renderer/src/components/TerminalView.tsx @@ -13,6 +13,7 @@ import { PlayRegular, DismissRegular, DeleteRegular } from '@fluentui/react-icon import { useSettings } from '../store/settings' import { newId } from '../id' import { ScreenHeader, useScreenStyles } from './ui/Screen' +import { EmptyState } from './ui/EmptyState' import { SPACE } from './ui/tokens' type LineKind = 'stdout' | 'stderr' | 'cmd' | 'sys' @@ -57,10 +58,12 @@ const useStyles = makeStyles({ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }, + // Center the "no output yet" placeholder in the log box, so it reads as an + // empty state rather than a stray line of text at the top (L117). + logEmpty: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, cmd: { color: tokens.colorBrandForeground1, fontWeight: tokens.fontWeightSemibold }, stderr: { color: tokens.colorPaletteRedForeground1 }, - sys: { color: tokens.colorNeutralForeground3 }, - empty: { color: tokens.colorNeutralForeground3 } + sys: { color: tokens.colorNeutralForeground3 } }) /** @@ -204,9 +207,9 @@ export function TerminalView(): React.JSX.Element {
-
+      
         {lines.length === 0 ? (
-          No output yet.
+          
         ) : (
           lines.map((l) => (
             
diff --git a/src/renderer/src/components/downloadBar/PlaylistPanel.tsx b/src/renderer/src/components/downloadBar/PlaylistPanel.tsx index a8f3d28..666d6f3 100644 --- a/src/renderer/src/components/downloadBar/PlaylistPanel.tsx +++ b/src/renderer/src/components/downloadBar/PlaylistPanel.tsx @@ -1,4 +1,4 @@ -import { Button, Checkbox, Caption1 } from '@fluentui/react-components' +import { Button, Checkbox, Caption1, mergeClasses } from '@fluentui/react-components' import { AppsListRegular, VideoClipRegular, MusicNote2Regular } from '@fluentui/react-icons' import { type PlaylistInfo } from '@shared/ipc' import { type MediaKind } from '../../store/downloads' @@ -35,7 +35,7 @@ export function PlaylistPanel({
- + {playlist.title} {playlist.uploader ? `${META_SEP}${playlist.uploader}` : ''} @@ -61,7 +61,7 @@ export function PlaylistPanel({ onChange={(_, d) => onToggleEntry(e.index, !!d.checked)} label={ - + {e.index}. {e.title} {(e.durationLabel || e.uploader) && ( diff --git a/src/renderer/src/components/downloadBar/styles.ts b/src/renderer/src/components/downloadBar/styles.ts index b7c5acb..06ac073 100644 --- a/src/renderer/src/components/downloadBar/styles.ts +++ b/src/renderer/src/components/downloadBar/styles.ts @@ -23,37 +23,6 @@ export const useDownloadBarStyles = makeStyles({ display: 'flex', gap: '8px' }, - suggestion: { - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '8px 8px 8px 12px', - backgroundColor: tokens.colorBrandBackground2, - color: tokens.colorBrandForeground2, - ...shorthands.borderRadius(tokens.borderRadiusLarge) - }, - suggestionText: { - flexGrow: 1, - minWidth: 0, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' - }, - // The channel/playlist nudge toward the Library (UX3): same brand-tinted banner - // as the link suggestion, but its message wraps instead of truncating. - channelHint: { - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '8px 8px 8px 12px', - backgroundColor: tokens.colorBrandBackground2, - color: tokens.colorBrandForeground2, - ...shorthands.borderRadius(tokens.borderRadiusLarge) - }, - channelHintText: { - flexGrow: 1, - minWidth: 0 - }, url: { flexGrow: 1 }, @@ -128,13 +97,10 @@ export const useDownloadBarStyles = makeStyles({ alignItems: 'center', gap: '8px' }, - plHeaderText: { - flexGrow: 1, - minWidth: 0, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - fontWeight: tokens.fontWeightSemibold + // Just the layout role; the truncation + weight come from the shared text.title + // (L114/L126), merged on at the call site. + plHeaderGrow: { + flexGrow: 1 }, plList: { display: 'flex', @@ -160,21 +126,6 @@ export const useDownloadBarStyles = makeStyles({ flexDirection: 'column', minWidth: 0 }, - plItemTitle: { - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' - }, - // --- duplicate warning --- - dupRow: { - display: 'flex', - alignItems: 'center', - gap: '8px', - padding: '8px 8px 8px 12px', - backgroundColor: tokens.colorStatusWarningBackground1, - color: tokens.colorStatusWarningForeground1, - ...shorthands.borderRadius(tokens.borderRadiusLarge) - }, // --- trim / schedule panels --- trimBlock: { display: 'flex', diff --git a/src/renderer/src/components/settings/DiagnosticsCard.tsx b/src/renderer/src/components/settings/DiagnosticsCard.tsx index 9f5ad6b..20f51ae 100644 --- a/src/renderer/src/components/settings/DiagnosticsCard.tsx +++ b/src/renderer/src/components/settings/DiagnosticsCard.tsx @@ -3,6 +3,7 @@ import { Button, Card, Subtitle2, Caption1, Text } from '@fluentui/react-compone import { BugRegular, CopyRegular, DeleteRegular } from '@fluentui/react-icons' import { useErrorLog } from '../../store/errorlog' import { useErrorTextStyles } from '../ui/errorText' +import { EmptyState } from '../ui/EmptyState' import { useSettingsStyles } from './settingsStyles' export function DiagnosticsCard(): React.JSX.Element { @@ -74,7 +75,7 @@ export function DiagnosticsCard(): React.JSX.Element {
{errorEntries.length === 0 ? ( - No errors yet. + ) : (
{errorEntries.slice(0, 20).map((e) => ( diff --git a/src/renderer/src/components/settings/DownloadsCard.tsx b/src/renderer/src/components/settings/DownloadsCard.tsx index 2c6c5ff..457f2e0 100644 --- a/src/renderer/src/components/settings/DownloadsCard.tsx +++ b/src/renderer/src/components/settings/DownloadsCard.tsx @@ -13,14 +13,9 @@ import { useSettings } from '../../store/settings' import { useDownloads } from '../../store/downloads' import { QUALITY_OPTIONS } from '../../qualityOptions' import { Select } from '../Select' +import { SegmentedControl } from '../ui/SegmentedControl' import { useSettingsStyles } from './settingsStyles' -// Combined "Type -- Quality" options for the default-format dropdown. -const FORMAT_OPTIONS = [ - ...QUALITY_OPTIONS.video.map((q) => ({ value: `video|${q}`, label: `Video -- ${q}` })), - ...QUALITY_OPTIONS.audio.map((q) => ({ value: `audio|${q}`, label: `Audio -- ${q}` })) -] - export function DownloadsCard(): React.JSX.Element { const styles = useSettingsStyles() const videoDir = useSettings((s) => s.videoDir) @@ -38,16 +33,14 @@ export function DownloadsCard(): React.JSX.Element { const update = useSettings((s) => s.update) const formatQuality = defaultKind === 'audio' ? defaultAudioQuality : defaultVideoQuality - const formatValue = `${defaultKind}|${formatQuality}` - function onFormatSelect(value: string): void { - const sep = value.indexOf('|') - const kind: MediaKind = sep >= 0 && value.slice(0, sep) === 'audio' ? 'audio' : 'video' - const quality = sep >= 0 ? value.slice(sep + 1) : value + function onKindSelect(kind: MediaKind): void { + update({ defaultKind: kind }) + } + + function onQualitySelect(quality: string): void { update( - kind === 'audio' - ? { defaultKind: 'audio', defaultAudioQuality: quality } - : { defaultKind: 'video', defaultVideoQuality: quality } + defaultKind === 'audio' ? { defaultAudioQuality: quality } : { defaultVideoQuality: quality } ) } @@ -105,7 +98,23 @@ export function DownloadsCard(): React.JSX.Element { - ({ value: q, label: q }))} + onChange={onQualitySelect} + /> +
+ {icon} +
{children}
+ {actions} +
+ ) +} diff --git a/src/renderer/src/components/ui/EmptyState.tsx b/src/renderer/src/components/ui/EmptyState.tsx new file mode 100644 index 0000000..6091b09 --- /dev/null +++ b/src/renderer/src/components/ui/EmptyState.tsx @@ -0,0 +1,57 @@ +import { Body1, Caption1, makeStyles, mergeClasses, tokens } from '@fluentui/react-components' +import { SPACE } from './tokens' + +/** + * One shared empty-state block (L116/L117). The screens had drifted into three + * shapes — Downloads (icon + line), History (colored badge + line + sub-hint), + * Library (icon + line, no sub-hint) — and the Terminal / Diagnostics "empty" + * text wasn't a centered block at all (inline text). This centers them all on one + * structure: an optional focal `icon` (a hero glyph or a colored badge), a `Body1` + * message at normal foreground, and an optional muted `hint` line. + * + * `compact` swaps the tall screen-level padding (56px) for a smaller inset (32px), + * for placeholders that sit inside an already-populated screen (the Terminal log, + * the Diagnostics list) rather than filling an empty one. + */ +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: SPACE.tight, + padding: '56px 16px', + // The container is muted, so a hero glyph and the hint line read as secondary; + // the message overrides back to the normal foreground so it stays legible. + color: tokens.colorNeutralForeground3, + textAlign: 'center' + }, + compact: { + padding: '32px 16px' + }, + message: { + color: tokens.colorNeutralForeground1 + } +}) + +export function EmptyState({ + icon, + message, + hint, + compact = false, + className +}: { + icon?: React.ReactNode + message: React.ReactNode + hint?: React.ReactNode + compact?: boolean + className?: string +}): React.JSX.Element { + const styles = useStyles() + return ( +
+ {icon} + {message} + {hint && {hint}} +
+ ) +} diff --git a/src/renderer/src/components/ui/LinkSuggestion.tsx b/src/renderer/src/components/ui/LinkSuggestion.tsx new file mode 100644 index 0000000..5d45441 --- /dev/null +++ b/src/renderer/src/components/ui/LinkSuggestion.tsx @@ -0,0 +1,50 @@ +import { Button, Caption1 } from '@fluentui/react-components' +import { LinkRegular, DismissRegular } from '@fluentui/react-icons' +import { Banner } from './Banner' + +/** + * The "use this copied/received link?" banner (UI19), shared by the DownloadBar + * and the Library add-source field, which had identical markup + CSS. Renders the + * link (with an optional lead-in like "Link received: ") and Use / Dismiss actions + * on the shared {@link Banner}. + */ +export function LinkSuggestion({ + prefix, + link, + onAccept, + onDismiss, + dismissLabel +}: { + /** Short lead-in before the link, e.g. "Use copied link? " or "Link received: ". */ + prefix: string + link: string + onAccept: () => void + onDismiss: () => void + /** Accessible name for the dismiss button — say what's being dismissed (L153). */ + dismissLabel: string +}): React.JSX.Element { + return ( + } + actions={ + <> + +