Pattern gallery

Typography composites

Two or more type roles meeting on one screen: a title stack, a label/value grid, a table header against its cells, an identity row, message text, prose in a card, and the four title levels in situ.

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.

Use it when
  • 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

3 files · Scrape · AC Delco Q3 taxonomy

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

PartSKUQty
Brake rotor, ventedWM-48213-A24
Brake pad set, ceramicWM-48214-B156
Caliper bracketWM-48215-C8

Identity row

AKAakarsh Choprausr_48213
active

Message text

One-time secret
This value shows once. Copy it now; it cannot be retrieved again.

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 reviewer

Context

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

12 running across 3 modules

Workspace

Everything scoped to this job's setup

Files

  • 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
patterns/typography-composites/example.tsxCopy this into a fresh route and it renders as above.
"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 meet
  • speedway/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.

@versable-git/ui · composites proven in the apps