Files
AeroFetch/src/renderer/src/components/ui/EmptyState.tsx
T
debont80 3d4e574916 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>
2026-07-01 16:37:32 -04:00

58 lines
1.8 KiB
TypeScript

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 (
<div className={mergeClasses(styles.root, compact && styles.compact, className)}>
{icon}
<Body1 className={styles.message}>{message}</Body1>
{hint && <Caption1>{hint}</Caption1>}
</div>
)
}