Component & design-token showcase

Layout

The spacing scale and layout primitives every page should build from — instead of a new one-off flex flex-col gap-3.5 px-4.5 every time. zeyo-ui/layout/*, plus a padding prop on Card.

Every page currently reimplements the same handful of shapes slightly differently — a page max-width wrapper, a filter/toolbar row, a stat-card grid, a sidebar+content split — each with its own hand-picked gap and its own breakpoint. These primitives are that shared vocabulary.
Spacing scale

zeyo-ui/spacing.css — plain CSS custom properties (deliberately not a Tailwind @theme key — see the file's own comment for why). xs-xl cover component-internal gaps/padding; 2xl-4xl are the section-band tier, for a full-bleed page section's own vertical padding (Section's `padding` prop below), a range component gaps never need to reach. Reach for these before a new arbitrary value; layout-only Tailwind spacing is still fine where none of these fit.

xs
8px
sm
12px
md
20px
lg
32px
xl
48px
2xl
64px
3xl
96px
4xl
128px
Code
--spacing-xs: calc(var(--spacing) * 2);   /* 8px */
--spacing-sm: calc(var(--spacing) * 3);   /* 12px */
--spacing-md: calc(var(--spacing) * 5);   /* 20px */
--spacing-lg: calc(var(--spacing) * 8);   /* 32px */
--spacing-xl: calc(var(--spacing) * 12);  /* 48px */
--spacing-2xl: calc(var(--spacing) * 16); /* 64px */
--spacing-3xl: calc(var(--spacing) * 24); /* 96px */
--spacing-4xl: calc(var(--spacing) * 32); /* 128px */
Shell

The page max-width wrapper every top-level route renders inside — variant='full' skips the cap entirely for edge-to-edge shells, and variant='fill' is a padded column exactly the height of the viewport under the TopBar, for screens like the Unibox whose panes scroll inside themselves.

variant="wide" (1200px)

Shell content sits here, centered, max-width: 1200px

variant="narrow" (720px)

max-width: 720px
Code
<Shell variant="wide">   <!-- max-width: 1200px -->
  ...
</Shell>
<Shell variant="narrow"> <!-- max-width: 720px -->
  ...
</Shell>
<Shell variant="fill">   <!-- viewport height minus the TopBar -->
  ...
</Shell>
Section — spacing

A vertical block with a semantic bottom margin — the space between one section of a page and the next.

spacing="sm" (12px below)
spacing="md" (20px below)
spacing="lg" (32px below)
(next block)
Code
<Section spacing="sm">...</Section>
<Section spacing="md">...</Section>
<Section spacing="lg">...</Section>
Section — padding + bg (page bands)

