@vegastack/design 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/check-updates.mjs
CHANGED
|
@@ -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
|
-
{
|
|
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
|
+
}
|
package/bin/vegastack-design.mjs
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.3.0",
|
|
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": {
|
|
@@ -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
|
-
**
|
|
6
|
+
**108 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`) — 554 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,10 +100,12 @@ 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
111
|
- **`progress-indicator`** — A compact circular pie-fill progress indicator (0–100%) — a server-safe SVG glyph in circle or squircle shapes.
|
|
@@ -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.
|