feat(ui): Skeleton, ErrorState, FilterChips, Tabs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/client-detail-redesign
parent
b767a0c191
commit
c299aea18c
@ -0,0 +1,27 @@
|
||||
/** Saved-views-lite: a pill row that filters a list by one dimension. */
|
||||
|
||||
export interface ChipDef { key: string; label: string; count?: number }
|
||||
|
||||
export function FilterChips(props: {
|
||||
chips: ChipDef[]
|
||||
active: string
|
||||
onChange: (key: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="wf-chips" role="tablist">
|
||||
{props.chips.map((c) => (
|
||||
<button
|
||||
key={c.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={c.key === props.active}
|
||||
className={`wf-chip${c.key === props.active ? ' active' : ''}`}
|
||||
onClick={() => props.onChange(c.key)}
|
||||
>
|
||||
{c.label}
|
||||
{c.count !== undefined && <span className="count">{c.count}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
/** Loading + error surfaces — every page swaps its "Loading…" text for these. */
|
||||
|
||||
const WIDTHS = ['92%', '70%', '84%', '55%', '78%']
|
||||
|
||||
export function Skeleton(props: { rows?: number }) {
|
||||
const rows = props.rows ?? 3
|
||||
return (
|
||||
<div className="wf-skel" aria-busy="true" aria-label="Loading">
|
||||
{Array.from({ length: rows }, (_x, i) => (
|
||||
<div key={i} className="wf-skel-bar" style={{ width: WIDTHS[i % WIDTHS.length] }} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ErrorState(props: { message: string; onRetry?: () => void }) {
|
||||
return (
|
||||
<div className="wf-error" role="alert">
|
||||
<span className="msg">{props.message}</span>
|
||||
{props.onRetry !== undefined && (
|
||||
<button type="button" className="wf" onClick={props.onRetry}>Try again</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
/** Underline tabs with optional mono counts (Direction A v2 pattern). */
|
||||
|
||||
export interface TabDef { key: string; label: string; count?: number }
|
||||
|
||||
export function Tabs(props: {
|
||||
tabs: TabDef[]
|
||||
active: string
|
||||
onChange: (key: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="wf-tabs" role="tablist">
|
||||
{props.tabs.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={t.key === props.active}
|
||||
className={`wf-tab${t.key === props.active ? ' active' : ''}`}
|
||||
onClick={() => props.onChange(t.key)}
|
||||
>
|
||||
{t.label}
|
||||
{t.count !== undefined && <span className="count">{t.count}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue