@vegastack/design 0.2.0 → 0.3.1

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.
@@ -653,19 +653,38 @@ export async function main(argv) {
653
653
  installed = installed.filter((c) => res.some((re) => re.test(c.name)));
654
654
  }
655
655
  if (installed.length === 0) {
656
+ // `--fail-on-update` is the CI drift gate. Exiting 0 here would make it FAIL OPEN: a project
657
+ // whose components live outside the default path (any monorepo, any package-based layout, or a
658
+ // wrong `--dir`) would get a permanently green gate that checked nothing at all. Zero components
659
+ // under an explicit gate is a misconfiguration, not a clean bill of health — say so and fail.
660
+ // Without the gate flag this stays informational and exits 0, since "no components yet" is a
661
+ // legitimate state for a project mid-setup.
662
+ const gateOnEmpty = opts.failOnUpdate === true;
656
663
  if (opts.json)
657
664
  console.log(
658
665
  JSON.stringify(
659
- { registry: idxUrl, checked: 0, updates: 0, items: [] },
666
+ {
667
+ registry: idxUrl,
668
+ checked: 0,
669
+ updates: 0,
670
+ items: [],
671
+ ...(gateOnEmpty ? { error: "no-components-found" } : {}),
672
+ },
660
673
  null,
661
674
  2,
662
675
  ),
663
676
  );
677
+ else if (gateOnEmpty)
678
+ console.error(
679
+ `✗ no VegaStack components found in ${terminalText(dir)}, but --fail-on-update was set.\n` +
680
+ ` A drift gate that scans nothing passes vacuously, so this is an error, not a pass.\n` +
681
+ ` Point it at the right directory (\`--dir <path>\`) or drop --fail-on-update.`,
682
+ );
664
683
  else
665
684
  console.log(
666
685
  `No VegaStack components found in ${terminalText(dir)}. (Add some with \`shadcn add @vegastack/<name>\`.)`,
667
686
  );
668
- return 0;
687
+ return gateOnEmpty ? 1 : 0;
669
688
  }
670
689
 
671
690
  const aliases = componentsJson?.aliases ?? {};
