@usetheo/ui 0.11.0-next.0 → 0.12.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0-next.0] - 2026-05-25
11
+
12
+ Minor (additive, zero breaking change) — ships two LLM-facing artifacts
13
+ that complement the existing `llms.txt`: a structured visual spec
14
+ (`DESIGN.md`) and a companion agent skill (`skills/theo-ui/`).
15
+
16
+ ### Added
17
+
18
+ - **`DESIGN.md` at repo root (NEW)** — plain-text design system spec
19
+ for LLM assistants generating UI against `@usetheo/ui`. Follows the
20
+ awesome-design-md 9-section canonical structure (Visual Theme &
21
+ Atmosphere · Color Palette & Roles · Typography Rules · Layout
22
+ Principles · Depth & Elevation · Component Stylings · Responsive
23
+ Behavior · Do's and Don'ts · Agent Prompt Guide). Tokens mirror
24
+ `src/styles/tokens.css` and `src/themes/violet-forge.ts`. Shipped
25
+ via `package.json > files` alongside `llms.txt` and `CHANGELOG.md`
26
+ so consumers see the visual spec at `node_modules/@usetheo/ui/DESIGN.md`.
27
+ Reference research lives at
28
+ `.claude/knowledge-base/reference/design-md-convention.md`.
29
+ - **`skills/theo-ui/` companion agent skill (NEW)** — library-aware
30
+ design skill for AI coding assistants (Claude Code, Cursor, Codex,
31
+ OpenCode, Windsurf, Copilot) installable via the `vercel-labs/skills`
32
+ CLI:
33
+
34
+ ```
35
+ npx skills add usetheodev/theo-ui
36
+ ```
37
+
38
+ Four verbs (default build / `audit` / `migrate` / `catalog`), 32
39
+ universal slop-test gates plus surface-specific extensions, pre-emit
40
+ self-critique on six axes (Library-fit · Token-fidelity · Composition ·
41
+ A11y · Restraint · Voice), 12 page archetypes (P1–P12), 5 surfaces
42
+ (agent-chat · cloud-dashboard · settings-form · marketing · auth).
43
+ Project memory at `.theo-ui-skill/log.json`. 30 files, ~9300 lines
44
+ of markdown, ~416 KB. Distributed via the GitHub repo (not the npm
45
+ package). Root README ships Option C in Quickstart pointing
46
+ consumers at the skill.
47
+
10
48
  ## [0.11.0-next.0] - 2026-05-25
11
49
 
12
50
  Minor (additive, zero breaking change) — ships Brief #5 from the
@@ -94,18 +132,27 @@ Brief: `theo/docs/handoff/2026-05-25-theo-ui-cloud-dashboard-brief-5.md`
94
132
  - Zero new peer-deps. `@radix-ui/react-dropdown-menu` was already
95
133
  bundled.
96
134
 
97
- ### Bundle delta (consumer canary)
135
+ ### Bundle delta (consumer canary, measured 2026-05-25)
98
136
 
99
137
  Measured against TheoCloud dashboard (no consumer migration to the
100
138
  new primitives yet — pure version bump):
101
139
 
102
140
  | Metric | 0.10.0-next.0 | 0.11.0-next.0 | Δ |
103
141
  |---|---|---|---|
