Skip to content

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

  1. Status legend and summary table are the at-a-glance view.
  2. Each primitive below has: Purpose · When NOT · Anatomy · States · Tech · A11y · Do / Don’t · Progress.
  3. 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.
  4. 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

SymbolMeaning
📝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

TierPrimitiveStatusOwnerStorySpec link
1Calendar / DatePickerhuypq6Data Entry/DatePickershipped
1DateRangePickerhuypq6Data Entry/DatePickershipped
1Combobox / Autocompletehuypq6Data Entry/Comboboxshipped
1Command Palette (⌘K)huypq6Navigation/CommandPaletteshipped
1Stepper / Wizardhuypq6Navigation/Steppershipped
1Timeline / ActivityFeedhuypq6Data Display/Timelineshipped
1Dropzone / FileUploadhuypq6Data Entry/Dropzoneshipped
1SegmentedControlhuypq6Data Entry/SegmentedControlshipped
1AutosizeTextareahuypq6(extends Data Entry/Textarea)shipped
1OTPInputhuypq6Data Entry/InputOTPshipped
1Bannerhuypq6Feedback/Bannershipped
1Sparkline🧊parked (Recharts deferred)
1DiffViewerhuypq6Data Display/DiffViewershipped
2Chart🧊parked (Recharts deferred)
2Timeline paginationhuypq6(extends Data Display/Timeline)shipped
2DataGrouphuypq6Data Display/DataGroupshipped
2CountdownChiphuypq6Feedback/CountdownChipshipped
2RadioCardGrouphuypq6Data Entry/RadioCardGroupshipped
2MaskedInput / NumericInputhuypq6Data Entry/MaskedInputshipped
2NumberInput (stepper)huypq6Data Entry/NumberInputshipped
2Fieldsethuypq6Data Entry/Fieldsetshipped

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)

StatusPrimitiveSlugNotes
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

StatusPrimitiveSlugNotes
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

StatusPrimitiveSlugNotes
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)

StatusPrimitiveSlugNotes
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.

StatusPrimitiveSlugNotes
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
  1. 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.
  2. Tier B, then C, in two more branches. Forms and overlays are the next-most-used.
  3. Tier D as compositions become the focus of feature work.
  4. 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.md and 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 storySort order.
  • 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 DataTable or InboxList.
  • 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.

VariantUse
defaultOne dot per event, full body inline.
compactSingle-line items, no body — for long audit logs.
groupedItems 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 are role="listitem".
  • Timestamps: visible relative (“2 min ago”) with a <time datetime> and a Tooltip showing 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 grouped variant; 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 (TimelineItem prop 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 — see Empty story)
  • Light + dark verified (manual check pending in Storybook)
  • Keyboard: focus ring per item, arrow-key nav between items, Enter on meta link — 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/Timeline with all variants + tones + empty
  • Vitest: rendering + tone class + empty branch (11 tests, all green)
  • Spec migrated into 04-components.md Data Display section
  • @na/ui index export (decision: skip — primitives import via @na/ui/components/Timeline per house rule)
  • Changeset (.changeset/timeline-component.md, @na/ui minor)

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 Intl defaults; override per app via <Calendar locale={...} weekStartsOn={...} />.

Do / Don’t.

  • ✅ Always pair with a Label. Always allow disabled={(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-fns to @na/ui deps (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.tsx or multiple prop (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>
// Anywhere
const { 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 + useCommandPalette hook
  • Hotkey suppression in editable contexts
  • Sections + group ordering
  • Recents persistence (localStorage, per-app key)
  • Light + dark
  • Story Navigation/Command Palette with mock entries
  • A11y addon green
  • Vitest: register/unregister, run callback, hotkey suppression
  • Add a 05-patterns.md section “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 as role="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, plus useStepper hook
  • 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.md example 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 · remove

Tech. 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 / Enter opens 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/Dropzone with 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 from ViewSwitcher)
  • Refactor ViewSwitcher to 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.tsx with autosize prop
  • Story Data Entry/Textarea updated with autosize variant
  • Vitest: grows, respects maxRows
  • Spec note added to 04-components.md Textarea 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.md auth section reference
  • Spec migrated, export, Changeset

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" for success / muted (polite).
  • role="alert" for warning / destructive (assertive — announces immediately).
  • Color is never the only signal — every variant pairs with an icon.
  • Dismiss button is a real <Button> with an aria-label.

Progress.

  • Spec accepted (variants + dismiss behavior)
  • Banner.tsx
  • Light + dark
  • Keyboard: dismiss reachable via Tab; Esc on focused dismiss closes
  • Story Feedback/Banner with 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 with aria-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-view and jsondiffpatch (size budget in Changeset)
  • DiffViewer.tsx
  • Light + dark
  • Keyboard: scroll, expand-collapse hunks
  • Story Data Display/DiffViewer for 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.

PrimitiveWhy parkedRe-open trigger
Color pickerNo product surface needs runtime color theming.A theming feature gets prioritized.
RichText / WYSIWYGHeavy, 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-playAlready have manual Carousel.Marketing page asks for hero carousel.
RouteMapScope-pinned to one feature (field-planner). Heavy dep (react-leaflet + leaflet).A second feature wants map UI.
KanbanBoardBoardView 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.

PatternCompositionWhere to document
UndoToasttoast.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.
DiffRowTwo <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.)