Typography composites
What this solves
Information. The typography ui page shows one role at a time; a real screen never renders a role alone. This page holds the seven shapes where two or more roles meet and have to agree with each other, so a builder sees the pairing before inventing a new one.
- A page header needs an eyebrow above itthe landing sections' Kicker over h2 (landing-sections.tsx:56-58)
- A settings or summary panel is too sparse for a tablespeedway's admin team usage dl grid (admin/team.tsx:396)
- A row needs a name, an id, and a state on one linean account menu or a member list row
Rendered
Fixture data; check both themes.Title stack
Job
Brake pads Q3
Label and value pairs
Workspace
A settings row, the same dl shape as admin/team.tsx:396- Workspace
- AC Delco
- Owner
- aakarsh@versable.ai
- Plan
- Team
- Created
- 2026-06-02
Table header versus cell
| Part | SKU | Qty |
|---|---|---|
| Brake rotor, vented | WM-48213-A | 24 |
| Brake pad set, ceramic | WM-48214-B | 156 |
| Caliper bracket | WM-48215-C | 8 |
Identity row
Message text
No parts yet
Upload a source file to get started.
Alert's title is font-semibold at the alert's own text-sm (alert.tsx:66,76). EmptyState (compact) runs its title at text-sm and its hint at text-xs (page-info.tsx:10,92-97). The toast keeps daisy's own alert body size, no Tailwind override in the kit (toast.tsx:81-83).
Prose in a card
Fitment note
Attached by the reviewerContext
This part fits the 2024-2026 range only. Earlier model years use a different bolt pattern and should not be mapped to this SKU without a manual check.
Added by the review team, 2026-08-12.
Four title levels, in situ
Jobs
Workspace
Everything scoped to this job's setupFiles
- brake-pads-q3.xlsxIngested
- fitment-update.csvMapping
Preview
A tile label reads as the frame's own name, not a page-level opener.
Rows
3 in this job- brake-pads-q3.xlsxIngested
- fitment-update.csvMapping
- lighting-full.xmlQueued
"use client";import { Alert, Button, Card, EmptyState, IdentityRow, Kicker, PageTitle, StatusPill, pushAlert,} from "@versable-git/ui";// Seven composites, each two or more type roles meeting on one shipping// screen (doc 60 owns them here, not the ui page, so a pairing appears// exactly once in the showcase).function TitleStack() { return ( <div className="flex flex-col gap-2"> <Kicker>Job</Kicker> <PageTitle Icon="Refresh" title="Brake pads Q3" subtitle="3 files · Scrape · AC Delco Q3 taxonomy" noAnimate /> </div> );}// The dl-as-stat-list canon doc 5 §A8 names: a two-column dl grid for// label/value summaries inside a card when a table is too heavy, the shape// speedway's admin team page uses (admin/team.tsx:396).function LabelValuePairs() { const rows: { label: string; value: string }[] = [ { label: "Workspace", value: "AC Delco" }, { label: "Owner", value: "aakarsh@versable.ai" }, { label: "Plan", value: "Team" }, { label: "Created", value: "2026-06-02" }, ]; return ( <Card title="Workspace" subtitle="A settings row, the same dl shape as admin/team.tsx:396" compact noAnimate> <dl className="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-[minmax(0,1fr)_auto]"> {rows.map((r) => ( <div key={r.label} className="contents"> <dt className="text-base-content/65 text-sm">{r.label}</dt> <dd className="text-base-content text-sm font-medium">{r.value}</dd> </div> ))} </dl> </Card> );}// Header case and weight from table.tsx:151-157 (Title Case per canon doc 06,// text-xs font-medium, /65 ink): the shouty tiny-caps header died 2026-07-10.// Cell ids are mono, numerals are right-aligned and tabular.function TableHeaderVsCell() { const rows = [ { part: "Brake rotor, vented", sku: "WM-48213-A", qty: 24 }, { part: "Brake pad set, ceramic", sku: "WM-48214-B", qty: 156 }, { part: "Caliper bracket", sku: "WM-48215-C", qty: 8 }, ]; return ( <div className="border-base-300 overflow-hidden rounded-lg border"> <table className="w-full border-collapse"> <thead> <tr> <th className="bg-base-200 border-base-300 border-b px-3 py-2 text-left text-xs font-medium text-base-content/65"> Part </th> <th className="bg-base-200 border-base-300 border-b px-3 py-2 text-left text-xs font-medium text-base-content/65"> SKU </th> <th className="bg-base-200 border-base-300 border-b px-3 py-2 text-right text-xs font-medium text-base-content/65"> Qty </th> </tr> </thead> <tbody> {rows.map((r) => ( <tr key={r.sku} className="border-base-300 border-b last:border-0"> <td className="px-3 py-2 text-sm">{r.part}</td> <td className="px-3 py-2 font-mono text-xs">{r.sku}</td> <td className="px-3 py-2 text-right font-mono text-sm tabular-nums">{r.qty}</td> </tr> ))} </tbody> </table> </div> );}function IdentityRowComposite() { return ( <div className="border-base-300 flex items-center justify-between gap-3 rounded-lg border p-2"> <IdentityRow avatar="AK" title="Aakarsh Chopra" subtitle={<span className="font-mono">usr_48213</span>} /> <StatusPill kind="ok" size="sm" dot> active </StatusPill> </div> );}// Alert title over body, EmptyState title over hint, and a live toast, so// the size steps between the three read side by side rather than described.function MessageText() { return ( <div className="flex flex-col gap-4"> <Alert tone="warning" title="One-time secret"> This value shows once. Copy it now; it cannot be retrieved again. </Alert> <div className="border-base-300 rounded-lg border"> <EmptyState compact Icon="CloudUpload" title="No parts yet" description="Upload a source file to get started." /> </div> <div> <Button size="sm" variant="outline" Icon="Success" content="Fire a toast" onClick={() => { pushAlert.success("Row part_h83k2m saved"); }} /> <p className="text-base-content/65 mt-2 text-xs"> Alert's title is font-semibold at the alert's own text-sm (alert.tsx:66,76). EmptyState (compact) runs its title at text-sm and its hint at text-xs (page-info.tsx:10,92-97). The toast keeps daisy's own alert body size, no Tailwind override in the kit (toast.tsx:81-83). </p> </div> </div> );}function ProseInCard() { return ( <Card title="Fitment note" subtitle="Attached by the reviewer" noAnimate> <div className="flex flex-col gap-3"> <Kicker>Context</Kicker> <p className="text-base-content text-base"> This part fits the 2024-2026 range only. Earlier model years use a different bolt pattern and should not be mapped to this SKU without a manual check. </p> <p className="text-base-content/65 text-sm">Added by the review team, 2026-08-12.</p> </div> </Card> );}// The four title levels plus the label, rendered in situ rather than// described: PageTitle above a Card whose body holds two Kicker-opened// groups, one being a nested Card with titleDivider.function FourLevelsInSitu() { return ( <div className="flex flex-col gap-4"> <PageTitle Icon="Dashboard" title="Jobs" subtitle="12 running across 3 modules" noAnimate /> <Card title="Workspace" subtitle="Everything scoped to this job's setup" noAnimate> <div className="flex flex-col gap-4"> <div className="flex flex-col gap-2"> <Kicker as="h3">Files</Kicker> <ul className="flex flex-col gap-1"> <li className="flex justify-between text-sm"> <span className="truncate">brake-pads-q3.xlsx</span> <span className="text-base-content/65">Ingested</span> </li> <li className="flex justify-between text-sm"> <span className="truncate">fitment-update.csv</span> <span className="text-base-content/65">Mapping</span> </li> </ul> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Preview</Kicker> <div className="border-base-300 rounded-lg border p-2"> <p className="text-base-content/65 text-xs">A tile label reads as the frame's own name, not a page-level opener.</p> </div> </div> <Card title="Rows" subtitle="3 in this job" titleDivider compact noAnimate> <ul className="flex flex-col gap-2"> <li className="flex justify-between text-sm"> <span className="truncate">brake-pads-q3.xlsx</span> <span className="text-base-content/65">Ingested</span> </li> <li className="flex justify-between text-sm"> <span className="truncate">fitment-update.csv</span> <span className="text-base-content/65">Mapping</span> </li> <li className="flex justify-between text-sm"> <span className="truncate">lighting-full.xml</span> <span className="text-base-content/65">Queued</span> </li> </ul> </Card> </div> </Card> </div> );}export function TypographyComposites() { return ( <div className="flex flex-col gap-8"> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Title stack</Kicker> <TitleStack /> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Label and value pairs</Kicker> <LabelValuePairs /> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Table header versus cell</Kicker> <TableHeaderVsCell /> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Identity row</Kicker> <IdentityRowComposite /> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Message text</Kicker> <MessageText /> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Prose in a card</Kicker> <ProseInCard /> </div> <div className="flex flex-col gap-2"> <Kicker variant="bricked">Four title levels, in situ</Kicker> <FourLevelsInSitu /> </div> </div> );}Where it ships
docs/plan/60-typography-structure.md:24the ruling that put composites here, not on the ui page: the ui page shows one role at a time, this page shows what happens where roles meetspeedway/app/routes/admin/team.tsx:396the dl-as-stat-list shape the label/value composite follows (canon doc 5 §A8)speedway/app/components/JobsTable.tsx:141-212the StatusPill-in-a-row shape the identity row and title stack composites draw on
App-specific: Real screens carry live data, loading and error states, and click handlers on the rows; the fixtures here are static so the type relationships stay the whole story.