Component Roadmap
The single source of truth for primitives we plan to add to @na/ui but haven’t shipped yet.
Read this before starting work on any new primitive. Update the checklist as you go — a spec without a checklist becomes folklore; a checklist without a spec becomes drift.
How this page works
- Status legend and summary table are the at-a-glance view.
- Each primitive below has: Purpose · When NOT · Anatomy · States · Tech · A11y · Do / Don’t · Progress.
- The Progress block is a literal markdown checklist. Tick items in the same PR that completes them. The PR description must reference the specific checklist line.
- When all boxes are ticked + Spec migrated to
04-components.md, move the entry to the bottom Shipped log and delete it from this page on the next quarterly cleanup.
Status legend
| Symbol | Meaning |
|---|---|
| 📝 | Spec drafted. Anatomy + API agreed. Nothing built yet. |
| 🛠 | Building. Branch open, partial component, not ready for consumers. |
| 🚧 | Blocked. Waiting on dep / decision / upstream lib. Note the blocker. |
| ✅ | Shipped. Exported from @na/ui, story in Storybook, spec in 04-components.md. |
| 🧊 | Parked. Promoted out of roadmap; rationale recorded. |
Summary
| Tier | Primitive | Status | Owner | Story | Spec link |
|---|---|---|---|---|---|
| 1 | Calendar / DatePicker | ✅ | huypq6 | Data Entry/DatePicker | shipped |
| 1 | DateRangePicker | ✅ | huypq6 | Data Entry/DatePicker | shipped |
| 1 | Combobox / Autocomplete | ✅ | huypq6 | Data Entry/Combobox | shipped |
| 1 | Command Palette (⌘K) | ✅ | huypq6 | Navigation/CommandPalette | shipped |
| 1 | Stepper / Wizard | ✅ | huypq6 | Navigation/Stepper | shipped |
| 1 | Timeline / ActivityFeed | ✅ | huypq6 | Data Display/Timeline | shipped |
| 1 | Dropzone / FileUpload | ✅ | huypq6 | Data Entry/Dropzone | shipped |
| 1 | SegmentedControl | ✅ | huypq6 | Data Entry/SegmentedControl | shipped |
| 1 | AutosizeTextarea | ✅ | huypq6 | (extends Data Entry/Textarea) | shipped |
| 1 | OTPInput | ✅ | huypq6 | Data Entry/InputOTP | shipped |
| 1 | Banner | ✅ | huypq6 | Feedback/Banner | shipped |
| 1 | Sparkline | 🧊 | — | — | parked (Recharts deferred) |
| 1 | DiffViewer | ✅ | huypq6 | Data Display/DiffViewer | shipped |
| 2 | Chart | 🧊 | — | — | parked (Recharts deferred) |
| 2 | Timeline pagination | ✅ | huypq6 | (extends Data Display/Timeline) | shipped |
| 2 | DataGroup | ✅ | huypq6 | Data Display/DataGroup | shipped |
| 2 | CountdownChip | ✅ | huypq6 | Feedback/CountdownChip | shipped |
| 2 | RadioCardGroup | ✅ | huypq6 | Data Entry/RadioCardGroup | shipped |
| 2 | MaskedInput / NumericInput | ✅ | huypq6 | Data Entry/MaskedInput | shipped |
| 2 | NumberInput (stepper) | ✅ | huypq6 | Data Entry/NumberInput | shipped |
| 2 | Fieldset | ✅ | huypq6 | Data Entry/Fieldset | shipped |
Component pages migration
Tracks the rollout of /components/<slug>/ pages — the live-preview docs portal — across the already-shipped primitives in @na/ui. Distinct from the new primitives tracker above.
For each primitive: 📝 = no page yet, 🛠 = page being authored, ✅ = page live with full preview + API + design rules. Author against the authoring guide and the template (apps/docs/src/templates/component-page.mdx).
Tier A — Core primitives (visual, low coupling, fast wins)
| Status | Primitive | Slug | Notes |
|---|---|---|---|
| ✅ | Button | /components/button/ | canonical template |
| ✅ | Badge | /components/badge/ | variants + with-icon + asChild link |
| ✅ | StatusBadge | /components/status-badge/ | status → variant map documented |
| ✅ | Alert | /components/alert/ | default + destructive + with-action |
| ✅ | EmptyState | /components/empty-state/ | all 4 variants live |
| ✅ | Tooltip | /components/tooltip/ | basic + icon-only + sides |
| ✅ | Avatar | /components/avatar/ | sizes + image-fallback + badge + group |
| ✅ | Separator | /components/separator/ | horizontal + vertical |
| ✅ | Skeleton | /components/skeleton/ | shapes + card placeholder |
| ✅ | Loader | /components/loader/ | overlay frame |
| ✅ | Progress | /components/progress/ | values + with label |
| ✅ | Card | /components/card/ | header/title/desc/action/content/footer + CardGrid |
| ✅ | Typography | /components/typography/ | H1–H6 + Text variants + as/asChild |
Tier B — Forms + interactive primitives
| Status | Primitive | Slug | Notes |
|---|---|---|---|
| ✅ | Input | /components/input/ | types + invalid + read-only |
| ✅ | Label | /components/label/ | htmlFor + wrapping |
| ✅ | Textarea | /components/textarea/ | autosize prop shipped |
| ✅ | PasswordInput | /components/password-input/ | reveal toggle documented |
| ✅ | Checkbox | /components/checkbox/ | + indeterminate |
| ✅ | Switch | /components/switch/ | sm + default sizes |
| ✅ | RadioGroup | /components/radio-group/ | basic + with descriptions |
| ✅ | Select | /components/select/ | groups + disabled |
| ✅ | Slider | /components/slider/ | single + range + valuetext |
| ✅ | Form | /components/form/ | full react-hook-form wiring |
| ✅ | FormError | /components/form-error/ | root error pattern |
| ✅ | FormSection | /components/form-section/ | titled section pattern |
| ✅ | InlineEditField | /components/inline-edit-field/ | optimistic + reject pattern |
Tier C — Disclosure + overlay primitives
| Status | Primitive | Slug | Notes |
|---|---|---|---|
| ✅ | Tabs | /components/tabs/ | basic + disabled |
| ✅ | Accordion | /components/accordion/ | single + multiple |
| ✅ | Collapsible | /components/collapsible/ | controlled toggle |
| ✅ | Dialog | /components/dialog/ | full anatomy |
| ✅ | AlertDialog | /components/alert-dialog/ | destructive pattern |
| ✅ | ConfirmDialog | /components/confirm-dialog/ | async + pending state |
| ✅ | ConfirmPopover | /components/confirm-popover/ | inline confirmation |
| ✅ | Sheet | /components/sheet/ | right-side drawer |
| ✅ | Popover | /components/popover/ | anchored panel |
| ✅ | DropdownMenu | /components/dropdown-menu/ | items + sub-menu + checkbox + radio |
| ✅ | HoverCardPreview | /components/hover-card-preview/ | entity peek |
| ✅ | Sonner | /components/sonner/ | toast showcase |
| ✅ | Breadcrumb | /components/breadcrumb/ | basic + ellipsis |
| ✅ | Carousel | /components/carousel/ | single + multi-visible |
| ✅ | ScrollArea | /components/scroll-area/ | bounded list |
| ✅ | Resizable | /components/resizable/ | horizontal + nested |
Tier D — Compositions + rows (domain shapes)
| Status | Primitive | Slug | Notes |
|---|---|---|---|
| ✅ | ActionButton | /components/action-button/ | mutation states |
| ✅ | MutationStatus | /components/mutation-status/ | inline status indicator |
| ✅ | ActionRow | /components/action-row/ | S10 settings row |
| ✅ | ActionBar | /components/action-bar/ | fixed footer toolbar |
| ✅ | ToggleRow | /components/toggle-row/ | S1 settings row |
| ✅ | SliderRow | /components/slider-row/ | S7 settings row |
| ✅ | SelectRow | /components/select-row/ | S5 settings row |
| ✅ | NumberInputRow | /components/number-input-row/ | S8 settings row |
| ✅ | StatRow | /components/stat-row/ | metric strip |
| ✅ | PropertyList | /components/property-list/ | key/value entity layout |
| ✅ | FilterChips | /components/filter-chips/ | active filters strip |
| ✅ | ViewSwitcher | /components/view-switcher/ | view rotation |
| ✅ | SegmentedControl | /components/segmented-control/ | shipped + page live |
| ✅ | CollapsibleSection | /components/collapsible-section/ | titled section + persistent open state |
| ✅ | ContentSection | /components/content-section/ | titled body section |
| ✅ | ContentToolbar | /components/content-toolbar/ | filter + actions toolbar |
| ✅ | CardGrid | /components/card-grid/ | responsive card grid |
| ✅ | Timeline | /components/timeline/ | shipped + page live |
Tier E — Layout primitives (need a contextual host)
These need a slimmed-down host inside the <Preview> (480×320 frame, fake nav, dummy rows). See Adding components that need a special context in the authoring guide.
| Status | Primitive | Slug | Notes |
|---|---|---|---|
| ✅ | PageHeader | /components/page-header/ | foundational page bar |
| ✅ | PageContent | /components/page-content/ | scrolling region |
| ✅ | FocusLayout | /components/focus-layout/ | wizard / onboarding shell |
| ✅ | TwoColumnLayout | /components/two-column-layout/ | resizable nav rail + body |
| ✅ | SplitContent | /components/split-content/ | static two-column body |
| ✅ | SplitScrollContent | /components/split-scroll-content/ | independent-scroll two-column |
| ✅ | Sidebar | /components/sidebar/ | app-wide nav rail |
| ✅ | ListPageBar | /components/list-page-bar/ | list-page header |
| ✅ | DetailPageBar | /components/detail-page-bar/ | entity-detail header |
| ✅ | RightPanel | /components/right-panel/ | persistent right column |
| ✅ | InboxList | /components/inbox-list/ | two-pane list + preview |
| ✅ | BoardView | /components/board-view/ | kanban |
| ✅ | DataTable | /components/data-table/ | high-level table |
| ✅ | Table | /components/table/ | low-level semantic table |
| ✅ | SortableTableHead | /components/sortable-table-head/ | sortable column header |
| ✅ | TablePagination | /components/table-pagination/ | pagination footer |
Migration order (recommended)
- Tier A in one branch (Badge, Alert, EmptyState, Tooltip, Avatar, Separator, Skeleton, Loader, Progress, Card, Typography). Each takes ~15 min with the template. Bulk seeds the Components sidebar so it doesn’t look empty after Button.
- Tier B, then C, in two more branches. Forms and overlays are the next-most-used.
- Tier D as compositions become the focus of feature work.
- Tier E last — the contextual-host requirement makes them the highest-effort pages.
Don’t gate tier promotion on “all of tier A done.” Ship pages individually as soon as they pass the reviewer’s eye checklist.
Conventions for every entry below
(Lifted from 04-components.md. Read this once.)
- File:
packages/ui/src/components/<Name>.tsx. Multi-piece primitives export a namespace; single-piece primitives export the named component. - Spec entry first. When a primitive ships, its block on this page is migrated verbatim into
04-components.mdand removed from here. - Tokens only — no raw Tailwind colors, no raw px outside the 8px ladder.
- Light + dark verified.
- Keyboard contract documented per 08 — Accessibility.
- Story in Storybook with a code-only canonical-import example. Story title slot per the
storySortorder. - New runtime deps go through one PR with a Changeset that includes the bundle-size delta.
- A primitive without a Storybook story is a primitive that doesn’t exist.
Tier 1 — Spec
Timeline / ActivityFeed
Purpose. Vertical, time-ordered list of events for a single entity (agent run history, deal-stage changes, audit log, comment thread). Events have a leading affordance (icon / avatar), a one-line title, an optional body, and a timestamp.
When to use. Entity detail pages where chronology is the data — agent execution traces, CRM activity tab, account audit log.
When NOT.
- A list of objects ordered by recency but where time isn’t the point — use
DataTableorInboxList. - Two-pane chat (use a chat-specific composition, not Timeline).
- Multi-day calendar visualization — use Calendar.
Anatomy.
Timeline (root, role="list")├─ TimelineItem (role="listitem")│ ├─ TimelineConnector (vertical line — automatic)│ ├─ TimelineDot (icon | avatar | status color)│ ├─ TimelineHeader → title + timestamp│ ├─ TimelineBody → optional rich content (Text, Code, attachments)│ └─ TimelineMeta → optional: actor, tags, links to entity└─ TimelineEmpty (when items.length === 0 — uses EmptyState)Variants.
| Variant | Use |
|---|---|
default | One dot per event, full body inline. |
compact | Single-line items, no body — for long audit logs. |
grouped | Items grouped by day with a sticky day-label divider — for chat-like feeds. |
States per item.
default— neutral.success/error/warning— colored dot only; never tint the whole row.current— pulsing ring on the dot for “in progress” agent runs.loading— skeleton dot + skeleton title (use during pagination/append).
API sketch.
import { Timeline, TimelineItem } from '@na/ui/components/Timeline';
<Timeline aria-label="Agent run history"> {events.map((e) => ( <TimelineItem key={e.id} icon={e.icon} tone={e.tone} // 'neutral' | 'success' | 'warning' | 'destructive' | 'current' title={e.title} timestamp={e.at} // ISO string; component formats relative + absolute via tooltip meta={<UserBadge user={e.actor} />} > {e.body /* optional ReactNode */} </TimelineItem> ))}</Timeline>Tech. Own primitive. No new dep. CSS grid for the connector + content alignment; aria-live="polite" if items append in real time.
Tokens. Connector: border-border. Dot ring: ring-ring/40. Tone colors map to existing semantic tokens (bg-primary = success, bg-destructive = error, bg-muted-foreground = warning per 02-foundations.md).
A11y.
- Root is
role="list"; items arerole="listitem". - Timestamps: visible relative (“2 min ago”) with a
<time datetime>and aTooltipshowing absolute time. - Color is never the only signal — every tone has an icon or text label.
- For the
current/loading state, announce “in progress” via a visually-hidden span.
Do / Don’t.
- ✅ Use Timeline for what happened to one thing.
- ✅ Lazy-paginate when length > 50.
- ✅ Group by day in
groupedvariant; never group by hour. - ❌ Don’t render a Timeline inside a Card if the parent surface is already a card — flat, per Attio.
- ❌ Don’t put Timeline next to a DataTable for the same data — pick one.
Progress.
- Spec accepted in design review
- API frozen (
TimelineItemprop names:icon,tone,title,timestamp,meta,children) - Component scaffolded in
packages/ui/src/components/Timeline.tsx - Variants implemented: default · compact · grouped
- Tones implemented: neutral · success · warning · destructive · current
- Empty state via
EmptyState(composed by caller — seeEmptystory) - Light + dark verified (manual check pending in Storybook)
- Keyboard: focus ring per item, arrow-key nav between items, Enter on
metalink — first cut renders interactive content via meta only; arrow-key roving focus deferred until a real consumer needs it - A11y addon green
- Story
Data Display/Timelinewith all variants + tones + empty - Vitest: rendering + tone class + empty branch (11 tests, all green)
- Spec migrated into
04-components.mdData Display section -
@na/uiindex export (decision: skip — primitives import via@na/ui/components/Timelineper house rule) - Changeset (
.changeset/timeline-component.md,@na/uiminor)
Calendar / DatePicker
Purpose. Single-date input. Trigger button shows the formatted date; clicking opens a Popover with a month grid (react-day-picker). Used in any form needing a date.
When NOT. Date ranges (use DateRangePicker). Time-of-day inputs (separate TimePicker; not yet planned). Free-text dates (use plain Input only when the date is part of a sentence, never for structured fields).
Anatomy.
DatePicker (controlled or uncontrolled)├─ Trigger (Button variant="outline", left icon = IconCalendar, formatted value or placeholder)├─ Popover.Content│ ├─ Calendar (react-day-picker styled with semantic tokens)│ └─ Footer → "Today" + "Clear" actions (optional)Tech. react-day-picker v9 + date-fns. Wrap, never re-export. Styles authored locally on top of react-day-picker’s class: slots.
A11y.
- Trigger has
aria-haspopup="dialog"and reflects the selected value. - Calendar uses native focus model from
react-day-picker. - Locale + week-start respect
Intldefaults; override per app via<Calendar locale={...} weekStartsOn={...} />.
Do / Don’t.
- ✅ Always pair with a
Label. Always allowdisabled={(date) => …}for business-rule blocking. - ❌ Don’t use a free-text date input as a fallback. Either DatePicker works or the field is wrong.
Progress.
- Spec accepted
- Add
react-day-picker,date-fnsto@na/uideps (Changeset with bundle delta) -
Calendar.tsx(presentational primitive) — used by DatePicker and DateRangePicker -
DatePicker.tsx(Calendar + Popover + Trigger) - Light + dark
- Keyboard: ←↑→↓ between days, PageUp/Down between months, Enter selects, Esc closes
- Story
Data Entry/DatePicker - A11y addon green
- Vitest: open/close, select, disabled-days callback
- Spec migrated into
04-components.md - Export
- Changeset
DateRangePicker
Purpose. Same shape as DatePicker but selects a { from, to } range. Used for filters, reports, audit-window pickers.
Anatomy. Same as DatePicker; calendar is in mode="range". Footer adds quick presets (Last 7 days, Last 30 days, This month).
Progress.
- Spec accepted (presets list reviewed)
- Reuse
Calendar.tsx -
DateRangePicker.tsx - Presets API:
presets={[{ label, value }]} - Story
Data Entry/DateRangePicker - Vitest: range selection, preset click
- Spec migrated, export, Changeset
Combobox / Autocomplete
Purpose. Searchable single-select where options are too many for a Radix Select (typically > 10) or come from an async source. Dropdown is filtered as the user types.
When NOT. Static list ≤ 10 — use Select. Multi-select — use the multiple mode of this same primitive (one component, two modes).
Anatomy. Popover + cmdk Command + Input + List + Item.
API sketch.
<Combobox items={users} value={selectedId} onChange={setSelectedId} getOptionLabel={(u) => u.name} getOptionKey={(u) => u.id} loadOptions={async (q) => api.searchUsers(q)} // async mode placeholder="Assign to…" emptyState="No matches"/>Tech. cmdk + Popover. Same dep as Command Palette — install once.
A11y. ARIA combobox pattern (handled by cmdk): role="combobox", aria-expanded, aria-controls, aria-activedescendant. Announce result count via aria-live.
Progress.
- Spec accepted (single + multi modes finalized)
- Add
cmdk -
Combobox.tsx(single) -
MultiCombobox.tsxormultipleprop (decide one — track decision in PR) - Async loading + debounce
- Light + dark
- Keyboard: type-ahead, ↑↓, Enter selects, Esc closes, Backspace removes last chip in multi
- Story
Data Entry/Combobox - A11y addon green
- Vitest: filter, async, controlled value
- Spec migrated, export, Changeset
Command Palette (⌘K)
Purpose. App-wide quick-actions menu opened with ⌘K / Ctrl+K. Linear-style. Already promised by references/attio-layout-patterns.md.
Anatomy. Modal Dialog containing cmdk Command. Sections (CommandGroup) for Actions, Recent, Navigation, Help. Per app, callers register entries via a context provider.
Tech. cmdk + Radix Dialog. Reuses the dep from Combobox.
API sketch.
// App root<CommandPaletteProvider hotkey="mod+k"> <App /></CommandPaletteProvider>
// Anywhereconst { register } = useCommandPalette();register({ id: 'create-deal', label: 'Create deal', icon: IconPlus, run: openCreateDeal });A11y.
- Hotkey is documented in onboarding + a “Help” entry within the palette itself.
- Focus is trapped while open; Escape closes; first item is auto-focused.
- Hotkey ignored when typing in an
<input>/<textarea>/[contenteditable].
Progress.
- Spec accepted (entry shape, sections, hotkey behavior, scoping rules)
-
CommandPalette.tsx+ Provider +useCommandPalettehook - Hotkey suppression in editable contexts
- Sections + group ordering
- Recents persistence (localStorage, per-app key)
- Light + dark
- Story
Navigation/Command Palettewith mock entries - A11y addon green
- Vitest: register/unregister, run callback, hotkey suppression
- Add a
05-patterns.mdsection “Command palette” - Spec migrated into
04-components.md - Export, Changeset
Stepper / Wizard
Purpose. Multi-step linear flows (onboarding, “create agent” wizard). Renders the step indicator and is the controller for active step + validation state.
When NOT. Branching workflows (use a state machine + custom UI). Save-as-you-go forms (just use FormSection per chunk).
Anatomy.
Stepper (controls active step, exposes context)├─ StepperHeader → indicator (numbered chips with connecting lines)├─ StepperContent → renders only the active step│ └─ StepperStep (id, label, content)└─ StepperFooter → Back / Next / Submit (Submit only on last step)Variants. horizontal (default), vertical (for sidebars). Indicator: numbered or named.
A11y.
- Header:
role="tablist"with steps asrole="tab"(only the current and visited steps are clickable to go back). - Content:
role="tabpanel"for the active step. - Live announce on step change.
Progress.
- Spec accepted (controlled API + validation hook)
-
Stepper.tsx+StepperStep, plususeStepperhook - Validation gate:
canAdvance(stepId): boolean - Horizontal + vertical variants
- Light + dark
- Keyboard: ←/→ between visited steps, Enter to activate
- Story
Layout/Stepper - A11y addon green
- Vitest: advance, retreat, blocked advance, submit
- Add
06-flows.mdexample referencing this primitive (replace prose-only wizard section) - Spec migrated, export, Changeset
Dropzone / FileUpload
Purpose. Drag-and-drop file area with click-to-browse fallback. Shows accepted state, rejection reasons, and a list of staged files with progress + per-file remove.
Anatomy.
Dropzone (root, role="button", drop target)├─ Affordance → icon + "Drop files or click to browse"└─ DropzoneItem[] → filename · size · progress · status · removeTech. react-dropzone. No raw <input type="file"> UX — even though it’s used internally.
Limits. Per-file size, total size, MIME types — all configurable. maxFiles controls multi vs single.
A11y.
- Root is keyboard-activatable (
Space/Enteropens file dialog). - Each staged file is a list item with a remove button labeled
Remove <filename>.
Progress.
- Spec accepted
- Add
react-dropzone -
Dropzone.tsx - Per-file progress slot
- Reject reasons (size / mime / count) with messages from
07-voice.md - Light + dark
- Story
Data Entry/Dropzonewith single + multi - A11y addon green
- Vitest: drop, reject, remove
- Spec migrated, export, Changeset
SegmentedControl
Purpose. Generalize the segmented switch baked into ViewSwitcher into a reusable primitive. Two-to-five mutually-exclusive choices, all visible at once.
When NOT. More than 5 options (use Select). Toggling boolean (use Switch). View switching specifically — keep using ViewSwitcher, which composes this.
Anatomy. ToggleGroup from Radix in type="single" with custom skin. Each item is a button with optional leading icon.
A11y. Inherits from Radix ToggleGroup. Arrow keys move between items.
Progress.
- Spec accepted
-
SegmentedControl.tsx(extract the inner segmented control fromViewSwitcher) - Refactor
ViewSwitcherto compose this primitive (no behavioral change) - Light + dark
- Story
Data Entry/SegmentedControl - A11y addon green
- Vitest: select, controlled, disabled item
- Spec migrated, export, Changeset
AutosizeTextarea
Purpose. <textarea> that grows with its content up to a maxRows. Required for chat/agent prompt UIs.
Tech. react-textarea-autosize. Drop-in replacement for the existing Textarea.
Decision. Either (a) ship as a separate primitive AutosizeTextarea, or (b) add a prop autosize?: boolean | { minRows, maxRows } to existing Textarea. Prefer (b) — fewer entry points, identical surface for callers.
Progress.
- Decision recorded (a vs. b) — leaning b
- Add
react-textarea-autosize - Extend
textarea.tsxwithautosizeprop - Story
Data Entry/Textareaupdated with autosize variant - Vitest: grows, respects maxRows
- Spec note added to
04-components.mdTextarea section - Changeset
OTPInput
Purpose. Code-entry input for auth flows (6 digits typically). Each character has its own slot; paste fills all slots.
Tech. input-otp (the shadcn-canon library).
A11y. Single visible focus, advances per character. Screen-reader-friendly group label.
Progress.
- Spec accepted
- Add
input-otp -
OTPInput.tsx - Story
Data Entry/OTPInput - A11y addon green
- Vitest: type, paste, complete callback
-
06-flows.mdauth section reference - Spec migrated, export, Changeset
Banner
Purpose. Page- or region-top sentiment ribbon — sentiment trend, alert message, compliance pre-flight, kill-switch warning. Distinct from Alert (inline message scoped to a region) and Sonner (transient toast).
When to use.
- Page-top warning that applies to the whole surface (“This integration is in beta”, “Kill-switch armed”).
- Region-top sentiment summary (“12% MoM increase”).
- Compliance pre-flight states (“Pending legal review”).
When NOT.
- A single field error (use
FormError). - Transient feedback after an action (use
Sonner). - Inline guidance with a CTA (use
Alert).
Anatomy.
Banner (root, role="status" or "alert")├─ Indicator dot / icon (left, 16×16)├─ Title + Description column└─ Actions / Dismiss (right)Variants. success · warning · destructive · muted (info-neutral).
API sketch.
<Banner variant="warning" title="Kill-switch armed" description="Outbound calls suspended for this campaign until manual unlock." action={<Button size="sm" variant="outline">Unlock</Button>} dismissable onDismiss={() => …}/>Tech. Compose Card shape + semantic-color border-left (4px) + aria-live="polite". ~80 LOC, no new dep.
A11y.
role="status"forsuccess/muted(polite).role="alert"forwarning/destructive(assertive — announces immediately).- Color is never the only signal — every variant pairs with an icon.
- Dismiss button is a real
<Button>with anaria-label.
Progress.
- Spec accepted (variants + dismiss behavior)
-
Banner.tsx - Light + dark
- Keyboard: dismiss reachable via Tab; Esc on focused dismiss closes
- Story
Feedback/Bannerwith all 4 variants + dismissable - A11y addon green
- Vitest: dismiss callback, role per variant
- Spec migrated, export, Changeset
Sparkline
Purpose. 80×24 (default) inline mini-chart for trend at-a-glance — payment history, scoring trajectory, KPI delta. Always paired with the absolute number it summarizes; the sparkline is the trend, not the value.
When to use.
- KPI cell in a list / table — “Last 24 months: ▁▂▄▃▅▇”.
- Detail-page metric strip alongside the current value.
- Stat row trend indicator.
When NOT.
- Standalone chart (use
Chart). - Comparing multiple series (use Chart line / bar).
- Showing exact values (use a Table).
API sketch.
<Sparkline data={[12, 14, 13, 18, 20, 17, 22]} kind="line" /><Sparkline data={[…]} kind="bars" tone="success" />Tech. Recharts <LineChart> / <BarChart> with axes + tooltip suppressed. ~50 LOC. Adds Recharts as a runtime dep (shared with Chart — install once).
A11y. role="img" with aria-label summarizing the trend (e.g. "Payment trend over 24 months: increasing"). Decorative when paired with an adjacent numeric label that already conveys the trend.
Progress.
- Spec accepted (variants
line/bars, tone tints) - Add Recharts dep (size budget in Changeset)
-
Sparkline.tsx - Light + dark
- Story
Data Display/Sparkline - A11y addon green
- Vitest: renders, supports both kinds
- Spec migrated, export, Changeset
DiffViewer
Purpose. Visualize the difference between two versions — text or JSON. Used for audit trails, version compare, workflow inbox, legal version diffs.
When to use.
- Side-by-side compare in version history.
- “What changed in this audit log entry?” expand-pane.
- Workflow approval that needs to surface what differs from the baseline.
When NOT.
- Image / PDF diff (out of scope).
- Real-time collaborative editing (use a CRDT-aware editor).
API sketch.
<DiffViewer kind="text" before={oldDoc} after={newDoc} hideUnchanged/>
<DiffViewer kind="json" before={oldRule} after={newRule} expanded/>Tech. react-diff-view (text mode) + jsondiffpatch (JSON mode). One primitive, two render paths via the kind prop. ~100 LOC + 2 deps.
A11y.
- Removed lines marked with
aria-label="Removed"; added witharia-label="Added"so screen readers don’t rely on color alone. - Provide a “Plain text” toggle that shows the after-version unannotated, for users who prefer raw output.
Progress.
- Spec accepted (text + JSON modes finalized)
- Add
react-diff-viewandjsondiffpatch(size budget in Changeset) -
DiffViewer.tsx - Light + dark
- Keyboard: scroll, expand-collapse hunks
- Story
Data Display/DiffViewerfor both modes - A11y addon green
- Vitest: rendering + hunk collapse + JSON deep diff
- Spec migrated, export, Changeset
Tier 2 — Spec (lighter)
Chart
Purpose. Token-bound wrappers around Recharts. Variants: line, area, bar, donut. Never re-export Recharts; expose a small typed API per variant.
Progress. [ ] Spec [ ] Recharts dep + size budget [ ] Chart.tsx per variant [ ] Stories [ ] Spec migrated
RadioCardGroup
Purpose. Tabler “Form selectgroup” — Radix RadioGroup rendered as cards. Used in onboarding / setup.
Progress. [ ] Spec [ ] RadioCardGroup.tsx [ ] Story [ ] Spec migrated
MaskedInput / NumericInput
Purpose. Bounded data entry: phone, currency, formatted numbers. Uses react-imask.
Progress. [ ] Spec [ ] Add dep [ ] MaskedInput.tsx + NumericInput.tsx [ ] Story [ ] Refactor NumberInputRow to compose NumericInput [ ] Spec migrated
NumberInput (stepper)
Purpose. Quantity input with + / - buttons. Distinct from NumericInput (which is text-formatted).
Progress. [ ] Spec [ ] NumberInput.tsx [ ] Story [ ] Spec migrated
Fieldset
Purpose. Semantic group around related form rows with a legend. Replaces ad-hoc <div>s in long forms.
Progress. [ ] Spec [ ] Fieldset.tsx [ ] Story [ ] Spec migrated
Timeline pagination (loadMore)
Purpose. Extend the shipped Timeline with cursor-paginated load-older. Closes the gap that motivated <TimelineRail> in the agentic-ui backlog (case detail, legal case detail).
Decision. Extend the existing Timeline rather than ship a separate TimelineRail primitive — same anatomy + tones + variants, just one new branch. Avoid two near-identical primitives.
API sketch.
<Timeline aria-label="Case activity" onLoadMore={async () => fetchOlderEvents(cursor)} hasMore={cursor !== null}> {events.map(...)}</Timeline>Progress. [ ] Spec [ ] loadMore + hasMore props on Timeline [ ] Sentinel + IntersectionObserver to auto-fetch [ ] Story Data Display/Timeline updated [ ] Vitest [ ] Spec migrated
DataGroup
Purpose. Composition primitive: a Collapsible group around a DataTable, repeated per group. Used by case-list grouped-by-collector, team-queue grouped-by-stage. Per feedback_minimal_new_infra.md, this is preferred over a <DataTable rowGroups> primitive.
API sketch.
<DataGroup groups={[ { id: 'mine', label: 'Mine', count: 12, defaultOpen: true, rows: minRows }, { id: 'team', label: 'Team', count: 47, rows: teamRows }, ]} columns={columns} rowKey={(r) => r.id}/>Progress. [ ] Spec [ ] DataGroup.tsx (Collapsible + DataTable) [ ] Story Data Display/DataGroup [ ] Vitest [ ] Spec migrated
CountdownChip
Purpose. Badge + monotonic timer. Used for PII unmask windows (“auto-locks in 4:32”), workflow SLA timers, undo grace periods.
API sketch.
<CountdownChip endsAt={Date.now() + 5 * 60_000} variant="warning" onComplete={() => lockAgain()} formatLabel={(ms) => `auto-locks in ${formatMmSs(ms)}`}/>Tech. ~40 LOC. Compose Badge + a small useTimer hook. No new dep.
Progress. [ ] Spec [ ] useTimer hook + CountdownChip.tsx [ ] Story Feedback/CountdownChip [ ] Vitest (fake timers) [ ] Spec migrated
Tier 3 — Parked
These have no roadmap slot until a screen demands them. Listed only to record the decision so we don’t re-relitigate.
| Primitive | Why parked | Re-open trigger |
|---|---|---|
| Color picker | No product surface needs runtime color theming. | A theming feature gets prioritized. |
| RichText / WYSIWYG | Heavy, drift-prone. | A real long-form text surface (proposal editor, knowledge base) is approved. |
| Countup (animated number) | Decorative. Restraint. | Marketing dashboard with explicit hero stats. |
| Image check (image radio grid) | Niche; can compose with RadioCardGroup. | A repeated visual-pick UI shows up across two surfaces. |
| Carousel auto-play | Already have manual Carousel. | Marketing page asks for hero carousel. |
| RouteMap | Scope-pinned to one feature (field-planner). Heavy dep (react-leaflet + leaflet). | A second feature wants map UI. |
| KanbanBoard | BoardView already covers this. Per design review, prefer feature-local compositions. | 3+ features adopt the same drag-drop column shape. |
| TimelineRail (separate primitive) | Subsumed by Timeline pagination — extend, don’t fork. | Timeline pagination ships and a real consumer needs a fundamentally different anatomy. |
| JsonDiffViewer (separate primitive) | Subsumed by DiffViewer kind="json". | n/a |
| DescriptionList (separate primitive) | PropertyList covers this — same <dl> semantic. | If a horizontal-only variant proves to be a distinct surface, add a horizontal prop to PropertyList. |
Patterns (composition recipes, not primitives)
These don’t live in @na/ui. They’re documented patterns or feature-local compositions; promote to a primitive only when 3+ unrelated features want the same shape.
| Pattern | Composition | Where to document |
|---|---|---|
| UndoToast | toast.success("Removed.", { action: { label: 'Undo', onClick }, duration: countdownMs }) paired with a <CountdownChip> inside the toast description. | 05-patterns.md § Latency feedback. |
| SearchInput | <Input type="search" /> with a leading <IconSearch> via Tailwind pl-9 + absolute-positioned icon span. | 05-patterns.md § Filters. |
| DiffRow | Two <Text> cells in a 2-col row + a small ✓/✗ match indicator. Lives inside the dedup-pair feature. | Feature-local — don’t promote. |
Shipped log
(Empty — move entries here once their checklist is fully ticked. Format: **<Name>** — shipped <YYYY-MM-DD> in <PR/Changeset>. See [04 — Components](/guides/04-components/#anchor) for spec.)