104
- | `@usetheo/ui` chunk | TBD | TBD | TBD |
105
- | TOTAL initial JS | TBD | TBD | TBD |
142
+ | `@usetheo/ui` chunk | 10.96 KB brotli | 10.98 KB brotli | **+0.02 KB (+0.2%)** |
143
+ | TOTAL initial JS | 134.68 KB brotli | 135.56 KB brotli | +0.88 KB (+0.6%) |
144
+
145
+ Per-chunk cap (18 KB): passes with 7.02 KB headroom.
146
+ Total hard gate (180 KB): passes with 44.44 KB headroom.
147
+
148
+ The +0.02 KB chunk delta is effectively noise — confirms Brief #4's
149
+ per-component dist + tree-shaking works: 5 new components ship as
150
+ separate chunks and the consumer correctly drops all of them while
151
+ they aren't imported. Post-consumer-migration delta is expected at
152
+ +10-15 KB brotli (Brief #6 follow-up).
106
153
 
107
- (Evidence file:
108
- `.claude/knowledge-base/baselines/2026-05-26-post-brief-5/theocloud-bundle-delta.txt`)
154
+ Evidence:
155
+ `.claude/knowledge-base/baselines/2026-05-26-post-brief-5/theocloud-bundle-delta.txt`
109
156
 
110
157
  ## [0.10.0-next.0] - 2026-05-25
111
158
 
package/DESIGN.md ADDED
@@ -0,0 +1,456 @@
1
+ # DESIGN.md — `@usetheo/ui` (Violet Forge)
2
+
3
+ > Plain-text design system spec for LLM assistants generating UI against `@usetheo/ui`. Companion to [`llms.txt`](./llms.txt) (component catalog) and [`docs/design-system.md`](./docs/design-system.md) (human-facing long-form). Read these three together; this file is the visual layer.
4
+ >
5
+ > Tokens here are normative — they mirror [`src/styles/tokens.css`](./src/styles/tokens.css) and [`src/themes/violet-forge.ts`](./src/themes/violet-forge.ts). Drift between this file and those is enforced in CI.
6
+
7
+ ---
8
+
9
+ ## 1. Visual Theme & Atmosphere
10
+
11
+ `@usetheo/ui` ships under the design system codename **Violet Forge** — a Vercel-inspired neutral-grayscale system with a Theo violet primary (`#7C3AED`) and a burnt-sienna accent (`#C96442`). The voice is **engineered, calm, agent-surface-ready**. Surfaces are pure neutrals (zero hue tint); color enters only through `primary`, `accent`, and semantic states (success / warning / destructive / info).
12
+
13
+ The system targets two adjacent surfaces:
14
+
15
+ 1. **AI agent surfaces** — chat threads, tool calls, agent timelines, streaming responses. These need legible monospace, dense information layouts, and visually quiet chrome.
16
+ 2. **Cloud dashboards** — deployment lists, environment configs, billing tables, settings panels. These need scannable data tables, predictable form rhythm, and elevation that signals action without shouting.
17
+
18
+ The default mode is **dark dominant** — `#0A0A0A` canvas with `#F5F5F5` foreground. Light mode is the polarity flip. The brand never mixes light + dark inside the same view; mode is a top-level decision.
19
+
20
+ **Anti-glass principle** (named guideline): Violet Forge does NOT use backdrop-filter blur, frosted-glass overlays, or chrome-glass effects as decorative depth. Elevation is built from theme-aware ink shadows (derived from `--foreground`) plus a primary-derived glow for signature actions. Reason: glass effects fight legibility on dense dashboards and inflate render cost on long-lived agent surfaces.
21
+
22
+ **Key characteristics**
23
+
24
+ - Pure-neutral surfaces (0% saturation on `background`, `card`, `secondary`, `muted`, `border`). All color comes from `primary` / `accent` / semantic tokens.
25
+ - Geist Sans + Geist Mono throughout. Three strict weights: 400 body, 500 UI, 600 display.
26
+ - 4-px base spacing scale; container caps at 1280 px.
27
+ - Theme-aware shadow tokens — they recolor when the theme swaps because they derive from `--foreground` (ink) and `--primary` (glow), not from baked hex.
28
+ - Density is a runtime tri-state (`compact` 32 px / `comfortable` 36 px / `spacious` 44 px control height) set on `<ThemeProvider>` or via `useDensity()`.
29
+ - 10 built-in themes (Violet Forge default + Classic Paper + Aurora Terminal + 7 RFC-0007 themes). Each is a frozen bundle of the same token slots — swapping themes never changes geometry or typography rules, only color values.
30
+
31
+ ---
32
+
33
+ ## 2. Color Palette & Roles
34
+
35
+ ### 2.1 Light mode
36
+
37
+ #### Surface
38
+
39
+ - **Background** (`{colors.background}` — `#FFFFFF`): page canvas. Pure white.
40
+ - **Card** (`{colors.card}` — `#FFFFFF`): card / dialog surface. Same as background — depth comes from elevation, not tint.
41
+ - **Popover** (`{colors.popover}` — `#FFFFFF`): floating layer (dropdowns, tooltips, hovercards).
42
+ - **Secondary** (`{colors.secondary}` — `#F5F5F5`): muted surface for nested cards, code-block backgrounds, hover lifts.
43
+ - **Muted** (`{colors.muted}` — `#F5F5F5`): identical to secondary; aliased for semantic clarity.
44
+ - **Border** (`{colors.border}` — `#E8E8E8`): hairline dividers, card edges, input borders.
45
+ - **Input** (`{colors.input}` — `#E8E8E8`): same as border; aliased for forms.
46
+
47
+ #### Text
48
+
49
+ - **Foreground** (`{colors.foreground}` — `#0A0A0A`): every heading and body paragraph.
50
+ - **Muted Foreground** (`{colors.muted-foreground}` — `#737373`): secondary text — captions, helper text, inactive nav.
51
+ - **Card Foreground** (`{colors.card-foreground}` — `#0A0A0A`): text on card surfaces.
52
+
53
+ #### Brand
54
+
55
+ - **Primary** (`{colors.primary}` — `#7C3AED`): Theo violet. Single canonical primary CTA color, focus-ring color, brand accent. Used as fill on primary buttons, badges, active tabs.
56
+ - **Primary Deep** (`{colors.primary-deep}` — `#5B21B6`): pressed / active state of primary.
57
+ - **Primary Glow** (`{colors.primary-glow}` — `#A78BFA`): hover halo, signature shadow component.
58
+ - **Primary Foreground** (`{colors.primary-foreground}` — `#FFFFFF`): text on primary surfaces.
59
+
60
+ #### Accent
61
+
62
+ - **Accent** (`{colors.accent}` — `#C96442`): burnt sienna. Celebratory secondary actions, rare-use highlight (success milestones, premium tier badges).
63
+ - **Accent Deep** (`{colors.accent-deep}` — `#9C4A2E`): pressed accent.
64
+ - **Accent Foreground** (`{colors.accent-foreground}` — `#FFFFFF`): text on accent surfaces.
65
+
66
+ #### Ring
67
+
68
+ - **Ring** (`{colors.ring}` — `#7C3AED`): focus ring — matches primary. Always 2 px width with 2 px offset.
69
+
70
+ #### Semantic
71
+
72
+ - **Success** (`{colors.success}` — `#16A34A`): positive confirmation, healthy status dots, build-passed badges.
73
+ - **Warning** (`{colors.warning}` — `#D97706`): caution, pending state, soft alerts.
74
+ - **Destructive** (`{colors.destructive}` — `#DC2626`): irreversible action, error states, danger zone CTAs.
75
+ - **Info** (`{colors.info}` — `#3B82F6`): informational callouts, neutral status indicators.
76
+
77
+ ### 2.2 Dark mode (dominant)
78
+
79
+ #### Surface
80
+
81
+ - **Background** (`{colors.background}` — `#0A0A0A`): page canvas.
82
+ - **Card** (`{colors.card}` — `#121212`): elevated surface — one step lighter than background.
83
+ - **Popover** (`{colors.popover}` — `#171717`): floating layer — one step lighter than card.
84
+ - **Secondary** (`{colors.secondary}` — `#1C1C1C`): nested cards, code blocks.
85
+ - **Muted** (`{colors.muted}` — `#1C1C1C`).
86
+ - **Border** (`{colors.border}` — `#292929`): hairline dividers.
87
+ - **Input** (`{colors.input}` — `#1C1C1C`).
88
+
89
+ #### Text
90
+
91
+ - **Foreground** (`{colors.foreground}` — `#F5F5F5`): every heading and body paragraph.
92
+ - **Muted Foreground** (`{colors.muted-foreground}` — `#999999`): Vercel gray-500 — secondary text.
93
+
94
+ #### Brand, Accent, Semantic (dark mode delta)
95
+
96
+ `primary` / `primary-deep` / `primary-glow` / `accent` / `accent-deep` keep the same hex values across modes — the brand identity is mode-invariant. Semantics shift toward the brighter end of the hue:
97
+
98
+ - **Success** — `#22E58C` (was `#16A34A`).
99
+ - **Warning** — `#F59E0B` (was `#D97706`).
100
+ - **Destructive** — `#FF4F6D` (was `#DC2626`).
101
+ - **Info** — `#5FB3FF` (was `#3B82F6`).
102
+
103
+ ### 2.3 Implementation note
104
+
105
+ All color tokens are stored as HSL triplets (`262 83% 58%`) in CSS custom properties, not hex. This enables alpha modulation via `hsl(var(--primary) / 0.4)` and `color-mix(in oklch, hsl(var(--primary)) 50%, transparent)`. Hex values shown above are the rendered equivalents.
106
+
107
+ ---
108
+
109
+ ## 3. Typography Rules
110
+
111
+ ### 3.1 Font families
112
+
113
+ - **Display + Body**: `Geist` — Vercel's open-source geometric sans. Loaded from Google Fonts with the full 100–900 weight axis; the design system uses only 400 / 500 / 600.
114
+ - **Mono**: `Geist Mono` — paired Vercel face. Code, paths, metrics, timestamps, terminal output, agent tool calls.
115
+
116
+ Fallbacks: `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` (display + body); `ui-monospace, SFMono-Regular, Menlo, monospace` (mono).
117
+
118
+ ### 3.2 Hierarchy
119
+
120
+ | Token | Size | Weight | Line Height | Letter Spacing | Use |
121
+ |---|---|---|---|---|---|
122
+ | `{typography.display-2xl}` | 64 px | 600 | 1.0 | -0.0464em | Hero headline (marketing surfaces only). |
123
+ | `{typography.display-xl}` | 48 px | 600 | 1.05 | -0.05em | Display headline — landing pages, empty-state heros. |
124
+ | `{typography.display-lg}` | 40 px | 600 | 1.1 | -0.05em | Section headline — splash dialogs. |
125
+ | `{typography.display-md}` | 32 px | 600 | 1.2 | -0.04em | Page title (PageShell `title` slot). |
126
+ | `{typography.headline}` | 28 px | 600 | 1.25 | -0.035em | Card cluster heads, settings section heads. |
127
+ | `{typography.title-lg}` | 24 px | 600 | 1.33 | -0.04em | Modal title, card title (`Card.Header`). |
128
+ | `{typography.title-md}` | 20 px | 600 | 1.4 | -0.03em | Sub-section title, inline heading. |
129
+ | `{typography.body-lg}` | 18 px | 400 | 1.56 | -0.01em | Lead paragraph under section headline. |
130
+ | `{typography.body-md}` | 14 px | 400 | 1.43 | 0 | Default body — paragraphs, list items, table cells. |
131
+ | `{typography.body-sm}` | 13 px | 400 | 1.46 | 0 | Helper text, captions, secondary metadata. |
132
+ | `{typography.label}` | 14 px | 500 | 1.43 | 0 | Button labels, form labels, nav links. |
133
+ | `{typography.label-caps}` | 12 px | 500 | 1.33 | 0.04em | Eyebrow caps, section dividers, badge uppercase. |
134
+ | `{typography.code-md}` | 14 px | 400 | 1.5 | 0 | Default code blocks, inline `<code>`. |
135
+ | `{typography.code-sm}` | 13 px | 500 | 1.54 | 0 | Tight code blocks, terminal output, agent tool calls. |
136
+
137
+ ### 3.3 Principles
138
+
139
+ - **Three strict weights**. The system uses only 400 / 500 / 600. Weight 700 / 800 is never used; the display ceiling is 600. This produces a calmer visual register than typical SaaS systems that lean on bold display weights.
140
+ - **Aggressive negative letter-spacing on display tier**. Every `display-*` and `title-*` token tracks negative (`-0.05em` to `-0.03em`). Reverting to default tracking visibly breaks the brand voice.
141
+ - **Sentence-case for headlines**. The system does not use ALL-CAPS headlines outside of `label-caps` (which is reserved for eyebrows). Page titles, section heads, dialog titles are all sentence case.
142
+ - **Mono only for the technical layer**. Code, paths, IDs (tenant_id, deployment_id), timestamps, terminal mockups, tool-call output. Body paragraphs never set in mono.
143
+ - **Tabular numerals on data cells**. `<code>` / `<pre>` / `<kbd>` / `<samp>` get `font-feature-settings: "tnum"` via the preset. Numbers in DataTable cells align column-wise.
144
+
145
+ ### 3.4 Font substitutes
146
+
147
+ The two faces (`Geist` and `Geist Mono`) are Apache-2.0-licensed and free for commercial use. No substitute is required for license reasons. If a consumer wants a different brand voice while keeping the design system geometry, swap to:
148
+
149
+ - **Geometric sans** — *Inter* (400 / 500 / 600) is the closest match. *Satoshi* is a passable second.
150
+ - **Mono** — *JetBrains Mono* (400) or *IBM Plex Mono* match the technical voice.
151
+
152
+ Theme switching covers this — `classic-paper` swaps to Inter, `aurora-terminal` swaps to Geist Mono as body.
153
+
154
+ ---
155
+
156
+ ## 4. Layout Principles
157
+
158
+ ### 4.1 Spacing scale
159
+
160
+ Base unit **4 px**. Tokens follow Tailwind's geometric scale.
161
+
162
+ | Token | Value | Use |
163
+ |---|---|---|
164
+ | `{spacing.1}` | 4 px | Inline gap between icon + label, tightest separation. |
165
+ | `{spacing.2}` | 8 px | Default inline gap (button row, badge row, chip row). |
166
+ | `{spacing.3}` | 12 px | Form-control internal padding, default `gap-3` row. |
167
+ | `{spacing.4}` | 16 px | Section gutter, card content gap. |
168
+ | `{spacing.5}` | 20 px | Card padding (`md` size, default density). |
169
+ | `{spacing.6}` | 24 px | Card padding (spacious density), section header gap. |
170
+ | `{spacing.8}` | 32 px | Section-to-section spacing inside a page. |
171
+ | `{spacing.10}` | 40 px | Page header gap below `PageShell.title`. |
172
+ | `{spacing.12}` | 48 px | Major section break inside long pages. |
173
+ | `{spacing.16}` | 64 px | Hero-band top/bottom padding. |
174
+ | `{spacing.20}` | 80 px | Landing-page section padding. |
175
+ | `{spacing.24}` | 96 px | Hero-section vertical rhythm. |
176
+ | `{spacing.32}` | 128 px | Top-of-page hero stretch. |
177
+
178
+ ### 4.2 Grid & container
179
+
180
+ - **Max width**: container caps at `1280 px` (the Tailwind `2xl` breakpoint). Content centers with `1 rem` horizontal padding at all sizes.
181
+ - **Column patterns** seen across composites:
182
+ - PageShell content area — single column, max 1280 px.
183
+ - DataTable — full-width, with sticky header and horizontal scroll on overflow.
184
+ - Dashboard grids — 1-up (mobile) → 2-up (tablet) → 3-up (desktop) for card clusters.
185
+ - ChatThread / AgentTimeline — single column with internal max-width ~768 px for readability.
186
+
187
+ ### 4.3 Density tri-state (runtime)
188
+
189
+ | Density | Control height (md tier) | Textarea min-h | Card `md` padding | Body text |
190
+ |---|---|---|---|---|
191
+ | `compact` | 32 px (`h-8`) | 96 px | 20 px | 14 px |
192
+ | `comfortable` *(default)* | **36 px** | 96 px | 20 px | **14 px** |
193
+ | `spacious` | 44 px (`h-11`) | 128 px | 24 px | 14 px |
194
+
195
+ Set globally via `<ThemeProvider defaultDensity="compact">` or runtime via `useDensity()`. Density only affects the `md` size tier — explicit `size="sm"` / `size="lg"` overrides density.
196
+
197
+ ### 4.4 Whitespace philosophy
198
+
199
+ The system reads as engineered — large outer gaps + tight interior gaps, never the other way around. Section-to-section uses `{spacing.8}`–`{spacing.12}` (32–48 px). Inside a card, the title-to-body gap is `{spacing.2}` (8 px); body-to-CTA gap is `{spacing.4}` (16 px). The page's calm cadence comes from this contrast.
200
+
201
+ ---
202
+
203
+ ## 5. Depth & Elevation
204
+
205
+ Elevation is a numbered ladder. Theme-aware — every shadow derives from `--foreground` (ink) and `--primary` (glow), so swapping themes recolors them automatically.
206
+
207
+ | Level | Treatment | Use |
208
+ |---|---|---|
209
+ | Level 0 — Flat | No shadow, no border. | Full-bleed bands, hero surfaces. |
210
+ | Level 1 — Hairline | `1 px` solid `{colors.border}`. | Default card chrome, input borders, table dividers. |
211
+ | Level 2 — Subtle (`shadow-sm`) | `0 1px 2px 0 hsl(var(--foreground) / 0.06)`. | Slightly elevated cards (deployment rows, table headers). |
212
+ | Level 3 — Medium (`shadow-md`) | `0 2px 8px -2px hsl(var(--foreground) / 0.08), 0 1px 3px hsl(var(--foreground) / 0.06)`. | Floating cards (hover state), popovers, dropdowns. |
213
+ | Level 4 — High (`shadow-lg`) | `0 12px 32px -8px hsl(var(--foreground) / 0.12), 0 4px 12px hsl(var(--foreground) / 0.08)`. | Modals, command palettes, drawer surfaces. |
214
+ | Level 5 — Glow (`shadow-glow`) | `0 0 24px hsl(var(--primary) / 0.25)`. | Primary button hover state, signature emphasis. |
215
+ | Level 6 — Strong glow (`shadow-glow-strong`) | `0 0 32px hsl(var(--primary) / 0.4)`. | Primary button focus + hover combined, "now playing" emphasis. |
216
+
217
+ ### 5.1 Decorative depth (separate from elevation)
218
+
219
+ - **Dark-mode polarity flip** is the system's chief depth cue between sections. Switching a band from `{colors.background}` to `{colors.card}` (one step lighter in dark mode, identical in light mode) signals a new "depth zone."
220
+ - **Primary glow** is the only non-ink decoration. Reserved for the canonical primary CTA hover state. Never apply it to secondary buttons, cards, or surfaces — it's the brand signature.
221
+ - **No glass / backdrop-filter**. Violet Forge does not use frosted glass, backdrop blur, or chrome-glass effects. Stated explicitly in `docs/design-system.md > Anti-glass guideline`.
222
+
223
+ ---
224
+
225
+ ## 6. Component Stylings
226
+
227
+ The library exposes 121 components (92 primitives + 29 composites). The set below is the spec-defining subset — patterns here propagate to the rest.
228
+
229
+ ### 6.1 Buttons
230
+
231
+ **`<Button variant="primary">`** — canonical Theo CTA.
232
+ - Background `{colors.primary}`, text `{colors.primary-foreground}`, label set in `{typography.label}`, padding `{spacing.3}` horizontal, height tracks density (default 36 px), shape `{rounded.lg}` (10 px).
233
+ - Hover: adds `{shadow.glow}` (no fill change).
234
+ - Active: `bg-primary-deep`, `scale-[0.98]`, removes glow.
235
+ - Disabled: `opacity-50`, `pointer-events-none`.
236
+
237
+ **`<Button variant="secondary">`** — paired secondary action.
238
+ - Background `{colors.secondary}`, text `{colors.secondary-foreground}`, `1 px` `{colors.border}` border. Hover: `bg-muted`. Active: `scale-[0.98]`.
239
+
240
+ **`<Button variant="accent">`** — celebratory / premium action.
241
+ - Background `{colors.accent}`, text `{colors.accent-foreground}`. Hover: `bg-accent-deep`. Use sparingly.
242
+
243
+ **`<Button variant="ghost">`** — minimal action, embedded in dense rows.
244
+ - Background `transparent`, text `{colors.foreground}`. Hover: `bg-muted`. Active: `bg-secondary` + `scale-[0.98]`.
245
+
246
+ **`<Button variant="link">`** — inline text action.
247
+ - Background `transparent`, text `{colors.primary}`, `underline-offset-4`. Hover: `text-primary-deep` + underline. Height auto, padding zero.
248
+
249
+ **`<Button variant="destructive">`** — irreversible action.
250
+ - Background `{colors.destructive}`, text `{colors.destructive-foreground}`. Hover: `bg-destructive/90`. Active: `scale-[0.98]`. Used inside `<DangerZone>` composites and `<ConfirmDialog>` destructive flows.
251
+
252
+ **Sizes**: `sm` (32 px), `md` (36 px default, density-aware), `lg` (48 px), `icon` (square at md height).
253
+
254
+ ### 6.2 Cards & containers
255
+
256
+ **`<Card>`** — universal container primitive.
257
+ - Background `{colors.card}`, text `{colors.card-foreground}`, border `1 px` `{colors.border}`, shape `{rounded.xl}` (14 px), padding `{spacing.5}` (20 px) at `md` size.
258
+ - Sub-components: `Card.Header`, `Card.Title` (`{typography.title-lg}`), `Card.Description` (`{typography.body-sm}` + `text-muted-foreground`), `Card.Content`, `Card.Footer`.
259
+
260
+ **`<Dialog>` (modal surface)**.
261
+ - Background `{colors.popover}`, shape `{rounded.xl}`, padding `{spacing.6}` (24 px), elevation Level 4 (`shadow-lg`). Backdrop is `bg-black/80` (no blur — anti-glass principle).
262
+
263
+ **`<Popover>` / `<DropdownMenu>` / `<Tooltip>`**.
264
+ - Background `{colors.popover}`, shape `{rounded.lg}` (10 px), elevation Level 3 (`shadow-md`), border `1 px` `{colors.border}`.
265
+
266
+ ### 6.3 Inputs & forms
267
+
268
+ **`<Input>` / `<Textarea>` / `<Select.Trigger>`**.
269
+ - Background `{colors.input}`, text `{colors.foreground}`, border `1 px` `{colors.border}`, shape `{rounded.md}` (6 px), padding `{spacing.3}` (12 px) horizontal, height tracks density (36 px default).
270
+ - Focus: ring `{colors.ring}` (matches primary), `ring-2` `ring-offset-2`.
271
+ - Disabled: `opacity-50`, `cursor-not-allowed`.
272
+
273
+ **`<Checkbox>` / `<Switch>` / `<RadioGroup>`**.
274
+ - Effective tap area ≥ 24×24 CSS px (WCAG 2.5.8 AA floor); visual size 16 px, with padding extending the focus zone.
275
+ - Checked state: `bg-primary` fill, `border-primary`.
276
+
277
+ **`<PinInput>`** (Brief #5 — agent verification, OTP).
278
+ - 4 / 6 / 8-digit grid. Each slot is a `<Input>` styled `text-center` `{typography.title-md}` with auto-advance on key press.
279
+
280
+ ### 6.4 Data display
281
+
282
+ **`<DataTable>`** (composite).
283
+ - Header row uses `{typography.label-caps}` (12 px, uppercase, weight 500) with `text-muted-foreground`. Header background `{colors.card}`.
284
+ - Body cells use `{typography.body-md}` (14 px), padding `{spacing.3}` (12 px) vertical, `{spacing.4}` (16 px) horizontal.
285
+ - Row dividers: `1 px` `{colors.border}`.
286
+ - Hover row: `bg-secondary`.
287
+ - Sticky header on scroll. Selectable rows show a `{colors.primary}` ring on the leftmost cell.
288
+
289
+ **`<Badge>`**.
290
+ - Default variant: `bg-secondary`, `text-secondary-foreground`, `{rounded.md}` (6 px), padding `{spacing.2}` (8 px) horizontal, height auto, `{typography.label-caps}` or `{typography.body-sm}`.
291
+ - Variants: `primary` / `accent` / `success` / `warning` / `destructive` / `info` / `outline` — fill follows the semantic color.
292
+
293
+ **`<StatusDot>` / `<StatTile>` / `<UsageMeter>` / `<Progress>`**.
294
+ - Dashboard primitives. Status colors map directly to `success` / `warning` / `destructive` / `info` semantic tokens. Numeric values in `{typography.display-md}` or `{typography.title-md}` with `font-mono` for the digits.
295
+
296
+ ### 6.5 Navigation
297
+
298
+ **`<PageShell>`** (composite — Brief #5).
299
+ - Owns the page header (title + description + optional ActionBar) and the state precedence (loading > error > empty > children). Title uses `{typography.display-md}` (32 px), description uses `{typography.body-sm}` `text-muted-foreground`.
300
+ - Reserves `{spacing.10}` (40 px) gap below the header before children.
301
+
302
+ **`<ActionBar>`** (Brief #5).
303
+ - Horizontal flex row: search input (grows `flex-1`), optional filter icon button, optional primary action button right-aligned. Returns `null` when empty. Composes inside `PageShell` or standalone.
304
+
305
+ **`<DropdownMenu>` / `<CommandPalette>`**.
306
+ - Popover surface, elevation Level 3, items use `{typography.body-md}`, padding `{spacing.2}` (8 px) vertical, `{spacing.3}` (12 px) horizontal. Active/highlighted item: `bg-secondary`. Destructive items: `text-destructive`.
307
+
308
+ ### 6.6 Agent surfaces (signature components)
309
+
310
+ **`<ChatMessage>`** — message bubble with `parts[]` API (text / tool-call / tool-result / file / image).
311
+ - Background `{colors.card}`, text `{colors.foreground}`, padding `{spacing.4}` (16 px), shape `{rounded.lg}` (10 px). Role-based variants: `user` (right-aligned, `bg-primary/10`), `assistant` (left-aligned, `bg-card`), `system` (full-width, muted).
312
+ - Markdown rendering via the bundled engine. Code blocks use `<CodeBlock>` (mono, syntax highlight).
313
+
314
+ **`<AgentEvent>` / `<ToolCall>` / `<ToolResult>`**.
315
+ - Compact inline-block surfaces inside the chat stream. Background `{colors.secondary}`, text `{colors.foreground}`, shape `{rounded.md}` (6 px), padding `{spacing.3}` (12 px). Mono labels (`{typography.code-sm}`), prose body (`{typography.body-sm}`).
316
+
317
+ **`<CodeBlock>`**.
318
+ - Background `{colors.secondary}` (or `{colors.popover}` in dark mode for slightly deeper register), shape `{rounded.lg}` (10 px), padding `{spacing.4}` (16 px), `{typography.code-md}`. Syntax highlighting via Shiki (optional peer-dep).
319
+
320
+ **`<AgentTimeline>` / `<AgentStream>`**.
321
+ - Vertical list of events with a left-aligned status dot per row. Time stamps use `font-mono` and `text-muted-foreground`.
322
+
323
+ ---
324
+
325
+ ## 7. Responsive Behavior
326
+
327
+ ### 7.1 Breakpoints (Tailwind defaults)
328
+
329
+ | Name | Width | Key changes |
330
+ |---|---|---|
331
+ | `sm` | ≥ 640 px | Stacked layouts unstack. Side-by-side button rows resume. |
332
+ | `md` | ≥ 768 px | Two-column grids enable. Nav stays horizontal. |
333
+ | `lg` | ≥ 1024 px | Three-column grids enable. PageShell + sidebar layout common. |
334
+ | `xl` | ≥ 1280 px | Container caps here. Full dashboard layouts. |
335
+ | `2xl` | ≥ 1536 px | Content stays centered at 1280 px. Bands stretch edge-to-edge in color. |
336
+
337
+ ### 7.2 Touch targets (WCAG 2.5.8 AA)
338
+
339
+ The system targets **WCAG 2.5.8 Level AA** — minimum 24×24 CSS px effective tap area. The default `comfortable` density (36 px control height) comfortably exceeds this. Compact mode (32 px) still meets AA because the 2 px focus ring on each side expands the focusable area to ~36×36 effective.
340
+
341
+ The system does **not** target 2.5.5 Level AAA (44 px) at `comfortable`. Consumers requiring AAA can opt into `spacious` mode globally or use `size="lg"` per call site.
342
+
343
+ ### 7.3 Collapsing strategy per signature composite
344
+
345
+ - **`<PageShell>`** — title stays `{typography.display-md}` across all breakpoints. `<ActionBar>` collapses from horizontal flex to stacked at `<sm`. Search input always grows `flex-1`.
346
+ - **`<ChatThread>`** — internal max-width 768 px. Below `sm`, padding tightens from `{spacing.4}` to `{spacing.3}`.
347
+ - **`<DataTable>`** — sticky header preserved. Below `md`, low-priority columns hide (consumer specifies via column `hideBelow` prop) and remaining columns enable horizontal scroll.
348
+ - **`<DropdownMenu>` / `<CommandPalette>`** — full-screen drawer below `sm`, floating popover at `≥ sm`.
349
+ - **`<Dialog>`** — full-screen below `sm`, centered modal `≥ sm`. Max-width ~600 px at `lg`.
350
+
351
+ ### 7.4 Reduced motion
352
+
353
+ The token layer respects `prefers-reduced-motion: reduce` — all `transition-*` durations neutralize, scale transforms remove, `shadow-glow` becomes a static border. Consumers don't opt in; it's automatic.
354
+
355
+ ---
356
+
357
+ ## 8. Do's and Don'ts
358
+
359
+ ### Do
360
+
361
+ - **Reserve `{colors.primary}` for the canonical Theo CTA.** Primary buttons, focus rings, active tab indicators, brand accents. Never for body text, never for body backgrounds.
362
+ - **Use the density tri-state as a global preference**, not as a per-component override. Set `<ThemeProvider defaultDensity="compact">` once and let the system propagate.
363
+ - **Compose composites from primitives via the barrel**, not by re-implementing layout primitives. A new dashboard page is `<PageShell>` + `<ActionBar>` + `<DataTable>`, not custom flex/grid.
364
+ - **Set every code surface and technical label in `{typography.code-*}` (Geist Mono).** Tool calls, file paths, IDs, timestamps, terminal output.
365
+ - **Pair Do's and Don'ts when adding a new component spec.** Each component should declare its allowed states and its forbidden states.
366
+ - **Layer theme-aware shadows over hardcoded ones.** Use `{shadow.sm/md/lg/glow}` tokens; never write `0 4px 12px rgba(0,0,0,0.1)` inline.
367
+ - **Use sentence-case for headlines.** Page titles, dialog titles, card titles. ALL-CAPS is reserved for `label-caps` eyebrows only.
368
+
369
+ ### Don't
370
+
371
+ - **Don't introduce a sixth surface tint.** The system operates on background / card / popover / secondary / muted — five neutrals. New tints flatten the elevation language.
372
+ - **Don't promote Geist to weight 700.** The display ceiling is 600. The calm visual register depends on this.
373
+ - **Don't use `backdrop-filter: blur(…)` or glass effects.** Anti-glass principle is named in `docs/design-system.md > Principles`. Elevation is built from ink shadows + glow, never from blur.
374
+ - **Don't render body paragraphs in `{font.mono}`.** Mono is for the technical layer only.
375
+ - **Don't apply `{shadow.glow}` to secondary buttons, cards, or surfaces.** The glow is the primary CTA signature; spreading it dilutes the brand.
376
+ - **Don't use emojis in component labels, button text, error messages, or markdown content authored by the system.** Consumers' user content may contain emojis (chat messages, names) — that's user data, not authored UI.
377
+ - **Don't reference external brand names (Vercel, Linear, etc.) as if endorsed.** When citing inspiration in docs or theme descriptions, prefix with "Inspired by, not affiliated with" — see `seven-themes-edge-cases-2026-05-22.md` for the trademark rule.
378
+ - **Don't pair the `comfortable` density on one screen with `spacious` controls on another.** Density is a global choice — pick one tier and stay consistent within a surface.
379
+
380
+ ---
381
+
382
+ ## 9. Agent Prompt Guide
383
+
384
+ Quick fragments for LLM assistants generating UI against `@usetheo/ui`. Drop into a prompt verbatim.
385
+
386
+ ### 9.1 Quick token reference
387
+
388
+ ```
389
+ PRIMARY var(--primary) hsl(262 83% 58%) #7C3AED Theo violet
390
+ ACCENT var(--accent) hsl(15 54% 53%) #C96442 Burnt sienna
391
+ FOREGROUND var(--foreground) hsl(0 0% 4%) #0A0A0A Ink (light) / inverted in dark
392
+ BACKGROUND var(--background) hsl(0 0% 100%) #FFFFFF Canvas
393
+ MUTED-FG var(--muted-foreground) hsl(0 0% 45%) #737373 Secondary text
394
+ BORDER var(--border) hsl(0 0% 91%) #E8E8E8 Hairline
395
+ SUCCESS var(--success) hsl(142 71% 36%) #16A34A
396
+ WARNING var(--warning) hsl(33 92% 44%) #D97706
397
+ DESTRUCTIVE var(--destructive) hsl(0 72% 51%) #DC2626
398
+ INFO var(--info) hsl(217 91% 60%) #3B82F6
399
+
400
+ FONT-DISPLAY Geist (weights 400/500/600)
401
+ FONT-BODY Geist (weights 400/500/600)
402
+ FONT-MONO Geist Mono (weights 400/500/600)
403
+
404
+ SPACING 4 px base — space-1 (4) … space-32 (128). Default control height 36 px.
405
+ RADIUS sm 4 / md 6 / lg 10 / xl 14 / 2xl 20 / full 9999
406
+ ```
407
+
408
+ ### 9.2 Prompt: build a dashboard list page
409
+
410
+ > Build a [DOMAIN] list page using `@usetheo/ui`. Compose `<PageShell title="…" description="…">` with an `<ActionBar>` (search input + primary action button). Inside, render `<DataTable>` with sticky header. Use `{typography.display-md}` for the title via PageShell's built-in. Status indicators use `<StatusDot variant="success|warning|destructive|info">`. Row actions use `<DropdownMenu>` with `variant="ghost"` trigger. No emojis, no inline hex — only token references. Match the `comfortable` density default.
411
+
412
+ ### 9.3 Prompt: build a settings panel
413
+
414
+ > Build a settings page using `@usetheo/ui`. Wrap content in `<PageShell title="Settings" description="…">`. Inside, stack `<Card>` sections per setting group. Each Card has `Card.Header` (title `{typography.title-lg}` + description `{typography.body-sm} text-muted-foreground`), `Card.Content` with form fields (`<Input>`, `<Switch>`, `<Select>`), and `Card.Footer` with a `<Button variant="primary">` save action. Destructive actions go in a final `<DangerZone>` composite. Spacing between Cards is `{spacing.6}` (24 px).
415
+
416
+ ### 9.4 Prompt: build an agent chat surface
417
+
418
+ > Build a chat surface using `@usetheo/ui`. Wrap in a flex column with internal max-width 768 px. Render messages via `<ChatMessage role="user|assistant|system" parts={…}>`. Tool calls and tool results inside `parts[]` render as `<ToolCall>` / `<ToolResult>` blocks (mono labels via `{typography.code-sm}`, prose via `{typography.body-sm}`). Use `<AgentEvent>` for non-message stream events. Composer at the bottom uses `<ChatComposer>` (a composite with `<Textarea>` + send button). No emojis. Streaming state uses `<AgentStreaming>`.
419
+
420
+ ### 9.5 Prompt: build a billing / pricing surface
421
+
422
+ > Build a pricing/billing page using `@usetheo/ui`. Wrap in `<PageShell title="Billing" description="…">`. Pricing tiers render as a 3-up grid of `<Card>` (tablet 2-up, mobile 1-up). Featured tier uses `<PlanBadge variant="primary">` and an outline on the Card (`border-primary`). Tier name in `{typography.title-lg}`, price in `{typography.display-xl}` with `font-mono` for digits, feature list in `{typography.body-md}` rows with a `<Check>` icon. CTA at the bottom: `<Button variant="primary" size="lg">` for the featured tier, `<Button variant="secondary" size="lg">` for the rest. Usage meters use `<UsageMeter>` and `<CostMeter>`.
423
+
424
+ ### 9.6 Prompt: use the design system tokens
425
+
426
+ > All styling must use `@usetheo/ui` tokens. Colors via Tailwind preset classes (`bg-primary`, `text-foreground`, `border-border`) or CSS vars (`hsl(var(--primary))`). Typography via preset (`text-display-md`, `text-body-md`, `text-label-caps`) — never raw `text-4xl`. Spacing via Tailwind utilities (`gap-4`, `p-5`) which map to the 4-px base. Radii via `rounded-lg` / `rounded-xl` / `rounded-md` mapped to system tokens. Never inline hex, never inline pixel values for spacing — always tokens. The `@usetheo/ui` Tailwind preset must be installed via `presets: [theoUiPreset]` in `tailwind.config.{ts,js}`.
427
+
428
+ ### 9.7 Component subpath import map (post-Brief-4 tree-shaking)
429
+
430
+ For Tailwind v4 / Vite projects, prefer subpath imports — they tree-shake per-component:
431
+
432
+ ```ts
433
+ import { Button } from "@usetheo/ui/button";
434
+ import { Card } from "@usetheo/ui/card";
435
+ import { Input } from "@usetheo/ui/input";
436
+ import { DataTable } from "@usetheo/ui/data-table";
437
+ import { PageShell } from "@usetheo/ui/page-shell";
438
+ import { ActionBar } from "@usetheo/ui/action-bar";
439
+ import { ChatMessage } from "@usetheo/ui/chat-message";
440
+ ```
441
+
442
+ Barrel imports (`import { Button } from "@usetheo/ui"`) work but ship the full barrel — acceptable for prototyping, not for production bundles.
443
+
444
+ ---
445
+
446
+ ## See also
447
+
448
+ - [`README.md`](./README.md) — package overview + install instructions
449
+ - [`llms.txt`](./llms.txt) — component catalog + anti-patterns + import recipes
450
+ - [`docs/design-system.md`](./docs/design-system.md) — long-form spec with ADR links
451
+ - [`CLAUDE.md`](./CLAUDE.md) — locked names, voice rules, quality gates
452
+ - [`CHANGELOG.md`](./CHANGELOG.md) — version history
453
+
454
+ ---
455
+
456
+ **End of DESIGN.md** — Violet Forge, `@usetheo/ui` 0.11.0-next.0
package/README.md CHANGED
@@ -128,6 +128,16 @@ import { ThemeProvider, ThemeScript } from "@usetheo/ui";
128
128
  </html>
129
129
  ```
130
130
 
131
+ ### Option C — install the agent skill
132
+
133
+ For AI coding assistants (Claude Code, Cursor, Codex), `@usetheo/ui` ships a companion **skill** that teaches the assistant how to use the library correctly — pick the right composite, respect the design tokens, run the slop test before shipping.
134
+
135
+ ```bash
136
+ npx skills add usetheodev/theo-ui
137
+ ```
138
+
139
+ The skill lives at [`skills/theo-ui/`](./skills/theo-ui/) and is installable via the [`vercel-labs/skills`](https://github.com/vercel-labs/skills) CLI. Four verbs: default (build), `audit`, `migrate`, `catalog`. See [`skills/theo-ui/README.md`](./skills/theo-ui/README.md) for details.
140
+
131
141
  ---
132
142
 
133
143
  ## Component catalog
@@ -141,7 +141,7 @@ function DataTable(props) {
141
141
  col.key
142
142
  );
143
143
  }),
144
- rowActions ? /* @__PURE__ */ jsx(Table.HeaderCell, { "aria-label": "Actions" }) : null
144
+ rowActions ? /* @__PURE__ */ jsx(Table.HeaderCell, { children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Actions" }) }) : null
145
145
  ] }) }),
146
146
  /* @__PURE__ */ jsx(Table.Body, { children: visibleData.map((row) => {
147
147
  const key = rowKey(row);
@@ -195,5 +195,5 @@ function DataTable(props) {
195
195
  }
196
196
 
197
197
  export { DataTable };
198
- //# sourceMappingURL=chunk-IWSLOBYG.js.map
199
- //# sourceMappingURL=chunk-IWSLOBYG.js.map
198
+ //# sourceMappingURL=chunk-ZNILW4G5.js.map
199
+ //# sourceMappingURL=chunk-ZNILW4G5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/composites/data-table/data-table.tsx"],"names":["_"],"mappings":"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","file":"chunk-ZNILW4G5.js","sourcesContent":["import { ChevronDown, ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { Fragment, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../../lib/cn.js\";\nimport { DropdownMenu } from \"../../primitives/dropdown-menu/index.js\";\nimport { EmptyState } from \"../../primitives/empty-state/index.js\";\nimport { Pagination } from \"../../primitives/pagination/index.js\";\nimport { Skeleton } from \"../../primitives/skeleton/index.js\";\nimport { Table } from \"../../primitives/table/index.js\";\n\n/**\n * DataTable — generic, sortable, expandable composite over `<Table>`.\n *\n * Adds operator-grade entity-list patterns on top of the plain Table\n * primitive: sortable headers, sticky header, expandable rows\n * (multi-row by default), row action menus (Dropdown), client-side\n * pagination, loading skeleton rows, empty state. Both sort and\n * pagination support controlled OR uncontrolled mode (consumer\n * passes onSortChange / onPageChange to take over state).\n *\n * @example\n * <DataTable\n * columns={[\n * { key: \"name\", label: \"Name\", sortable: true },\n * { key: \"status\", label: \"Status\" },\n * ]}\n * data={domains}\n * rowKey={(d) => d.id}\n * expandable={(d) => d.status === \"pending\" ? <DnsRecords domain={d} /> : null}\n * rowActions={(d) => (\n * <>\n * <DropdownMenu.Item onSelect={() => editDomain(d)}>Edit</DropdownMenu.Item>\n * <DropdownMenu.Item onSelect={() => deleteDomain(d)}>Delete</DropdownMenu.Item>\n * </>\n * )}\n * />\n */\nexport interface DataTableColumn<T> {\n key: string;\n label: ReactNode;\n align?: \"left\" | \"center\" | \"right\";\n sortable?: boolean;\n width?: string;\n render?: (row: T) => ReactNode;\n className?: string;\n}\n\nexport interface DataTableSort {\n key: string;\n direction: \"asc\" | \"desc\";\n}\n\nexport interface DataTableProps<T> {\n data: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string;\n stickyHeader?: boolean;\n expandable?: (row: T) => ReactNode | null;\n expandMode?: \"single\" | \"multiple\";\n rowActions?: (row: T) => ReactNode;\n pagination?: {\n pageSize: number;\n controlledPage?: number;\n onPageChange?: (page: number) => void;\n } | null;\n defaultSort?: DataTableSort;\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n loading?: boolean;\n emptyState?: ReactNode;\n className?: string;\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === null || a === undefined) return -1;\n if (b === null || b === undefined) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction DataTable<T>(props: DataTableProps<T>): ReactNode {\n const {\n data,\n columns,\n rowKey,\n stickyHeader = true,\n expandable,\n expandMode = \"multiple\",\n rowActions,\n pagination,\n defaultSort,\n sort: controlledSort,\n onSortChange,\n loading = false,\n emptyState,\n className,\n } = props;\n\n const isControlledSort = onSortChange !== undefined;\n const [uncontrolledSort, setUncontrolledSort] = useState<DataTableSort | null>(\n defaultSort ?? null,\n );\n const sort = isControlledSort ? (controlledSort ?? null) : uncontrolledSort;\n\n const isControlledPage = pagination?.controlledPage !== undefined;\n const [uncontrolledPage, setUncontrolledPage] = useState(0);\n const currentPage = isControlledPage ? (pagination?.controlledPage ?? 0) : uncontrolledPage;\n\n // EC-9: clamp pageSize to >= 1 to avoid divide-by-zero / infinite render\n const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);\n\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n\n function handleSort(columnKey: string) {\n // Cycle: none → asc → desc → none\n let nextSort: DataTableSort | null;\n if (sort?.key !== columnKey) {\n nextSort = { key: columnKey, direction: \"asc\" };\n } else if (sort.direction === \"asc\") {\n nextSort = { key: columnKey, direction: \"desc\" };\n } else {\n nextSort = null;\n }\n if (isControlledSort) {\n onSortChange?.(nextSort);\n } else {\n setUncontrolledSort(nextSort);\n // EC-8: sort change resets pagination to page 0\n if (!isControlledPage) setUncontrolledPage(0);\n }\n }\n\n function handlePageChange(page: number) {\n // Pagination uses 1-indexed; internal state 0-indexed\n const zeroIdx = page - 1;\n if (isControlledPage) {\n pagination?.onPageChange?.(zeroIdx);\n } else {\n setUncontrolledPage(zeroIdx);\n }\n }\n\n function toggleExpand(key: string) {\n if (expandMode === \"single\") {\n setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])));\n } else {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }\n }\n\n // Apply client-side sort in uncontrolled mode\n const sortedData = useMemo(() => {\n if (isControlledSort || sort === null) return data;\n const col = columns.find((c) => c.key === sort.key);\n if (!col) return data;\n const sorted = [...data].sort((a, b) => {\n const aVal = col.render\n ? null\n : (a as Record<string, unknown>)[sort.key as keyof T as string];\n const bVal = col.render\n ? null\n : (b as Record<string, unknown>)[sort.key as keyof T as string];\n const cmp = compareValues(aVal, bVal);\n return sort.direction === \"asc\" ? cmp : -cmp;\n });\n return sorted;\n }, [data, sort, isControlledSort, columns]);\n\n // Apply client-side pagination in uncontrolled mode\n const visibleData = useMemo(() => {\n if (!pagination) return sortedData;\n if (isControlledPage) return sortedData; // consumer pre-sliced\n const start = currentPage * effectivePageSize;\n return sortedData.slice(start, start + effectivePageSize);\n }, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);\n\n // EC-1 fix: compute colSpan accounting for chevron + actions columns\n const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);\n const expandedColSpan = columns.length + extraCols;\n const totalCols = columns.length + extraCols;\n\n // Loading state (EC-7: loading > empty)\n if (loading) {\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => (\n <Table.HeaderCell key={col.key} align={col.align}>\n {col.label}\n </Table.HeaderCell>\n ))}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {Array.from({ length: 5 }, (_, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders\n <Table.Row key={`skeleton-${i}`}>\n {Array.from({ length: totalCols }, (_, j) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders\n <Table.Cell key={`s-${i}-${j}`}>\n <Skeleton className=\"h-4 w-full\" />\n </Table.Cell>\n ))}\n </Table.Row>\n ))}\n </Table.Body>\n </Table>\n </div>\n );\n }\n\n // Empty state (after loading check)\n if (sortedData.length === 0) {\n return (\n <div className={cn(\"w-full\", className)}>\n {emptyState ?? <EmptyState title=\"No data\" description=\"There's nothing here yet.\" />}\n </div>\n );\n }\n\n const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;\n\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 z-10 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => {\n const isSortable = col.sortable === true;\n const isActive = sort?.key === col.key;\n return (\n <Table.HeaderCell\n key={col.key}\n align={col.align}\n onSort={isSortable ? () => handleSort(col.key) : undefined}\n sortDirection={isSortable ? (isActive ? sort?.direction : \"none\") : undefined}\n style={col.width ? { width: col.width } : undefined}\n >\n {col.label}\n </Table.HeaderCell>\n );\n })}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {visibleData.map((row) => {\n const key = rowKey(row);\n const expandedContent = expandable ? expandable(row) : null;\n const isExpandable = expandedContent !== null && expandedContent !== undefined;\n const isExpanded = expanded.has(key);\n return (\n <Fragment key={key}>\n <Table.Row>\n {expandable ? (\n <Table.Cell>\n {isExpandable ? (\n <button\n type=\"button\"\n onClick={() => toggleExpand(key)}\n aria-expanded={isExpanded}\n aria-controls={`expanded-${key}`}\n aria-label={isExpanded ? \"Collapse row\" : \"Expand row\"}\n className=\"inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted\"\n >\n {isExpanded ? (\n <ChevronDown aria-hidden=\"true\" className=\"size-4\" />\n ) : (\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n )}\n </button>\n ) : null}\n </Table.Cell>\n ) : null}\n {columns.map((col) => (\n <Table.Cell key={col.key} align={col.align} className={col.className}>\n {col.render\n ? col.render(row)\n : String((row as Record<string, unknown>)[col.key] ?? \"\")}\n </Table.Cell>\n ))}\n {rowActions ? (\n <Table.Cell align=\"right\">\n <DropdownMenu>\n <DropdownMenu.Trigger\n aria-label=\"Row actions\"\n className={cn(\n \"inline-flex size-7 items-center justify-center rounded-md\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">{rowActions(row)}</DropdownMenu.Content>\n </DropdownMenu>\n </Table.Cell>\n ) : null}\n </Table.Row>\n {isExpanded && isExpandable ? (\n <tr id={`expanded-${key}`}>\n <td colSpan={expandedColSpan} className=\"bg-muted/30 p-4\">\n {expandedContent}\n </td>\n </tr>\n ) : null}\n </Fragment>\n );\n })}\n </Table.Body>\n </Table>\n {pagination && totalPages > 1 ? (\n <div className=\"mt-4 flex items-center justify-end\">\n <Pagination\n currentPage={currentPage + 1}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable };\n"]}