@vegastack/design 0.7.0 → 0.7.2
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 +499 -1
- package/bin/vegastack-design.mjs +2 -1
- package/package.json +3 -3
- package/preset.css +5 -4
- package/skills/vegastack-design-audit/SKILL.md +20 -10
- package/skills/vegastack-design-system/SKILL.md +23 -1
- package/skills/vegastack-design-system/references/components.md +3 -1
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,467 @@ 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
|
+
"progress-indicator": "Progress (determinate) or Spinner (indeterminate)",
|
|
188
|
+
"field-inline": "EditableCell",
|
|
189
|
+
"floating-surface": "Popover / HoverCard — FloatingSurface was internal only",
|
|
190
|
+
"section-header":
|
|
191
|
+
"nothing — removed with the marketing layer; compose your own heading",
|
|
192
|
+
sonner:
|
|
193
|
+
"toast (`@/components/ui/toast`, `toast.add({ title })`) — Sonner was retired in 0.12.0",
|
|
194
|
+
};
|
|
195
|
+
const B = String.raw`(?<![\w-])`; // a utility or custom property starts here
|
|
196
|
+
const E = String.raw`(?![\w-])`; // …and ends here
|
|
197
|
+
/** An alternation of exactly these names, longest first so `border-subtle` wins over `border`. */
|
|
198
|
+
const oneOf = (names) =>
|
|
199
|
+
[...names].sort((a, b) => b.length - a.length).join("|");
|
|
200
|
+
// The four STATUS families are the only ones that ever shipped a `-subtle` step (guide section 4.5);
|
|
201
|
+
// `--tag-*-subtle` is kept, and a `muted`/`accent`/project family never had one, so neither is matched.
|
|
202
|
+
const SUBTLE_FAMILIES = ["destructive", "success", "warning", "info"];
|
|
203
|
+
export const RETIRED_VOCABULARY = [
|
|
204
|
+
{
|
|
205
|
+
id: "text-h*",
|
|
206
|
+
re: new RegExp(`${B}text-h[1-4]${E}`, "g"),
|
|
207
|
+
hint: (m) => HEADING_HINT[m],
|
|
208
|
+
declared: (m) => `--text-${m.slice(5)}`,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "text-label*",
|
|
212
|
+
re: new RegExp(`${B}text-label(?:-sm)?${E}`, "g"),
|
|
213
|
+
hint: (m) =>
|
|
214
|
+
m.endsWith("-sm") ? "text-xs font-medium" : "text-sm font-medium",
|
|
215
|
+
declared: (m) => `--${m}`,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "text-mono-label",
|
|
219
|
+
re: new RegExp(`${B}text-mono-label${E}`, "g"),
|
|
220
|
+
hint: () =>
|
|
221
|
+
"font-mono text-xs for code/data — a label is font-sans text-xs font-medium; no tracking, no uppercase",
|
|
222
|
+
declared: (m) => `--${m}`,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "text-code*",
|
|
226
|
+
re: new RegExp(`${B}text-code(?:-sm)?${E}`, "g"),
|
|
227
|
+
hint: (m) =>
|
|
228
|
+
m.endsWith("-sm") ? "font-mono text-xs" : "font-mono text-sm",
|
|
229
|
+
declared: (m) => `--${m}`,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "text-strong",
|
|
233
|
+
re: new RegExp(`${B}text-strong${E}`, "g"),
|
|
234
|
+
hint: () => "text-sm font-semibold",
|
|
235
|
+
declared: (m) => `--${m}`,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "text-display-*",
|
|
239
|
+
re: new RegExp(`${B}text-display-(?:sm|md|lg|xl)${E}`, "g"),
|
|
240
|
+
hint: (m) =>
|
|
241
|
+
`${DISPLAY_HINT[m.slice(13)]} — the ramp supplies tracking; write no tracking-*`,
|
|
242
|
+
declared: (m) => `--${m}`,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
// The four STATUS families lost their -subtle step — and only those four ever had one, so the
|
|
246
|
+
// hint's `text-<family>-text` always names a real token.
|
|
247
|
+
id: "*-subtle",
|
|
248
|
+
re: new RegExp(
|
|
249
|
+
`${B}(?:bg|text|border|ring|outline|fill|stroke|divide|from|to|via)-(${SUBTLE_FAMILIES.join("|")})-subtle(?:-hover|-active)?${E}`,
|
|
250
|
+
"g",
|
|
251
|
+
),
|
|
252
|
+
hint: (m, family) => {
|
|
253
|
+
return m.includes("-subtle-")
|
|
254
|
+
? `hover:bg-${family}/20 (the -hover/-active steps are gone)`
|
|
255
|
+
: `bg-${family}/10 (a tint of the family), with text-${family}-text for text on it`;
|
|
256
|
+
},
|
|
257
|
+
declared: (m) => {
|
|
258
|
+
const name = m.replace(/^[a-z]+-/, "");
|
|
259
|
+
return [`--${name}`, `--color-${name}`];
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
// Only the names the system shipped (see ALPHA). `--alpha-*` is an open prefix other
|
|
264
|
+
// libraries use too, and a name we never shipped is not ours to call retired.
|
|
265
|
+
id: "--alpha-*",
|
|
266
|
+
re: new RegExp(`${B}--alpha-(?:${oneOf(Object.keys(ALPHA))})${E}`, "g"),
|
|
267
|
+
hint: (m) => {
|
|
268
|
+
const pct = ALPHA[m.slice(8)];
|
|
269
|
+
if (Array.isArray(pct))
|
|
270
|
+
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]}`;
|
|
271
|
+
return `the literal /${pct}, e.g. bg-foreground/${pct}`;
|
|
272
|
+
},
|
|
273
|
+
declared: (m) => m,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "--opacity-*",
|
|
277
|
+
re: new RegExp(`${B}--opacity-(?:${oneOf(Object.keys(OPACITY))})${E}`, "g"),
|
|
278
|
+
hint: (m) => `opacity-${OPACITY[m.slice(10)]}`,
|
|
279
|
+
declared: (m) => m,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
// `--z-index`, `--z-modal`, … belong to other libraries; only the three named bands are ours.
|
|
283
|
+
id: "--z-*",
|
|
284
|
+
re: new RegExp(`${B}--z-(?:${oneOf(Object.keys(Z))})${E}`, "g"),
|
|
285
|
+
hint: (m) => Z[m.slice(4)],
|
|
286
|
+
declared: (m) => m,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "shadow-overlay",
|
|
290
|
+
re: new RegExp(`${B}(?:--)?shadow-overlay${E}`, "g"),
|
|
291
|
+
hint: () => "shadow-md (a popover) or shadow-lg (a modal)",
|
|
292
|
+
declared: () => "--shadow-overlay",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: "backdrop-blur-glass",
|
|
296
|
+
re: new RegExp(`${B}backdrop-blur-glass${E}`, "g"),
|
|
297
|
+
hint: () =>
|
|
298
|
+
"delete it — the glass effect went with the marketing layer (backdrop-blur-xs is upstream's scrim blur)",
|
|
299
|
+
declared: () => "--blur-glass",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "retired component",
|
|
303
|
+
// An import/require/dynamic-import specifier whose LAST path segment is a retired item — and
|
|
304
|
+
// only when the specifier resolves INTO the ui alias directory (`accept`): a project's own
|
|
305
|
+
// `@/components/layout/section-header` or `./sonner` is its code, not a registry copy. A bare
|
|
306
|
+
// `"sonner"` (the npm package) is not a registry import and is left alone.
|
|
307
|
+
re: new RegExp(
|
|
308
|
+
String.raw`(?:from\s+|import\s*\(\s*|require\s*\(\s*|import\s+)["']([^"']*)\/(${Object.keys(RETIRED_COMPONENTS).join("|")})(?:\.[jt]sx?)?["']`,
|
|
309
|
+
"g",
|
|
310
|
+
),
|
|
311
|
+
accept: (m, ctx) => importsIntoUi(m[1], ctx),
|
|
312
|
+
hint: (_m, _dir, name) => RETIRED_COMPONENTS[name],
|
|
313
|
+
declared: () => null,
|
|
314
|
+
label: (_m, _dir, name) => `import …/${name}`,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
// Code and stylesheets only. Prose (`.md`/`.mdx`) that NAMES a retired token — a changelog, a
|
|
319
|
+
// migration note — is not a use of it, and would bury the real findings.
|
|
320
|
+
const SCAN_EXTENSIONS = /\.(?:[cm]?[jt]sx?|css|scss)$/;
|
|
321
|
+
const SCAN_SKIP_DIRS = new Set([
|
|
322
|
+
"node_modules",
|
|
323
|
+
"dist",
|
|
324
|
+
"build",
|
|
325
|
+
"out",
|
|
326
|
+
"coverage",
|
|
327
|
+
"storybook-static",
|
|
328
|
+
]);
|
|
329
|
+
const SCAN_MAX_FILES = 20000;
|
|
330
|
+
const SCAN_MAX_BYTES = 1024 * 1024;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The directories components.json's `ui` alias names, resolved against the file's own folder.
|
|
334
|
+
* Those files are registry copies — `check-updates` owns them, and a re-pull replaces them — so
|
|
335
|
+
* a retired name inside one is not the consumer's own code. `@/x` and `~/x` resolve to `x` or
|
|
336
|
+
* `src/x`, whichever exists (the shadcn convention for a src-dir project).
|
|
337
|
+
*/
|
|
338
|
+
export function uiAliasDirs(componentsJson, componentsDir) {
|
|
339
|
+
const alias = componentsJson?.aliases?.ui;
|
|
340
|
+
const candidates = [];
|
|
341
|
+
if (typeof alias === "string" && alias.length > 0) {
|
|
342
|
+
const bare = alias.replace(/^(?:@|~)\//, "");
|
|
343
|
+
candidates.push(
|
|
344
|
+
join(componentsDir, bare),
|
|
345
|
+
join(componentsDir, "src", bare),
|
|
346
|
+
);
|
|
347
|
+
} else {
|
|
348
|
+
candidates.push(
|
|
349
|
+
join(componentsDir, "components", "ui"),
|
|
350
|
+
join(componentsDir, "src", "components", "ui"),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return candidates.filter((d) => existsSync(d)).map((d) => resolve(d));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Does an import specifier (the part before the retired name) resolve into the ui alias directory?
|
|
358
|
+
* Three spellings do: the components.json `ui` alias (`@/components/ui`), the registry's own
|
|
359
|
+
* `@/components/ui` convention, and a relative path that lands in one of the ui alias dirs.
|
|
360
|
+
*/
|
|
361
|
+
function importsIntoUi(specDir, { file, uiAliases, uiDirs }) {
|
|
362
|
+
if (uiAliases.includes(specDir)) return true;
|
|
363
|
+
if (specDir === "." || specDir === ".." || /^\.\.?\//.test(specDir)) {
|
|
364
|
+
const target = resolve(dirname(file), specDir);
|
|
365
|
+
return uiDirs.includes(target);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Blank what is not class or CSS usage, keeping every newline so line numbers survive: `//` and
|
|
372
|
+
* `/* *\/` comments, and — in script files — a string literal that reads as prose ("Use text-strong
|
|
373
|
+
* for emphasis"). A class string is lower-case utility tokens; a capitalised word or sentence
|
|
374
|
+
* punctuation marks a sentence (never `!`, which is Tailwind's important modifier: `hidden!`).
|
|
375
|
+
*
|
|
376
|
+
* In a script file, JSX TEXT is not code, so nothing in it may open a comment or a string: an
|
|
377
|
+
* apostrophe ("Don't"), a URL's `//`, a glob's `/*`. Without a parser, position decides — a string
|
|
378
|
+
* opens only where an expression can begin (after `=`, `(`, `,`, `[`, `{`, `:`, `?`, an operator,
|
|
379
|
+
* `=>`, or a keyword such as `return`), and a comment only after whitespace or code punctuation,
|
|
380
|
+
* never after `:` (`https://`) or a word (`src/*.ts`).
|
|
381
|
+
*/
|
|
382
|
+
const EXPRESSION_KEYWORDS = new Set([
|
|
383
|
+
"return",
|
|
384
|
+
"case",
|
|
385
|
+
"typeof",
|
|
386
|
+
"in",
|
|
387
|
+
"of",
|
|
388
|
+
"yield",
|
|
389
|
+
"await",
|
|
390
|
+
"void",
|
|
391
|
+
"delete",
|
|
392
|
+
"throw",
|
|
393
|
+
"from",
|
|
394
|
+
"import",
|
|
395
|
+
"export",
|
|
396
|
+
"default",
|
|
397
|
+
"else",
|
|
398
|
+
"do",
|
|
399
|
+
]);
|
|
400
|
+
function canStartExpression(src, i) {
|
|
401
|
+
let k = i - 1;
|
|
402
|
+
while (k >= 0 && /\s/.test(src[k])) k -= 1;
|
|
403
|
+
if (k < 0) return true;
|
|
404
|
+
const p = src[k];
|
|
405
|
+
if (p === ">") return src[k - 1] === "="; // `=>`, never a JSX tag's `>`
|
|
406
|
+
if ("=(,[{:?&|!+-*%^~<;".includes(p)) return true;
|
|
407
|
+
if (/[\w$]/.test(p)) {
|
|
408
|
+
let w = k;
|
|
409
|
+
while (w >= 0 && /[\w$]/.test(src[w])) w -= 1;
|
|
410
|
+
return EXPRESSION_KEYWORDS.has(src.slice(w + 1, k + 1));
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const canStartComment = (src, i) => i === 0 || /[\s;,{}()[\]]/.test(src[i - 1]);
|
|
415
|
+
|
|
416
|
+
export function maskNonUsage(
|
|
417
|
+
src,
|
|
418
|
+
{ script = true, lineComments = script } = {},
|
|
419
|
+
) {
|
|
420
|
+
const out = src.split("");
|
|
421
|
+
const blank = (from, to) => {
|
|
422
|
+
for (let k = from; k < to; k += 1) if (out[k] !== "\n") out[k] = " ";
|
|
423
|
+
};
|
|
424
|
+
const isProse = (text) =>
|
|
425
|
+
/(?:^|\s)[A-Z][a-z]+(?=[\s,.;:!?]|$)|[a-z][.,;:?](?:\s|$)/.test(
|
|
426
|
+
text.replace(/\[[^\]]*\]|\([^)]*\)/g, ""),
|
|
427
|
+
);
|
|
428
|
+
let i = 0;
|
|
429
|
+
while (i < src.length) {
|
|
430
|
+
const c = src[i];
|
|
431
|
+
const next = src[i + 1];
|
|
432
|
+
if (c === "/" && next === "*" && (!script || canStartComment(src, i))) {
|
|
433
|
+
const end = src.indexOf("*/", i + 2);
|
|
434
|
+
const stop = end === -1 ? src.length : end + 2;
|
|
435
|
+
blank(i, stop);
|
|
436
|
+
i = stop;
|
|
437
|
+
} else if (
|
|
438
|
+
c === "/" &&
|
|
439
|
+
next === "/" &&
|
|
440
|
+
lineComments &&
|
|
441
|
+
(script ? canStartComment(src, i) : i === 0 || /\s/.test(src[i - 1]))
|
|
442
|
+
) {
|
|
443
|
+
const end = src.indexOf("\n", i);
|
|
444
|
+
const stop = end === -1 ? src.length : end;
|
|
445
|
+
blank(i, stop);
|
|
446
|
+
i = stop;
|
|
447
|
+
} else if (
|
|
448
|
+
(c === '"' || c === "'" || c === "`") &&
|
|
449
|
+
(!script || canStartExpression(src, i))
|
|
450
|
+
) {
|
|
451
|
+
let j = i + 1;
|
|
452
|
+
while (j < src.length && src[j] !== c) {
|
|
453
|
+
if (src[j] === "\\") j += 1;
|
|
454
|
+
else if (src[j] === "\n" && c !== "`") break;
|
|
455
|
+
j += 1;
|
|
456
|
+
}
|
|
457
|
+
if (script && isProse(src.slice(i + 1, j))) blank(i + 1, j);
|
|
458
|
+
i = j + 1;
|
|
459
|
+
} else {
|
|
460
|
+
i += 1;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return out.join("");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Class names a stylesheet DEFINES — `@utility text-h2 { … }` (a `-*` functional utility as a
|
|
468
|
+
* prefix) and plain class selectors such as `.text-strong { … }`. A retired name the project defines
|
|
469
|
+
* itself compiles, so it is the project's, not a silent no-op.
|
|
470
|
+
*/
|
|
471
|
+
function definedClasses(css) {
|
|
472
|
+
const exact = new Set();
|
|
473
|
+
const prefixes = [];
|
|
474
|
+
for (const m of css.matchAll(/([^{};]+)\{/g)) {
|
|
475
|
+
const prelude = m[1].trim();
|
|
476
|
+
const utility = prelude.match(/^@utility\s+([\w-]+?)(-\*)?$/);
|
|
477
|
+
if (utility) {
|
|
478
|
+
if (utility[2]) prefixes.push(`${utility[1]}-`);
|
|
479
|
+
else exact.add(utility[1]);
|
|
480
|
+
} else if (!prelude.startsWith("@")) {
|
|
481
|
+
for (const c of prelude.matchAll(/\.((?:\\.|[\w-])+)/g))
|
|
482
|
+
exact.add(c[1].replace(/\\(.)/g, "$1"));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return { exact, prefixes };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Every retired-vocabulary occurrence under `root`, skipping build output and the ui alias dirs. */
|
|
489
|
+
export function scanRetiredVocabulary(
|
|
490
|
+
root,
|
|
491
|
+
{ skipDirs = [], uiAlias = null } = {},
|
|
492
|
+
) {
|
|
493
|
+
const skip = new Set(skipDirs.map((d) => resolve(d)));
|
|
494
|
+
const uiAliases = [
|
|
495
|
+
...new Set(
|
|
496
|
+
[uiAlias, "@/components/ui"]
|
|
497
|
+
.filter((a) => typeof a === "string" && a.length > 0)
|
|
498
|
+
.map((a) => a.replace(/\/+$/, "")),
|
|
499
|
+
),
|
|
500
|
+
];
|
|
501
|
+
const uiDirs = [...skip];
|
|
502
|
+
const files = [];
|
|
503
|
+
const walk = (dir) => {
|
|
504
|
+
if (files.length >= SCAN_MAX_FILES) return;
|
|
505
|
+
let entries;
|
|
506
|
+
try {
|
|
507
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
508
|
+
} catch {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
for (const e of entries) {
|
|
512
|
+
if (e.name.startsWith(".") || SCAN_SKIP_DIRS.has(e.name)) continue;
|
|
513
|
+
const full = join(dir, e.name);
|
|
514
|
+
if (e.isDirectory()) {
|
|
515
|
+
if (!skip.has(resolve(full))) walk(full);
|
|
516
|
+
} else if (e.isFile() && SCAN_EXTENSIONS.test(e.name)) {
|
|
517
|
+
files.push(full);
|
|
518
|
+
if (files.length >= SCAN_MAX_FILES) return;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
walk(root);
|
|
523
|
+
|
|
524
|
+
const sources = [];
|
|
525
|
+
const declared = new Set();
|
|
526
|
+
const declaredClasses = new Set();
|
|
527
|
+
const classPrefixes = [];
|
|
528
|
+
for (const file of files) {
|
|
529
|
+
try {
|
|
530
|
+
if (statSync(file).size > SCAN_MAX_BYTES) continue;
|
|
531
|
+
} catch {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const raw = readIfExists(file);
|
|
535
|
+
if (raw == null) continue;
|
|
536
|
+
const css = /\.s?css$/.test(file);
|
|
537
|
+
const src = maskNonUsage(raw, {
|
|
538
|
+
script: !css,
|
|
539
|
+
lineComments: !css || file.endsWith(".scss"),
|
|
540
|
+
});
|
|
541
|
+
sources.push({ file, src });
|
|
542
|
+
for (const m of src.matchAll(/(?<![\w-])(--[\w-]+)\s*:/g))
|
|
543
|
+
declared.add(m[1]);
|
|
544
|
+
if (css) {
|
|
545
|
+
const defined = definedClasses(src);
|
|
546
|
+
for (const name of defined.exact) declaredClasses.add(name);
|
|
547
|
+
classPrefixes.push(...defined.prefixes);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const definesClass = (name) =>
|
|
551
|
+
declaredClasses.has(name) ||
|
|
552
|
+
classPrefixes.some((prefix) => name.startsWith(prefix));
|
|
553
|
+
|
|
554
|
+
const findings = [];
|
|
555
|
+
for (const { file, src } of sources) {
|
|
556
|
+
const lines = src.split("\n");
|
|
557
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
558
|
+
const line = lines[i];
|
|
559
|
+
if (line.length > 4000) continue; // minified output that slipped past the dir skips
|
|
560
|
+
for (const rule of RETIRED_VOCABULARY) {
|
|
561
|
+
rule.re.lastIndex = 0;
|
|
562
|
+
for (const m of line.matchAll(rule.re)) {
|
|
563
|
+
const groups = m.slice(1);
|
|
564
|
+
if (rule.accept && !rule.accept(m, { file, uiAliases, uiDirs }))
|
|
565
|
+
continue;
|
|
566
|
+
const own = [rule.declared(m[0], ...groups)].flat().filter(Boolean);
|
|
567
|
+
if (own.some((name) => declared.has(name))) continue;
|
|
568
|
+
if (!m[0].startsWith("--") && definesClass(m[0])) continue;
|
|
569
|
+
findings.push({
|
|
570
|
+
file: relative(root, file).split(sep).join("/"),
|
|
571
|
+
line: i + 1,
|
|
572
|
+
match: rule.label ? rule.label(m[0], ...groups) : m[0],
|
|
573
|
+
hint: rule.hint(m[0], ...groups),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
files: sources.length,
|
|
581
|
+
findings,
|
|
582
|
+
truncated: files.length >= SCAN_MAX_FILES,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
117
586
|
export function main(argv = []) {
|
|
118
587
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
119
588
|
console.log(USAGE);
|
|
@@ -304,6 +773,34 @@ export function main(argv = []) {
|
|
|
304
773
|
}
|
|
305
774
|
}
|
|
306
775
|
|
|
776
|
+
// ---- 7. no retired vocabulary in the project's own source --------------------------------
|
|
777
|
+
const skipDirs = componentsJson
|
|
778
|
+
? uiAliasDirs(componentsJson, dirname(componentsJsonPath))
|
|
779
|
+
: uiAliasDirs(null, root);
|
|
780
|
+
const scan = scanRetiredVocabulary(root, {
|
|
781
|
+
skipDirs,
|
|
782
|
+
uiAlias: componentsJson?.aliases?.ui ?? null,
|
|
783
|
+
});
|
|
784
|
+
const scanned = `${scan.files} source file(s)${
|
|
785
|
+
skipDirs.length
|
|
786
|
+
? `, skipping ${skipDirs.map((d) => relative(root, d) || ".").join(", ")}`
|
|
787
|
+
: ""
|
|
788
|
+
}${scan.truncated ? ` (stopped at ${SCAN_MAX_FILES} files)` : ""}`;
|
|
789
|
+
if (scan.findings.length === 0) {
|
|
790
|
+
ok("no retired vocabulary", scanned);
|
|
791
|
+
} else {
|
|
792
|
+
const inFiles = new Set(scan.findings.map((f) => f.file)).size;
|
|
793
|
+
results.push({
|
|
794
|
+
level: "fail",
|
|
795
|
+
name: "no retired vocabulary",
|
|
796
|
+
detail: `${scan.findings.length} occurrence(s) in ${inFiles} file(s) of ${scanned} — these compile to nothing, silently`,
|
|
797
|
+
fix: `rewrite each as its hint says; every row is in ${GUIDE}`,
|
|
798
|
+
findings: scan.findings.map(
|
|
799
|
+
(f) => `${f.file}:${f.line} ${f.match} → ${f.hint}`,
|
|
800
|
+
),
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
307
804
|
// ---- report -------------------------------------------------------------------------------
|
|
308
805
|
const glyph = { ok: "✓", warn: "!", fail: "✗" };
|
|
309
806
|
console.log("");
|
|
@@ -311,6 +808,7 @@ export function main(argv = []) {
|
|
|
311
808
|
console.log(
|
|
312
809
|
` ${glyph[r.level]} ${r.name}${r.detail ? ` — ${r.detail}` : ""}`,
|
|
313
810
|
);
|
|
811
|
+
for (const finding of r.findings ?? []) console.log(` ${finding}`);
|
|
314
812
|
if (r.fix) console.log(` fix: ${r.fix}`);
|
|
315
813
|
}
|
|
316
814
|
|
package/bin/vegastack-design.mjs
CHANGED
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vegastack/design",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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.7.
|
|
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-
|
|
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
|
|
23
|
-
* `@source` through node_modules — it treats `@vegastack/ui` as a
|
|
24
|
-
* glob and silently scans nothing (verified
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
|
@@ -103,7 +110,7 @@ compatibility layer** — an import resolves to nothing and a deleted token sile
|
|
|
103
110
|
nothing, which is the worse half. Both searches are mechanical:
|
|
104
111
|
|
|
105
112
|
```bash
|
|
106
|
-
rg -n 'IconButton|OTPInput|
|
|
113
|
+
rg -n 'IconButton|OTPInput|CheckboxGroup|FieldInline|Segmented|SplitButton|ProgressIndicator|OnboardingChecklist|FloatingSurface|MarketingSurface|ComparisonMatrix|FigureFrame|LogoRow|ParticleField|PricingSection|RuledBand|SectionHeader|Testimonial|StaggeredTextReveal' --glob '!components/ui/**'
|
|
107
114
|
rg -n 'surface-(1|2|3|raised)|--alpha-|--opacity-|--size-|--icon-|--panel-width-|--z-(raised|overlay|toast)|shadow-overlay|text-(h[1-4]|label|label-sm|code|mono-label|display-)|muted-foreground-faint|surfaceInteractive|fillInteractive|fieldControl|selectedChipVariants' --glob '!components/ui/**'
|
|
108
115
|
```
|
|
109
116
|
|
|
@@ -139,9 +146,11 @@ rg -n 'surface-(1|2|3|raised)|--alpha-|--opacity-|--size-|--icon-|--panel-width-
|
|
|
139
146
|
rg -n '@vegastack' components/ui/ -l
|
|
140
147
|
```
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
145
154
|
|
|
146
155
|
A missing `// @vegastack …` provenance header is **normal** and never a finding on its own: the
|
|
147
156
|
shadcn CLI strips leading comments during copy-in.
|
|
@@ -150,9 +159,10 @@ shadcn CLI strips leading comments during copy-in.
|
|
|
150
159
|
|
|
151
160
|
Group by file. Each finding: `file:line` · rule · suggested fix · severity.
|
|
152
161
|
|
|
153
|
-
- **error** — a hardcoded visual value, an accessibility violation, or an
|
|
162
|
+
- **error** — a hardcoded visual value, an accessibility violation, retired vocabulary, or an
|
|
163
|
+
unintended edit to a copied-in component.
|
|
154
164
|
- **warning** — raw HTML where a component exists, a missing state, an off-system utility with a
|
|
155
|
-
working fallback.
|
|
165
|
+
working fallback, a deliberately edited (now owned) copied-in component.
|
|
156
166
|
- **info** — a component with an available update worth a deliberate `--diff` review.
|
|
157
167
|
|
|
158
168
|
Never auto-fix. Report, and let the owner decide.
|
|
@@ -20,7 +20,7 @@ live contract for any single component is its page at
|
|
|
20
20
|
writes:
|
|
21
21
|
|
|
22
22
|
- **Retired, with no drop-in:** `IconButton` → `Button size="icon*"` · `OTPInput` → `InputOTP` ·
|
|
23
|
-
`
|
|
23
|
+
`CheckboxGroup` → `FieldSet` + `Checkbox` ·
|
|
24
24
|
`FieldInline` → `EditableCell` · `Segmented` → a joined `ToggleGroup` · `SplitButton` → a
|
|
25
25
|
`ButtonGroup` composition · `ProgressIndicator` → `Progress` + `Spinner` · `OnboardingChecklist` →
|
|
26
26
|
the `onboarding-01` block. The ten marketing components were deleted outright.
|
|
@@ -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` | `Button size="icon"` + `aria-label` · joined `ToggleGroup` |
|
|
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
|
-
**
|
|
6
|
+
**112 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`) — 692 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
|
|
@@ -37,6 +37,7 @@ starts with `icon-` is a component and never an icon.
|
|
|
37
37
|
- **`label`** — A styled native label for form controls.
|
|
38
38
|
- **`native-select`** — The platform <select>, tokenized — the OS picker on mobile, with option groups.
|
|
39
39
|
- **`number-field`** — Locale-aware numeric input on Base UI's NumberField in upstream's InputGroup chrome, with full-height flanking steppers.
|
|
40
|
+
- **`password-input`** — A password field with a show/hide toggle, composed from InputGroup, forwarding native input props and the ref to the inner input.
|
|
40
41
|
- **`radio-group`** — Mutually-exclusive options with arrow-key navigation and a 24px invisible hit area (A11Y-2).
|
|
41
42
|
- **`region-select`** — A searchable picker of states/provinces for a country, with a free-text fallback for countries with no subdivisions. A thin wrapper over SearchableSelect fed by the geo-data item.
|
|
42
43
|
- **`search-input`** — A token-safe search field with a consistent clear action and controlled or uncontrolled native input semantics.
|
|
@@ -77,6 +78,7 @@ starts with `icon-` is a component and never an icon.
|
|
|
77
78
|
|
|
78
79
|
- **`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
80
|
- **`data-list`** — A generic, typed data table — configurable columns, row selection, sortable headers, plus loading and empty states.
|
|
81
|
+
- **`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
82
|
- **`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
83
|
- **`filter-bar`** — A row of removable filter chips, an "Add filter" dropdown, and an optional search input — for list and table filter toolbars.
|
|
82
84
|
- **`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.
|