@vegastack/design 0.6.1 → 0.7.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.
package/bin/doctor.mjs CHANGED
@@ -12,11 +12,17 @@
12
12
  // command and not a paragraph in a guide. Every check here maps to a documented failure
13
13
  // mode in the Troubleshooting guide.
14
14
  //
15
+ // It also scans the project's OWN source for vocabulary the shadcn reset retired (0.5.0 → now):
16
+ // `text-h2`, `bg-destructive-subtle`, `var(--z-toast)`, an `@/components/ui/icon-button` import.
17
+ // A retired utility compiles to NOTHING — no build error, no type error — so a heading silently
18
+ // renders as body text. The Regent consumer carried 338 such classes past every other check
19
+ // (2026-09-23), which is why this is a failing check and not a guide section.
20
+ //
15
21
  // Read-only: it never writes, installs, or edits. Exit 0 = all good, 1 = a real problem,
16
22
  // so it composes into CI as `vegastack-design doctor`.
17
23
 
18
24
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
19
- import { dirname, join, relative } from "node:path";
25
+ import { dirname, join, relative, resolve, sep } from "node:path";
20
26
 
21
27
  const POSTCSS_CONFIGS = [
22
28
  "postcss.config.mjs",
@@ -41,6 +47,8 @@ const USAGE = `
41
47
  Usage: vegastack-design doctor [options]
42
48
 
43
49
  Checks a consuming project's VegaStack setup and reports what is wrong and how to fix it.
50
+ Also scans the project's own source (not node_modules, build output, or the components.json
51
+ \`ui\` alias directory) for vocabulary the shadcn reset retired, and reports each as file:line.
44
52
 
45
53
  Options:
46
54
  --dir <path> Project root to inspect (default: the current directory)
@@ -114,6 +122,469 @@ function readJson(path) {
114
122
  }
115
123
  }
116
124
 
125
+ // ---- retired vocabulary ----------------------------------------------------------------------
126
+ // Each entry: a pattern over one source line, a replacement hint (from the "Migrating to the shadcn
127
+ // reset" guide, where every row has its full table), and — for token families a project may
128
+ // legitimately define for itself — the custom property whose DECLARATION in the scanned source
129
+ // means "this is yours, not ours". Kept here and dependency-free on purpose: doctor must run in a
130
+ // fresh consumer before anything else is installed correctly.
131
+ const GUIDE = "https://design.vegastack.com/docs/guides/migrating-shadcn-reset";
132
+ const HEADING_HINT = {
133
+ "text-h1": "text-3xl font-semibold (page heading)",
134
+ "text-h2": "text-2xl font-semibold (section heading)",
135
+ "text-h3": "text-lg font-semibold",
136
+ "text-h4": "text-base font-medium (card / dialog title)",
137
+ };
138
+ const DISPLAY_HINT = {
139
+ sm: "text-4xl",
140
+ md: "text-5xl",
141
+ lg: "text-6xl",
142
+ xl: "text-7xl",
143
+ };
144
+ // The literal each deleted `--alpha-*` token carried. Derived from the token HISTORY, not only the
145
+ // last pre-reset snapshot: every name ever declared in packages/design-tokens (git log -S over
146
+ // src/tokens.ts and tokens/*.tokens.json) is here. Nineteen are the migration guide's section 4.2
147
+ // ladder, retired by the shadcn reset; five were retired before it, per the design-tokens
148
+ // CHANGELOG — fill-hover, input-hover and surface-subtle with `track` (#77), soft-hover and
149
+ // soft-surface with the Bubble contrast fix (#113). A theme-split token gives [light, dark].
150
+ const ALPHA = {
151
+ "fill-hover": 80,
152
+ "input-hover": 50,
153
+ "surface-subtle": 10,
154
+ "soft-surface": [10, 20],
155
+ "soft-hover": [20, 30],
156
+ "surface-faint": 5,
157
+ hover: 7,
158
+ border: 8,
159
+ pressed: 10,
160
+ "ink-tint": 10,
161
+ "ink-tint-strong": 15,
162
+ "border-subtle": 20,
163
+ "border-soft": 30,
164
+ input: 30,
165
+ "wash-faint": 40,
166
+ wash: 50,
167
+ "outline-border": 50,
168
+ "outline-soft": 50,
169
+ "wash-strong": 60,
170
+ "backdrop-soft": 60,
171
+ "tint-border": 70,
172
+ "link-hover": 88,
173
+ glass: 90,
174
+ "glass-hover": 95,
175
+ };
176
+ const OPACITY = { track: 25, dim: 50, "hint-soft": 60, hint: 70 };
177
+ const Z = {
178
+ raised: "z-10",
179
+ overlay: "z-50 — one overlay band, DOM order decides",
180
+ toast:
181
+ "nothing — the Toast viewport sets its own z-60, the one exception to z-50",
182
+ };
183
+ const RETIRED_COMPONENTS = {
184
+ "icon-button":
185
+ 'Button with size="icon" (or icon-sm / icon-lg) and an aria-label',
186
+ segmented: "a joined ToggleGroup",
187
+ "password-input":
188
+ "an InputGroup composition (Input + an InputGroupButton reveal toggle)",
189
+ "progress-indicator": "Progress (determinate) or Spinner (indeterminate)",
190
+ "field-inline": "EditableCell",
191
+ "floating-surface": "Popover / HoverCard — FloatingSurface was internal only",
192
+ "section-header":
193
+ "nothing — removed with the marketing layer; compose your own heading",
194
+ sonner:
195
+ "toast (`@/components/ui/toast`, `toast.add({ title })`) — Sonner was retired in 0.12.0",
196
+ };
197
+ const B = String.raw`(?<![\w-])`; // a utility or custom property starts here
198
+ const E = String.raw`(?![\w-])`; // …and ends here
199
+ /** An alternation of exactly these names, longest first so `border-subtle` wins over `border`. */
200
+ const oneOf = (names) =>
201
+ [...names].sort((a, b) => b.length - a.length).join("|");
202
+ // The four STATUS families are the only ones that ever shipped a `-subtle` step (guide section 4.5);
203
+ // `--tag-*-subtle` is kept, and a `muted`/`accent`/project family never had one, so neither is matched.
204
+ const SUBTLE_FAMILIES = ["destructive", "success", "warning", "info"];
205
+ export const RETIRED_VOCABULARY = [
206
+ {
207
+ id: "text-h*",
208
+ re: new RegExp(`${B}text-h[1-4]${E}`, "g"),
209
+ hint: (m) => HEADING_HINT[m],
210
+ declared: (m) => `--text-${m.slice(5)}`,
211
+ },
212
+ {
213
+ id: "text-label*",
214
+ re: new RegExp(`${B}text-label(?:-sm)?${E}`, "g"),
215
+ hint: (m) =>
216
+ m.endsWith("-sm") ? "text-xs font-medium" : "text-sm font-medium",
217
+ declared: (m) => `--${m}`,
218
+ },
219
+ {
220
+ id: "text-mono-label",
221
+ re: new RegExp(`${B}text-mono-label${E}`, "g"),
222
+ hint: () =>
223
+ "font-mono text-xs for code/data — a label is font-sans text-xs font-medium; no tracking, no uppercase",
224
+ declared: (m) => `--${m}`,
225
+ },
226
+ {
227
+ id: "text-code*",
228
+ re: new RegExp(`${B}text-code(?:-sm)?${E}`, "g"),
229
+ hint: (m) =>
230
+ m.endsWith("-sm") ? "font-mono text-xs" : "font-mono text-sm",
231
+ declared: (m) => `--${m}`,
232
+ },
233
+ {
234
+ id: "text-strong",
235
+ re: new RegExp(`${B}text-strong${E}`, "g"),
236
+ hint: () => "text-sm font-semibold",
237
+ declared: (m) => `--${m}`,
238
+ },
239
+ {
240
+ id: "text-display-*",
241
+ re: new RegExp(`${B}text-display-(?:sm|md|lg|xl)${E}`, "g"),
242
+ hint: (m) =>
243
+ `${DISPLAY_HINT[m.slice(13)]} — the ramp supplies tracking; write no tracking-*`,
244
+ declared: (m) => `--${m}`,
245
+ },
246
+ {
247
+ // The four STATUS families lost their -subtle step — and only those four ever had one, so the
248
+ // hint's `text-<family>-text` always names a real token.
249
+ id: "*-subtle",
250
+ re: new RegExp(
251
+ `${B}(?:bg|text|border|ring|outline|fill|stroke|divide|from|to|via)-(${SUBTLE_FAMILIES.join("|")})-subtle(?:-hover|-active)?${E}`,
252
+ "g",
253
+ ),
254
+ hint: (m, family) => {
255
+ return m.includes("-subtle-")
256
+ ? `hover:bg-${family}/20 (the -hover/-active steps are gone)`
257
+ : `bg-${family}/10 (a tint of the family), with text-${family}-text for text on it`;
258
+ },
259
+ declared: (m) => {
260
+ const name = m.replace(/^[a-z]+-/, "");
261
+ return [`--${name}`, `--color-${name}`];
262
+ },
263
+ },
264
+ {
265
+ // Only the names the system shipped (see ALPHA). `--alpha-*` is an open prefix other
266
+ // libraries use too, and a name we never shipped is not ours to call retired.
267
+ id: "--alpha-*",
268
+ re: new RegExp(`${B}--alpha-(?:${oneOf(Object.keys(ALPHA))})${E}`, "g"),
269
+ hint: (m) => {
270
+ const pct = ALPHA[m.slice(8)];
271
+ if (Array.isArray(pct))
272
+ return `the literal /${pct[0]} in light with a dark: variant at /${pct[1]}, e.g. bg-destructive/${pct[0]} dark:bg-destructive/${pct[1]}`;
273
+ return `the literal /${pct}, e.g. bg-foreground/${pct}`;
274
+ },
275
+ declared: (m) => m,
276
+ },
277
+ {
278
+ id: "--opacity-*",
279
+ re: new RegExp(`${B}--opacity-(?:${oneOf(Object.keys(OPACITY))})${E}`, "g"),
280
+ hint: (m) => `opacity-${OPACITY[m.slice(10)]}`,
281
+ declared: (m) => m,
282
+ },
283
+ {
284
+ // `--z-index`, `--z-modal`, … belong to other libraries; only the three named bands are ours.
285
+ id: "--z-*",
286
+ re: new RegExp(`${B}--z-(?:${oneOf(Object.keys(Z))})${E}`, "g"),
287
+ hint: (m) => Z[m.slice(4)],
288
+ declared: (m) => m,
289
+ },
290
+ {
291
+ id: "shadow-overlay",
292
+ re: new RegExp(`${B}(?:--)?shadow-overlay${E}`, "g"),
293
+ hint: () => "shadow-md (a popover) or shadow-lg (a modal)",
294
+ declared: () => "--shadow-overlay",
295
+ },
296
+ {
297
+ id: "backdrop-blur-glass",
298
+ re: new RegExp(`${B}backdrop-blur-glass${E}`, "g"),
299
+ hint: () =>
300
+ "delete it — the glass effect went with the marketing layer (backdrop-blur-xs is upstream's scrim blur)",
301
+ declared: () => "--blur-glass",
302
+ },
303
+ {
304
+ id: "retired component",
305
+ // An import/require/dynamic-import specifier whose LAST path segment is a retired item — and
306
+ // only when the specifier resolves INTO the ui alias directory (`accept`): a project's own
307
+ // `@/components/layout/section-header` or `./sonner` is its code, not a registry copy. A bare
308
+ // `"sonner"` (the npm package) is not a registry import and is left alone.
309
+ re: new RegExp(
310
+ String.raw`(?:from\s+|import\s*\(\s*|require\s*\(\s*|import\s+)["']([^"']*)\/(${Object.keys(RETIRED_COMPONENTS).join("|")})(?:\.[jt]sx?)?["']`,
311
+ "g",
312
+ ),
313
+ accept: (m, ctx) => importsIntoUi(m[1], ctx),
314
+ hint: (_m, _dir, name) => RETIRED_COMPONENTS[name],
315
+ declared: () => null,
316
+ label: (_m, _dir, name) => `import …/${name}`,
317
+ },
318
+ ];
319
+
320
+ // Code and stylesheets only. Prose (`.md`/`.mdx`) that NAMES a retired token — a changelog, a
321
+ // migration note — is not a use of it, and would bury the real findings.
322
+ const SCAN_EXTENSIONS = /\.(?:[cm]?[jt]sx?|css|scss)$/;
323
+ const SCAN_SKIP_DIRS = new Set([
324
+ "node_modules",
325
+ "dist",
326
+ "build",
327
+ "out",
328
+ "coverage",
329
+ "storybook-static",
330
+ ]);
331
+ const SCAN_MAX_FILES = 20000;
332
+ const SCAN_MAX_BYTES = 1024 * 1024;
333
+
334
+ /**
335
+ * The directories components.json's `ui` alias names, resolved against the file's own folder.
336
+ * Those files are registry copies — `check-updates` owns them, and a re-pull replaces them — so
337
+ * a retired name inside one is not the consumer's own code. `@/x` and `~/x` resolve to `x` or
338
+ * `src/x`, whichever exists (the shadcn convention for a src-dir project).
339
+ */
340
+ export function uiAliasDirs(componentsJson, componentsDir) {
341
+ const alias = componentsJson?.aliases?.ui;
342
+ const candidates = [];
343
+ if (typeof alias === "string" && alias.length > 0) {
344
+ const bare = alias.replace(/^(?:@|~)\//, "");
345
+ candidates.push(
346
+ join(componentsDir, bare),
347
+ join(componentsDir, "src", bare),
348
+ );
349
+ } else {
350
+ candidates.push(
351
+ join(componentsDir, "components", "ui"),
352
+ join(componentsDir, "src", "components", "ui"),
353
+ );
354
+ }
355
+ return candidates.filter((d) => existsSync(d)).map((d) => resolve(d));
356
+ }
357
+
358
+ /**
359
+ * Does an import specifier (the part before the retired name) resolve into the ui alias directory?
360
+ * Three spellings do: the components.json `ui` alias (`@/components/ui`), the registry's own
361
+ * `@/components/ui` convention, and a relative path that lands in one of the ui alias dirs.
362
+ */
363
+ function importsIntoUi(specDir, { file, uiAliases, uiDirs }) {
364
+ if (uiAliases.includes(specDir)) return true;
365
+ if (specDir === "." || specDir === ".." || /^\.\.?\//.test(specDir)) {
366
+ const target = resolve(dirname(file), specDir);
367
+ return uiDirs.includes(target);
368
+ }
369
+ return false;
370
+ }
371
+
372
+ /**
373
+ * Blank what is not class or CSS usage, keeping every newline so line numbers survive: `//` and
374
+ * `/* *\/` comments, and — in script files — a string literal that reads as prose ("Use text-strong
375
+ * for emphasis"). A class string is lower-case utility tokens; a capitalised word or sentence
376
+ * punctuation marks a sentence (never `!`, which is Tailwind's important modifier: `hidden!`).
377
+ *
378
+ * In a script file, JSX TEXT is not code, so nothing in it may open a comment or a string: an
379
+ * apostrophe ("Don't"), a URL's `//`, a glob's `/*`. Without a parser, position decides — a string
380
+ * opens only where an expression can begin (after `=`, `(`, `,`, `[`, `{`, `:`, `?`, an operator,
381
+ * `=>`, or a keyword such as `return`), and a comment only after whitespace or code punctuation,
382
+ * never after `:` (`https://`) or a word (`src/*.ts`).
383
+ */
384
+ const EXPRESSION_KEYWORDS = new Set([
385
+ "return",
386
+ "case",
387
+ "typeof",
388
+ "in",
389
+ "of",
390
+ "yield",
391
+ "await",
392
+ "void",
393
+ "delete",
394
+ "throw",
395
+ "from",
396
+ "import",
397
+ "export",
398
+ "default",
399
+ "else",
400
+ "do",
401
+ ]);
402
+ function canStartExpression(src, i) {
403
+ let k = i - 1;
404
+ while (k >= 0 && /\s/.test(src[k])) k -= 1;
405
+ if (k < 0) return true;
406
+ const p = src[k];
407
+ if (p === ">") return src[k - 1] === "="; // `=>`, never a JSX tag's `>`
408
+ if ("=(,[{:?&|!+-*%^~<;".includes(p)) return true;
409
+ if (/[\w$]/.test(p)) {
410
+ let w = k;
411
+ while (w >= 0 && /[\w$]/.test(src[w])) w -= 1;
412
+ return EXPRESSION_KEYWORDS.has(src.slice(w + 1, k + 1));
413
+ }
414
+ return false;
415
+ }
416
+ const canStartComment = (src, i) => i === 0 || /[\s;,{}()[\]]/.test(src[i - 1]);
417
+
418
+ export function maskNonUsage(
419
+ src,
420
+ { script = true, lineComments = script } = {},
421
+ ) {
422
+ const out = src.split("");
423
+ const blank = (from, to) => {
424
+ for (let k = from; k < to; k += 1) if (out[k] !== "\n") out[k] = " ";
425
+ };
426
+ const isProse = (text) =>
427
+ /(?:^|\s)[A-Z][a-z]+(?=[\s,.;:!?]|$)|[a-z][.,;:?](?:\s|$)/.test(
428
+ text.replace(/\[[^\]]*\]|\([^)]*\)/g, ""),
429
+ );
430
+ let i = 0;
431
+ while (i < src.length) {
432
+ const c = src[i];
433
+ const next = src[i + 1];
434
+ if (c === "/" && next === "*" && (!script || canStartComment(src, i))) {
435
+ const end = src.indexOf("*/", i + 2);
436
+ const stop = end === -1 ? src.length : end + 2;
437
+ blank(i, stop);
438
+ i = stop;
439
+ } else if (
440
+ c === "/" &&
441
+ next === "/" &&
442
+ lineComments &&
443
+ (script ? canStartComment(src, i) : i === 0 || /\s/.test(src[i - 1]))
444
+ ) {
445
+ const end = src.indexOf("\n", i);
446
+ const stop = end === -1 ? src.length : end;
447
+ blank(i, stop);
448
+ i = stop;
449
+ } else if (
450
+ (c === '"' || c === "'" || c === "`") &&
451
+ (!script || canStartExpression(src, i))
452
+ ) {
453
+ let j = i + 1;
454
+ while (j < src.length && src[j] !== c) {
455
+ if (src[j] === "\\") j += 1;
456
+ else if (src[j] === "\n" && c !== "`") break;
457
+ j += 1;
458
+ }
459
+ if (script && isProse(src.slice(i + 1, j))) blank(i + 1, j);
460
+ i = j + 1;
461
+ } else {
462
+ i += 1;
463
+ }
464
+ }
465
+ return out.join("");
466
+ }
467
+
468
+ /**
469
+ * Class names a stylesheet DEFINES — `@utility text-h2 { … }` (a `-*` functional utility as a
470
+ * prefix) and plain class selectors such as `.text-strong { … }`. A retired name the project defines
471
+ * itself compiles, so it is the project's, not a silent no-op.
472
+ */
473
+ function definedClasses(css) {
474
+ const exact = new Set();
475
+ const prefixes = [];
476
+ for (const m of css.matchAll(/([^{};]+)\{/g)) {
477
+ const prelude = m[1].trim();
478
+ const utility = prelude.match(/^@utility\s+([\w-]+?)(-\*)?$/);
479
+ if (utility) {
480
+ if (utility[2]) prefixes.push(`${utility[1]}-`);
481
+ else exact.add(utility[1]);
482
+ } else if (!prelude.startsWith("@")) {
483
+ for (const c of prelude.matchAll(/\.((?:\\.|[\w-])+)/g))
484
+ exact.add(c[1].replace(/\\(.)/g, "$1"));
485
+ }
486
+ }
487
+ return { exact, prefixes };
488
+ }
489
+
490
+ /** Every retired-vocabulary occurrence under `root`, skipping build output and the ui alias dirs. */
491
+ export function scanRetiredVocabulary(
492
+ root,
493
+ { skipDirs = [], uiAlias = null } = {},
494
+ ) {
495
+ const skip = new Set(skipDirs.map((d) => resolve(d)));
496
+ const uiAliases = [
497
+ ...new Set(
498
+ [uiAlias, "@/components/ui"]
499
+ .filter((a) => typeof a === "string" && a.length > 0)
500
+ .map((a) => a.replace(/\/+$/, "")),
501
+ ),
502
+ ];
503
+ const uiDirs = [...skip];
504
+ const files = [];
505
+ const walk = (dir) => {
506
+ if (files.length >= SCAN_MAX_FILES) return;
507
+ let entries;
508
+ try {
509
+ entries = readdirSync(dir, { withFileTypes: true });
510
+ } catch {
511
+ return;
512
+ }
513
+ for (const e of entries) {
514
+ if (e.name.startsWith(".") || SCAN_SKIP_DIRS.has(e.name)) continue;
515
+ const full = join(dir, e.name);
516
+ if (e.isDirectory()) {
517
+ if (!skip.has(resolve(full))) walk(full);
518
+ } else if (e.isFile() && SCAN_EXTENSIONS.test(e.name)) {
519
+ files.push(full);
520
+ if (files.length >= SCAN_MAX_FILES) return;
521
+ }
522
+ }
523
+ };
524
+ walk(root);
525
+
526
+ const sources = [];
527
+ const declared = new Set();
528
+ const declaredClasses = new Set();
529
+ const classPrefixes = [];
530
+ for (const file of files) {
531
+ try {
532
+ if (statSync(file).size > SCAN_MAX_BYTES) continue;
533
+ } catch {
534
+ continue;
535
+ }
536
+ const raw = readIfExists(file);
537
+ if (raw == null) continue;
538
+ const css = /\.s?css$/.test(file);
539
+ const src = maskNonUsage(raw, {
540
+ script: !css,
541
+ lineComments: !css || file.endsWith(".scss"),
542
+ });
543
+ sources.push({ file, src });
544
+ for (const m of src.matchAll(/(?<![\w-])(--[\w-]+)\s*:/g))
545
+ declared.add(m[1]);
546
+ if (css) {
547
+ const defined = definedClasses(src);
548
+ for (const name of defined.exact) declaredClasses.add(name);
549
+ classPrefixes.push(...defined.prefixes);
550
+ }
551
+ }
552
+ const definesClass = (name) =>
553
+ declaredClasses.has(name) ||
554
+ classPrefixes.some((prefix) => name.startsWith(prefix));
555
+
556
+ const findings = [];
557
+ for (const { file, src } of sources) {
558
+ const lines = src.split("\n");
559
+ for (let i = 0; i < lines.length; i += 1) {
560
+ const line = lines[i];
561
+ if (line.length > 4000) continue; // minified output that slipped past the dir skips
562
+ for (const rule of RETIRED_VOCABULARY) {
563
+ rule.re.lastIndex = 0;
564
+ for (const m of line.matchAll(rule.re)) {
565
+ const groups = m.slice(1);
566
+ if (rule.accept && !rule.accept(m, { file, uiAliases, uiDirs }))
567
+ continue;
568
+ const own = [rule.declared(m[0], ...groups)].flat().filter(Boolean);
569
+ if (own.some((name) => declared.has(name))) continue;
570
+ if (!m[0].startsWith("--") && definesClass(m[0])) continue;
571
+ findings.push({
572
+ file: relative(root, file).split(sep).join("/"),
573
+ line: i + 1,
574
+ match: rule.label ? rule.label(m[0], ...groups) : m[0],
575
+ hint: rule.hint(m[0], ...groups),
576
+ });
577
+ }
578
+ }
579
+ }
580
+ }
581
+ return {
582
+ files: sources.length,
583
+ findings,
584
+ truncated: files.length >= SCAN_MAX_FILES,
585
+ };
586
+ }
587
+
117
588
  export function main(argv = []) {
118
589
  if (argv.includes("-h") || argv.includes("--help")) {
119
590
  console.log(USAGE);
@@ -304,6 +775,34 @@ export function main(argv = []) {
304
775
  }
305
776
  }
306
777
 
778
+ // ---- 7. no retired vocabulary in the project's own source --------------------------------
779
+ const skipDirs = componentsJson
780
+ ? uiAliasDirs(componentsJson, dirname(componentsJsonPath))
781
+ : uiAliasDirs(null, root);
782
+ const scan = scanRetiredVocabulary(root, {
783
+ skipDirs,
784
+ uiAlias: componentsJson?.aliases?.ui ?? null,
785
+ });
786
+ const scanned = `${scan.files} source file(s)${
787
+ skipDirs.length
788
+ ? `, skipping ${skipDirs.map((d) => relative(root, d) || ".").join(", ")}`
789
+ : ""
790
+ }${scan.truncated ? ` (stopped at ${SCAN_MAX_FILES} files)` : ""}`;
791
+ if (scan.findings.length === 0) {
792
+ ok("no retired vocabulary", scanned);
793
+ } else {
794
+ const inFiles = new Set(scan.findings.map((f) => f.file)).size;
795
+ results.push({
796
+ level: "fail",
797
+ name: "no retired vocabulary",
798
+ detail: `${scan.findings.length} occurrence(s) in ${inFiles} file(s) of ${scanned} — these compile to nothing, silently`,
799
+ fix: `rewrite each as its hint says; every row is in ${GUIDE}`,
800
+ findings: scan.findings.map(
801
+ (f) => `${f.file}:${f.line} ${f.match} → ${f.hint}`,
802
+ ),
803
+ });
804
+ }
805
+
307
806
  // ---- report -------------------------------------------------------------------------------
308
807
  const glyph = { ok: "✓", warn: "!", fail: "✗" };
309
808
  console.log("");
@@ -311,6 +810,7 @@ export function main(argv = []) {
311
810
  console.log(
312
811
  ` ${glyph[r.level]} ${r.name}${r.detail ? ` — ${r.detail}` : ""}`,
313
812
  );
813
+ for (const finding of r.findings ?? []) console.log(` ${finding}`);
314
814
  if (r.fix) console.log(` fix: ${r.fix}`);
315
815
  }
316
816
 
@@ -5,7 +5,8 @@
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
+ // doctor Check a consuming project's setup (PostCSS plugin, preset import, registry,
9
+ // retired design-system vocabulary in its own source).
9
10
  //
10
11
  // The bin is named `vegastack-design` (NOT `vegastack`) so it never collides with a platform CLI.
11
12
  // `check-updates` is imported in-process; `verify` is spawned (it's the standalone, hash-parity-tested
@@ -4,8 +4,22 @@ import { twMerge as tailwindMerge } from "tailwind-merge";
4
4
 
5
5
  // src/prose.ts
6
6
  var prose = {
7
- /** The root's own ink and size — every rule below is relative to this. */
8
- root: "text-sm text-foreground",
7
+ /**
8
+ * The root's own FAMILY, ink and size — every rule below is relative to this.
9
+ *
10
+ * `font-sans` is load-bearing and was missing. A prose root has to be self-describing, because
11
+ * neither of its two consumers sets a family of its own: `MarkdownView` and `TextEdit` both wear
12
+ * nothing but `proseClassName`. With no family declared here, prose inherited whatever surrounded
13
+ * it — drop either surface inside a mono container (`terminal-body` is one in this very
14
+ * registry, and a chat or log panel is the obvious consumer case) and the WHOLE tree went mono:
15
+ * headings, paragraphs, table cells, and the `1.` / `2.` markers of an ordered list, because
16
+ * `::marker` inherits font properties from its originating element.
17
+ *
18
+ * Geist Mono is now the exception the recipe names explicitly — `code`, `pre` and `pre code` —
19
+ * rather than something prose falls into by accident. A consumer who genuinely wants mono prose
20
+ * still says so on the root, where it reads as a decision.
21
+ */
22
+ root: "font-sans text-sm text-foreground",
9
23
  // Headings. `scroll-m-20` keeps an anchored heading clear of a sticky header; the weight is
10
24
  // `font-semibold`, the ordinary Tailwind weight upstream uses — Batch 1 of the shadcn reset
11
25
  // deleted both the 400/500 ladder and the `text-h*` roles this comment used to name.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  cn
4
- } from "./chunk-42HJ4OYZ.js";
4
+ } from "./chunk-MSOIZXDR.js";
5
5
 
6
6
  // src/icons/create-animated-icon.tsx
7
7
  import * as React from "react";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cn
3
- } from "../chunk-42HJ4OYZ.js";
3
+ } from "../chunk-MSOIZXDR.js";
4
4
 
5
5
  // src/icons/icon.tsx
6
6
  import "react";
package/dist/index.cjs CHANGED
@@ -33,8 +33,22 @@ var import_tailwind_merge = require("tailwind-merge");
33
33
 
34
34
  // src/prose.ts
35
35
  var prose = {
36
- /** The root's own ink and size — every rule below is relative to this. */
37
- root: "text-sm text-foreground",
36
+ /**
37
+ * The root's own FAMILY, ink and size — every rule below is relative to this.
38
+ *
39
+ * `font-sans` is load-bearing and was missing. A prose root has to be self-describing, because
40
+ * neither of its two consumers sets a family of its own: `MarkdownView` and `TextEdit` both wear
41
+ * nothing but `proseClassName`. With no family declared here, prose inherited whatever surrounded
42
+ * it — drop either surface inside a mono container (`terminal-body` is one in this very
43
+ * registry, and a chat or log panel is the obvious consumer case) and the WHOLE tree went mono:
44
+ * headings, paragraphs, table cells, and the `1.` / `2.` markers of an ordered list, because
45
+ * `::marker` inherits font properties from its originating element.
46
+ *
47
+ * Geist Mono is now the exception the recipe names explicitly — `code`, `pre` and `pre code` —
48
+ * rather than something prose falls into by accident. A consumer who genuinely wants mono prose
49
+ * still says so on the root, where it reads as a decision.
50
+ */
51
+ root: "font-sans text-sm text-foreground",
38
52
  // Headings. `scroll-m-20` keeps an anchored heading clear of a sticky header; the weight is
39
53
  // `font-semibold`, the ordinary Tailwind weight upstream uses — Batch 1 of the shadcn reset
40
54
  // deleted both the 400/500 ladder and the `text-h*` roles this comment used to name.
package/dist/index.d.cts CHANGED
@@ -40,8 +40,22 @@ import * as React from 'react';
40
40
  * <div className={cn(prose.root, prose.p, prose.code)} />
41
41
  */
42
42
  declare const prose: {
43
- /** The root's own ink and size — every rule below is relative to this. */
44
- readonly root: "text-sm text-foreground";
43
+ /**
44
+ * The root's own FAMILY, ink and size — every rule below is relative to this.
45
+ *
46
+ * `font-sans` is load-bearing and was missing. A prose root has to be self-describing, because
47
+ * neither of its two consumers sets a family of its own: `MarkdownView` and `TextEdit` both wear
48
+ * nothing but `proseClassName`. With no family declared here, prose inherited whatever surrounded
49
+ * it — drop either surface inside a mono container (`terminal-body` is one in this very
50
+ * registry, and a chat or log panel is the obvious consumer case) and the WHOLE tree went mono:
51
+ * headings, paragraphs, table cells, and the `1.` / `2.` markers of an ordered list, because
52
+ * `::marker` inherits font properties from its originating element.
53
+ *
54
+ * Geist Mono is now the exception the recipe names explicitly — `code`, `pre` and `pre code` —
55
+ * rather than something prose falls into by accident. A consumer who genuinely wants mono prose
56
+ * still says so on the root, where it reads as a decision.
57
+ */
58
+ readonly root: "font-sans text-sm text-foreground";
45
59
  readonly h1: "[&_h1]:mt-6 [&_h1]:mb-3 [&_h1]:scroll-m-20 [&_h1]:text-3xl [&_h1]:font-semibold [&_h1]:text-foreground [&_h1]:first:mt-0";
46
60
  readonly h2: "[&_h2]:mt-6 [&_h2]:mb-3 [&_h2]:scroll-m-20 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:text-foreground [&_h2]:first:mt-0";
47
61
  readonly h3: "[&_h3]:mt-5 [&_h3]:mb-2 [&_h3]:scroll-m-20 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-foreground [&_h3]:first:mt-0";
package/dist/index.d.ts CHANGED
@@ -40,8 +40,22 @@ import * as React from 'react';
40
40
  * <div className={cn(prose.root, prose.p, prose.code)} />
41
41
  */
42
42
  declare const prose: {
43
- /** The root's own ink and size — every rule below is relative to this. */
44
- readonly root: "text-sm text-foreground";
43
+ /**
44
+ * The root's own FAMILY, ink and size — every rule below is relative to this.
45
+ *
46
+ * `font-sans` is load-bearing and was missing. A prose root has to be self-describing, because
47
+ * neither of its two consumers sets a family of its own: `MarkdownView` and `TextEdit` both wear
48
+ * nothing but `proseClassName`. With no family declared here, prose inherited whatever surrounded
49
+ * it — drop either surface inside a mono container (`terminal-body` is one in this very
50
+ * registry, and a chat or log panel is the obvious consumer case) and the WHOLE tree went mono:
51
+ * headings, paragraphs, table cells, and the `1.` / `2.` markers of an ordered list, because
52
+ * `::marker` inherits font properties from its originating element.
53
+ *
54
+ * Geist Mono is now the exception the recipe names explicitly — `code`, `pre` and `pre code` —
55
+ * rather than something prose falls into by accident. A consumer who genuinely wants mono prose
56
+ * still says so on the root, where it reads as a decision.
57
+ */
58
+ readonly root: "font-sans text-sm text-foreground";
45
59
  readonly h1: "[&_h1]:mt-6 [&_h1]:mb-3 [&_h1]:scroll-m-20 [&_h1]:text-3xl [&_h1]:font-semibold [&_h1]:text-foreground [&_h1]:first:mt-0";
46
60
  readonly h2: "[&_h2]:mt-6 [&_h2]:mb-3 [&_h2]:scroll-m-20 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:text-foreground [&_h2]:first:mt-0";
47
61
  readonly h3: "[&_h3]:mt-5 [&_h3]:mb-2 [&_h3]:scroll-m-20 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-foreground [&_h3]:first:mt-0";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  mergeRefs,
6
6
  prose,
7
7
  proseClassName
8
- } from "./chunk-42HJ4OYZ.js";
8
+ } from "./chunk-MSOIZXDR.js";
9
9
  export {
10
10
  FLOATING,
11
11
  TIMINGS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegastack/design",
3
- "version": "0.6.1",
3
+ "version": "0.7.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": {
@@ -67,10 +67,10 @@
67
67
  "lint": "eslint . && node ../../tooling/design-lint.mjs src && pnpm run verify",
68
68
  "verify": "node ../../tooling/verify-preset-source.mjs",
69
69
  "typecheck": "tsc --noEmit",
70
- "test": "node test/compare.test.mjs && node test/check-updates.test.mjs && node test/skills-install.test.mjs"
70
+ "test": "node test/compare.test.mjs && node test/check-updates.test.mjs && node test/skills-install.test.mjs && node test/doctor.test.mjs"
71
71
  },
72
72
  "dependencies": {
73
- "@vegastack/design-tokens": "^0.5.0",
73
+ "@vegastack/design-tokens": "^0.7.1",
74
74
  "clsx": "^2.1.1",
75
75
  "tailwind-merge": "^3.6.0",
76
76
  "tsconfig-paths": "^4.2.0",
package/preset.css CHANGED
@@ -13,15 +13,16 @@
13
13
  *
14
14
  * This package's own `dist` ships the icon runtime (`BrandIcon`'s
15
15
  * `inline-flex shrink-0 [&>svg]:size-full`), and `@vegastack/ui` ships a compiled
16
- * provider + `Toaster` whose Base UI Toast classes (e.g. `bg-popover shadow-overlay`)
16
+ * provider + `Toaster` whose Base UI Toast classes (e.g. `bg-popover shadow-lg`)
17
17
  * live in its published `dist/*.js`, NOT in the consumer's own source — so without
18
18
  * an explicit `@source` a real npm consumer importing ONLY this preset would get
19
19
  * a partially-unstyled provider/Toaster + icon UI.
20
20
  *
21
21
  * Why `./dist` + a RELATIVE `../ui/dist` path and NOT `@source "@vegastack/ui"`:
22
- * - Tailwind v4.3.1 (the pinned version) does NOT resolve a bare package-name
23
- * `@source` through node_modules — it treats `@vegastack/ui` as a literal
24
- * glob and silently scans nothing (verified: tooling/verify-preset-source.mjs).
22
+ * - Tailwind v4 (whatever version the lockfile pins) does NOT resolve a bare
23
+ * package-name `@source` through node_modules — it treats `@vegastack/ui` as a
24
+ * literal glob and silently scans nothing (re-verified against the installed
25
+ * version by tooling/verify-preset-source.mjs).
25
26
  * - Tailwind resolves `@source` relative to this file's realpath. The icon runtime
26
27
  * is INSIDE this package now (`./dist`). `@vegastack/ui` — when present — sits as
27
28
  * a sibling under the same `@vegastack` scope in node_modules, making `../ui`
@@ -17,13 +17,20 @@ These are mechanical and catch the highest-value problems:
17
17
 
18
18
  ```bash
19
19
  npx --package=@vegastack/design vegastack-design check-updates
20
+ npx --package=@vegastack/design vegastack-design doctor
20
21
  ```
21
22
 
23
+ `doctor` checks setup and also scans the app's own source (skipping `node_modules`, build output
24
+ and the `components.json` `ui` directory) for vocabulary the shadcn reset retired — `text-h1`,
25
+ `text-label`, `bg-destructive-subtle`, `--z-toast`, an `icon-button` import — and exits non-zero
26
+ with `file:line` and the replacement for each. Its findings are §3's retired-vocabulary **errors**;
27
+ cite them rather than re-deriving them.
28
+
22
29
  `⬆ update` means the registry has a newer version. `≈ drift` means the installed file differs from
23
- the registry item — either an upstream change or a local edit to a file you do not own. Both are
24
- findings; a local edit to a copied-in component is a **high** finding, because the next
25
- `--overwrite` silently destroys it. The fix is to move the customisation into your own wrapper
26
- component or a token override.
30
+ the registry item — either an upstream change or a local edit. Both are findings. The rule for
31
+ edits is the one the Components guide states: **don't edit a copied-in component; if you must, it
32
+ becomes yours** — `check-updates` reports it as drifted from then on and every update is a `--diff`
33
+ re-applied by hand (§5).
27
34
 
28
35
  Then verify setup, since these failures look like component bugs:
29
36
 
@@ -79,10 +86,22 @@ rg -n 'ring-3\b|ring-\[3px\]|ring-ring/[0-9]+|focus-visible:ring-|shadow-\[0_0_0
79
86
  - `React.forwardRef` — React 19 takes `ref` as a normal prop. **error**
80
87
 
81
88
  **Things that are NOT findings any more**, and reporting them is noise: `rounded-xl`, `shadow-md`,
82
- `text-4xl`, `font-semibold`, `tracking-tight`, `transition-all`, `transition-colors`,
83
- `duration-100`, `ease-in-out`, `z-50`, `opacity-50`, a raw `/NN` alpha, `h-8`/`size-4`,
84
- `cursor-default` on a menu row, an arbitrary `h-[18.4px]`, and a `hover:` with no `active:` beside
85
- it. Every one of those is upstream's own vocabulary, which this system now adopts.
89
+ `text-4xl`, `font-semibold`, `transition-all`, `transition-colors`, `duration-100`, `ease-in-out`,
90
+ `z-50`, `opacity-50`, a raw `/NN` alpha, `h-8`/`size-4`, `cursor-default` on a menu row, an
91
+ arbitrary `h-[18.4px]`, and a `hover:` with no `active:` beside it. Every one of those is upstream's
92
+ own vocabulary, which this system now adopts.
93
+
94
+ **`tracking-tight` left that list on 2026-09-22 and IS a finding again.** The `@theme` bridge now
95
+ declares the heading tier's letter-spacing per size, and Tailwind compiles it as
96
+ `letter-spacing: var(--tw-tracking, …)` — so a local `tracking-*` silently beats the ramp and that
97
+ element stops matching the system. Only `tracking-widest` (a keyboard-shortcut hint) is allowed.
98
+ Two more in the same family:
99
+
100
+ - an arbitrary font size (`text-[13px]`, `text-[0.8rem]`) — it bypasses the `--text-*` namespace and
101
+ receives neither the ramp's line-height nor its letter-spacing. **error**
102
+ - the `uppercase` utility or a `textTransform: "uppercase"` — this system is sentence case
103
+ everywhere, and the transform rewrites whatever it is handed (it once turned the token name
104
+ `--text-lg` into `--TEXT-LG`). If a string is uppercase, write it uppercase. **error**
86
105
 
87
106
  ## 3b. Names and tokens the shadcn reset removed
88
107
 
@@ -127,9 +146,11 @@ rg -n 'surface-(1|2|3|raised)|--alpha-|--opacity-|--size-|--icon-|--panel-width-
127
146
  rg -n '@vegastack' components/ui/ -l
128
147
  ```
129
148
 
130
- A copied-in component is yours to keep but not to edit — the next `--overwrite` overwrites it. Any
131
- diff reported by `check-updates` as `≈ drift` on a file you did not intend to change is a **high**
132
- finding. Route customisation through a token override, a wrapper component, or a `className` prop.
149
+ Don't edit a copied-in component; if you must, it becomes yours. Report every `≈ drift` as a
150
+ **warning** — the file no longer receives registry fixes and the next `--overwrite` replaces the
151
+ edit — and name the way back: move the customisation into a token override, a wrapper component,
152
+ or a `className` at the call site, then re-pull. A drifted file whose edit nobody meant to make
153
+ (no commit or comment owns it) is an **error**: it is lost work waiting to happen.
133
154
 
134
155
  A missing `// @vegastack …` provenance header is **normal** and never a finding on its own: the
135
156
  shadcn CLI strips leading comments during copy-in.
@@ -138,9 +159,10 @@ shadcn CLI strips leading comments during copy-in.
138
159
 
139
160
  Group by file. Each finding: `file:line` · rule · suggested fix · severity.
140
161
 
141
- - **error** — a hardcoded visual value, an accessibility violation, or an edited copied-in component.
162
+ - **error** — a hardcoded visual value, an accessibility violation, retired vocabulary, or an
163
+ unintended edit to a copied-in component.
142
164
  - **warning** — raw HTML where a component exists, a missing state, an off-system utility with a
143
- working fallback.
165
+ working fallback, a deliberately edited (now owned) copied-in component.
144
166
  - **info** — a component with an available update worth a deliberate `--diff` review.
145
167
 
146
168
  Never auto-fix. Report, and let the owner decide.
@@ -83,20 +83,20 @@ info`, each an ink on the `card` surface with a required icon; **`announcement-b
83
83
  Semantic CSS custom properties from `@vegastack/design-tokens/theme.css` (OKLCH, `:root` + `.dark`),
84
84
  on shadcn's `neutral` base. Always use the utility, never a raw value.
85
85
 
86
- | Role | Utilities |
87
- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
88
- | Surface | `bg-background` (page) · `bg-card` · `bg-popover` · `bg-sidebar` |
89
- | Fill | `bg-primary` (solid action, every checked control) · `bg-secondary` (soft) · `bg-muted` (well, track, skeleton) · `bg-accent` (hover) |
90
- | Text | `text-foreground` · `text-muted-foreground` · `text-{primary,secondary,accent,card,popover}-foreground` |
91
- | Status | `bg-{destructive,success,warning,info}` · `-foreground` (ink ON the fill) · `-text` (ink on the page or on the family's own tint) |
92
- | Border | `border-border` · `border-input` — there are no rings; focus is one global outline |
93
- | Radius | `rounded-{sm,md,lg,xl,2xl}` — all derived from the single `--radius` |
94
- | Type | Tailwind's own `text-{xs…7xl}`. `text-sm` is 14px, `text-base` is 16px |
95
- | Font | `font-sans` `font-mono` `font-serif` `font-heading` |
96
- | Motion | `duration-{fast,base,slow}` · `ease-{standard,emphasized,exit,spring}` — or Tailwind's own steps |
97
- | Entrance | `motion-pop-in` `motion-enter-up` `motion-shake` `motion-flash` |
98
- | Docked | `motion-dock-in` / `motion-dock-out` — a control parked at a viewport edge, 150ms in / 100ms out |
99
- | Prose | `proseClassName` from `@vegastack/design` — the whole rendered-rich-text recipe, one class |
86
+ | Role | Utilities |
87
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | Surface | `bg-background` (page) · `bg-card` · `bg-popover` · `bg-sidebar` |
89
+ | Fill | `bg-primary` (solid action, every checked control) · `bg-secondary` (soft) · `bg-muted` (well, track, skeleton) · `bg-accent` (hover) |
90
+ | Text | `text-foreground` · `text-muted-foreground` · `text-{primary,secondary,accent,card,popover}-foreground` |
91
+ | Status | `bg-{destructive,success,warning,info}` · `-foreground` (ink ON the fill) · `-text` (ink on the page or on the family's own tint) |
92
+ | Border | `border-border` · `border-input` — there are no rings; focus is one global outline |
93
+ | Radius | `rounded-{sm,md,lg,xl,2xl}` — all derived from the single `--radius` |
94
+ | Type | Tailwind's own `text-{xs…7xl}`. `text-sm` is 14px, `text-base` is 16px. Line-height and letter-spacing above `text-base` come from the theme — never write `tracking-*`, an arbitrary `text-[13px]`, or `uppercase` |
95
+ | Font | `font-sans` `font-mono` `font-serif` `font-heading` |
96
+ | Motion | `duration-{fast,base,slow}` · `ease-{standard,emphasized,exit,spring}` — or Tailwind's own steps |
97
+ | Entrance | `motion-pop-in` `motion-enter-up` `motion-shake` `motion-flash` |
98
+ | Docked | `motion-dock-in` / `motion-dock-out` — a control parked at a viewport edge, 150ms in / 100ms out |
99
+ | Prose | `proseClassName` from `@vegastack/design` — the whole rendered-rich-text recipe, one class |
100
100
 
101
101
  **Hover and pressed are written, not imported.** A component owns its own interaction chrome, the
102
102
  way shadcn writes it:
@@ -209,7 +209,29 @@ contract.
209
209
  - Hand-roll a removable pill, or a `role="status"` live region with its own sequence counter.
210
210
  - Give a form control a fixed width (`w-56`, `w-64`) — it reads fine on the page it was tuned for
211
211
  and overflows at 320px. Constrain the parent instead.
212
+ - Write an arbitrary text size (`text-[13px]`, `text-[0.8rem]`, `text-[2rem]/9`). It bypasses the
213
+ `--text-*` namespace, so it receives neither the ramp's line-height nor its letter-spacing, and
214
+ lint rejects it. Take the nearest ramp step. Likewise no local `tracking-*` and no `uppercase`.
215
+ - Draw a surface edge with a ring (`ring-1 ring-foreground/10`). Cards and floating surfaces use
216
+ `border border-border` (BRD-1); lint rejects the ring.
212
217
  - Expect a compatibility shim from before the reset. There is none — see the migration guide.
218
+ - Write retired vocabulary. It compiles to **nothing** — no build error, no type error — so a heading
219
+ silently renders as body text. `vegastack-design doctor` scans your source and lists every
220
+ occurrence with `file:line`; run it after any upgrade or generated change.
221
+
222
+ | Retired | Write instead |
223
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
224
+ | `text-h1` · `text-h2` · `text-h3` · `text-h4` | `text-3xl` · `text-2xl` · `text-lg` + `font-semibold`; `text-base font-medium` |
225
+ | `text-label` · `text-label-sm` · `text-strong` | `text-sm font-medium` · `text-xs font-medium` · `text-sm font-semibold` |
226
+ | `text-mono-label` · `text-code` · `text-code-sm` | `text-xs font-medium` (a label is sans) · `font-mono text-sm` · `font-mono text-xs` |
227
+ | `text-display-{sm,md,lg,xl}` | `text-4xl` · `text-5xl` · `text-6xl` · `text-7xl` |
228
+ | `bg-{destructive,success,warning,info}-subtle` | `bg-destructive/10` (the family at `/10`), `-text` ink on it |
229
+ | `--alpha-*` · `--opacity-*` | the literal: `bg-foreground/10`, `opacity-50` |
230
+ | `--z-*` | `z-10` (raised) · `z-50` (every overlay; DOM order decides) |
231
+ | `shadow-overlay` · `backdrop-blur-glass` | `shadow-md` (popover) / `shadow-lg` (modal) · delete it |
232
+ | `icon-button` · `segmented` · `password-input` | `Button size="icon"` + `aria-label` · joined `ToggleGroup` · `InputGroup` composition |
233
+ | `progress-indicator` · `field-inline` · `floating-surface` | `Progress` / `Spinner` · `EditableCell` · `Popover` |
234
+ | `section-header` · `sonner` | your own heading markup · `toast` (`toast.add({ title })`) |
213
235
 
214
236
  ## Reference
215
237
 
@@ -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
- **110 components**, plus 467 animated-icon items, 11 hooks (`use-animation-replay`, `use-announcer`, `use-drag-reorder`, `use-file-drop`, `use-inline-edit`, `use-list-nav`, `use-media-query`, `use-mobile`, `use-modal-inert`, `use-overflow`, `use-platform`), 32 starter blocks (`app-shell-01`, `board-01`, `dashboard-01`, `login-01`, `login-02`, `login-03`, `login-04`, `login-05`, `onboarding-01`, `preview-03`, `settings-01`, `sidebar-01`, `sidebar-02`, `sidebar-03`, `sidebar-04`, `sidebar-05`, `sidebar-06`, `sidebar-07`, `sidebar-08`, `sidebar-09`, `sidebar-10`, `sidebar-11`, `sidebar-12`, `sidebar-13`, `sidebar-14`, `sidebar-15`, `sidebar-16`, `signup-01`, `signup-02`, `signup-03`, `signup-04`, `signup-05`), 68 chart blocks across 7 families, and 2 data libs (`geo-data`, `drag-item`) — 690 registry items in total.
6
+ **111 components**, plus 467 animated-icon items, 11 hooks (`use-animation-replay`, `use-announcer`, `use-drag-reorder`, `use-file-drop`, `use-inline-edit`, `use-list-nav`, `use-media-query`, `use-mobile`, `use-modal-inert`, `use-overflow`, `use-platform`), 32 starter blocks (`app-shell-01`, `board-01`, `dashboard-01`, `login-01`, `login-02`, `login-03`, `login-04`, `login-05`, `onboarding-01`, `preview-03`, `settings-01`, `sidebar-01`, `sidebar-02`, `sidebar-03`, `sidebar-04`, `sidebar-05`, `sidebar-06`, `sidebar-07`, `sidebar-08`, `sidebar-09`, `sidebar-10`, `sidebar-11`, `sidebar-12`, `sidebar-13`, `sidebar-14`, `sidebar-15`, `sidebar-16`, `signup-01`, `signup-02`, `signup-03`, `signup-04`, `signup-05`), 68 chart blocks across 7 families, and 2 data libs (`geo-data`, `drag-item`) — 691 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 a component whose name
@@ -77,6 +77,7 @@ starts with `icon-` is a component and never an icon.
77
77
 
78
78
  - **`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.
79
79
  - **`data-list`** — A generic, typed data table — configurable columns, row selection, sortable headers, plus loading and empty states.
80
+ - **`data-list-pager`** — A controlled paging footer for DataList — a tabular-numeral range summary, a rows-per-page Select, and a windowed Pagination that hides on a single page.
80
81
  - **`data-table-parts`** — The chrome DataList and DataGrid share — sort header, selection cells, skeleton rows, the empty row, column class rules, and the selection/sort/controlled-state hooks.
81
82
  - **`filter-bar`** — A row of removable filter chips, an "Add filter" dropdown, and an optional search input — for list and table filter toolbars.
82
83
  - **`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.
@@ -102,11 +103,12 @@ starts with `icon-` is a component and never an icon.
102
103
  - **`breadcrumb`** — A hierarchical navigation trail — links, separators, the current page, and ellipsis collapse for long paths.
103
104
  - **`command`** — A searchable command palette — filtered, grouped items with keyboard navigation, optionally inside a ⌘K dialog.
104
105
  - **`menubar`** — A persistent horizontal bar of menus — application-style File / Edit / View navigation.
106
+ - **`multi-step-form`** — A guarded, branching flow around a Stepper — conditional steps, sync and async advance guards, locking, reachability-derived deep links and resume, and a phone layout chosen from the same predicate. Owns no fields and no validator.
105
107
  - **`navigation-menu`** — A collection of links for navigating websites — triggers that open one shared panel, and plain links styled to match.
106
108
  - **`page-header`** — The standardized header at the top of a page — back button, breadcrumb trail, title, description, actions, secondary menu, and a favorite star.
107
109
  - **`pagination`** — Page navigation — previous/next, numbered page links, an ellipsis for long ranges, and the active page.
108
110
  - **`sidebar`** — A composable, themeable and customizable sidebar — a provider, a collapsible panel with header, content and footer, labelled groups, menu rows with actions, badges and submenus, a rail and a trigger.
109
- - **`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.
111
+ - **`stepper`** — A bounded linear process as an ordered list — seven step states on a numbered rail that fills in behind you, aria-current=step, orientation chosen from the step count, and a compact summary below a container width.
110
112
  - **`tabs`** — A set of layered sections of content — known as tab panels — that are displayed one at a time.
111
113
 
112
114
  ## Feedback
@@ -116,7 +118,6 @@ starts with `icon-` is a component and never an icon.
116
118
  - **`progress`** — Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.
117
119
  - **`provider`** — The single app-root wrapper — theme (next-themes), Base UI toasts, tooltip delays, and text direction in one mount-once component.
118
120
  - **`skeleton`** — A pulsing placeholder that reserves layout space while content loads.
119
- - **`sonner`** — The sonner toaster, themed onto the token contract — an alternative notification engine with its own imperative API.
120
121
  - **`spinner`** — An indeterminate loading indicator that inherits its host's ink.
121
122
  - **`toast`** — Brief, non-blocking notifications — a stacking Base UI Toast surface with typed icons, actions and promise toasts.
122
123