@work-rjkashyap/unified-ui 0.3.1 → 0.3.3

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/cli.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  // ============================================================================
4
3
  // Unified UI — CLI
5
4
  // ============================================================================
@@ -13,7 +12,7 @@
13
12
  // npx @work-rjkashyap/unified-ui init
14
13
  //
15
14
  // Components are fetched from the registry at:
16
- // https://unified-ui-rajeshwar.vercel.app/r/<name>.json
15
+ // https://unified-ui.space/r/<name>.json
17
16
  //
18
17
  // Files are written into the user's project at:
19
18
  // src/components/ui/<component>.tsx
@@ -24,32 +23,27 @@
24
23
  // This CLI resolves the full dependency tree — if you add "confirm-dialog",
25
24
  // it also pulls in "alert-dialog", "button", "cn", "focus-ring", etc.
26
25
  // ============================================================================
27
-
26
+ import { execSync } from "node:child_process";
28
27
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
29
28
  import { dirname, join, resolve } from "node:path";
30
29
  import { createInterface } from "node:readline";
31
- import { execSync, spawnSync } from "node:child_process";
32
30
 
33
31
  // ---------------------------------------------------------------------------
34
32
  // Config
35
33
  // ---------------------------------------------------------------------------
36
-
37
34
  const REGISTRY_BASE_URL =
38
- process.env.UNIFIED_UI_REGISTRY_URL ||
39
- "https://unified-ui-rajeshwar.vercel.app/r";
40
-
35
+ process.env.UNIFIED_UI_REGISTRY_URL || "https://www.unified-ui.space/r";
41
36
  const CONFIG_FILE = "unified-ui.json";
42
-
43
37
  // ---------------------------------------------------------------------------
44
38
  // Starter kit templates
45
39
  // ---------------------------------------------------------------------------
46
-
47
40
  const FRAMEWORKS = [
48
41
  {
49
42
  name: "vite-react",
50
43
  label: "Vite + React",
51
44
  description: "Vite + React 19 SPA with full component library",
52
- scaffoldCmd: (name) => `npm create vite@latest ${name} -- --template react-ts`,
45
+ scaffoldCmd: (name) =>
46
+ `npm create vite@latest ${name} -- --template react-ts`,
53
47
  deps: ["@work-rjkashyap/unified-ui"],
54
48
  devDeps: ["@tailwindcss/vite", "tailwindcss"],
55
49
  },
@@ -57,7 +51,8 @@ const FRAMEWORKS = [
57
51
  name: "nextjs",
58
52
  label: "Next.js",
59
53
  description: "Next.js App Router with SSR + full component library",
60
- scaffoldCmd: (name) => `npx create-next-app@latest ${name} --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --yes`,
54
+ scaffoldCmd: (name) =>
55
+ `npx create-next-app@latest ${name} --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --yes`,
61
56
  deps: ["@work-rjkashyap/unified-ui", "next-themes"],
62
57
  devDeps: [],
63
58
  },
@@ -78,9 +73,8 @@ const FRAMEWORKS = [
78
73
  devDeps: ["@tailwindcss/vite", "tailwindcss"],
79
74
  },
80
75
  ];
81
-
82
76
  const DEFAULT_CONFIG = {
83
- $schema: "https://unified-ui-rajeshwar.vercel.app/r/schema/config.json",
77
+ $schema: "https://unified-ui.space/r/schema/config.json",
84
78
  srcDir: "src",
85
79
  aliases: {
86
80
  components: "@/components/ui",
@@ -89,7 +83,6 @@ const DEFAULT_CONFIG = {
89
83
  },
90
84
  typescript: true,
91
85
  };
92
-
93
86
  const COLORS = {
94
87
  reset: "\x1b[0m",
95
88
  bold: "\x1b[1m",
@@ -101,25 +94,19 @@ const COLORS = {
101
94
  magenta: "\x1b[35m",
102
95
  cyan: "\x1b[36m",
103
96
  };
104
-
105
97
  const c = (color, text) => `${COLORS[color]}${text}${COLORS.reset}`;
106
-
107
98
  // ---------------------------------------------------------------------------
108
99
  // Helpers
109
100
  // ---------------------------------------------------------------------------
110
-
111
101
  function log(msg = "") {
112
102
  console.log(msg);
113
103
  }
114
-
115
104
  function logStep(icon, msg) {
116
105
  console.log(` ${icon} ${msg}`);
117
106
  }
118
-
119
107
  function logError(msg) {
120
108
  console.error(`\n ${c("red", "✗")} ${msg}\n`);
121
109
  }
122
-
123
110
  async function confirm(question) {
124
111
  const rl = createInterface({ input: process.stdin, output: process.stdout });
125
112
  return new Promise((res) => {
@@ -129,7 +116,6 @@ async function confirm(question) {
129
116
  });
130
117
  });
131
118
  }
132
-
133
119
  async function promptText(question, defaultValue = "") {
134
120
  const rl = createInterface({ input: process.stdin, output: process.stdout });
135
121
  const hint = defaultValue ? ` ${c("dim", `(${defaultValue})`)}` : "";
@@ -140,25 +126,25 @@ async function promptText(question, defaultValue = "") {
140
126
  });
141
127
  });
142
128
  }