package/bin/doctor.mjs ADDED
@@ -0,0 +1,332 @@
1
+ // `vegastack-design doctor` — check a consuming project's setup.
2
+ //
3
+ // Motivated by a real consumer failure (VegaStack CRM, 2026-07-27): a missing
4
+ // `@tailwindcss/postcss` plugin produces two different, equally misleading results.
5
+ // Under Turbopack the build dies with `Can't resolve 'tw-animate-css'`, naming a
6
+ // dependency that is installed and fine. Under webpack the build SUCCEEDS, the token
7
+ // theme lands (it is literal CSS inside preset.css), and zero utility classes are
8
+ // generated — so the app renders with correct colours and no spacing or layout, which
9
+ // reads as "the design system is broken".
10
+ //
11
+ // A human will not attribute either symptom correctly, which is exactly why this is a
12
+ // command and not a paragraph in a guide. Every check here maps to a documented failure
13
+ // mode in the Troubleshooting guide.
14
+ //
15
+ // Read-only: it never writes, installs, or edits. Exit 0 = all good, 1 = a real problem,
16
+ // so it composes into CI as `vegastack-design doctor`.
17
+
18
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
19
+ import { dirname, join, relative } from "node:path";
20
+
21
+ const POSTCSS_CONFIGS = [
22
+ "postcss.config.mjs",
23
+ "postcss.config.js",
24
+ "postcss.config.cjs",
25
+ "postcss.config.ts",
26
+ "postcss.config.json",
27
+ ".postcssrc",
28
+ ".postcssrc.json",
29
+ ];
30
+
31
+ const CSS_SEARCH_DIRS = [
32
+ "app",
33
+ "src/app",
34
+ "src/styles",
35
+ "styles",
36
+ "src",
37
+ "apps",
38
+ ];
39
+
40
+ const USAGE = `
41
+ Usage: vegastack-design doctor [options]
42
+
43
+ Checks a consuming project's VegaStack setup and reports what is wrong and how to fix it.
44
+
45
+ Options:
46
+ --dir <path> Project root to inspect (default: the current directory)
47
+ -h, --help Show this help
48
+
49
+ Exit codes: 0 = no problems · 1 = at least one failure · 2 = bad usage
50
+ `.trim();
51
+
52
+ /** Collect .css files under a few conventional roots, shallowly, without walking node_modules. */
53
+ function findCssFiles(root, max = 200) {
54
+ const out = [];
55
+ const seen = new Set();
56
+ const walk = (dir, depth) => {
57
+ if (out.length >= max || depth > 4) return;
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(dir, { withFileTypes: true });
61
+ } catch {
62
+ return;
63
+ }
64
+ for (const e of entries) {
65
+ if (out.length >= max) return;
66
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
67
+ const full = join(dir, e.name);
68
+ if (seen.has(full)) continue;
69
+ seen.add(full);
70
+ if (e.isDirectory()) walk(full, depth + 1);
71
+ else if (e.name.endsWith(".css")) out.push(full);
72
+ }
73
+ };
74
+ for (const d of CSS_SEARCH_DIRS) {
75
+ const full = join(root, d);
76
+ if (existsSync(full) && statSync(full).isDirectory()) walk(full, 0);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /** CSS comments frequently *mention* the thing we are looking for — strip them before matching. */
82
+ function stripCssComments(src) {
83
+ return src.replace(/\/\*[\s\S]*?\*\//g, "");
84
+ }
85
+
86
+ /** Nearest ancestor of `from` (inclusive) that has a package.json, bounded by `root`. */
87
+ function nearestPackageRoot(from, root) {
88
+ let dir = from;
89
+ for (let i = 0; i < 8; i += 1) {
90
+ if (existsSync(join(dir, "package.json"))) return dir;
91
+ if (dir === root) break;
92
+ const parent = dirname(dir);
93
+ if (parent === dir) break;
94
+ dir = parent;
95
+ }
96
+ return root;
97
+ }
98
+
99
+ function readIfExists(path) {
100
+ try {
101
+ return readFileSync(path, "utf8");
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function readJson(path) {
108
+ const raw = readIfExists(path);
109
+ if (raw == null) return null;
110
+ try {
111
+ return JSON.parse(raw);
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ export function main(argv = []) {
118
+ if (argv.includes("-h") || argv.includes("--help")) {
119
+ console.log(USAGE);
120
+ return 0;
121
+ }
122
+ const dirFlag = argv.indexOf("--dir");
123
+ if (dirFlag !== -1 && argv[dirFlag + 1] == null) {
124
+ console.error("doctor: --dir requires a path\n");
125
+ console.error(USAGE);
126
+ return 2;
127
+ }
128
+ const root = dirFlag === -1 ? process.cwd() : argv[dirFlag + 1];
129
+
130
+ if (!existsSync(root)) {
131
+ console.error(`doctor: no such directory: ${root}`);
132
+ return 2;
133
+ }
134
+
135
+ const results = [];
136
+ const ok = (name, detail) => results.push({ level: "ok", name, detail });
137
+ const warn = (name, detail, fix) =>
138
+ results.push({ level: "warn", name, detail, fix });
139
+ const fail = (name, detail, fix) =>
140
+ results.push({ level: "fail", name, detail, fix });
141
+
142
+ // ---- 1. the design system is installed -------------------------------------------------
143
+ const pkg = readJson(join(root, "package.json"));
144
+ const deps = {
145
+ ...(pkg?.dependencies ?? {}),
146
+ ...(pkg?.devDependencies ?? {}),
147
+ };
148
+ const designInstalled =
149
+ "@vegastack/design" in deps ||
150
+ existsSync(join(root, "node_modules", "@vegastack", "design"));
151
+
152
+ if (designInstalled) {
153
+ const installed = readJson(
154
+ join(root, "node_modules", "@vegastack", "design", "package.json"),
155
+ );
156
+ ok(
157
+ "@vegastack/design installed",
158
+ installed?.version
159
+ ? `v${installed.version}`
160
+ : (deps["@vegastack/design"] ?? ""),
161
+ );
162
+ } else {
163
+ fail(
164
+ "@vegastack/design installed",
165
+ "not found in package.json or node_modules",
166
+ "pnpm add @vegastack/design",
167
+ );
168
+ }
169
+
170
+ // ---- 2. the preset is imported ----------------------------------------------------------
171
+ const cssFiles = findCssFiles(root);
172
+ const presetFiles = cssFiles.filter((f) =>
173
+ (readIfExists(f) ?? "").includes("@vegastack/design/preset.css"),
174
+ );
175
+
176
+ if (presetFiles.length > 0) {
177
+ ok(
178
+ "preset.css imported",
179
+ presetFiles.map((f) => relative(root, f)).join(", "),
180
+ );
181
+ } else {
182
+ fail(
183
+ "preset.css imported",
184
+ cssFiles.length === 0
185
+ ? "no .css files found under app/, src/, or styles/"
186
+ : `none of ${cssFiles.length} .css file(s) import it`,
187
+ 'add `@import "@vegastack/design/preset.css";` to your global stylesheet',
188
+ );
189
+ }
190
+
191
+ // ---- 3. THE BIG ONE: the Tailwind PostCSS plugin ----------------------------------------
192
+ // Without it Tailwind never runs: no utilities are generated, and depending on the bundler
193
+ // you either get a misleading `Can't resolve 'tw-animate-css'` or a silently unstyled app.
194
+ // In a workspace the config correctly lives in the APP package, not the repo root, so search
195
+ // every package that owns a preset-importing stylesheet as well as the root itself.
196
+ const postcssRoots = [
197
+ root,
198
+ ...presetFiles.map((f) => nearestPackageRoot(dirname(f), root)),
199
+ ].filter((d, i, a) => a.indexOf(d) === i);
200
+ const postcssPath = postcssRoots
201
+ .flatMap((d) => POSTCSS_CONFIGS.map((n) => join(d, n)))
202
+ .find(existsSync);
203
+ const postcssOwnerPkg = postcssPath
204
+ ? readJson(join(dirname(postcssPath), "package.json"))
205
+ : null;
206
+ const postcssInline = (postcssOwnerPkg ?? pkg)?.postcss
207
+ ? JSON.stringify((postcssOwnerPkg ?? pkg).postcss)
208
+ : null;
209
+ const postcssSource = postcssPath ? readIfExists(postcssPath) : postcssInline;
210
+ const hasPlugin =
211
+ postcssSource != null && postcssSource.includes("@tailwindcss/postcss");
212
+
213
+ if (hasPlugin) {
214
+ ok(
215
+ "Tailwind PostCSS plugin",
216
+ postcssPath ? relative(root, postcssPath) : "package.json#postcss",
217
+ );
218
+ } else if (postcssSource != null) {
219
+ fail(
220
+ "Tailwind PostCSS plugin",
221
+ `${postcssPath ? relative(root, postcssPath) : "package.json#postcss"} exists but does not configure @tailwindcss/postcss`,
222
+ 'add `"@tailwindcss/postcss": {}` to its plugins',
223
+ );
224
+ } else {
225
+ fail(
226
+ "Tailwind PostCSS plugin",
227
+ "no PostCSS config found — Tailwind will not run, so NO utility classes are generated",
228
+ 'pnpm add -D @tailwindcss/postcss, then create postcss.config.mjs:\n const config = { plugins: { "@tailwindcss/postcss": {} } };\n export default config;',
229
+ );
230
+ }
231
+
232
+ // ---- 4. no duplicate Tailwind import ----------------------------------------------------
233
+ // preset.css already imports Tailwind; a second bare import is the documented cause of
234
+ // "utilities exist but everything is unstyled".
235
+ const duplicateTailwind = presetFiles.filter((f) => {
236
+ const src = stripCssComments(readIfExists(f) ?? "");
237
+ return /@import\s+["']tailwindcss["']/.test(src);
238
+ });
239
+ if (duplicateTailwind.length > 0) {
240
+ warn(
241
+ "no duplicate Tailwind import",
242
+ `${duplicateTailwind.map((f) => relative(root, f)).join(", ")} also imports "tailwindcss" directly`,
243
+ "remove it — preset.css imports Tailwind itself",
244
+ );
245
+ } else if (presetFiles.length > 0) {
246
+ ok("no duplicate Tailwind import", "preset.css is the only Tailwind entry");
247
+ }
248
+
249
+ // ---- 5. registry access is configured ---------------------------------------------------
250
+ let componentsJsonPath = join(root, "components.json");
251
+ let componentsJson = readJson(componentsJsonPath);
252
+ if (componentsJson == null) {
253
+ // Walk up: in a workspace the canonical components.json commonly sits at the repo root.
254
+ let dir = root;
255
+ for (let i = 0; i < 5 && componentsJson == null; i += 1) {
256
+ const parent = dirname(dir);
257
+ if (parent === dir) break;
258
+ dir = parent;
259
+ const candidate = join(dir, "components.json");
260
+ if (existsSync(candidate)) {
261
+ componentsJsonPath = candidate;
262
+ componentsJson = readJson(candidate);
263
+ }
264
+ }
265
+ }
266
+ if (componentsJson == null) {
267
+ warn(
268
+ "registry configured",
269
+ "no components.json here or in any parent directory",
270
+ "see the Quickstart — needed before `shadcn add @vegastack/<name>`",
271
+ );
272
+ } else if (componentsJson.registries?.["@vegastack"] == null) {
273
+ fail(
274
+ "registry configured",
275
+ 'components.json has no `registries["@vegastack"]` entry',
276
+ "add the registries block from the Quickstart",
277
+ );
278
+ } else {
279
+ ok(
280
+ "registry configured",
281
+ `${relative(root, componentsJsonPath) || "components.json"} declares @vegastack`,
282
+ );
283
+ }
284
+
285
+ // ---- 6. monorepo hint --------------------------------------------------------------------
286
+ // Source detection is relative to the CSS file, so components living outside the app's tree
287
+ // compile to nothing unless declared. Only worth saying when this actually looks like a workspace.
288
+ const isWorkspace =
289
+ existsSync(join(root, "pnpm-workspace.yaml")) ||
290
+ Array.isArray(pkg?.workspaces) ||
291
+ pkg?.workspaces != null;
292
+ if (isWorkspace && presetFiles.length > 0) {
293
+ const declaresSource = presetFiles.some((f) =>
294
+ (readIfExists(f) ?? "").includes("@source"),
295
+ );
296
+ if (declaresSource) {
297
+ ok("monorepo sources declared", "@source directives present");
298
+ } else {
299
+ warn(
300
+ "monorepo sources declared",
301
+ "workspace detected but no @source directive — components outside this app's tree will compile to nothing",
302
+ 'add e.g. `@source "../../../../packages/ui/src";` next to the preset import',
303
+ );
304
+ }
305
+ }
306
+
307
+ // ---- report -------------------------------------------------------------------------------
308
+ const glyph = { ok: "✓", warn: "!", fail: "✗" };
309
+ console.log("");
310
+ for (const r of results) {
311
+ console.log(
312
+ ` ${glyph[r.level]} ${r.name}${r.detail ? ` — ${r.detail}` : ""}`,
313
+ );
314
+ if (r.fix) console.log(` fix: ${r.fix}`);
315
+ }
316
+
317
+ const failures = results.filter((r) => r.level === "fail").length;
318
+ const warnings = results.filter((r) => r.level === "warn").length;
319
+ console.log("");
320
+ if (failures > 0) {
321
+ console.log(
322
+ ` ${failures} problem(s), ${warnings} warning(s). See https://design.vegastack.com/docs/guides/troubleshooting`,
323
+ );
324
+ return 1;
325
+ }
326
+ console.log(
327
+ warnings > 0
328
+ ? ` setup looks correct (${warnings} warning(s)).`
329
+ : " setup looks correct.",
330
+ );
331
+ return 0;
332
+ }
@@ -5,6 +5,7 @@
5
5
  // check-updates Show which copied-in components have newer registry versions (what to re-pull).
6
6
  // verify Verify a registry item's integrity before/after `shadcn add` (Sigstore + hash).
7
7
  // skills Install the bundled VegaStack agent skills into the consuming project.
8
+ // doctor Check a consuming project's setup (PostCSS plugin, preset import, registry).
8
9
  //
9
10
  // The bin is named `vegastack-design` (NOT `vegastack`) so it never collides with a platform CLI.
10
11
  // `check-updates` is imported in-process; `verify` is spawned (it's the standalone, hash-parity-tested
@@ -23,6 +24,7 @@ Commands:
23
24
  check-updates Show which copied-in components have newer registry versions
24
25
  verify Verify a registry item's integrity (pre/post \`shadcn add\`)
25
26
  skills Install the VegaStack agent skills (Claude Code + Codex)
27
+ doctor Check this project's setup and report what is wrong
26
28
 
27
29
  Run \`vegastack-design <command> --help\` for command options.
28
30
  -v, --version Print version
@@ -61,6 +63,12 @@ if (cmd === "skills") {
61
63
  process.exit(main(rest));
62
64
  }
63
65
 
66
+ if (cmd === "doctor") {
67
+ // imported in-process (our own code, read-only, no network, no credentials)
68
+ const { main } = await import(new URL("./doctor.mjs", HERE).href);
69
+ process.exit(main(rest));
70
+ }
71
+
64
72
  if (cmd === "verify") {
65
73
  // spawn the standalone verifier untouched; mark the dispatch so it skips its deprecation notice.
66
74
  const verifier = fileURLToPath(new URL("./verify-registry-item.mjs", HERE));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegastack/design",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "VegaStack design system — cn utility, icon runtime, Tailwind v4 preset, and the vegastack-design CLI (tokens ship separately as @vegastack/design-tokens)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -94,8 +94,8 @@
94
94
  "thesvg": "^3.2.6",
95
95
  "tw-animate-css": "^1.4.0",
96
96
  "typescript": "^6.0.3",
97
- "@vegastack/eslint-config": "0.0.0",
98
- "@vegastack/typescript-config": "0.0.0"
97
+ "@vegastack/typescript-config": "0.0.0",
98
+ "@vegastack/eslint-config": "0.0.0"
99
99
  },
100
100
  "publishConfig": {
101
101
  "access": "public"
@@ -81,7 +81,9 @@ contract.
81
81
  - **Compound parts import flat** — `import { DialogTrigger, DialogContent }`. Sub-property access
82
82
  (`<Dialog.Trigger>`) only works inside a `'use client'` file, because across the RSC boundary the
83
83
  compound is a client-reference proxy and the sub-property is `undefined`.
84
- - **Polymorphism** uses Base UI's `render` prop, never Radix's `asChild`.
84
+ - **Polymorphism** uses Base UI's `render` prop, never Radix's `asChild`. When `render` swaps a
85
+ button-like component's element for a non-button (e.g. `Button render={<Link/>}`), also pass
86
+ `nativeButton={false}` — Base UI warns otherwise.
85
87
 
86
88
  ## Do / Don't
87
89
 
@@ -3,7 +3,7 @@
3
3
  <!-- GENERATED — do not hand-edit. Regenerated from the design system's component contract,
4
4
  which is the authority for membership and counts. -->
5
5
 
6
- **96 components**, plus 439 animated-icon items, 2 hooks (`use-animation-replay`, `use-mobile`), and 1 starter block (`dashboard-01`) — 538 registry items in total.
6
+ **110 components**, plus 439 animated-icon items, 6 hooks (`use-animation-replay`, `use-drag-reorder`, `use-file-drop`, `use-list-nav`, `use-mobile`, `use-platform`), and 1 starter block (`dashboard-01`) — 556 registry items in total.
7
7
 
8
8
  Install any of them with `shadcn add @vegastack/<name>`. Animated icons install as
9
9
  `@vegastack/icon-<name>`; the bare name is reserved for components, so `icon-button` is the
@@ -23,15 +23,19 @@ component and never an icon.
23
23
 
24
24
  - **`auto-save-input`** — An input that debounces edits and persists them via an async onSave, with an inline idle/saving/saved/error status.
25
25
  - **`checkbox`** — A binary (or tri-state) toggle — checked, unchecked, indeterminate, disabled, built on Base UI Checkbox.
26
+ - **`chip-input`** — Free-token entry field — Enter/comma/paste commits chips, Backspace removes, per-chip validation marks invalid entries instead of dropping them. Combobox field chrome + real Tag chips.
26
27
  - **`color-picker`** — A swatch-triggered popover presenting a grid of preset colors — pick one, fire onValueChange, mark the selection.
27
28
  - **`combobox`** — A filterable, keyboard-navigable listbox behind a text input — type-to-filter, grouped items, async status, and a multi-select chip mode.
28
29
  - **`country-select`** — A searchable country combobox returning the ISO 3166-1 alpha-2 code, with flag + name. Built on Combobox.
29
30
  - **`date-picker`** — Pick a single date or a date range from a calendar popover — token-styled, keyboard-navigable, with optional quick presets.
31
+ - **`dropzone`** — File acquisition surface — drop, click-to-browse, and paste — as a thin shell over use-file-drop; the surface is the named focusable control over a hidden picker-bridge input; data-dragging/data-drag-invalid styling flags.
32
+ - **`editable-cell`** — Inline-editable value with an async commit lifecycle — optimistic display, saving/saved/error status, revert on a rejected write, and a typed text/select/custom editor registry.
30
33
  - **`emoji-picker`** — A popover with a searchable, category-grouped grid of emoji that returns the selected character via onSelect (curated set, not full Unicode).
31
34
  - **`field`** — A form-field wrapper — label, inline label action, description, and error/success message, built on Base UI Field.
32
35
  - **`field-inline`** — Click-to-edit text — displays a value, swaps to a focused input on click, commits on Enter or blur, cancels on Escape.
33
36
  - **`input`** — A styled Base UI input — all input types, Field state data attributes, error and disabled states, focus-visible ring, and optional prefix/suffix addons.
34
37
  - **`label`** — A styled native label for form controls — htmlFor association, disabled dimming, optional required indicator.
38
+ - **`number-field`** — Locale-aware numeric input on Base UI's NumberField in Input's field chrome — Intl formatting (money is a format prop), min/max/step, keyboard stepping, wheel scrub, full-height steppers.
35
39
  - **`otp-input`** — A multi-slot one-time-passcode input — keyboard navigation, paste distribution, masking, disabled, built on Base UI OTP Field.
36
40
  - **`password-input`** — A password field with a show/hide eye toggle and an optional live requirements checklist.
37
41
  - **`radio-group`** — A set of mutually-exclusive options — single selection, arrow-key navigation, disabled, built on Base UI Radio Group.
@@ -64,13 +68,17 @@ component and never an icon.
64
68
  - **`relative-time`** — Render a date as a human-relative string ("2 hours ago", "yesterday") with native Intl.RelativeTimeFormat — self-updating, with an absolute-date tooltip.
65
69
  - **`status-icon`** — A small status indicator icon — todo, in progress, blocked, done — each mapping to a lucide icon and semantic color.
66
70
  - **`table`** — Styled semantic table primitives — a scrollable container plus header, body, footer, row, head, cell, caption.
71
+ - **`timeline`** — Rail geometry for chronological records — a continuous connector with a node per entry. Rows compose Item parts; separators render through Marker; entries carry content-visibility render skipping.
67
72
  - **`truncated-text`** — Truncate text to one line or N lines with an ellipsis, revealing the full text in a tooltip only when it overflows.
68
73
 
69
74
  ## Data
70
75
 
76
+ - **`data-grid`** — The full-parity grid — TanStack-sorted multi-key sort, column picker with responsive revelation, collapsible grouping, keyboard-continuous load-more, opt-in virtualization, and an APG grid keyboard layer with inline cell editing.
71
77
  - **`data-list`** — A generic, typed data table — configurable columns, row selection, sortable headers, plus loading and empty states.
72
78
  - **`filter-bar`** — A row of removable filter chips, an "Add filter" dropdown, and an optional search input — for list and table filter toolbars.
79
+ - **`filter-bar-managed`** — The stateful nested and/or filter builder — host-injected field grammar (vocabulary + per-type value editors), depth and condition caps, focus-managed removal, and a removable FilterChip summary.
73
80
  - **`property-list`** — Record-facts rows: an icon+label column beside a value column, as an accessible definition list.
81
+ - **`sortable-list`** — Reorderable rows on ItemGroup/Item via use-drag-reorder — pointer drag with drop indicators, keyboard move mode, a lossless Move menu, and server-refusable moves. Controlled; the host owns the order.
74
82
 
75
83
  ## Overlay
76
84
 
@@ -81,6 +89,7 @@ component and never an icon.
81
89
  - **`hover-card`** — A rich preview panel that opens on hover or focus — interactive content, four directions, forgiving delays.
82
90
  - **`popover`** — A click-triggered floating panel for arbitrary content — positioning, an optional arrow, and built-in dismiss.
83
91
  - **`sheet`** — A dialog that slides in from a screen edge — four sides, header/footer layout, focus trapping, animated slide.
92
+ - **`shortcut-overlay`** — The ?-triggered dialog listing keyboard shortcuts, rendered from a declaration registry (keys, label, category, when) — grouped, filterable, platform-aware via use-platform + Kbd.
84
93
  - **`tooltip`** — A floating label on hover or focus — smart shared delay, rich content, optional keyboard hints, collision-aware positioning.
85
94
 
86
95
  ## Navigation
@@ -91,13 +100,15 @@ component and never an icon.
91
100
  - **`page-header`** — The standardized header at the top of a page — back button, breadcrumb trail, title, description, actions, secondary menu, and a favorite star.
92
101
  - **`pagination`** — Page navigation — previous/next, numbered page links, an ellipsis for long ranges, and the active page.
93
102
  - **`sidebar`** — A collapsible app navigation rail — header/content/footer, labelled groups, menu items with active state, and an expand/collapse trigger.
103
+ - **`stepper`** — A bounded linear process as an ordered list — complete/current/upcoming/error states on StatusIcon's vocabulary, aria-current=step, advance-gating message, focus follows the process.
94
104
  - **`tabs`** — Layered content sections — line or pill variants, optional icons and count badges, horizontal or vertical, full keyboard navigation.
95
105
 
96
106
  ## Feedback
97
107
 
108
+ - **`action-bar`** — Floating contextual bar — status region + action children, CSS-only enter/exit, raised band. Bulk selection, unsaved changes, and batch progress are recipes over it.
98
109
  - **`alert`** — A status banner — five semantic variants, an optional icon, and an optional dismiss button.
99
110
  - **`progress`** — A determinate horizontal progress bar for measurable, ongoing tasks — built on Base UI Progress.
100
- - **`progress-indicator`** — A compact circular pie-fill progress indicator (0–100%) — a server-safe SVG glyph in circle or squircle shapes.
111
+ - **`progress-indicator`** — A compact circular pie-fill progress indicator (0–100%) with optional visible percentage variants.
101
112
  - **`provider`** — The single app-root wrapper — theme (next-themes), Sonner toasts, tooltip coordination, and text direction in one mount-once component.
102
113
  - **`skeleton`** — A token-driven loading placeholder — line, circle, rect, card shapes, configurable count, reduced-motion-aware pulse.
103
114
  - **`sonner`** — Brief, non-blocking notifications — a token-styled Sonner toaster with success/error/warning/info variants that follows the theme.
@@ -106,6 +117,7 @@ component and never an icon.
106
117
  ## Layout
107
118
 
108
119
  - **`app-shell`** — The shared dashboard layout — a skip-linked sidebar + header + scrollable main region, composing Sidebar/SidebarTrigger into one reusable, hash-tracked shell.
120
+ - **`board`** — Kanban columns over use-drag-reorder — content/chrome split (host renders card content only), pointer drag, keyboard move mode + roving focus, lossless per-card Move menu with lock reasons, server-refusable moves, collapsed lanes, Empty-bordered drop targets.
109
121
  - **`resizable`** — Draggable, keyboard-resizable split panes — horizontal or vertical, nestable, with an optional collapsible panel. Built on react-resizable-panels.
110
122
  - **`scroll-area`** — A scroll container with custom, auto-hiding scrollbars — dual-axis, token-styled, built on Base UI ScrollArea.
111
123
  - **`separator`** — A thin rule dividing content — horizontal or vertical, decorative by default, built on Base UI.
@@ -113,8 +125,10 @@ component and never an icon.
113
125
 
114
126
  ## Media
115
127
 
128
+ - **`audio-player`** — A compact custom audio transport with standard media controls, seek, mute, settings, and keyboard shortcuts.
116
129
  - **`image`** — A presentational framed image with aspect-ratio, rounding, a loading skeleton, and an error fallback.
117
130
  - **`notification-bell`** — A bell icon button with an unread-count badge overlay. Presentational — the app supplies the count.
131
+ - **`video-player`** — A framed video player with the same grouped custom transport controls as Audio Player.
118
132
 
119
133
  ## Rich text
120
134