padding + bg turn Section into a full-bleed page band — the colored, padded section every landing page reimplements slightly differently (background: X; padding: Npx var(--spacing-xl);) — instead of a new hand-written CSS rule per section. padding sets top/bottom via the section-band tier (2xl-4xl reach here, xs-xl don't); left/right is always var(--spacing-xl), dropping to var(--spacing-md) under 700px — that half doesn't vary section to section, only the vertical number does. Omit padding for the plain spacer-only behavior above.

bg="tertiary" padding="2xl"

Code
<Section bg="tertiary" padding="4xl">
  <div class="page-inner">...</div>
</Section>
Section — contentWidth + centered

contentWidth caps + centers children in an inner max-width/margin:0 auto wrapper — the div every full-bleed section band needs when its content shouldn't stretch the full width. centered adds text-align: center on the section itself, cascading down. Both replace a page-local max-width/margin/text-align class on the section's own wrapper div.

Capped + centered

Full-bleed background, capped/centered content.

Code
<Section bg="sf" padding="lg" contentWidth="360px" centered>
  <Title level={5}>Capped + centered</Title>
  <Text color="secondary" size="sm">Full-bleed background, capped/centered content.</Text>
</Section>
Section — ref (DOM binding)

ref is a bindable DOM ref — bind:ref gets the real section element, same as bind:this on a plain element. This is what unblocks a Section that also needs an IntersectionObserver/scroll target, without falling back to a hand-written section.

bind:ref={section} points at this element

Code
let section: HTMLElement | null = $state(null);
</script>

<Section bind:ref={section} bg="tertiary" padding="lg">...</Section>
<button onclick={() => section?.scrollIntoView({ behavior: 'smooth' })}>Scroll to it</button>
Stack

Vertical flex list with a scale-backed gap — for any stacked column of rows/cards. gap='none' is for a dense list whose rows are meant to sit flush, relying on each row's own border-bottom for separation instead of a visible gap between them.

Row one
Row two
Row three
Code
<Stack gap="sm">
  <div>Row one</div>
  <div>Row two</div>
  <div>Row three</div>
</Stack>

<!-- gap="none" — flush rows, separated only by their own border -->
<Stack gap="none">
  <div class="list-row">Row one</div>
  <div class="list-row">Row two</div>
</Stack>
Cluster

Horizontal, wrapping flex row with a scale-backed gap — the filter/toolbar row above a table (resize the window to see it wrap). nowrap keeps the items on one line instead, for a row that truncates one of them, such as a list row's header.

Filter 1
Filter 2
Filter 3
Filter 4
Filter 5
Filter 6
Filter 7
Filter 8
Code
<Cluster gap="sm">
  <Badge color="info">Active</Badge>
  <Badge color="warning">Paused</Badge>
  <Badge color="success">Completed</Badge>
</Cluster>
<Cluster gap="xs" nowrap><!-- one line: a truncating item + badges --></Cluster>
Grid

One component for fixed-column stat grids AND two-column splits. minColumnWidth uses auto-fit/minmax so the column count adapts continuously to the available width — no breakpoint to hand-pick. stack collapses a fixed columns split to 1fr below 960px — Grid sets its columns via a style: directive (an inline style), which no external stylesheet can override without !important, so that override lives inside Grid itself instead of every caller hand-rolling a page-local :global(.grid-layout) media query with !important. fill is for a grid inside a flex column (like a Shell variant='fill'): it takes the space left and lets the rows shrink to it, so cards that hold their own scrolling list scroll inside the grid instead of stretching it.

columns={4}

Total sent
Reply rate
Bounced
Opted out

columns="2fr 1fr"

2fr
1fr

minColumnWidth="120px" — resize the window

Card 1
Card 2
Card 3
Card 4
Card 5
Card 6

columns="1fr 300px" stack — shrink the browser below 960px

1fr
300px, collapses below 960px
Code
<Grid columns={4} gap="md">
  <Stat>...</Stat>
  <Stat>...</Stat>
  <Stat>...</Stat>
  <Stat>...</Stat>
</Grid>

<Grid columns="2fr 1fr" gap="md"><!-- two-column split --></Grid>

<Grid minColumnWidth="140px" gap="sm"><!-- responsive, no breakpoint to pick --></Grid>

<Grid columns="1fr 300px" gap="md" stack><!-- collapses to 1 column below 960px --></Grid>
<Grid columns="1fr 2fr" fill><!-- fills a flex column; cards inside scroll, not stretch --></Grid>
SplitShell

Sidebar+content app shell — one shared collapse point (1024px) instead of every page picking its own breakpoint. Shrink the window below that to see it stack.

Content panel
Code
<SplitShell sidebarWidth={200}>
  {#snippet sidebar()}...{/snippet}
  {#snippet content()}...{/snippet}
</SplitShell>
Card padding

Card's own padding prop — reach for this before a hand-picked p-5 or an arbitrary py-[22px] px-6.

padding="sm"
padding="md"
padding="lg"
Code
<Card padding="sm">...</Card>
<Card padding="md">...</Card>
<Card padding="lg">...</Card>
bg — shared across every layout primitive

Every layout primitive (Card, Stack, Cluster, Grid, Section) takes the same bg prop — a surface token (bg/sf/sfh/tertiary, mapped to the matching CSS var) or 'rainbow', the animated gradient panel used for feature/product-UI showcase blocks (previously hand-copied across 9+ marketing files). Omit for transparent, the historical default for every one of these.

sf
sfh
tertiary
rainbow
Code
<Card bg="tertiary" padding="md">...</Card>
<Stack bg="sf" gap="sm">...</Stack>
<Cluster bg="sfh" gap="sm">...</Cluster>
<Grid bg="tertiary" columns={2} gap="sm">...</Grid>
<Section bg="rainbow" padding="lg">...</Section>
class — conditional/dynamic classes

class is layout-only, same rule as Button/Text — margin, or (as here) a conditional class computed by the caller for state the primitive itself has no prop for. Cluster/Stack/Grid/Section always render their own base class too (cluster/stack/grid-layout/nothing) — yours is appended, not a replacement. This is the escape hatch for a highlighted/active row in a list, without going back to a hand-written flex div.

Row one
Row two
Row three
Code
<Cluster class={active === i ? 'row-active' : ''} gap="sm">
  Row {i}
</Cluster>