143
-
144
129
  async function promptSelect(question, options) {
145
130
  log(` ${question}`);
146
131
  log();
147
132
  for (let i = 0; i < options.length; i++) {
148
133
  const opt = options[i];
149
- log(` ${c("cyan", String(i + 1))}. ${c("bold", opt.label.padEnd(18))} ${c("dim", opt.description)}`);
134
+ log(
135
+ ` ${c("cyan", String(i + 1))}. ${c("bold", opt.label.padEnd(18))} ${c("dim", opt.description)}`,
136
+ );
150
137
  }
151
138
  log();
152
139
  const rl = createInterface({ input: process.stdin, output: process.stdout });
153
140
  return new Promise((res) => {
154
- rl.question(` ${c("dim", `Select (1-${options.length}):`) } `, (answer) => {
141
+ rl.question(` ${c("dim", `Select (1-${options.length}):`)} `, (answer) => {
155
142
  rl.close();
156
143
  const idx = parseInt(answer.trim(), 10) - 1;
157
144
  res(idx >= 0 && idx < options.length ? options[idx] : null);
158
145
  });
159
146
  });
160
147
  }
161
-
162
148
  function runCmd(cmd, cwd, stdio = "inherit") {
163
149
  try {
164
150
  execSync(cmd, { cwd, stdio });
@@ -167,28 +153,25 @@ function runCmd(cmd, cwd, stdio = "inherit") {
167
153
  return false;
168
154
  }
169
155
  }
170
-
171
156
  function ensureDir(dir) {
172
157
  mkdirSync(dir, { recursive: true });
173
158
  }
174
-
175
159
  function writeOverlay(targetPath, content) {
176
160
  ensureDir(dirname(targetPath));
177
161
  writeFileSync(targetPath, content);
178
162
  }
179
-
180
163
  async function fetchJSON(url) {
181
164
  const response = await fetch(url);
182
165
  if (!response.ok) {
183
- throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
166
+ throw new Error(
167
+ `Failed to fetch ${url}: ${response.status} ${response.statusText}`,
168
+ );
184
169
  }
185
170
  return response.json();
186
171
  }
187
-
188
172
  // ---------------------------------------------------------------------------
189
173
  // Config management
190
174
  // ---------------------------------------------------------------------------
191
-
192
175
  function findProjectRoot() {
193
176
  let dir = process.cwd();
194
177
  while (dir !== dirname(dir)) {
@@ -197,11 +180,9 @@ function findProjectRoot() {
197
180
  }
198
181
  return process.cwd();
199
182
  }
200
-
201
183
  function loadConfig() {
202
184
  const root = findProjectRoot();
203
185
  const configPath = join(root, CONFIG_FILE);
204
-
205
186
  if (existsSync(configPath)) {
206
187
  try {
207
188
  return {
@@ -213,28 +194,27 @@ function loadConfig() {
213
194
  return { root, ...DEFAULT_CONFIG };
214
195
  }
215
196
  }
216
-
217
197
  return { root, ...DEFAULT_CONFIG };
218
198
  }
219
-
220
199
  function saveConfig(config) {
221
200
  const root = findProjectRoot();
222
201
  const configPath = join(root, CONFIG_FILE);
223
202
  const { root: _root, ...rest } = config;
224
- writeFileSync(configPath, JSON.stringify(rest, null, 2) + "\n");
203
+ writeFileSync(configPath, `${JSON.stringify(rest, null, 2)}\n`);
225
204
  }
226
-
227
205
  // ---------------------------------------------------------------------------
228
206
  // Path resolution
229
207
  // ---------------------------------------------------------------------------
230
-
231
208
  function resolveTargetPath(config, file) {
232
209
  const srcDir = join(config.root, config.srcDir);
233
-
234
210
  switch (file.type) {
235
211
  case "component":
236
212
  return join(srcDir, "components", "ui", basename(file.path));
237
213
  case "util":
214
+ // Preserve full subdirectory structure for lib/tokens/* and lib/motion/*
215
+ if (file.path.startsWith("lib/tokens/")) {
216
+ return join(srcDir, file.path);
217
+ }
238
218
  if (file.path.includes("motion/")) {
239
219
  return join(srcDir, "lib", "motion", basename(file.path));
240
220
  }
@@ -247,11 +227,9 @@ function resolveTargetPath(config, file) {
247
227
  return join(srcDir, file.target || file.path);
248
228
  }
249
229
  }
250
-
251
230
  function basename(p) {
252
231
  return p.split("/").pop();
253
232
  }
254
-
255
233
  // ---------------------------------------------------------------------------
256
234
  // Import path rewriting
257
235
  // ---------------------------------------------------------------------------
@@ -262,10 +240,8 @@ function basename(p) {
262
240
  //
263
241
  // We rewrite these to match the user's alias config.
264
242
  // ---------------------------------------------------------------------------
265
-
266
243
  function rewriteContentImports(content, config) {
267
244
  let result = content;
268
-
269
245
  // Rewrite @/lib/* -> user's lib alias
270
246
  if (config.aliases.lib !== "@/lib") {
271
247
  result = result.replace(
@@ -273,7 +249,6 @@ function rewriteContentImports(content, config) {
273
249
  `from "${config.aliases.lib}/`,
274
250
  );
275
251
  }
276
-
277
252
  // Rewrite @/components/ui/* -> user's components alias
278
253
  if (config.aliases.components !== "@/components/ui") {
279
254
  result = result.replace(
@@ -281,35 +256,28 @@ function rewriteContentImports(content, config) {
281
256
  `from "${config.aliases.components}/`,
282
257
  );
283
258
  }
284
-
285
259
  return result;
286
260
  }
287
-
288
261
  // ---------------------------------------------------------------------------
289
262
  // Dependency resolution
290
263
  // ---------------------------------------------------------------------------
291
-
292
264
  async function resolveFullDependencyTree(names, registryUrl) {
293
265
  const resolved = new Map();
294
266
  const queue = [...names];
295
267
  const visited = new Set();
296
-
297
268
  while (queue.length > 0) {
298
269
  const name = queue.shift();
299
270
  if (visited.has(name)) continue;
300
271
  visited.add(name);
301
-
302
272
  try {
303
273
  const item = await fetchJSON(`${registryUrl}/${name}.json`);
304
274
  resolved.set(name, item);
305
-
306
275
  // Queue registry dependencies (other components)
307
276
  if (item.registryDependencies) {
308
277
  for (const dep of item.registryDependencies) {
309
278
  if (!visited.has(dep)) queue.push(dep);
310
279
  }
311
280
  }
312
-
313
281
  // Queue internal util dependencies
314
282
  if (item.internalDependencies) {
315
283
  for (const util of item.internalDependencies.utils || []) {
@@ -323,21 +291,18 @@ async function resolveFullDependencyTree(names, registryUrl) {
323
291
  logError(`Could not fetch "${name}" from registry: ${err.message}`);
324
292
  }
325
293
  }
326
-
327
294
  return resolved;
328
295
  }
329
-
330
296
  // ---------------------------------------------------------------------------
331
297
  // npm dependency installer
332
298
  // ---------------------------------------------------------------------------
333
-
334
299
  async function detectPackageManager(root) {
335
- if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) return "bun";
300
+ if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb")))
301
+ return "bun";
336
302
  if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
337
303
  if (existsSync(join(root, "yarn.lock"))) return "yarn";
338
304
  return "npm";
339
305
  }
340
-
341
306
  function getInstallCommand(pm, deps) {
342
307
  const packages = deps.join(" ");
343
308
  switch (pm) {
@@ -351,59 +316,289 @@ function getInstallCommand(pm, deps) {
351
316
  return `npm install ${packages}`;
352
317
  }
353
318
  }
354
-
355
319
  async function installNpmDeps(deps, root) {
356
320
  if (deps.length === 0) return;
357
-
358
321
  const pm = await detectPackageManager(root);
359
322
  const cmd = getInstallCommand(pm, deps);
360
-
361
323
  logStep("📦", `Installing npm dependencies with ${c("cyan", pm)}...`);
362
324
  logStep(" ", c("dim", cmd));
363
-
364
325
  const { execSync } = await import("node:child_process");
365
326
  try {
366
327
  execSync(cmd, { cwd: root, stdio: "pipe" });
367
328
  logStep("✓", c("green", `${deps.length} package(s) installed`));
368
- } catch (err) {
329
+ } catch (_err) {
330
+ logStep(
331
+ "⚠",
332
+ c("yellow", `Auto-install failed. Run manually:\n ${cmd}`),
333
+ );
334
+ }
335
+ }
336
+ async function installNpmDevDeps(deps, root) {
337
+ if (deps.length === 0) return;
338
+ const pm = await detectPackageManager(root);
339
+ const cmd = getInstallCommand(pm, deps)
340
+ .replace(" add ", " add -D ")
341
+ .replace(" install ", " install -D ");
342
+ logStep("📦", `Installing dev dependencies with ${c("cyan", pm)}...`);
343
+ logStep(" ", c("dim", cmd));
344
+ try {
345
+ execSync(cmd, { cwd: root, stdio: "pipe" });
346
+ logStep("✓", c("green", `${deps.length} dev package(s) installed`));
347
+ } catch (_err) {
369
348
  logStep(
370
349
  "⚠",
371
350
  c("yellow", `Auto-install failed. Run manually:\n ${cmd}`),
372
351
  );
373
352
  }
374
353
  }
354
+ // ---------------------------------------------------------------------------
355
+ // Vite + Tailwind CSS detection & setup
356
+ // ---------------------------------------------------------------------------
357
+ function detectViteConfig(root) {
358
+ for (const name of [
359
+ "vite.config.ts",
360
+ "vite.config.js",
361
+ "vite.config.mjs",
362
+ "vite.config.mts",
363
+ ]) {
364
+ if (existsSync(join(root, name))) return name;
365
+ }
366
+ return null;
367
+ }
368
+ function hasDependency(root, pkg) {
369
+ try {
370
+ const raw = readFileSync(join(root, "package.json"), "utf-8");
371
+ const json = JSON.parse(raw);
372
+ const all = {
373
+ ...json.dependencies,
374
+ ...json.devDependencies,
375
+ ...json.peerDependencies,
376
+ };
377
+ return Boolean(all[pkg]);
378
+ } catch {
379
+ return false;
380
+ }
381
+ }
382
+ async function setupTailwindForVite(root, config) {
383
+ const viteConfigName = detectViteConfig(root);
384
+ if (!viteConfigName) return; // Not a Vite project
385
+
386
+ logStep("🔍", "Detected Vite project");
387
+
388
+ // 1. Install tailwindcss + @tailwindcss/vite if missing
389
+ const missingDevDeps = [];
390
+ if (!hasDependency(root, "tailwindcss")) missingDevDeps.push("tailwindcss");
391
+ if (!hasDependency(root, "@tailwindcss/vite"))
392
+ missingDevDeps.push("@tailwindcss/vite");
393
+ if (missingDevDeps.length > 0) {
394
+ await installNpmDevDeps(missingDevDeps, root);
395
+ } else {
396
+ logStep("✓", c("dim", "tailwindcss + @tailwindcss/vite already installed"));
397
+ }
398
+
399
+ // 2. Patch vite.config to add tailwindcss plugin, path import, and @ alias
400
+ const viteConfigPath = join(root, viteConfigName);
401
+ let viteContent = readFileSync(viteConfigPath, "utf-8");
402
+ let viteModified = false;
403
+
404
+ // 2a. Add @tailwindcss/vite import + plugin
405
+ if (!viteContent.includes("@tailwindcss/vite")) {
406
+ const tailwindImport = 'import tailwindcss from "@tailwindcss/vite";\n';
407
+ const importMatch = viteContent.match(/^(import\s.+\n)+/m);
408
+ if (importMatch) {
409
+ const lastImportEnd = importMatch.index + importMatch[0].length;
410
+ viteContent =
411
+ viteContent.slice(0, lastImportEnd) +
412
+ tailwindImport +
413
+ viteContent.slice(lastImportEnd);
414
+ } else {
415
+ viteContent = tailwindImport + viteContent;
416
+ }
417
+ if (viteContent.includes("plugins:")) {
418
+ viteContent = viteContent.replace(
419
+ /plugins:\s*\[/,
420
+ "plugins: [\n tailwindcss(),",
421
+ );
422
+ } else if (viteContent.includes("defineConfig({")) {
423
+ viteContent = viteContent.replace(
424
+ /defineConfig\(\{/,
425
+ "defineConfig({\n plugins: [tailwindcss()],",
426
+ );
427
+ }
428
+ viteModified = true;
429
+ } else {
430
+ logStep("✓", c("dim", `${viteConfigName} already has @tailwindcss/vite`));
431
+ }
432
+
433
+ // 2b. Add path import + resolve.alias for @
434
+ if (
435
+ !viteContent.includes("node:path") &&
436
+ !viteContent.match(/import\s+path\s+from\s+["']path["']/)
437
+ ) {
438
+ const pathImport = 'import path from "node:path";\n';
439
+ const importMatch = viteContent.match(/^(import\s.+\n)+/m);
440
+ if (importMatch) {
441
+ const lastImportEnd = importMatch.index + importMatch[0].length;
442
+ viteContent =
443
+ viteContent.slice(0, lastImportEnd) +
444
+ pathImport +
445
+ viteContent.slice(lastImportEnd);
446
+ } else {
447
+ viteContent = pathImport + viteContent;
448
+ }
449
+ viteModified = true;
450
+ }
451
+
452
+ if (
453
+ !viteContent.includes("resolve:") &&
454
+ !viteContent.includes("resolve.alias")
455
+ ) {
456
+ // Add resolve.alias block before the closing of defineConfig
457
+ const resolveBlock = ` resolve: {\n alias: {\n "@": path.resolve(__dirname, "src"),\n },\n },`;
458
+ if (viteContent.includes("plugins:")) {
459
+ // Insert after plugins block — find the plugins array closing and add after
460
+ viteContent = viteContent.replace(
461
+ /(plugins:\s*\[[\s\S]*?\],?\n)/m,
462
+ `$1${resolveBlock}\n`,
463
+ );
464
+ } else if (viteContent.includes("defineConfig({")) {
465
+ viteContent = viteContent.replace(
466
+ /defineConfig\(\{/,
467
+ `defineConfig({\n${resolveBlock}`,
468
+ );
469
+ }
470
+ viteModified = true;
471
+ } else {
472
+ logStep("✓", c("dim", `${viteConfigName} already has resolve.alias`));
473
+ }
474
+
475
+ if (viteModified) {
476
+ writeFileSync(viteConfigPath, viteContent);
477
+ logStep(
478
+ "✓",
479
+ `Patched ${c("cyan", viteConfigName)} with Tailwind CSS plugin + @ alias`,
480
+ );
481
+ }
482
+
483
+ // 2c. Patch tsconfig to add @/* path alias
484
+ const tsconfigCandidates = ["tsconfig.app.json", "tsconfig.json"];
485
+ for (const tscName of tsconfigCandidates) {
486
+ const tscPath = join(root, tscName);
487
+ if (!existsSync(tscPath)) continue;
488
+ const tscContent = readFileSync(tscPath, "utf-8");
489
+ if (tscContent.includes('"@/*"')) {
490
+ logStep("✓", c("dim", `${tscName} already has @/* path alias`));
491
+ break;
492
+ }
493
+ try {
494
+ // Strip comments for JSON parsing (single-line // comments)
495
+ const stripped = tscContent.replace(/^\s*\/\/.*$/gm, "");
496
+ const tsconfig = JSON.parse(stripped);
497
+ if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
498
+ if (!tsconfig.compilerOptions.paths) tsconfig.compilerOptions.paths = {};
499
+ tsconfig.compilerOptions.paths["@/*"] = ["./src/*"];
500
+ // Also ensure baseUrl is set for paths to work
501
+ if (!tsconfig.compilerOptions.baseUrl) {
502
+ tsconfig.compilerOptions.baseUrl = ".";
503
+ }
504
+ writeFileSync(tscPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
505
+ logStep("✓", `Patched ${c("cyan", tscName)} with @/* path alias`);
506
+ } catch {
507
+ logStep(
508
+ "⚠",
509
+ c("yellow", `Could not patch ${tscName} — add @/* path alias manually`),
510
+ );
511
+ }
512
+ break;
513
+ }
375
514
 
515
+ // 3. Patch CSS entry file
516
+ const srcDir = join(root, config.srcDir);
517
+ const cssCandidates = [
518
+ "index.css",
519
+ "App.css",
520
+ "style.css",
521
+ "main.css",
522
+ "globals.css",
523
+ ];
524
+ let cssPath = null;
525
+ for (const name of cssCandidates) {
526
+ const candidate = join(srcDir, name);
527
+ if (existsSync(candidate)) {
528
+ cssPath = candidate;
529
+ break;
530
+ }
531
+ }
532
+ if (!cssPath) {
533
+ // Create src/index.css if no CSS file found
534
+ cssPath = join(srcDir, "index.css");
535
+ }
536
+ const tailwindDirective = '@import "tailwindcss";';
537
+ const unifiedUiImport = '@import "@/styles/unified-ui.css";';
538
+ if (existsSync(cssPath)) {
539
+ let cssContent = readFileSync(cssPath, "utf-8");
540
+ let modified = false;
541
+ if (!cssContent.includes(tailwindDirective)) {
542
+ cssContent = `${tailwindDirective}\n${cssContent}`;
543
+ modified = true;
544
+ }
545
+ if (!cssContent.includes(unifiedUiImport)) {
546
+ // Insert right after the tailwindcss import
547
+ cssContent = cssContent.replace(
548
+ tailwindDirective,
549
+ `${tailwindDirective}\n${unifiedUiImport}`,
550
+ );
551
+ modified = true;
552
+ }
553
+ if (modified) {
554
+ writeFileSync(cssPath, cssContent);
555
+ logStep(
556
+ "✓",
557
+ `Updated ${c("cyan", cssPath.replace(`${root}/`, ""))} with Tailwind imports`,
558
+ );
559
+ } else {
560
+ logStep(
561
+ "✓",
562
+ c(
563
+ "dim",
564
+ `${cssPath.replace(`${root}/`, "")} already has Tailwind imports`,
565
+ ),
566
+ );
567
+ }
568
+ } else {
569
+ const content = `${tailwindDirective}\n${unifiedUiImport}\n`;
570
+ mkdirSync(dirname(cssPath), { recursive: true });
571
+ writeFileSync(cssPath, content);
572
+ logStep(
573
+ "✓",
574
+ `Created ${c("cyan", cssPath.replace(`${root}/`, ""))} with Tailwind imports`,
575
+ );
576
+ }
577
+ }
376
578
  // ---------------------------------------------------------------------------
377
579
  // File writer
378
580
  // ---------------------------------------------------------------------------
379
-
380
581
  function writeFile(targetPath, content, config, overwrite = false) {
381
582
  const rewritten = rewriteContentImports(content, config);
382
-
383
583
  if (existsSync(targetPath) && !overwrite) {
384
584
  return { path: targetPath, status: "skipped" };
385
585
  }
386
-
387
586
  mkdirSync(dirname(targetPath), { recursive: true });
388
587
  writeFileSync(targetPath, rewritten);
389
588
  return { path: targetPath, status: "created" };
390
589
  }
391
-
392
590
  // ---------------------------------------------------------------------------
393
591
  // Commands
394
592
  // ---------------------------------------------------------------------------
395
-
396
593
  // ---------------------------------------------------------------------------
397
594
  // Starter kit overlays (embedded content)
398
595
  // ---------------------------------------------------------------------------
399
-
400
596
  const OVERLAYS = {
401
597
  "vite-react": {
402
598
  files: {
403
599
  "vite.config.ts": `import tailwindcss from "@tailwindcss/vite";
404
600
  import react from "@vitejs/plugin-react";
405
601
  import { defineConfig } from "vite";
406
-
407
602
  export default defineConfig({
408
603
  plugins: [react(), tailwindcss()],
409
604
  resolve: {
@@ -415,7 +610,6 @@ export default defineConfig({
415
610
  `,
416
611
  "src/index.css": `@import "tailwindcss";
417
612
  @import "@work-rjkashyap/unified-ui/styles.css";
418
-
419
613
  body {
420
614
  min-height: 100svh;
421
615
  }
@@ -425,7 +619,6 @@ import { createRoot } from "react-dom/client";
425
619
  import { DSThemeProvider } from "@work-rjkashyap/unified-ui/theme";
426
620
  import App from "./App";
427
621
  import "./index.css";
428
-
429
622
  createRoot(document.getElementById("root")!).render(
430
623
  <StrictMode>
431
624
  <DSThemeProvider manageHtmlClass>
@@ -437,10 +630,8 @@ createRoot(document.getElementById("root")!).render(
437
630
  "src/App.tsx": `import { Button } from "@work-rjkashyap/unified-ui/components";
438
631
  import { Heading, Body } from "@work-rjkashyap/unified-ui/primitives";
439
632
  import { useDSTheme } from "@work-rjkashyap/unified-ui/theme";
440
-
441
633
  function App() {
442
634
  const { theme, setTheme } = useDSTheme();
443
-
444
635
  return (
445
636
  <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-8 text-foreground">
446
637
  <div className="w-full max-w-md space-y-6 rounded-lg border border-border bg-card p-8">
@@ -450,7 +641,6 @@ function App() {
450
641
  Your starter project is ready. Start building!
451
642
  </Body>
452
643
  </div>
453
-
454
644
  <div className="flex items-center justify-center gap-3">
455
645
  <Button variant="primary">Get Started</Button>
456
646
  <Button
@@ -464,17 +654,14 @@ function App() {
464
654
  </div>
465
655
  );
466
656
  }
467
-
468
657
  export default App;
469
658
  `,
470
659
  },
471
660
  },
472
-
473
661
  nextjs: {
474
662
  files: {
475
663
  "src/app/globals.css": `@import "tailwindcss";
476
664
  @import "@work-rjkashyap/unified-ui/styles.css";
477
-
478
665
  body {
479
666
  min-height: 100svh;
480
667
  }
@@ -483,12 +670,10 @@ body {
483
670
  import { ThemeProvider } from "next-themes";
484
671
  import { DSThemeProvider } from "@work-rjkashyap/unified-ui/theme";
485
672
  import "./globals.css";
486
-
487
673
  export const metadata: Metadata = {
488
674
  title: "Unified UI App",
489
675
  description: "Built with Unified UI and Next.js",
490
676
  };
491
-
492
677
  export default function RootLayout({
493
678
  children,
494
679
  }: {
@@ -511,14 +696,11 @@ export default function RootLayout({
511
696
  }
512
697
  `,
513
698
  "src/app/page.tsx": `"use client";
514
-
515
699
  import { Button } from "@work-rjkashyap/unified-ui/components";
516
700
  import { Heading, Body } from "@work-rjkashyap/unified-ui/primitives";
517
701
  import { useDSTheme } from "@work-rjkashyap/unified-ui/theme";
518
-
519
702
  export default function Home() {
520
703
  const { theme, setTheme } = useDSTheme();
521
-
522
704
  return (
523
705
  <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-8 text-foreground">
524
706
  <div className="w-full max-w-md space-y-6 rounded-lg border border-border bg-card p-8">
@@ -528,7 +710,6 @@ export default function Home() {
528
710
  Your Next.js project is ready. Start building!
529
711
  </Body>
530
712
  </div>
531
-
532
713
  <div className="flex items-center justify-center gap-3">
533
714
  <Button variant="primary">Get Started</Button>
534
715
  <Button
@@ -545,13 +726,11 @@ export default function Home() {
545
726
  `,
546
727
  },
547
728
  },
548
-
549
729
  vuejs: {
550
730
  files: {
551
731
  "vite.config.ts": `import tailwindcss from "@tailwindcss/vite";
552
732
  import vue from "@vitejs/plugin-vue";
553
733
  import { defineConfig } from "vite";
554
-
555
734
  export default defineConfig({
556
735
  plugins: [vue(), tailwindcss()],
557
736
  resolve: {
@@ -563,7 +742,6 @@ export default defineConfig({
563
742
  `,
564
743
  "src/style.css": `@import "tailwindcss";
565
744
  @import "@work-rjkashyap/unified-ui/styles.css";
566
-
567
745
  body {
568
746
  min-height: 100svh;
569
747
  }
@@ -571,12 +749,10 @@ body {
571
749
  "src/main.ts": `import { createApp } from "vue";
572
750
  import App from "./App.vue";
573
751
  import "./style.css";
574
-
575
752
  createApp(App).mount("#app");
576
753
  `,
577
754
  "src/lib/cn.ts": `import { type ClassValue, clsx } from "clsx";
578
755
  import { twMerge } from "tailwind-merge";
579
-
580
756
  export function cn(...inputs: ClassValue[]) {
581
757
  return twMerge(clsx(inputs));
582
758
  }
@@ -596,10 +772,8 @@ import {
596
772
  UiText,
597
773
  } from "./components/ui";
598
774
  import { ref } from "vue";
599
-
600
775
  const email = ref("");
601
776
  </script>
602
-
603
777
  <template>
604
778
  <div
605
779
  class="flex min-h-svh flex-col items-center justify-center gap-8 bg-background p-8 text-foreground"
@@ -611,7 +785,6 @@ const email = ref("");
611
785
  Your Vue.js project is ready with components. Start building!
612
786
  </UiText>
613
787
  </UiCardHeader>
614
-
615
788
  <UiCardBody class="space-y-6">
616
789
  <!-- Buttons -->
617
790
  <div class="space-y-2">
@@ -624,7 +797,6 @@ const email = ref("");
624
797
  <UiButton variant="primary" :loading="true" size="sm">Loading</UiButton>
625
798
  </div>
626
799
  </div>
627
-
628
800
  <!-- Badges -->
629
801
  <div class="space-y-2">
630
802
  <UiText variant="label">Badges</UiText>
@@ -638,19 +810,16 @@ const email = ref("");
638
810
  <UiBadge variant="outline">Outline</UiBadge>
639
811
  </div>
640
812
  </div>
641
-
642
813
  <!-- Input -->
643
814
  <div class="space-y-2">
644
815
  <UiText variant="label">Input</UiText>
645
816
  <UiInput v-model="email" placeholder="you@example.com" />
646
817
  </div>
647
-
648
818
  <!-- Alert -->
649
819
  <UiAlert variant="info" title="All set!">
650
820
  Your design system components are working in Vue.
651
821
  </UiAlert>
652
822
  </UiCardBody>
653
-
654
823
  <UiCardFooter class="justify-between">
655
824
  <UiButton variant="primary">Get Started</UiButton>
656
825
  <ThemeToggle />
@@ -661,9 +830,7 @@ const email = ref("");
661
830
  `,
662
831
  "src/components/ThemeToggle.vue": `<script setup lang="ts">
663
832
  import { ref, onMounted } from "vue";
664
-
665
833
  const theme = ref<"light" | "dark">("light");
666
-
667
834
  onMounted(() => {
668
835
  const stored = localStorage.getItem("theme");
669
836
  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
@@ -671,18 +838,15 @@ onMounted(() => {
671
838
  (stored as "light" | "dark") || (prefersDark ? "dark" : "light");
672
839
  applyTheme();
673
840
  });
674
-
675
841
  function toggle() {
676
842
  theme.value = theme.value === "dark" ? "light" : "dark";
677
843
  applyTheme();
678
844
  }
679
-
680
845
  function applyTheme() {
681
846
  document.documentElement.classList.toggle("dark", theme.value === "dark");
682
847
  localStorage.setItem("theme", theme.value);
683
848
  }
684
849
  </script>
685
-
686
850
  <template>
687
851
  <button
688
852
  class="inline-flex h-9 items-center justify-center rounded-md border border-border bg-background px-4 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
@@ -706,10 +870,8 @@ export { default as UiText } from "./Text.vue";
706
870
  "src/components/ui/Button.vue": `<script setup lang="ts">
707
871
  import { computed, type HTMLAttributes } from "vue";
708
872
  import { cn } from "@/lib/cn";
709
-
710
873
  type Variant = "primary" | "secondary" | "ghost" | "danger";
711
874
  type Size = "sm" | "md" | "lg";
712
-
713
875
  interface Props {
714
876
  variant?: Variant;
715
877
  size?: Size;
@@ -720,7 +882,6 @@ interface Props {
720
882
  as?: string;
721
883
  class?: HTMLAttributes["class"];
722
884
  }
723
-
724
885
  const props = withDefaults(defineProps<Props>(), {
725
886
  variant: "primary",
726
887
  size: "md",
@@ -730,7 +891,6 @@ const props = withDefaults(defineProps<Props>(), {
730
891
  loading: false,
731
892
  disabled: false,
732
893
  });
733
-
734
894
  const variantClasses: Record<Variant, string> = {
735
895
  primary:
736
896
  "bg-primary text-primary-foreground hover:bg-primary-hover active:bg-primary-active",
@@ -741,19 +901,16 @@ const variantClasses: Record<Variant, string> = {
741
901
  danger:
742
902
  "bg-danger text-danger-foreground hover:bg-danger-hover active:bg-danger-active",
743
903
  };
744
-
745
904
  const sizeClasses: Record<Size, string> = {
746
905
  sm: "h-8 px-3 text-xs gap-1.5",
747
906
  md: "h-[var(--ds-control-height,36px)] px-[var(--ds-padding-button-x,16px)] text-sm gap-2",
748
907
  lg: "h-10 px-5 text-sm gap-2",
749
908
  };
750
-
751
909
  const iconOnlySizeClasses: Record<Size, string> = {
752
910
  sm: "w-8 !px-0",
753
911
  md: "w-9 !px-0",
754
912
  lg: "w-10 !px-0",
755
913
  };
756
-
757
914
  const classes = computed(() =>
758
915
  cn(
759
916
  // base
@@ -776,7 +933,6 @@ const classes = computed(() =>
776
933
  ),
777
934
  );
778
935
  </script>
779
-
780
936
  <template>
781
937
  <component
782
938
  :is="as"
@@ -814,7 +970,6 @@ const classes = computed(() =>
814
970
  "src/components/ui/Badge.vue": `<script setup lang="ts">
815
971
  import { computed, type HTMLAttributes } from "vue";
816
972
  import { cn } from "@/lib/cn";
817
-
818
973
  type Variant =
819
974
  | "default"
820
975
  | "primary"
@@ -825,22 +980,18 @@ type Variant =
825
980
  | "info"
826
981
  | "outline";
827
982
  type Size = "sm" | "md" | "lg";
828
-
829
983
  interface Props {
830
984
  variant?: Variant;
831
985
  size?: Size;
832
986
  dismissible?: boolean;
833
987
  class?: HTMLAttributes["class"];
834
988
  }
835
-
836
989
  const props = withDefaults(defineProps<Props>(), {
837
990
  variant: "default",
838
991
  size: "md",
839
992
  dismissible: false,
840
993
  });
841
-
842
994
  const emit = defineEmits<{ dismiss: [] }>();
843
-
844
995
  const variantClasses: Record<Variant, string> = {
845
996
  default: "bg-muted text-foreground border border-transparent",
846
997
  primary:
@@ -855,13 +1006,11 @@ const variantClasses: Record<Variant, string> = {
855
1006
  info: "bg-info-muted text-info-muted-foreground border border-transparent",
856
1007
  outline: "bg-transparent text-foreground border border-border",
857
1008
  };
858
-
859
1009
  const sizeClasses: Record<Size, string> = {
860
1010
  sm: "px-2 py-0.5 text-[11px] gap-1",
861
1011
  md: "px-2.5 py-1 text-xs gap-1.5",
862
1012
  lg: "px-3 py-1.5 text-sm gap-2",
863
1013
  };
864
-
865
1014
  const classes = computed(() =>
866
1015
  cn(
867
1016
  "inline-flex items-center gap-1.5 rounded-full font-medium leading-none whitespace-nowrap",
@@ -873,7 +1022,6 @@ const classes = computed(() =>
873
1022
  ),
874
1023
  );
875
1024
  </script>
876
-
877
1025
  <template>
878
1026
  <span :class="classes" data-ds data-ds-component="badge">
879
1027
  <slot />
@@ -904,10 +1052,8 @@ const classes = computed(() =>
904
1052
  "src/components/ui/Card.vue": `<script setup lang="ts">
905
1053
  import { computed, provide, type HTMLAttributes, type InjectionKey } from "vue";
906
1054
  import { cn } from "@/lib/cn";
907
-
908
1055
  type Variant = "default" | "outlined" | "elevated" | "interactive";
909
1056
  type Padding = "compact" | "comfortable";
910
-
911
1057
  interface Props {
912
1058
  variant?: Variant;
913
1059
  padding?: Padding;
@@ -915,17 +1061,14 @@ interface Props {
915
1061
  as?: string;
916
1062
  class?: HTMLAttributes["class"];
917
1063
  }
918
-
919
1064
  const props = withDefaults(defineProps<Props>(), {
920
1065
  variant: "default",
921
1066
  padding: "compact",
922
1067
  as: "div",
923
1068
  fullWidth: false,
924
1069
  });
925
-
926
1070
  export const cardPaddingKey = Symbol("cardPadding") as InjectionKey<Padding>;
927
1071
  provide(cardPaddingKey, props.padding);
928
-
929
1072
  const variantClasses: Record<Variant, string> = {
930
1073
  default: "bg-surface border border-border",
931
1074
  outlined: "bg-transparent border border-border-strong",
@@ -933,7 +1076,6 @@ const variantClasses: Record<Variant, string> = {
933
1076
  interactive:
934
1077
  "bg-surface border border-border transition-[border-color,box-shadow,transform] duration-[var(--duration-normal,200ms)] ease-[var(--easing-standard,cubic-bezier(0.4,0,0.2,1))] hover:border-border-strong hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 active:shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring cursor-pointer",
935
1078
  };
936
-
937
1079
  const classes = computed(() =>
938
1080
  cn(
939
1081
  "flex flex-col rounded-md overflow-hidden text-sm text-foreground",
@@ -943,7 +1085,6 @@ const classes = computed(() =>
943
1085
  ),
944
1086
  );
945
1087
  </script>
946
-
947
1088
  <template>
948
1089
  <component :is="as" :class="classes" data-ds data-ds-component="card">
949
1090
  <slot />
@@ -954,14 +1095,11 @@ const classes = computed(() =>
954
1095
  import { inject, computed, type HTMLAttributes } from "vue";
955
1096
  import { cn } from "@/lib/cn";
956
1097
  import { cardPaddingKey } from "./Card.vue";
957
-
958
1098
  interface Props {
959
1099
  class?: HTMLAttributes["class"];
960
1100
  }
961
-
962
1101
  const props = defineProps<Props>();
963
1102
  const padding = inject(cardPaddingKey, "compact");
964
-
965
1103
  const classes = computed(() =>
966
1104
  cn(
967
1105
  "flex flex-col",
@@ -970,7 +1108,6 @@ const classes = computed(() =>
970
1108
  ),
971
1109
  );
972
1110
  </script>
973
-
974
1111
  <template>
975
1112
  <div :class="classes" data-ds data-ds-component="card-header">
976
1113
  <slot />
@@ -981,14 +1118,11 @@ const classes = computed(() =>
981
1118
  import { inject, computed, type HTMLAttributes } from "vue";
982
1119
  import { cn } from "@/lib/cn";
983
1120
  import { cardPaddingKey } from "./Card.vue";
984
-
985
1121
  interface Props {
986
1122
  class?: HTMLAttributes["class"];
987
1123
  }
988
-
989
1124
  const props = defineProps<Props>();
990
1125
  const padding = inject(cardPaddingKey, "compact");
991
-
992
1126
  const classes = computed(() =>
993
1127
  cn(
994
1128
  "flex flex-col flex-1",
@@ -997,7 +1131,6 @@ const classes = computed(() =>
997
1131
  ),
998
1132
  );
999
1133
  </script>
1000
-
1001
1134
  <template>
1002
1135
  <div :class="classes" data-ds data-ds-component="card-body">
1003
1136
  <slot />
@@ -1008,14 +1141,11 @@ const classes = computed(() =>
1008
1141
  import { inject, computed, type HTMLAttributes } from "vue";
1009
1142
  import { cn } from "@/lib/cn";
1010
1143
  import { cardPaddingKey } from "./Card.vue";
1011
-
1012
1144
  interface Props {
1013
1145
  class?: HTMLAttributes["class"];
1014
1146
  }
1015
-
1016
1147
  const props = defineProps<Props>();
1017
1148
  const padding = inject(cardPaddingKey, "compact");
1018
-
1019
1149
  const classes = computed(() =>
1020
1150
  cn(
1021
1151
  "flex items-center",
@@ -1024,7 +1154,6 @@ const classes = computed(() =>
1024
1154
  ),
1025
1155
  );
1026
1156
  </script>
1027
-
1028
1157
  <template>
1029
1158
  <div :class="classes" data-ds data-ds-component="card-footer">
1030
1159
  <slot />
@@ -1034,25 +1163,20 @@ const classes = computed(() =>
1034
1163
  "src/components/ui/Input.vue": `<script setup lang="ts">
1035
1164
  import { computed, type HTMLAttributes } from "vue";
1036
1165
  import { cn } from "@/lib/cn";
1037
-
1038
1166
  type Variant = "default" | "error" | "success";
1039
1167
  type Size = "sm" | "md" | "lg";
1040
-
1041
1168
  interface Props {
1042
1169
  variant?: Variant;
1043
1170
  size?: Size;
1044
1171
  disabled?: boolean;
1045
1172
  class?: HTMLAttributes["class"];
1046
1173
  }
1047
-
1048
1174
  const props = withDefaults(defineProps<Props>(), {
1049
1175
  variant: "default",
1050
1176
  size: "md",
1051
1177
  disabled: false,
1052
1178
  });
1053
-
1054
1179
  const model = defineModel<string>();
1055
-
1056
1180
  const variantClasses: Record<Variant, string> = {
1057
1181
  default:
1058
1182
  "border-input hover:border-border-strong focus-visible:border-border-strong",
@@ -1061,13 +1185,11 @@ const variantClasses: Record<Variant, string> = {
1061
1185
  success:
1062
1186
  "border-success text-foreground focus-visible:border-success placeholder:text-input-placeholder",
1063
1187
  };
1064
-
1065
1188
  const sizeClasses: Record<Size, string> = {
1066
1189
  sm: "h-8 px-2.5 text-xs",
1067
1190
  md: "h-[var(--ds-control-height,36px)] px-3 text-sm",
1068
1191
  lg: "h-10 px-3.5 text-sm",
1069
1192
  };
1070
-
1071
1193
  const classes = computed(() =>
1072
1194
  cn(
1073
1195
  "flex w-full text-sm leading-5 rounded-md border bg-background text-input-foreground",
@@ -1082,7 +1204,6 @@ const classes = computed(() =>
1082
1204
  ),
1083
1205
  );
1084
1206
  </script>
1085
-
1086
1207
  <template>
1087
1208
  <input
1088
1209
  v-model="model"
@@ -1096,23 +1217,18 @@ const classes = computed(() =>
1096
1217
  "src/components/ui/Alert.vue": `<script setup lang="ts">
1097
1218
  import { computed, ref, type HTMLAttributes } from "vue";
1098
1219
  import { cn } from "@/lib/cn";
1099
-
1100
1220
  type Variant = "info" | "success" | "warning" | "danger" | "default";
1101
-
1102
1221
  interface Props {
1103
1222
  variant?: Variant;
1104
1223
  title?: string;
1105
1224
  dismissible?: boolean;
1106
1225
  class?: HTMLAttributes["class"];
1107
1226
  }
1108
-
1109
1227
  const props = withDefaults(defineProps<Props>(), {
1110
1228
  variant: "info",
1111
1229
  dismissible: false,
1112
1230
  });
1113
-
1114
1231
  const dismissed = ref(false);
1115
-
1116
1232
  const variantClasses: Record<Variant, string> = {
1117
1233
  info: "bg-info-muted text-info-muted-foreground border-info/20",
1118
1234
  success: "bg-success-muted text-success-muted-foreground border-success/20",
@@ -1120,7 +1236,6 @@ const variantClasses: Record<Variant, string> = {
1120
1236
  danger: "bg-danger-muted text-danger-muted-foreground border-danger/20",
1121
1237
  default: "bg-muted text-muted-foreground border-border",
1122
1238
  };
1123
-
1124
1239
  const iconColorClasses: Record<Variant, string> = {
1125
1240
  info: "text-info",
1126
1241
  success: "text-success",
@@ -1128,7 +1243,6 @@ const iconColorClasses: Record<Variant, string> = {
1128
1243
  danger: "text-danger",
1129
1244
  default: "text-muted-foreground",
1130
1245
  };
1131
-
1132
1246
  const classes = computed(() =>
1133
1247
  cn(
1134
1248
  "relative flex gap-3 rounded-md p-4 text-sm leading-5 border",
@@ -1137,7 +1251,6 @@ const classes = computed(() =>
1137
1251
  props.class,
1138
1252
  ),
1139
1253
  );
1140
-
1141
1254
  // SVG icon paths by variant
1142
1255
  const iconPaths: Record<Variant, string> = {
1143
1256
  info: "M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10z",
@@ -1147,7 +1260,6 @@ const iconPaths: Record<Variant, string> = {
1147
1260
  default: "M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10z",
1148
1261
  };
1149
1262
  </script>
1150
-
1151
1263
  <template>
1152
1264
  <div
1153
1265
  v-if="!dismissed"
@@ -1201,42 +1313,34 @@ const iconPaths: Record<Variant, string> = {
1201
1313
  "src/components/ui/Heading.vue": `<script setup lang="ts">
1202
1314
  import { computed, type HTMLAttributes } from "vue";
1203
1315
  import { cn } from "@/lib/cn";
1204
-
1205
1316
  type Level = 1 | 2 | 3 | 4;
1206
1317
  type Color = "default" | "foreground" | "muted" | "primary";
1207
-
1208
1318
  interface Props {
1209
1319
  level?: Level;
1210
1320
  color?: Color;
1211
1321
  class?: HTMLAttributes["class"];
1212
1322
  }
1213
-
1214
1323
  const props = withDefaults(defineProps<Props>(), {
1215
1324
  level: 1,
1216
1325
  color: "default",
1217
1326
  });
1218
-
1219
1327
  const levelClasses: Record<Level, string> = {
1220
1328
  1: "text-[30px] leading-[36px] font-bold tracking-tight",
1221
1329
  2: "text-[24px] leading-[32px] font-semibold tracking-tight",
1222
1330
  3: "text-[20px] leading-[28px] font-semibold tracking-normal",
1223
1331
  4: "text-[18px] leading-[28px] font-medium tracking-normal",
1224
1332
  };
1225
-
1226
1333
  const colorClasses: Record<Color, string> = {
1227
1334
  default: "text-foreground",
1228
1335
  foreground: "text-foreground",
1229
1336
  muted: "text-muted-foreground",
1230
1337
  primary: "text-primary",
1231
1338
  };
1232
-
1233
1339
  const tag = computed(() => \`h\${props.level}\` as const);
1234
-
1235
1340
  const classes = computed(() =>
1236
1341
  cn(levelClasses[props.level], colorClasses[props.color], props.class),
1237
1342
  );
1238
1343
  </script>
1239
-
1240
1344
  <template>
1241
1345
  <component :is="tag" :class="classes" data-ds data-ds-component="heading">
1242
1346
  <slot />
@@ -1246,7 +1350,6 @@ const classes = computed(() =>
1246
1350
  "src/components/ui/Text.vue": `<script setup lang="ts">
1247
1351
  import { computed, type HTMLAttributes } from "vue";
1248
1352
  import { cn } from "@/lib/cn";
1249
-
1250
1353
  type Variant = "body" | "bodySm" | "caption" | "label" | "overline" | "code";
1251
1354
  type Color =
1252
1355
  | "default"
@@ -1257,20 +1360,17 @@ type Color =
1257
1360
  | "warning"
1258
1361
  | "danger"
1259
1362
  | "info";
1260
-
1261
1363
  interface Props {
1262
1364
  variant?: Variant;
1263
1365
  color?: Color;
1264
1366
  as?: string;
1265
1367
  class?: HTMLAttributes["class"];
1266
1368
  }
1267
-
1268
1369
  const props = withDefaults(defineProps<Props>(), {
1269
1370
  variant: "body",
1270
1371
  color: "default",
1271
1372
  as: "p",
1272
1373
  });
1273
-
1274
1374
  const variantClasses: Record<Variant, string> = {
1275
1375
  body: "text-[16px] leading-[24px] font-normal tracking-normal",
1276
1376
  bodySm: "text-[14px] leading-[20px] font-normal tracking-normal",
@@ -1281,7 +1381,6 @@ const variantClasses: Record<Variant, string> = {
1281
1381
  "text-[12px] leading-[16px] font-semibold tracking-wider uppercase text-muted-foreground",
1282
1382
  code: "text-[14px] leading-[20px] font-normal tracking-normal font-mono",
1283
1383
  };
1284
-
1285
1384
  const colorClasses: Record<Color, string> = {
1286
1385
  default: "text-foreground",
1287
1386
  foreground: "text-foreground",
@@ -1292,12 +1391,10 @@ const colorClasses: Record<Color, string> = {
1292
1391
  danger: "text-danger",
1293
1392
  info: "text-info",
1294
1393
  };
1295
-
1296
1394
  const classes = computed(() =>
1297
1395
  cn(variantClasses[props.variant], colorClasses[props.color], props.class),
1298
1396
  );
1299
1397
  </script>
1300
-
1301
1398
  <template>
1302
1399
  <component :is="as" :class="classes" data-ds data-ds-component="text">
1303
1400
  <slot />
@@ -1306,13 +1403,11 @@ const classes = computed(() =>
1306
1403
  `,
1307
1404
  },
1308
1405
  },
1309
-
1310
1406
  "laravel-blade": {
1311
1407
  files: {
1312
1408
  "vite.config.js": `import tailwindcss from "@tailwindcss/vite";
1313
1409
  import laravel from "laravel-vite-plugin";
1314
1410
  import { defineConfig } from "vite";
1315
-
1316
1411
  export default defineConfig({
1317
1412
  plugins: [
1318
1413
  laravel({
@@ -1328,7 +1423,6 @@ export default defineConfig({
1328
1423
  `,
1329
1424
  "resources/js/app.js": `// Unified UI — Laravel Blade Starter
1330
1425
  // Design tokens are loaded via CSS. This file handles theme toggling.
1331
-
1332
1426
  function initTheme() {
1333
1427
  const stored = localStorage.getItem("theme");
1334
1428
  const prefersDark = window.matchMedia(
@@ -1337,15 +1431,12 @@ function initTheme() {
1337
1431
  const theme = stored || (prefersDark ? "dark" : "light");
1338
1432
  document.documentElement.classList.toggle("dark", theme === "dark");
1339
1433
  }
1340
-
1341
1434
  function toggleTheme() {
1342
1435
  const isDark = document.documentElement.classList.toggle("dark");
1343
1436
  localStorage.setItem("theme", isDark ? "dark" : "light");
1344
1437
  }
1345
-
1346
1438
  // Initialize on load
1347
1439
  initTheme();
1348
-
1349
1440
  // Expose globally for Blade onclick handlers
1350
1441
  window.toggleTheme = toggleTheme;
1351
1442
  `,
@@ -1364,7 +1455,6 @@ window.toggleTheme = toggleTheme;
1364
1455
  </html>
1365
1456
  `,
1366
1457
  "resources/views/welcome.blade.php": `@extends('layouts.app')
1367
-
1368
1458
  @section('content')
1369
1459
  <div class="flex min-h-svh flex-col items-center justify-center gap-8 p-8">
1370
1460
  <x-ui.card class="w-full max-w-lg">
@@ -1374,7 +1464,6 @@ window.toggleTheme = toggleTheme;
1374
1464
  Your Laravel project is ready with components. Start building!
1375
1465
  </x-ui.text>
1376
1466
  </x-ui.card-header>
1377
-
1378
1467
  <x-ui.card-body class="space-y-6">
1379
1468
  {{-- Buttons --}}
1380
1469
  <div class="space-y-2">
@@ -1387,7 +1476,6 @@ window.toggleTheme = toggleTheme;
1387
1476
  <x-ui.button variant="primary" :loading="true" size="sm">Loading</x-ui.button>
1388
1477
  </div>
1389
1478
  </div>
1390
-
1391
1479
  {{-- Badges --}}
1392
1480
  <div class="space-y-2">
1393
1481
  <x-ui.text variant="label">Badges</x-ui.text>
@@ -1401,19 +1489,16 @@ window.toggleTheme = toggleTheme;
1401
1489
  <x-ui.badge variant="outline">Outline</x-ui.badge>
1402
1490
  </div>
1403
1491
  </div>
1404
-
1405
1492
  {{-- Input --}}
1406
1493
  <div class="space-y-2">
1407
1494
  <x-ui.text variant="label">Input</x-ui.text>
1408
1495
  <x-ui.input placeholder="you@example.com" />
1409
1496
  </div>
1410
-
1411
1497
  {{-- Alert --}}
1412
1498
  <x-ui.alert variant="info" title="All set!">
1413
1499
  Your design system components are working in Laravel.
1414
1500
  </x-ui.alert>
1415
1501
  </x-ui.card-body>
1416
-
1417
1502
  <x-ui.card-footer class="justify-between">
1418
1503
  <x-ui.button variant="primary">Get Started</x-ui.button>
1419
1504
  <x-ui.button variant="secondary" onclick="toggleTheme()">Toggle Theme</x-ui.button>
@@ -1431,7 +1516,6 @@ window.toggleTheme = toggleTheme;
1431
1516
  'loading' => false,
1432
1517
  'disabled' => false,
1433
1518
  ])
1434
-
1435
1519
  @php
1436
1520
  $variants = [
1437
1521
  'primary' => 'bg-primary text-primary-foreground hover:bg-primary-hover active:bg-primary-active',
@@ -1439,19 +1523,16 @@ $variants = [
1439
1523
  'ghost' => 'bg-transparent text-foreground hover:bg-muted hover:text-foreground active:bg-secondary-active',
1440
1524
  'danger' => 'bg-danger text-danger-foreground hover:bg-danger-hover active:bg-danger-active',
1441
1525
  ];
1442
-
1443
1526
  $sizes = [
1444
1527
  'sm' => 'h-8 px-3 text-xs gap-1.5',
1445
1528
  'md' => 'h-[var(--ds-control-height,36px)] px-[var(--ds-padding-button-x,16px)] text-sm gap-2',
1446
1529
  'lg' => 'h-10 px-5 text-sm gap-2',
1447
1530
  ];
1448
-
1449
1531
  $iconOnlySizes = [
1450
1532
  'sm' => 'w-8 !px-0',
1451
1533
  'md' => 'w-9 !px-0',
1452
1534
  'lg' => 'w-10 !px-0',
1453
1535
  ];
1454
-
1455
1536
  $classes = implode(' ', array_filter([
1456
1537
  'inline-flex items-center justify-center gap-2 text-sm font-medium leading-5 rounded-md',
1457
1538
  'transition-[color,background-color,border-color,box-shadow,opacity,transform] duration-[var(--duration-fast,150ms)] ease-[var(--easing-standard,cubic-bezier(0.4,0,0.2,1))]',
@@ -1465,7 +1546,6 @@ $classes = implode(' ', array_filter([
1465
1546
  $loading ? 'pointer-events-none opacity-70' : '',
1466
1547
  ]));
1467
1548
  @endphp
1468
-
1469
1549
  <{{ $as }}
1470
1550
  {{ $attributes->merge(['class' => $classes, 'disabled' => $disabled || $loading]) }}
1471
1551
  data-ds
@@ -1486,7 +1566,6 @@ $classes = implode(' ', array_filter([
1486
1566
  'size' => 'md',
1487
1567
  'dismissible' => false,
1488
1568
  ])
1489
-
1490
1569
  @php
1491
1570
  $variants = [
1492
1571
  'default' => 'bg-muted text-foreground border border-transparent',
@@ -1498,13 +1577,11 @@ $variants = [
1498
1577
  'info' => 'bg-info-muted text-info-muted-foreground border border-transparent',
1499
1578
  'outline' => 'bg-transparent text-foreground border border-border',
1500
1579
  ];
1501
-
1502
1580
  $sizes = [
1503
1581
  'sm' => 'px-2 py-0.5 text-[11px] gap-1',
1504
1582
  'md' => 'px-2.5 py-1 text-xs gap-1.5',
1505
1583
  'lg' => 'px-3 py-1.5 text-sm gap-2',
1506
1584
  ];
1507
-
1508
1585
  $classes = implode(' ', [
1509
1586
  'inline-flex items-center gap-1.5 rounded-full font-medium leading-none whitespace-nowrap',
1510
1587
  'transition-[color,background-color,border-color,box-shadow,opacity] duration-[var(--duration-fast,150ms)] ease-[var(--easing-standard,cubic-bezier(0.4,0,0.2,1))]',
@@ -1513,7 +1590,6 @@ $classes = implode(' ', [
1513
1590
  $sizes[$size] ?? $sizes['md'],
1514
1591
  ]);
1515
1592
  @endphp
1516
-
1517
1593
  <span {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="badge">
1518
1594
  {{ $slot }}
1519
1595
  @if($dismissible)
@@ -1533,7 +1609,6 @@ $classes = implode(' ', [
1533
1609
  'fullWidth' => false,
1534
1610
  'as' => 'div',
1535
1611
  ])
1536
-
1537
1612
  @php
1538
1613
  $variants = [
1539
1614
  'default' => 'bg-surface border border-border',
@@ -1541,50 +1616,42 @@ $variants = [
1541
1616
  'elevated' => 'bg-surface-raised border border-border-muted shadow-md',
1542
1617
  'interactive' => 'bg-surface border border-border transition-[border-color,box-shadow,transform] duration-[var(--duration-normal,200ms)] ease-[var(--easing-standard,cubic-bezier(0.4,0,0.2,1))] hover:border-border-strong hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 active:shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring cursor-pointer',
1543
1618
  ];
1544
-
1545
1619
  $classes = implode(' ', array_filter([
1546
1620
  'flex flex-col rounded-md overflow-hidden text-sm text-foreground',
1547
1621
  $variants[$variant] ?? $variants['default'],
1548
1622
  $fullWidth ? 'w-full' : '',
1549
1623
  ]));
1550
1624
  @endphp
1551
-
1552
1625
  <{{ $as }} {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card" data-ds-padding="{{ $padding }}">
1553
1626
  {{ $slot }}
1554
1627
  </{{ $as }}>
1555
1628
  `,
1556
1629
  "resources/views/components/ui/card-header.blade.php": `@aware(['padding' => 'compact'])
1557
-
1558
1630
  @php
1559
1631
  $classes = $padding === 'comfortable'
1560
1632
  ? 'flex flex-col px-6 pt-6 gap-1.5'
1561
1633
  : 'flex flex-col px-[var(--ds-padding-card,16px)] pt-[var(--ds-padding-card,16px)] gap-1';
1562
1634
  @endphp
1563
-
1564
1635
  <div {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card-header">
1565
1636
  {{ $slot }}
1566
1637
  </div>
1567
1638
  `,
1568
1639
  "resources/views/components/ui/card-body.blade.php": `@aware(['padding' => 'compact'])
1569
-
1570
1640
  @php
1571
1641
  $classes = $padding === 'comfortable'
1572
1642
  ? 'flex flex-col flex-1 px-6 py-4 gap-4'
1573
1643
  : 'flex flex-col flex-1 px-[var(--ds-padding-card,16px)] py-3 gap-[var(--ds-gap-default,0.75rem)]';
1574
1644
  @endphp
1575
-
1576
1645
  <div {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card-body">
1577
1646
  {{ $slot }}
1578
1647
  </div>
1579
1648
  `,
1580
1649
  "resources/views/components/ui/card-footer.blade.php": `@aware(['padding' => 'compact'])
1581
-
1582
1650
  @php
1583
1651
  $classes = $padding === 'comfortable'
1584
1652
  ? 'flex items-center px-6 pb-6 gap-3'
1585
1653
  : 'flex items-center px-[var(--ds-padding-card,16px)] pb-[var(--ds-padding-card,16px)] gap-2';
1586
1654
  @endphp
1587
-
1588
1655
  <div {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card-footer">
1589
1656
  {{ $slot }}
1590
1657
  </div>
@@ -1594,20 +1661,17 @@ $classes = $padding === 'comfortable'
1594
1661
  'size' => 'md',
1595
1662
  'disabled' => false,
1596
1663
  ])
1597
-
1598
1664
  @php
1599
1665
  $variants = [
1600
1666
  'default' => 'border-input hover:border-border-strong focus-visible:border-border-strong',
1601
1667
  'error' => 'border-danger text-foreground focus-visible:border-danger placeholder:text-input-placeholder',
1602
1668
  'success' => 'border-success text-foreground focus-visible:border-success placeholder:text-input-placeholder',
1603
1669
  ];
1604
-
1605
1670
  $sizes = [
1606
1671
  'sm' => 'h-8 px-2.5 text-xs',
1607
1672
  'md' => 'h-[var(--ds-control-height,36px)] px-3 text-sm',
1608
1673
  'lg' => 'h-10 px-3.5 text-sm',
1609
1674
  ];
1610
-
1611
1675
  $classes = implode(' ', [
1612
1676
  'flex w-full text-sm leading-5 rounded-md border bg-background text-input-foreground',
1613
1677
  'placeholder:text-input-placeholder',
@@ -1619,7 +1683,6 @@ $classes = implode(' ', [
1619
1683
  $sizes[$size] ?? $sizes['md'],
1620
1684
  ]);
1621
1685
  @endphp
1622
-
1623
1686
  <input {{ $attributes->merge(['class' => $classes, 'disabled' => $disabled, 'type' => 'text']) }} data-ds data-ds-component="input" />
1624
1687
  `,
1625
1688
  "resources/views/components/ui/alert.blade.php": `@props([
@@ -1627,7 +1690,6 @@ $classes = implode(' ', [
1627
1690
  'title' => null,
1628
1691
  'dismissible' => false,
1629
1692
  ])
1630
-
1631
1693
  @php
1632
1694
  $variants = [
1633
1695
  'info' => 'bg-info-muted text-info-muted-foreground border-info/20',
@@ -1636,7 +1698,6 @@ $variants = [
1636
1698
  'danger' => 'bg-danger-muted text-danger-muted-foreground border-danger/20',
1637
1699
  'default' => 'bg-muted text-muted-foreground border-border',
1638
1700
  ];
1639
-
1640
1701
  $iconColors = [
1641
1702
  'info' => 'text-info',
1642
1703
  'success' => 'text-success',
@@ -1644,7 +1705,6 @@ $iconColors = [
1644
1705
  'danger' => 'text-danger',
1645
1706
  'default' => 'text-muted-foreground',
1646
1707
  ];
1647
-
1648
1708
  $iconPaths = [
1649
1709
  'info' => 'M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10z',
1650
1710
  'success' => 'M9 12l2 2 4-4m6 2a10 10 0 11-20 0 10 10 0 0120 0z',
@@ -1652,14 +1712,12 @@ $iconPaths = [
1652
1712
  'danger' => 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a10 10 0 11-20 0 10 10 0 0120 0z',
1653
1713
  'default' => 'M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10z',
1654
1714
  ];
1655
-
1656
1715
  $classes = implode(' ', [
1657
1716
  'relative flex gap-3 rounded-md p-4 text-sm leading-5 border',
1658
1717
  'transition-colors duration-[var(--duration-fast,150ms)]',
1659
1718
  $variants[$variant] ?? $variants['info'],
1660
1719
  ]);
1661
1720
  @endphp
1662
-
1663
1721
  <div {{ $attributes->merge(['class' => $classes]) }} role="alert" data-ds data-ds-component="alert">
1664
1722
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 shrink-0 mt-0.5 {{ $iconColors[$variant] ?? $iconColors['info'] }}">
1665
1723
  <path d="{{ $iconPaths[$variant] ?? $iconPaths['info'] }}"/>
@@ -1685,7 +1743,6 @@ $classes = implode(' ', [
1685
1743
  'level' => 1,
1686
1744
  'color' => 'default',
1687
1745
  ])
1688
-
1689
1746
  @php
1690
1747
  $levels = [
1691
1748
  1 => 'text-[30px] leading-[36px] font-bold tracking-tight',
@@ -1693,22 +1750,18 @@ $levels = [
1693
1750
  3 => 'text-[20px] leading-[28px] font-semibold tracking-normal',
1694
1751
  4 => 'text-[18px] leading-[28px] font-medium tracking-normal',
1695
1752
  ];
1696
-
1697
1753
  $colors = [
1698
1754
  'default' => 'text-foreground',
1699
1755
  'foreground' => 'text-foreground',
1700
1756
  'muted' => 'text-muted-foreground',
1701
1757
  'primary' => 'text-primary',
1702
1758
  ];
1703
-
1704
1759
  $classes = implode(' ', [
1705
1760
  $levels[$level] ?? $levels[1],
1706
1761
  $colors[$color] ?? $colors['default'],
1707
1762
  ]);
1708
-
1709
1763
  $tag = 'h' . min(max((int)$level, 1), 6);
1710
1764
  @endphp
1711
-
1712
1765
  <{{ $tag }} {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="heading">
1713
1766
  {{ $slot }}
1714
1767
  </{{ $tag }}>
@@ -1718,7 +1771,6 @@ $tag = 'h' . min(max((int)$level, 1), 6);
1718
1771
  'color' => 'default',
1719
1772
  'as' => 'p',
1720
1773
  ])
1721
-
1722
1774
  @php
1723
1775
  $variants = [
1724
1776
  'body' => 'text-[16px] leading-[24px] font-normal tracking-normal',
@@ -1728,7 +1780,6 @@ $variants = [
1728
1780
  'overline' => 'text-[12px] leading-[16px] font-semibold tracking-wider uppercase text-muted-foreground',
1729
1781
  'code' => 'text-[14px] leading-[20px] font-normal tracking-normal font-mono',
1730
1782
  ];
1731
-
1732
1783
  $colors = [
1733
1784
  'default' => 'text-foreground',
1734
1785
  'foreground' => 'text-foreground',
@@ -1739,13 +1790,11 @@ $colors = [
1739
1790
  'danger' => 'text-danger',
1740
1791
  'info' => 'text-info',
1741
1792
  ];
1742
-
1743
1793
  $classes = implode(' ', [
1744
1794
  $variants[$variant] ?? $variants['body'],
1745
1795
  $colors[$color] ?? $colors['default'],
1746
1796
  ]);
1747
1797
  @endphp
1748
-
1749
1798
  <{{ $as }} {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="text">
1750
1799
  {{ $slot }}
1751
1800
  </{{ $as }}>
@@ -1753,20 +1802,17 @@ $classes = implode(' ', [
1753
1802
  },
1754
1803
  },
1755
1804
  };
1756
-
1757
1805
  // ---------------------------------------------------------------------------
1758
1806
  // Starter kit scaffolding command
1759
1807
  // ---------------------------------------------------------------------------
1760
-
1761
1808
  async function cmdInitWithTemplate(positional, flags) {
1762
1809
  log();
1763
1810
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Create a new project")}`);
1764
1811
  log();
1765
-
1766
1812
  // 1. Pick framework
1767
1813
  let framework;
1768
- const templateFlag = typeof flags.template === "string" ? flags.template : null;
1769
-
1814
+ const templateFlag =
1815
+ typeof flags.template === "string" ? flags.template : null;
1770
1816
  if (templateFlag) {
1771
1817
  framework = FRAMEWORKS.find((f) => f.name === templateFlag);
1772
1818
  if (!framework) {
@@ -1788,27 +1834,21 @@ async function cmdInitWithTemplate(positional, flags) {
1788
1834
  }
1789
1835
  log();
1790
1836
  }
1791
-
1792
1837
  logStep("✓", `Framework: ${c("cyan", framework.label)}`);
1793
-
1794
1838
  // 2. Get project name
1795
1839
  let projectName = positional[0];
1796
1840
  if (!projectName) {
1797
1841
  projectName = await promptText("Project name:", "my-unified-app");
1798
1842
  }
1799
-
1800
1843
  const targetDir = resolve(process.cwd(), projectName);
1801
1844
  logStep("✓", `Project: ${c("cyan", projectName)}`);
1802
1845
  log();
1803
-
1804
1846
  // 3. Run the official scaffolding command
1805
1847
  logStep("📦", `Scaffolding ${c("cyan", framework.label)} project...`);
1806
1848
  log();
1807
-
1808
1849
  const scaffoldCmd = framework.scaffoldCmd(projectName);
1809
1850
  logStep(" ", c("dim", `> ${scaffoldCmd}`));
1810
1851
  log();
1811
-
1812
1852
  const scaffoldOk = runCmd(scaffoldCmd, process.cwd());
1813
1853
  if (!scaffoldOk) {
1814
1854
  logError(
@@ -1819,40 +1859,35 @@ async function cmdInitWithTemplate(positional, flags) {
1819
1859
  );
1820
1860
  process.exit(1);
1821
1861
  }
1822
-
1823
1862
  if (!existsSync(targetDir)) {
1824
- logError(`Expected directory "${projectName}" was not created by the scaffolding tool.`);
1863
+ logError(
1864
+ `Expected directory "${projectName}" was not created by the scaffolding tool.`,
1865
+ );
1825
1866
  process.exit(1);
1826
1867
  }
1827
-
1828
1868
  log();
1829
1869
  logStep("✓", c("green", `${framework.label} project scaffolded`));
1830
-
1831
1870
  // 4. Install Unified UI + extra deps
1832
1871
  logStep("📦", "Installing Unified UI design system...");
1833
-
1834
1872
  const pm = await detectPackageManager(targetDir);
1835
1873
  const allDeps = [...framework.deps];
1836
1874
  const allDevDeps = [...framework.devDeps];
1837
-
1838
1875
  if (allDeps.length > 0) {
1839
1876
  const depCmd = getInstallCommand(pm, allDeps);
1840
1877
  logStep(" ", c("dim", depCmd));
1841
1878
  runCmd(depCmd, targetDir, "pipe");
1842
1879
  }
1843
-
1844
1880
  if (allDevDeps.length > 0) {
1845
- const devDepCmd = getInstallCommand(pm, allDevDeps).replace(" add ", " add -D ").replace(" install ", " install -D ");
1881
+ const devDepCmd = getInstallCommand(pm, allDevDeps)
1882
+ .replace(" add ", " add -D ")
1883
+ .replace(" install ", " install -D ");
1846
1884
  logStep(" ", c("dim", devDepCmd));
1847
1885
  runCmd(devDepCmd, targetDir, "pipe");
1848
1886
  }
1849
-
1850
1887
  logStep("✓", c("green", "Dependencies installed"));
1851
-
1852
1888
  // 5. Apply overlay files
1853
1889
  log();
1854
1890
  logStep("✏️ ", "Applying Unified UI starter files...");
1855
-
1856
1891
  const overlay = OVERLAYS[framework.name];
1857
1892
  if (overlay) {
1858
1893
  for (const [filePath, content] of Object.entries(overlay.files)) {
@@ -1861,22 +1896,28 @@ async function cmdInitWithTemplate(positional, flags) {
1861
1896
  logStep("✓", c("green", filePath));
1862
1897
  }
1863
1898
  }
1864
-
1865
1899
  // 6. Git commit (if git was initialized by the scaffold tool)
1866
1900
  const gitDir = join(targetDir, ".git");
1867
1901
  if (existsSync(gitDir)) {
1868
1902
  runCmd("git add -A", targetDir, "pipe");
1869
- runCmd('git commit -m "chore: add Unified UI design system" --no-verify', targetDir, "pipe");
1903
+ runCmd(
1904
+ 'git commit -m "chore: add Unified UI design system" --no-verify',
1905
+ targetDir,
1906
+ "pipe",
1907
+ );
1870
1908
  logStep("✓", c("green", "Committed Unified UI changes"));
1871
1909
  } else {
1872
1910
  // Initialize git if it wasn't done by the scaffold tool
1873
1911
  if (runCmd("git init", targetDir, "pipe")) {
1874
1912
  runCmd("git add -A", targetDir, "pipe");
1875
- runCmd('git commit -m "chore: initial commit with Unified UI" --no-verify', targetDir, "pipe");
1913
+ runCmd(
1914
+ 'git commit -m "chore: initial commit with Unified UI" --no-verify',
1915
+ targetDir,
1916
+ "pipe",
1917
+ );
1876
1918
  logStep("✓", c("green", "Initialized git repository"));
1877
1919
  }
1878
1920
  }
1879
-
1880
1921
  // 7. Print success
1881
1922
  log();
1882
1923
  logStep("🎉", c("green", `Project "${projectName}" is ready!`));
@@ -1884,40 +1925,39 @@ async function cmdInitWithTemplate(positional, flags) {
1884
1925
  log(` ${c("dim", "Next steps:")}`);
1885
1926
  log();
1886
1927
  log(` ${c("cyan", `cd ${projectName}`)}`);
1887
-
1888
1928
  if (framework.name === "laravel-blade") {
1889
1929
  log(` ${c("cyan", "npm run dev")}`);
1890
1930
  log(` ${c("cyan", "php artisan serve")}`);
1891
1931
  } else {
1892
1932
  log(` ${c("cyan", "npm run dev")}`);
1893
1933
  }
1894
-
1895
1934
  log();
1896
-
1897
1935
  if (framework.name === "vuejs" || framework.name === "laravel-blade") {
1898
- log(` ${c("dim", "Note: This template includes design tokens (CSS variables + Tailwind")}`);
1899
- log(` ${c("dim", "utilities) only. React components are not available in this framework.")}`);
1936
+ log(
1937
+ ` ${c("dim", "Note: This template includes design tokens (CSS variables + Tailwind")}`,
1938
+ );
1939
+ log(
1940
+ ` ${c("dim", "utilities) only. React components are not available in this framework.")}`,
1941
+ );
1900
1942
  log(` ${c("dim", "See: https://www.unified-ui.space/docs/tokens")}`);
1901
1943
  log();
1902
1944
  } else {
1903
1945
  log(` ${c("dim", "Start adding components:")}`);
1904
- log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add button card badge")}`);
1946
+ log(
1947
+ ` ${c("cyan", "npx @work-rjkashyap/unified-ui add button card badge")}`,
1948
+ );
1905
1949
  log();
1906
1950
  }
1907
1951
  }
1908
-
1909
1952
  async function cmdInit(positional = [], flags = {}) {
1910
1953
  // If --template flag is present, run the full scaffolding flow
1911
1954
  if (flags.template) {
1912
1955
  return cmdInitWithTemplate(positional, flags);
1913
1956
  }
1914
-
1915
1957
  log();
1916
1958
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Initialize project")}`);
1917
1959
  log();
1918
-
1919
1960
  const config = loadConfig();
1920
-
1921
1961
  if (existsSync(join(config.root, CONFIG_FILE))) {
1922
1962
  const overwrite = await confirm(
1923
1963
  `${c("yellow", CONFIG_FILE)} already exists. Overwrite?`,
@@ -1928,10 +1968,8 @@ async function cmdInit(positional = [], flags = {}) {
1928
1968
  return;
1929
1969
  }
1930
1970
  }
1931
-
1932
1971
  saveConfig(DEFAULT_CONFIG);
1933
1972
  logStep("✓", `Created ${c("cyan", CONFIG_FILE)}`);
1934
-
1935
1973
  // Create directories
1936
1974
  const srcDir = join(config.root, config.srcDir);
1937
1975
  const dirs = [
@@ -1939,16 +1977,13 @@ async function cmdInit(positional = [], flags = {}) {
1939
1977
  join(srcDir, "lib"),
1940
1978
  join(srcDir, "styles"),
1941
1979
  ];
1942
-
1943
1980
  for (const dir of dirs) {
1944
1981
  mkdirSync(dir, { recursive: true });
1945
- logStep("✓", `Created ${c("dim", dir.replace(config.root + "/", ""))}`);
1982
+ logStep("✓", `Created ${c("dim", dir.replace(`${config.root}/`, ""))}`);
1946
1983
  }
1947
-
1948
1984
  // Fetch and write the base utilities (cn + focus-ring) and styles
1949
1985
  log();
1950
1986
  logStep("📡", "Fetching base utilities from registry...");
1951
-
1952
1987
  const baseItems = ["cn", "focus-ring", "styles"];
1953
1988
  for (const name of baseItems) {
1954
1989
  try {
@@ -1962,28 +1997,28 @@ async function cmdInit(positional = [], flags = {}) {
1962
1997
  logStep("⚠", c("yellow", `Could not fetch ${name} — add manually later`));
1963
1998
  }
1964
1999
  }
1965
-
1966
2000
  // Install base npm deps
1967
2001
  await installNpmDeps(
1968
- ["class-variance-authority", "clsx", "tailwind-merge"],
2002
+ ["class-variance-authority", "clsx", "tailwind-merge", "lucide-react"],
1969
2003
  config.root,
1970
2004
  );
1971
-
2005
+ // Set up Tailwind CSS for Vite projects
2006
+ log();
2007
+ await setupTailwindForVite(config.root, config);
1972
2008
  log();
1973
2009
  logStep("🎉", c("green", "Project initialized! Start adding components:"));
1974
2010
  log();
1975
2011
  log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add button")}`);
1976
- log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add card badge tabs")}`);
2012
+ log(
2013
+ ` ${c("cyan", "npx @work-rjkashyap/unified-ui add card badge tabs")}`,
2014
+ );
1977
2015
  log();
1978
2016
  }
1979
-
1980
2017
  async function cmdAdd(names, flags = {}) {
1981
2018
  log();
1982
2019
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Add components")}`);
1983
2020
  log();
1984
-
1985
2021
  const config = loadConfig();
1986
-
1987
2022
  if (!existsSync(join(config.root, CONFIG_FILE)) && !flags.yes) {
1988
2023
  const init = await confirm(
1989
2024
  `No ${c("cyan", CONFIG_FILE)} found. Initialize first?`,
@@ -1993,7 +2028,6 @@ async function cmdAdd(names, flags = {}) {
1993
2028
  log();
1994
2029
  }
1995
2030
  }
1996
-
1997
2031
  // If --all, fetch index and add everything
1998
2032
  if (flags.all) {
1999
2033
  logStep("📡", "Fetching full registry index...");
@@ -2008,27 +2042,27 @@ async function cmdAdd(names, flags = {}) {
2008
2042
  return;
2009
2043
  }
2010
2044
  }
2011
-
2012
2045
  if (names.length === 0) {
2013
- logError("No component names specified. Usage: npx @work-rjkashyap/unified-ui add <component...>");
2046
+ logError(
2047
+ "No component names specified. Usage: npx @work-rjkashyap/unified-ui add <component...>",
2048
+ );
2014
2049
  return;
2015
2050
  }
2016
-
2017
2051
  // Resolve the full dependency tree
2018
- logStep("🔍", `Resolving dependencies for: ${c("cyan", names.join(", "))}...`);
2052
+ logStep(
2053
+ "🔍",
2054
+ `Resolving dependencies for: ${c("cyan", names.join(", "))}...`,
2055
+ );
2019
2056
  const tree = await resolveFullDependencyTree(names, REGISTRY_BASE_URL);
2020
-
2021
2057
  if (tree.size === 0) {
2022
2058
  logError("No components resolved. Check the names and try again.");
2023
2059
  return;
2024
2060
  }
2025
-
2026
2061
  // Summarize what will be installed
2027
2062
  const components = [];
2028
2063
  const utils = [];
2029
2064
  const styles = [];
2030
2065
  const allNpmDeps = new Set();
2031
-
2032
2066
  for (const [name, item] of tree) {
2033
2067
  switch (item.type) {
2034
2068
  case "unified-ui:component":
@@ -2041,12 +2075,10 @@ async function cmdAdd(names, flags = {}) {
2041
2075
  styles.push(name);
2042
2076
  break;
2043
2077
  }
2044
-
2045
2078
  for (const dep of item.dependencies || []) {
2046
2079
  allNpmDeps.add(dep);
2047
2080
  }
2048
2081
  }
2049
-
2050
2082
  log();
2051
2083
  if (components.length > 0) {
2052
2084
  logStep("🧩", `Components: ${c("cyan", components.join(", "))}`);
@@ -2058,7 +2090,6 @@ async function cmdAdd(names, flags = {}) {
2058
2090
  logStep("📦", `Packages: ${c("dim", [...allNpmDeps].join(", "))}`);
2059
2091
  }
2060
2092
  log();
2061
-
2062
2093
  // Confirm unless --yes
2063
2094
  if (!flags.yes) {
2064
2095
  const proceed = await confirm("Proceed with installation?");
@@ -2069,11 +2100,9 @@ async function cmdAdd(names, flags = {}) {
2069
2100
  }
2070
2101
  log();
2071
2102
  }
2072
-
2073
2103
  // Write files
2074
2104
  const results = [];
2075
2105
  const overwrite = flags.overwrite || false;
2076
-
2077
2106
  for (const [_name, item] of tree) {
2078
2107
  for (const file of item.files) {
2079
2108
  const targetPath = resolveTargetPath(config, file);
@@ -2081,30 +2110,23 @@ async function cmdAdd(names, flags = {}) {
2081
2110
  results.push(result);
2082
2111
  }
2083
2112
  }
2084
-
2085
2113
  // Report file results
2086
2114
  const created = results.filter((r) => r.status === "created");
2087
2115
  const skipped = results.filter((r) => r.status === "skipped");
2088
-
2089
2116
  for (const r of created) {
2090
- logStep("✓", c("green", r.path.replace(config.root + "/", "")));
2117
+ logStep("✓", c("green", r.path.replace(`${config.root}/`, "")));
2091
2118
  }
2092
-
2093
2119
  if (skipped.length > 0) {
2094
2120
  log();
2095
2121
  for (const r of skipped) {
2096
2122
  logStep(
2097
2123
  "↩",
2098
- `${c("dim", r.path.replace(config.root + "/", ""))} ${c("yellow", "(exists, skipped)")}`,
2124
+ `${c("dim", r.path.replace(`${config.root}/`, ""))} ${c("yellow", "(exists, skipped)")}`,
2099
2125
  );
2100
2126
  }
2101
2127
  log();
2102
- logStep(
2103
- "💡",
2104
- c("dim", "Use --overwrite to replace existing files."),
2105
- );
2128
+ logStep("💡", c("dim", "Use --overwrite to replace existing files."));
2106
2129
  }
2107
-
2108
2130
  // Install npm dependencies
2109
2131
  const depsToInstall = [...allNpmDeps].filter((dep) => {
2110
2132
  // Check if already in package.json
@@ -2122,28 +2144,20 @@ async function cmdAdd(names, flags = {}) {
2122
2144
  return true;
2123
2145
  }
2124
2146
  });
2125
-
2126
2147
  if (depsToInstall.length > 0) {
2127
2148
  log();
2128
2149
  await installNpmDeps(depsToInstall, config.root);
2129
2150
  }
2130
-
2131
2151
  log();
2132
- logStep(
2133
- "🎉",
2134
- c("green", `Done! ${created.length} file(s) added.`),
2135
- );
2152
+ logStep("🎉", c("green", `Done! ${created.length} file(s) added.`));
2136
2153
  log();
2137
2154
  }
2138
-
2139
2155
  async function cmdList() {
2140
2156
  log();
2141
2157
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Available components")}`);
2142
2158
  log();
2143
-
2144
2159
  try {
2145
2160
  const index = await fetchJSON(`${REGISTRY_BASE_URL}/index.json`);
2146
-
2147
2161
  // Group by category
2148
2162
  const groups = {};
2149
2163
  for (const item of index.items) {
@@ -2152,13 +2166,11 @@ async function cmdList() {
2152
2166
  if (!groups[cat]) groups[cat] = [];
2153
2167
  groups[cat].push(item);
2154
2168
  }
2155
-
2156
2169
  // Find category labels
2157
2170
  const catLabels = {};
2158
2171
  for (const cat of index.categories || []) {
2159
2172
  catLabels[cat.name] = cat.label;
2160
2173
  }
2161
-
2162
2174
  for (const [cat, items] of Object.entries(groups)) {
2163
2175
  log(` ${c("bold", catLabels[cat] || cat)}`);
2164
2176
  for (const item of items) {
@@ -2166,27 +2178,27 @@ async function cmdList() {
2166
2178
  item.registryDependencies?.length > 0
2167
2179
  ? c("dim", ` → ${item.registryDependencies.join(", ")}`)
2168
2180
  : "";
2169
- log(` ${c("cyan", item.name.padEnd(22))} ${c("dim", item.description || "")}${deps}`);
2181
+ log(
2182
+ ` ${c("cyan", item.name.padEnd(22))} ${c("dim", item.description || "")}${deps}`,
2183
+ );
2170
2184
  }
2171
2185
  log();
2172
2186
  }
2173
-
2174
2187
  log(` ${c("dim", `${index.totalItems} items total`)}`);
2175
2188
  log();
2176
- log(` ${c("dim", "Add a component:")} npx @work-rjkashyap/unified-ui add ${c("cyan", "<name>")}`);
2189
+ log(
2190
+ ` ${c("dim", "Add a component:")} npx @work-rjkashyap/unified-ui add ${c("cyan", "<name>")}`,
2191
+ );
2177
2192
  log();
2178
2193
  } catch (err) {
2179
2194
  logError(`Could not fetch registry: ${err.message}`);
2180
2195
  }
2181
2196
  }
2182
-
2183
2197
  async function cmdDiff(names) {
2184
2198
  log();
2185
2199
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Diff local vs registry")}`);
2186
2200
  log();
2187
-
2188
2201
  const config = loadConfig();
2189
-
2190
2202
  for (const name of names) {
2191
2203
  try {
2192
2204
  const item = await fetchJSON(`${REGISTRY_BASE_URL}/${name}.json`);
@@ -2196,10 +2208,8 @@ async function cmdDiff(names) {
2196
2208
  logStep("✗", `${c("red", name)}: not installed locally`);
2197
2209
  continue;
2198
2210
  }
2199
-
2200
2211
  const localContent = readFileSync(targetPath, "utf-8");
2201
2212
  const registryContent = rewriteContentImports(file.content, config);
2202
-
2203
2213
  if (localContent === registryContent) {
2204
2214
  logStep("✓", `${c("green", name)}: up to date`);
2205
2215
  } else {
@@ -2210,37 +2220,65 @@ async function cmdDiff(names) {
2210
2220
  logStep("✗", `${c("red", name)}: ${err.message}`);
2211
2221
  }
2212
2222
  }
2213
-
2214
2223
  log();
2215
2224
  }
2216
-
2217
2225
  function cmdHelp() {
2218
2226
  log();
2219
2227
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Component Registry CLI")}`);
2220
2228
  log();
2221
2229
  log(" Usage:");
2222
- log(` ${c("cyan", "npx @work-rjkashyap/unified-ui")} ${c("green", "<command>")} [options]`);
2230
+ log(
2231
+ ` ${c("cyan", "npx @work-rjkashyap/unified-ui")} ${c("green", "<command>")} [options]`,
2232
+ );
2223
2233
  log();
2224
2234
  log(" Commands:");
2225
- log(` ${c("green", "init")} Initialize existing project (copy-paste mode)`);
2226
- log(` ${c("green", "init")} -t <framework> Scaffold a new project with full setup`);
2227
- log(` ${c("green", "add")} <component...> Add component(s) with dependencies`);
2235
+ log(
2236
+ ` ${c("green", "init")} Initialize existing project (copy-paste mode)`,
2237
+ );
2238
+ log(
2239
+ ` ${c("green", "init")} -t <framework> Scaffold a new project with full setup`,
2240
+ );
2241
+ log(
2242
+ ` ${c("green", "add")} <component...> Add component(s) with dependencies`,
2243
+ );
2228
2244
  log(` ${c("green", "add")} --all Add all components`);
2229
- log(` ${c("green", "list")} List all available components`);
2230
- log(` ${c("green", "diff")} <component...> Compare local files with registry`);
2231
- log(` ${c("green", "help")} Show this help message`);
2245
+ log(
2246
+ ` ${c("green", "list")} List all available components`,
2247
+ );
2248
+ log(
2249
+ ` ${c("green", "diff")} <component...> Compare local files with registry`,
2250
+ );
2251
+ log(
2252
+ ` ${c("green", "help")} Show this help message`,
2253
+ );
2232
2254
  log();
2233
2255
  log(" Templates (for init -t):");
2234
- log(` ${c("cyan", "vite-react")} Vite + React 19 SPA with full component library`);
2235
- log(` ${c("cyan", "nextjs")} Next.js App Router with SSR + full component library`);
2236
- log(` ${c("cyan", "vuejs")} Vue 3 + Vite with UI components & Tailwind theme`);
2237
- log(` ${c("cyan", "laravel-blade")} Laravel with Blade UI components & Tailwind theme`);
2256
+ log(
2257
+ ` ${c("cyan", "vite-react")} Vite + React 19 SPA with full component library`,
2258
+ );
2259
+ log(
2260
+ ` ${c("cyan", "nextjs")} Next.js App Router with SSR + full component library`,
2261
+ );
2262
+ log(
2263
+ ` ${c("cyan", "vuejs")} Vue 3 + Vite with UI components & Tailwind theme`,
2264
+ );
2265
+ log(
2266
+ ` ${c("cyan", "laravel-blade")} Laravel with Blade UI components & Tailwind theme`,
2267
+ );
2238
2268
  log();
2239
2269
  log(" Options:");
2240
- log(` ${c("yellow", "--template, -t")} Framework template (with 'init')`);
2241
- log(` ${c("yellow", "--yes, -y")} Skip confirmation prompts`);
2242
- log(` ${c("yellow", "--overwrite")} Overwrite existing files`);
2243
- log(` ${c("yellow", "--all")} Add all components (with 'add')`);
2270
+ log(
2271
+ ` ${c("yellow", "--template, -t")} Framework template (with 'init')`,
2272
+ );
2273
+ log(
2274
+ ` ${c("yellow", "--yes, -y")} Skip confirmation prompts`,
2275
+ );
2276
+ log(
2277
+ ` ${c("yellow", "--overwrite")} Overwrite existing files`,
2278
+ );
2279
+ log(
2280
+ ` ${c("yellow", "--all")} Add all components (with 'add')`,
2281
+ );
2244
2282
  log();
2245
2283
  log(" Examples:");
2246
2284
  log(` ${c("dim", "# Scaffold a new project (interactive)")} `);
@@ -2265,17 +2303,14 @@ function cmdHelp() {
2265
2303
  log(` Registry: ${c("cyan", REGISTRY_BASE_URL)}`);
2266
2304
  log();
2267
2305
  }
2268
-
2269
2306
  // ---------------------------------------------------------------------------
2270
2307
  // Argument parsing
2271
2308
  // ---------------------------------------------------------------------------
2272
-
2273
2309
  function parseArgs(argv) {
2274
2310
  const args = argv.slice(2);
2275
2311
  const command = args[0];
2276
2312
  const flags = {};
2277
2313
  const positional = [];
2278
-
2279
2314
  for (let i = 1; i < args.length; i++) {
2280
2315
  const arg = args[i];
2281
2316
  if (arg === "--yes" || arg === "-y") {
@@ -2303,17 +2338,13 @@ function parseArgs(argv) {
2303
2338
  positional.push(arg);
2304
2339
  }
2305
2340
  }
2306
-
2307
2341
  return { command, positional, flags };
2308
2342
  }
2309
-
2310
2343
  // ---------------------------------------------------------------------------
2311
2344
  // Main
2312
2345
  // ---------------------------------------------------------------------------
2313
-
2314
2346
  async function main() {
2315
2347
  const { command, positional, flags } = parseArgs(process.argv);
2316
-
2317
2348
  // Allow custom registry URL
2318
2349
  if (flags.registryUrl) {
2319
2350
  // Override the global — we use a let binding workaround below.
@@ -2321,7 +2352,6 @@ async function main() {
2321
2352
  // For simplicity, we set an env var that's already checked at the top.
2322
2353
  process.env.UNIFIED_UI_REGISTRY_URL = flags.registryUrl;
2323
2354
  }
2324
-
2325
2355
  switch (command) {
2326
2356
  case "init":
2327
2357
  await cmdInit(positional, flags);
@@ -2343,11 +2373,12 @@ async function main() {
2343
2373
  cmdHelp();
2344
2374
  break;
2345
2375
  default:
2346
- logError(`Unknown command: "${command}". Run "npx @work-rjkashyap/unified-ui help" for usage.`);
2376
+ logError(
2377
+ `Unknown command: "${command}". Run "npx @work-rjkashyap/unified-ui help" for usage.`,
2378
+ );
2347
2379
  process.exit(1);
2348
2380
  }
2349
2381
  }
2350
-
2351
2382
  main().catch((err) => {
2352
2383
  logError(err.message);
2353
2384
  process.exit(1);