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