Cards & data
Card is the base surface everything else builds on — Stat and MiniStat wrap it for numbers. Tables (Table/TableRow/TableCell/GroupedGrid) have their own dedicated page.
Card
zeyo-ui/card — the base surface. hover adds a subtle border transition, used for anything clickable. padding and gap (md, sm, xs, lg, or none) lay children out as a padded column; clip rounds the corners over the children and lets the card shrink inside a flex/grid parent, for a card that holds its own scrolling list or thread. loading adds the reload ring and glow (see Card loading below).
Plain content, any children.
hover adds a border transition on :hover.
<Card>
<Text size="sm">Plain content, any children.</Text>
</Card>
<Card hover>
<Text size="sm">hover adds a border transition on :hover.</Text>
</Card>Card parts
zeyo-ui/card — the parts of a card that holds its own scrolling region (use clip and gap='none' on the Card). CardHeader and CardFooter are pinned strips with a divider; CardBody takes the space left and scrolls, and busy frosts it (blur + dim, via the .frosted class) while it reloads. padding takes the same scale as Card (xs, sm, md, lg); CardHeader and CardFooter default to sm, CardBody to none.
Header
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
Row 7
Row 8
<Card clip gap="none">
<CardHeader>Header</CardHeader>
<CardBody padding="sm" busy={loading}>…scrolls…</CardBody>
<CardFooter>Footer</CardFooter>
</Card>Card loading
Loading goes on the content that is about to change, never on the controls that triggered it. Card's loading draws a spinning gradient ring on its border and a glow leaking outside it (a parent that clips needs ~12px of room around the card for the glow). CardBody's busy frosts the old rows — the .frosted class from base.css, usable on any element with aria-busy — and the new rows fade up with the .fi / .dN classes. Drive all of it from pendingFlag(), which only switches on after 150ms, so a fast reload shows nothing. Press Reload.
Results
Result 1
Result 2
Result 3
Result 4
Result 5
Result 6
Result 7
Result 8
const loading = pendingFlag(); // zeyo-ui/boundary/pending-flag — on only after 150ms
<Card clip gap="none" loading={loading.current}>
<CardHeader>…</CardHeader>
<CardBody busy={loading.current} padding="sm">
{#each rows as row, i (row.id)}
<div class="fi d{Math.min(i, 5)}">…</div>
{/each}
</CardBody>
</Card>List row
zeyo-ui/list-row — a clickable row for a list inside a card. selected marks the current row with a bar on its left edge (and sets aria-current); muted tints the background so the other rows stand out, e.g. read vs unread. leading is a slot before the content, outside the click target, for an icon, a status mark or a control such as a checkbox; what goes in the row stays with the caller.
<ListRow muted onclick={open} selected>
{#snippet leading()}<Mail size={13} />{/snippet}
<Text size="sm">Subject line</Text>
</ListRow>Stat
A Card preset for a single number — label / value / optional sub, all snippets so a label can hold an icon or richer markup. delta adds an up/down/neutral change badge (owns its own color coding); chart is any small visual under the value, a sparkline typically. The plain numerals variant swaps the DM Sans display face for the body sans at a slightly smaller size, for dense metric grids.
<Stat>
{#snippet label()}Total sent{/snippet}
{#snippet value()}48,204{/snippet}
{#snippet sub()}+12% this week{/snippet}
</Stat>
<!-- delta owns its own up/down/neutral color coding -->
<Stat delta={{ dir: 'up', pct: '+18%' }}>
{#snippet label()}Credits{/snippet}
{#snippet value()}4,820{/snippet}
</Stat>
<Stat delta={{ dir: 'down', pct: '-4%' }}>
{#snippet label()}Bounced{/snippet}
{#snippet value()}212{/snippet}
</Stat>
<Stat delta={{ dir: 'neutral', pct: '—' }}>
{#snippet label()}Reply rate{/snippet}
{#snippet value()}—{/snippet}
</Stat>
<!-- chart is any small visual, a sparkline typically -->
<Stat delta={{ dir: 'up', pct: '+6%' }}>
{#snippet label()}Active campaigns{/snippet}
{#snippet value()}12{/snippet}
{#snippet chart()}
<svg height="22" preserveAspectRatio="none" style="display:block" viewBox="0 0 64 20" width="100%">
<path d={sparkPath} fill="none" stroke="var(--tx)" stroke-linecap="round" stroke-opacity="0.55" stroke-width="1.25" />
</svg>
{/snippet}
</Stat>
<!-- numerals="plain" drops the DM Sans display face for the body sans -->
<Stat numerals="plain">
{#snippet label()}Open rate{/snippet}
{#snippet value()}38.4%{/snippet}
{#snippet sub()}18,504 opens{/snippet}
</Stat>MiniStat
A lightweight inline alternative to Stat for dense breakdowns where a full card wastes space.
<MiniStat dot="var(--ok)" label="delivered" value={4820} />
<MiniStat dot="var(--err)" label="bounced" value={212} />
<MiniStat label="pending" value={38} />DistributionBar
Pairs Bar with a MiniStat legend — one segment shape (color/label/value) drives both the proportional bar and the dot-labeled breakdown underneath. Zero-value segments are dropped automatically; if every segment is 0, emptyLabel renders instead of an empty bar.
<DistributionBar
segments={[
{ color: 'var(--ok)', label: 'valid', value: 142 },
{ color: 'var(--w)', label: 'risky', value: 38 },
{ color: 'var(--inf)', label: 'catch-all', value: 21 },
{ color: 'var(--err)', label: 'invalid', value: 14 },
]}
/>
<!-- every segment at 0 (or an empty array) falls back to emptyLabel -->
<DistributionBar emptyLabel="Not verified" segments={[]} />PercentageRing
A bare ring — dimmed track plus a filled arc, formatted text centered inside. Four size presets.
<PercentageRing color="var(--acc)" percentage={toPercentage(72)} size="medium" />FAQList
zeyo-ui/faq — an accordion list of question/answer pairs, one open at a time. Renders only the list; the page brings its own heading.
<FAQList
items={[
{ question: 'Is there a free plan?', answer: 'Yes — sign up and you start with free credits.' },
{ question: 'Can two answers be open?', answer: 'No, opening one closes the other.' }
]}
/>