@work-rjkashyap/unified-ui 0.2.4 → 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bin/cli.mjs +1635 -4
  3. package/package.json +3 -2
package/bin/cli.mjs CHANGED
@@ -28,6 +28,7 @@
28
28
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
29
29
  import { dirname, join, resolve } from "node:path";
30
30
  import { createInterface } from "node:readline";
31
+ import { execSync, spawnSync } from "node:child_process";
31
32
 
32
33
  // ---------------------------------------------------------------------------
33
34
  // Config
@@ -39,6 +40,45 @@ const REGISTRY_BASE_URL =
39
40
 
40
41
  const CONFIG_FILE = "unified-ui.json";
41
42
 
43
+ // ---------------------------------------------------------------------------
44
+ // Starter kit templates
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const FRAMEWORKS = [
48
+ {
49
+ name: "vite-react",
50
+ label: "Vite + React",
51
+ description: "Vite + React 19 SPA with full component library",
52
+ scaffoldCmd: (name) => `npm create vite@latest ${name} -- --template react-ts`,
53
+ deps: ["@work-rjkashyap/unified-ui"],
54
+ devDeps: ["@tailwindcss/vite", "tailwindcss"],
55
+ },
56
+ {
57
+ name: "nextjs",
58
+ label: "Next.js",
59
+ 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`,
61
+ deps: ["@work-rjkashyap/unified-ui", "next-themes"],
62
+ devDeps: [],
63
+ },
64
+ {
65
+ name: "vuejs",
66
+ label: "Vue.js",
67
+ description: "Vue 3 + Vite with UI components & Tailwind theme",
68
+ scaffoldCmd: (name) => `npm create vue@latest ${name} -- --typescript`,
69
+ deps: ["@work-rjkashyap/unified-ui", "clsx", "tailwind-merge"],
70
+ devDeps: ["@tailwindcss/vite", "tailwindcss"],
71
+ },
72
+ {
73
+ name: "laravel-blade",
74
+ label: "Laravel Blade",
75
+ description: "Laravel with Blade UI components & Tailwind theme",
76
+ scaffoldCmd: (name) => `composer create-project laravel/laravel ${name}`,
77
+ deps: ["@work-rjkashyap/unified-ui"],
78
+ devDeps: ["@tailwindcss/vite", "tailwindcss"],
79
+ },
80
+ ];
81
+
42
82
  const DEFAULT_CONFIG = {
43
83
  $schema: "https://unified-ui-rajeshwar.vercel.app/r/schema/config.json",
44
84
  srcDir: "src",
@@ -90,6 +130,53 @@ async function confirm(question) {
90
130
  });
91
131
  }
92
132
 
133
+ async function promptText(question, defaultValue = "") {
134
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
135
+ const hint = defaultValue ? ` ${c("dim", `(${defaultValue})`)}` : "";
136
+ return new Promise((res) => {
137
+ rl.question(` ${question}${hint} `, (answer) => {
138
+ rl.close();
139
+ res(answer.trim() || defaultValue);
140
+ });
141
+ });
142
+ }
143
+
144
+ async function promptSelect(question, options) {
145
+ log(` ${question}`);
146
+ log();
147
+ for (let i = 0; i < options.length; i++) {
148
+ const opt = options[i];
149
+ log(` ${c("cyan", String(i + 1))}. ${c("bold", opt.label.padEnd(18))} ${c("dim", opt.description)}`);
150
+ }
151
+ log();
152
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
153
+ return new Promise((res) => {
154
+ rl.question(` ${c("dim", `Select (1-${options.length}):`) } `, (answer) => {
155
+ rl.close();
156
+ const idx = parseInt(answer.trim(), 10) - 1;
157
+ res(idx >= 0 && idx < options.length ? options[idx] : null);
158
+ });
159
+ });
160
+ }
161
+
162
+ function runCmd(cmd, cwd, stdio = "inherit") {
163
+ try {
164
+ execSync(cmd, { cwd, stdio });
165
+ return true;
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ function ensureDir(dir) {
172
+ mkdirSync(dir, { recursive: true });
173
+ }
174
+
175
+ function writeOverlay(targetPath, content) {
176
+ ensureDir(dirname(targetPath));
177
+ writeFileSync(targetPath, content);
178
+ }
179
+
93
180
  async function fetchJSON(url) {
94
181
  const response = await fetch(url);
95
182
  if (!response.ok) {
@@ -306,7 +393,1523 @@ function writeFile(targetPath, content, config, overwrite = false) {
306
393
  // Commands
307
394
  // ---------------------------------------------------------------------------
308
395
 
309
- async function cmdInit() {
396
+ // ---------------------------------------------------------------------------
397
+ // Starter kit overlays (embedded content)
398
+ // ---------------------------------------------------------------------------
399
+
400
+ const OVERLAYS = {
401
+ "vite-react": {
402
+ files: {
403
+ "vite.config.ts": `import tailwindcss from "@tailwindcss/vite";
404
+ import react from "@vitejs/plugin-react";
405
+ import { defineConfig } from "vite";
406
+
407
+ export default defineConfig({
408
+ plugins: [react(), tailwindcss()],
409
+ resolve: {
410
+ alias: {
411
+ "@": "/src",
412
+ },
413
+ },
414
+ });
415
+ `,
416
+ "src/index.css": `@import "tailwindcss";
417
+ @import "@work-rjkashyap/unified-ui/styles.css";
418
+
419
+ body {
420
+ min-height: 100svh;
421
+ }
422
+ `,
423
+ "src/main.tsx": `import { StrictMode } from "react";
424
+ import { createRoot } from "react-dom/client";
425
+ import { DSThemeProvider } from "@work-rjkashyap/unified-ui/theme";
426
+ import App from "./App";
427
+ import "./index.css";
428
+
429
+ createRoot(document.getElementById("root")!).render(
430
+ <StrictMode>
431
+ <DSThemeProvider manageHtmlClass>
432
+ <App />
433
+ </DSThemeProvider>
434
+ </StrictMode>,
435
+ );
436
+ `,
437
+ "src/App.tsx": `import { Button, Heading, Text } from "@work-rjkashyap/unified-ui/components";
438
+ import { useDSTheme } from "@work-rjkashyap/unified-ui/theme";
439
+
440
+ function App() {
441
+ const { theme, setTheme } = useDSTheme();
442
+
443
+ return (
444
+ <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-8 text-foreground">
445
+ <div className="w-full max-w-md space-y-6 rounded-lg border border-border bg-card p-8">
446
+ <div className="space-y-2 text-center">
447
+ <Heading level={1}>Unified UI</Heading>
448
+ <Text variant="muted">
449
+ Your starter project is ready. Start building!
450
+ </Text>
451
+ </div>
452
+
453
+ <div className="flex items-center justify-center gap-3">
454
+ <Button variant="default">Get Started</Button>
455
+ <Button
456
+ variant="outline"
457
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
458
+ >
459
+ Toggle Theme
460
+ </Button>
461
+ </div>
462
+ </div>
463
+ </div>
464
+ );
465
+ }
466
+
467
+ export default App;
468
+ `,
469
+ },
470
+ },
471
+
472
+ nextjs: {
473
+ files: {
474
+ "src/app/globals.css": `@import "tailwindcss";
475
+ @import "@work-rjkashyap/unified-ui/styles.css";
476
+
477
+ body {
478
+ min-height: 100svh;
479
+ }
480
+ `,
481
+ "src/app/layout.tsx": `import type { Metadata } from "next";
482
+ import { ThemeProvider } from "next-themes";
483
+ import { DSThemeProvider } from "@work-rjkashyap/unified-ui/theme";
484
+ import "./globals.css";
485
+
486
+ export const metadata: Metadata = {
487
+ title: "Unified UI App",
488
+ description: "Built with Unified UI and Next.js",
489
+ };
490
+
491
+ export default function RootLayout({
492
+ children,
493
+ }: {
494
+ children: React.ReactNode;
495
+ }) {
496
+ return (
497
+ <html lang="en" suppressHydrationWarning>
498
+ <body>
499
+ <ThemeProvider
500
+ attribute="class"
501
+ defaultTheme="system"
502
+ enableSystem
503
+ disableTransitionOnChange
504
+ >
505
+ <DSThemeProvider>{children}</DSThemeProvider>
506
+ </ThemeProvider>
507
+ </body>
508
+ </html>
509
+ );
510
+ }
511
+ `,
512
+ "src/app/page.tsx": `"use client";
513
+
514
+ import { Button, Heading, Text } from "@work-rjkashyap/unified-ui/components";
515
+ import { useDSTheme } from "@work-rjkashyap/unified-ui/theme";
516
+
517
+ export default function Home() {
518
+ const { theme, setTheme } = useDSTheme();
519
+
520
+ return (
521
+ <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-8 text-foreground">
522
+ <div className="w-full max-w-md space-y-6 rounded-lg border border-border bg-card p-8">
523
+ <div className="space-y-2 text-center">
524
+ <Heading level={1}>Unified UI</Heading>
525
+ <Text variant="muted">
526
+ Your Next.js project is ready. Start building!
527
+ </Text>
528
+ </div>
529
+
530
+ <div className="flex items-center justify-center gap-3">
531
+ <Button variant="default">Get Started</Button>
532
+ <Button
533
+ variant="outline"
534
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
535
+ >
536
+ Toggle Theme
537
+ </Button>
538
+ </div>
539
+ </div>
540
+ </div>
541
+ );
542
+ }
543
+ `,
544
+ },
545
+ },
546
+
547
+ vuejs: {
548
+ files: {
549
+ "vite.config.ts": `import tailwindcss from "@tailwindcss/vite";
550
+ import vue from "@vitejs/plugin-vue";
551
+ import { defineConfig } from "vite";
552
+
553
+ export default defineConfig({
554
+ plugins: [vue(), tailwindcss()],
555
+ resolve: {
556
+ alias: {
557
+ "@": "/src",
558
+ },
559
+ },
560
+ });
561
+ `,
562
+ "src/style.css": `@import "tailwindcss";
563
+ @import "@work-rjkashyap/unified-ui/styles.css";
564
+
565
+ body {
566
+ min-height: 100svh;
567
+ }
568
+ `,
569
+ "src/main.ts": `import { createApp } from "vue";
570
+ import App from "./App.vue";
571
+ import "./style.css";
572
+
573
+ createApp(App).mount("#app");
574
+ `,
575
+ "src/lib/cn.ts": `import { type ClassValue, clsx } from "clsx";
576
+ import { twMerge } from "tailwind-merge";
577
+
578
+ export function cn(...inputs: ClassValue[]) {
579
+ return twMerge(clsx(inputs));
580
+ }
581
+ `,
582
+ "src/App.vue": `<script setup lang="ts">
583
+ import ThemeToggle from "./components/ThemeToggle.vue";
584
+ import {
585
+ UiButton,
586
+ UiBadge,
587
+ UiCard,
588
+ UiCardHeader,
589
+ UiCardBody,
590
+ UiCardFooter,
591
+ UiInput,
592
+ UiAlert,
593
+ UiHeading,
594
+ UiText,
595
+ } from "./components/ui";
596
+ import { ref } from "vue";
597
+
598
+ const email = ref("");
599
+ </script>
600
+
601
+ <template>
602
+ <div
603
+ class="flex min-h-svh flex-col items-center justify-center gap-8 bg-background p-8 text-foreground"
604
+ >
605
+ <UiCard class="w-full max-w-lg">
606
+ <UiCardHeader>
607
+ <UiHeading :level="2">Unified UI</UiHeading>
608
+ <UiText variant="bodySm" color="muted">
609
+ Your Vue.js project is ready with components. Start building!
610
+ </UiText>
611
+ </UiCardHeader>
612
+
613
+ <UiCardBody class="space-y-6">
614
+ <!-- Buttons -->
615
+ <div class="space-y-2">
616
+ <UiText variant="label">Buttons</UiText>
617
+ <div class="flex flex-wrap items-center gap-2">
618
+ <UiButton variant="primary">Primary</UiButton>
619
+ <UiButton variant="secondary">Secondary</UiButton>
620
+ <UiButton variant="ghost">Ghost</UiButton>
621
+ <UiButton variant="danger" size="sm">Danger</UiButton>
622
+ <UiButton variant="primary" :loading="true" size="sm">Loading</UiButton>
623
+ </div>
624
+ </div>
625
+
626
+ <!-- Badges -->
627
+ <div class="space-y-2">
628
+ <UiText variant="label">Badges</UiText>
629
+ <div class="flex flex-wrap items-center gap-2">
630
+ <UiBadge variant="default">Default</UiBadge>
631
+ <UiBadge variant="primary">Primary</UiBadge>
632
+ <UiBadge variant="success">Success</UiBadge>
633
+ <UiBadge variant="warning">Warning</UiBadge>
634
+ <UiBadge variant="danger">Danger</UiBadge>
635
+ <UiBadge variant="info">Info</UiBadge>
636
+ <UiBadge variant="outline">Outline</UiBadge>
637
+ </div>
638
+ </div>
639
+
640
+ <!-- Input -->
641
+ <div class="space-y-2">
642
+ <UiText variant="label">Input</UiText>
643
+ <UiInput v-model="email" placeholder="you@example.com" />
644
+ </div>
645
+
646
+ <!-- Alert -->
647
+ <UiAlert variant="info" title="All set!">
648
+ Your design system components are working in Vue.
649
+ </UiAlert>
650
+ </UiCardBody>
651
+
652
+ <UiCardFooter class="justify-between">
653
+ <UiButton variant="primary">Get Started</UiButton>
654
+ <ThemeToggle />
655
+ </UiCardFooter>
656
+ </UiCard>
657
+ </div>
658
+ </template>
659
+ `,
660
+ "src/components/ThemeToggle.vue": `<script setup lang="ts">
661
+ import { ref, onMounted } from "vue";
662
+
663
+ const theme = ref<"light" | "dark">("light");
664
+
665
+ onMounted(() => {
666
+ const stored = localStorage.getItem("theme");
667
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
668
+ theme.value =
669
+ (stored as "light" | "dark") || (prefersDark ? "dark" : "light");
670
+ applyTheme();
671
+ });
672
+
673
+ function toggle() {
674
+ theme.value = theme.value === "dark" ? "light" : "dark";
675
+ applyTheme();
676
+ }
677
+
678
+ function applyTheme() {
679
+ document.documentElement.classList.toggle("dark", theme.value === "dark");
680
+ localStorage.setItem("theme", theme.value);
681
+ }
682
+ </script>
683
+
684
+ <template>
685
+ <button
686
+ 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"
687
+ @click="toggle"
688
+ >
689
+ Toggle {{ theme === "dark" ? "Light" : "Dark" }}
690
+ </button>
691
+ </template>
692
+ `,
693
+ "src/components/ui/index.ts": `export { default as UiButton } from "./Button.vue";
694
+ export { default as UiBadge } from "./Badge.vue";
695
+ export { default as UiCard } from "./Card.vue";
696
+ export { default as UiCardHeader } from "./CardHeader.vue";
697
+ export { default as UiCardBody } from "./CardBody.vue";
698
+ export { default as UiCardFooter } from "./CardFooter.vue";
699
+ export { default as UiInput } from "./Input.vue";
700
+ export { default as UiAlert } from "./Alert.vue";
701
+ export { default as UiHeading } from "./Heading.vue";
702
+ export { default as UiText } from "./Text.vue";
703
+ `,
704
+ "src/components/ui/Button.vue": `<script setup lang="ts">
705
+ import { computed, type HTMLAttributes } from "vue";
706
+ import { cn } from "@/lib/cn";
707
+
708
+ type Variant = "primary" | "secondary" | "ghost" | "danger";
709
+ type Size = "sm" | "md" | "lg";
710
+
711
+ interface Props {
712
+ variant?: Variant;
713
+ size?: Size;
714
+ fullWidth?: boolean;
715
+ iconOnly?: boolean;
716
+ loading?: boolean;
717
+ disabled?: boolean;
718
+ as?: string;
719
+ class?: HTMLAttributes["class"];
720
+ }
721
+
722
+ const props = withDefaults(defineProps<Props>(), {
723
+ variant: "primary",
724
+ size: "md",
725
+ as: "button",
726
+ fullWidth: false,
727
+ iconOnly: false,
728
+ loading: false,
729
+ disabled: false,
730
+ });
731
+
732
+ const variantClasses: Record<Variant, string> = {
733
+ primary:
734
+ "bg-primary text-primary-foreground hover:bg-primary-hover active:bg-primary-active",
735
+ secondary:
736
+ "bg-secondary text-secondary-foreground border border-border hover:bg-secondary-hover active:bg-secondary-active",
737
+ ghost:
738
+ "bg-transparent text-foreground hover:bg-muted hover:text-foreground active:bg-secondary-active",
739
+ danger:
740
+ "bg-danger text-danger-foreground hover:bg-danger-hover active:bg-danger-active",
741
+ };
742
+
743
+ const sizeClasses: Record<Size, string> = {
744
+ sm: "h-8 px-3 text-xs gap-1.5",
745
+ md: "h-[var(--ds-control-height,36px)] px-[var(--ds-padding-button-x,16px)] text-sm gap-2",
746
+ lg: "h-10 px-5 text-sm gap-2",
747
+ };
748
+
749
+ const iconOnlySizeClasses: Record<Size, string> = {
750
+ sm: "w-8 !px-0",
751
+ md: "w-9 !px-0",
752
+ lg: "w-10 !px-0",
753
+ };
754
+
755
+ const classes = computed(() =>
756
+ cn(
757
+ // base
758
+ "inline-flex items-center justify-center gap-2 text-sm font-medium leading-5 rounded-md",
759
+ "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))]",
760
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
761
+ "disabled:pointer-events-none disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed select-none",
762
+ "active:scale-[0.98] disabled:active:scale-100",
763
+ // variant
764
+ variantClasses[props.variant],
765
+ // size
766
+ sizeClasses[props.size],
767
+ // icon only
768
+ props.iconOnly && iconOnlySizeClasses[props.size],
769
+ // fullWidth
770
+ props.fullWidth && "w-full",
771
+ // loading
772
+ props.loading && "pointer-events-none opacity-70",
773
+ props.class,
774
+ ),
775
+ );
776
+ </script>
777
+
778
+ <template>
779
+ <component
780
+ :is="as"
781
+ :class="classes"
782
+ :disabled="disabled || loading"
783
+ data-ds
784
+ data-ds-component="button"
785
+ :data-ds-loading="loading || undefined"
786
+ >
787
+ <svg
788
+ v-if="loading"
789
+ class="h-4 w-4 animate-spin"
790
+ xmlns="http://www.w3.org/2000/svg"
791
+ fill="none"
792
+ viewBox="0 0 24 24"
793
+ >
794
+ <circle
795
+ class="opacity-25"
796
+ cx="12"
797
+ cy="12"
798
+ r="10"
799
+ stroke="currentColor"
800
+ stroke-width="4"
801
+ />
802
+ <path
803
+ class="opacity-75"
804
+ fill="currentColor"
805
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
806
+ />
807
+ </svg>
808
+ <slot />
809
+ </component>
810
+ </template>
811
+ `,
812
+ "src/components/ui/Badge.vue": `<script setup lang="ts">
813
+ import { computed, type HTMLAttributes } from "vue";
814
+ import { cn } from "@/lib/cn";
815
+
816
+ type Variant =
817
+ | "default"
818
+ | "primary"
819
+ | "secondary"
820
+ | "success"
821
+ | "warning"
822
+ | "danger"
823
+ | "info"
824
+ | "outline";
825
+ type Size = "sm" | "md" | "lg";
826
+
827
+ interface Props {
828
+ variant?: Variant;
829
+ size?: Size;
830
+ dismissible?: boolean;
831
+ class?: HTMLAttributes["class"];
832
+ }
833
+
834
+ const props = withDefaults(defineProps<Props>(), {
835
+ variant: "default",
836
+ size: "md",
837
+ dismissible: false,
838
+ });
839
+
840
+ const emit = defineEmits<{ dismiss: [] }>();
841
+
842
+ const variantClasses: Record<Variant, string> = {
843
+ default: "bg-muted text-foreground border border-transparent",
844
+ primary:
845
+ "bg-primary-muted text-primary-muted-foreground border border-transparent",
846
+ secondary: "bg-secondary text-secondary-foreground border border-border",
847
+ success:
848
+ "bg-success-muted text-success-muted-foreground border border-transparent",
849
+ warning:
850
+ "bg-warning-muted text-warning-muted-foreground border border-transparent",
851
+ danger:
852
+ "bg-danger-muted text-danger-muted-foreground border border-transparent",
853
+ info: "bg-info-muted text-info-muted-foreground border border-transparent",
854
+ outline: "bg-transparent text-foreground border border-border",
855
+ };
856
+
857
+ const sizeClasses: Record<Size, string> = {
858
+ sm: "px-2 py-0.5 text-[11px] gap-1",
859
+ md: "px-2.5 py-1 text-xs gap-1.5",
860
+ lg: "px-3 py-1.5 text-sm gap-2",
861
+ };
862
+
863
+ const classes = computed(() =>
864
+ cn(
865
+ "inline-flex items-center gap-1.5 rounded-full font-medium leading-none whitespace-nowrap",
866
+ "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))]",
867
+ "select-none shrink-0",
868
+ variantClasses[props.variant],
869
+ sizeClasses[props.size],
870
+ props.class,
871
+ ),
872
+ );
873
+ </script>
874
+
875
+ <template>
876
+ <span :class="classes" data-ds data-ds-component="badge">
877
+ <slot />
878
+ <button
879
+ v-if="dismissible"
880
+ class="ml-0.5 inline-flex items-center justify-center rounded-full hover:bg-black/10 dark:hover:bg-white/10"
881
+ :class="size === 'sm' ? 'h-3 w-3' : size === 'md' ? 'h-3.5 w-3.5' : 'h-4 w-4'"
882
+ @click.stop="emit('dismiss')"
883
+ aria-label="Dismiss"
884
+ >
885
+ <svg
886
+ xmlns="http://www.w3.org/2000/svg"
887
+ viewBox="0 0 24 24"
888
+ fill="none"
889
+ stroke="currentColor"
890
+ stroke-width="2"
891
+ stroke-linecap="round"
892
+ stroke-linejoin="round"
893
+ class="h-full w-full"
894
+ >
895
+ <line x1="18" y1="6" x2="6" y2="18" />
896
+ <line x1="6" y1="6" x2="18" y2="18" />
897
+ </svg>
898
+ </button>
899
+ </span>
900
+ </template>
901
+ `,
902
+ "src/components/ui/Card.vue": `<script setup lang="ts">
903
+ import { computed, provide, type HTMLAttributes, type InjectionKey } from "vue";
904
+ import { cn } from "@/lib/cn";
905
+
906
+ type Variant = "default" | "outlined" | "elevated" | "interactive";
907
+ type Padding = "compact" | "comfortable";
908
+
909
+ interface Props {
910
+ variant?: Variant;
911
+ padding?: Padding;
912
+ fullWidth?: boolean;
913
+ as?: string;
914
+ class?: HTMLAttributes["class"];
915
+ }
916
+
917
+ const props = withDefaults(defineProps<Props>(), {
918
+ variant: "default",
919
+ padding: "compact",
920
+ as: "div",
921
+ fullWidth: false,
922
+ });
923
+
924
+ export const cardPaddingKey = Symbol("cardPadding") as InjectionKey<Padding>;
925
+ provide(cardPaddingKey, props.padding);
926
+
927
+ const variantClasses: Record<Variant, string> = {
928
+ default: "bg-surface border border-border",
929
+ outlined: "bg-transparent border border-border-strong",
930
+ elevated: "bg-surface-raised border border-border-muted shadow-md",
931
+ interactive:
932
+ "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",
933
+ };
934
+
935
+ const classes = computed(() =>
936
+ cn(
937
+ "flex flex-col rounded-md overflow-hidden text-sm text-foreground",
938
+ variantClasses[props.variant],
939
+ props.fullWidth && "w-full",
940
+ props.class,
941
+ ),
942
+ );
943
+ </script>
944
+
945
+ <template>
946
+ <component :is="as" :class="classes" data-ds data-ds-component="card">
947
+ <slot />
948
+ </component>
949
+ </template>
950
+ `,
951
+ "src/components/ui/CardHeader.vue": `<script setup lang="ts">
952
+ import { inject, computed, type HTMLAttributes } from "vue";
953
+ import { cn } from "@/lib/cn";
954
+ import { cardPaddingKey } from "./Card.vue";
955
+
956
+ interface Props {
957
+ class?: HTMLAttributes["class"];
958
+ }
959
+
960
+ const props = defineProps<Props>();
961
+ const padding = inject(cardPaddingKey, "compact");
962
+
963
+ const classes = computed(() =>
964
+ cn(
965
+ "flex flex-col",
966
+ padding === "comfortable" ? "px-6 pt-6 gap-1.5" : "px-[var(--ds-padding-card,16px)] pt-[var(--ds-padding-card,16px)] gap-1",
967
+ props.class,
968
+ ),
969
+ );
970
+ </script>
971
+
972
+ <template>
973
+ <div :class="classes" data-ds data-ds-component="card-header">
974
+ <slot />
975
+ </div>
976
+ </template>
977
+ `,
978
+ "src/components/ui/CardBody.vue": `<script setup lang="ts">
979
+ import { inject, computed, type HTMLAttributes } from "vue";
980
+ import { cn } from "@/lib/cn";
981
+ import { cardPaddingKey } from "./Card.vue";
982
+
983
+ interface Props {
984
+ class?: HTMLAttributes["class"];
985
+ }
986
+
987
+ const props = defineProps<Props>();
988
+ const padding = inject(cardPaddingKey, "compact");
989
+
990
+ const classes = computed(() =>
991
+ cn(
992
+ "flex flex-col flex-1",
993
+ padding === "comfortable" ? "px-6 py-4 gap-4" : "px-[var(--ds-padding-card,16px)] py-3 gap-[var(--ds-gap-default,0.75rem)]",
994
+ props.class,
995
+ ),
996
+ );
997
+ </script>
998
+
999
+ <template>
1000
+ <div :class="classes" data-ds data-ds-component="card-body">
1001
+ <slot />
1002
+ </div>
1003
+ </template>
1004
+ `,
1005
+ "src/components/ui/CardFooter.vue": `<script setup lang="ts">
1006
+ import { inject, computed, type HTMLAttributes } from "vue";
1007
+ import { cn } from "@/lib/cn";
1008
+ import { cardPaddingKey } from "./Card.vue";
1009
+
1010
+ interface Props {
1011
+ class?: HTMLAttributes["class"];
1012
+ }
1013
+
1014
+ const props = defineProps<Props>();
1015
+ const padding = inject(cardPaddingKey, "compact");
1016
+
1017
+ const classes = computed(() =>
1018
+ cn(
1019
+ "flex items-center",
1020
+ padding === "comfortable" ? "px-6 pb-6 gap-3" : "px-[var(--ds-padding-card,16px)] pb-[var(--ds-padding-card,16px)] gap-2",
1021
+ props.class,
1022
+ ),
1023
+ );
1024
+ </script>
1025
+
1026
+ <template>
1027
+ <div :class="classes" data-ds data-ds-component="card-footer">
1028
+ <slot />
1029
+ </div>
1030
+ </template>
1031
+ `,
1032
+ "src/components/ui/Input.vue": `<script setup lang="ts">
1033
+ import { computed, type HTMLAttributes } from "vue";
1034
+ import { cn } from "@/lib/cn";
1035
+
1036
+ type Variant = "default" | "error" | "success";
1037
+ type Size = "sm" | "md" | "lg";
1038
+
1039
+ interface Props {
1040
+ variant?: Variant;
1041
+ size?: Size;
1042
+ disabled?: boolean;
1043
+ class?: HTMLAttributes["class"];
1044
+ }
1045
+
1046
+ const props = withDefaults(defineProps<Props>(), {
1047
+ variant: "default",
1048
+ size: "md",
1049
+ disabled: false,
1050
+ });
1051
+
1052
+ const model = defineModel<string>();
1053
+
1054
+ const variantClasses: Record<Variant, string> = {
1055
+ default:
1056
+ "border-input hover:border-border-strong focus-visible:border-border-strong",
1057
+ error:
1058
+ "border-danger text-foreground focus-visible:border-danger placeholder:text-input-placeholder",
1059
+ success:
1060
+ "border-success text-foreground focus-visible:border-success placeholder:text-input-placeholder",
1061
+ };
1062
+
1063
+ const sizeClasses: Record<Size, string> = {
1064
+ sm: "h-8 px-2.5 text-xs",
1065
+ md: "h-[var(--ds-control-height,36px)] px-3 text-sm",
1066
+ lg: "h-10 px-3.5 text-sm",
1067
+ };
1068
+
1069
+ const classes = computed(() =>
1070
+ cn(
1071
+ "flex w-full text-sm leading-5 rounded-md border bg-background text-input-foreground",
1072
+ "placeholder:text-input-placeholder",
1073
+ "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))]",
1074
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
1075
+ "disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-foreground",
1076
+ "read-only:bg-muted read-only:cursor-default",
1077
+ variantClasses[props.variant],
1078
+ sizeClasses[props.size],
1079
+ props.class,
1080
+ ),
1081
+ );
1082
+ </script>
1083
+
1084
+ <template>
1085
+ <input
1086
+ v-model="model"
1087
+ :class="classes"
1088
+ :disabled="disabled"
1089
+ data-ds
1090
+ data-ds-component="input"
1091
+ />
1092
+ </template>
1093
+ `,
1094
+ "src/components/ui/Alert.vue": `<script setup lang="ts">
1095
+ import { computed, ref, type HTMLAttributes } from "vue";
1096
+ import { cn } from "@/lib/cn";
1097
+
1098
+ type Variant = "info" | "success" | "warning" | "danger" | "default";
1099
+
1100
+ interface Props {
1101
+ variant?: Variant;
1102
+ title?: string;
1103
+ dismissible?: boolean;
1104
+ class?: HTMLAttributes["class"];
1105
+ }
1106
+
1107
+ const props = withDefaults(defineProps<Props>(), {
1108
+ variant: "info",
1109
+ dismissible: false,
1110
+ });
1111
+
1112
+ const dismissed = ref(false);
1113
+
1114
+ const variantClasses: Record<Variant, string> = {
1115
+ info: "bg-info-muted text-info-muted-foreground border-info/20",
1116
+ success: "bg-success-muted text-success-muted-foreground border-success/20",
1117
+ warning: "bg-warning-muted text-warning-muted-foreground border-warning/20",
1118
+ danger: "bg-danger-muted text-danger-muted-foreground border-danger/20",
1119
+ default: "bg-muted text-muted-foreground border-border",
1120
+ };
1121
+
1122
+ const iconColorClasses: Record<Variant, string> = {
1123
+ info: "text-info",
1124
+ success: "text-success",
1125
+ warning: "text-warning",
1126
+ danger: "text-danger",
1127
+ default: "text-muted-foreground",
1128
+ };
1129
+
1130
+ const classes = computed(() =>
1131
+ cn(
1132
+ "relative flex gap-3 rounded-md p-4 text-sm leading-5 border",
1133
+ "transition-colors duration-[var(--duration-fast,150ms)]",
1134
+ variantClasses[props.variant],
1135
+ props.class,
1136
+ ),
1137
+ );
1138
+
1139
+ // SVG icon paths by variant
1140
+ const iconPaths: Record<Variant, string> = {
1141
+ 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",
1142
+ success: "M9 12l2 2 4-4m6 2a10 10 0 11-20 0 10 10 0 0120 0z",
1143
+ warning: "M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z",
1144
+ danger: "M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a10 10 0 11-20 0 10 10 0 0120 0z",
1145
+ 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",
1146
+ };
1147
+ </script>
1148
+
1149
+ <template>
1150
+ <div
1151
+ v-if="!dismissed"
1152
+ :class="classes"
1153
+ role="alert"
1154
+ data-ds
1155
+ data-ds-component="alert"
1156
+ >
1157
+ <svg
1158
+ xmlns="http://www.w3.org/2000/svg"
1159
+ viewBox="0 0 24 24"
1160
+ fill="none"
1161
+ stroke="currentColor"
1162
+ stroke-width="2"
1163
+ stroke-linecap="round"
1164
+ stroke-linejoin="round"
1165
+ class="h-4 w-4 shrink-0 mt-0.5"
1166
+ :class="iconColorClasses[variant]"
1167
+ >
1168
+ <path :d="iconPaths[variant]" />
1169
+ </svg>
1170
+ <div class="flex-1 space-y-1">
1171
+ <p v-if="title" class="font-medium leading-5">{{ title }}</p>
1172
+ <div class="text-sm leading-5">
1173
+ <slot />
1174
+ </div>
1175
+ </div>
1176
+ <button
1177
+ v-if="dismissible"
1178
+ class="absolute top-3 right-3 inline-flex items-center justify-center rounded-md h-6 w-6 hover:bg-black/10 dark:hover:bg-white/10"
1179
+ @click="dismissed = true"
1180
+ aria-label="Dismiss"
1181
+ >
1182
+ <svg
1183
+ xmlns="http://www.w3.org/2000/svg"
1184
+ viewBox="0 0 24 24"
1185
+ fill="none"
1186
+ stroke="currentColor"
1187
+ stroke-width="2"
1188
+ stroke-linecap="round"
1189
+ stroke-linejoin="round"
1190
+ class="h-4 w-4"
1191
+ >
1192
+ <line x1="18" y1="6" x2="6" y2="18" />
1193
+ <line x1="6" y1="6" x2="18" y2="18" />
1194
+ </svg>
1195
+ </button>
1196
+ </div>
1197
+ </template>
1198
+ `,
1199
+ "src/components/ui/Heading.vue": `<script setup lang="ts">
1200
+ import { computed, type HTMLAttributes } from "vue";
1201
+ import { cn } from "@/lib/cn";
1202
+
1203
+ type Level = 1 | 2 | 3 | 4;
1204
+ type Color = "default" | "foreground" | "muted" | "primary";
1205
+
1206
+ interface Props {
1207
+ level?: Level;
1208
+ color?: Color;
1209
+ class?: HTMLAttributes["class"];
1210
+ }
1211
+
1212
+ const props = withDefaults(defineProps<Props>(), {
1213
+ level: 1,
1214
+ color: "default",
1215
+ });
1216
+
1217
+ const levelClasses: Record<Level, string> = {
1218
+ 1: "text-[30px] leading-[36px] font-bold tracking-tight",
1219
+ 2: "text-[24px] leading-[32px] font-semibold tracking-tight",
1220
+ 3: "text-[20px] leading-[28px] font-semibold tracking-normal",
1221
+ 4: "text-[18px] leading-[28px] font-medium tracking-normal",
1222
+ };
1223
+
1224
+ const colorClasses: Record<Color, string> = {
1225
+ default: "text-foreground",
1226
+ foreground: "text-foreground",
1227
+ muted: "text-muted-foreground",
1228
+ primary: "text-primary",
1229
+ };
1230
+
1231
+ const tag = computed(() => \`h\${props.level}\` as const);
1232
+
1233
+ const classes = computed(() =>
1234
+ cn(levelClasses[props.level], colorClasses[props.color], props.class),
1235
+ );
1236
+ </script>
1237
+
1238
+ <template>
1239
+ <component :is="tag" :class="classes" data-ds data-ds-component="heading">
1240
+ <slot />
1241
+ </component>
1242
+ </template>
1243
+ `,
1244
+ "src/components/ui/Text.vue": `<script setup lang="ts">
1245
+ import { computed, type HTMLAttributes } from "vue";
1246
+ import { cn } from "@/lib/cn";
1247
+
1248
+ type Variant = "body" | "bodySm" | "caption" | "label" | "overline" | "code";
1249
+ type Color =
1250
+ | "default"
1251
+ | "foreground"
1252
+ | "muted"
1253
+ | "primary"
1254
+ | "success"
1255
+ | "warning"
1256
+ | "danger"
1257
+ | "info";
1258
+
1259
+ interface Props {
1260
+ variant?: Variant;
1261
+ color?: Color;
1262
+ as?: string;
1263
+ class?: HTMLAttributes["class"];
1264
+ }
1265
+
1266
+ const props = withDefaults(defineProps<Props>(), {
1267
+ variant: "body",
1268
+ color: "default",
1269
+ as: "p",
1270
+ });
1271
+
1272
+ const variantClasses: Record<Variant, string> = {
1273
+ body: "text-[16px] leading-[24px] font-normal tracking-normal",
1274
+ bodySm: "text-[14px] leading-[20px] font-normal tracking-normal",
1275
+ caption:
1276
+ "text-[12px] leading-[16px] font-normal tracking-wide text-muted-foreground",
1277
+ label: "text-[14px] leading-[20px] font-medium tracking-normal",
1278
+ overline:
1279
+ "text-[12px] leading-[16px] font-semibold tracking-wider uppercase text-muted-foreground",
1280
+ code: "text-[14px] leading-[20px] font-normal tracking-normal font-mono",
1281
+ };
1282
+
1283
+ const colorClasses: Record<Color, string> = {
1284
+ default: "text-foreground",
1285
+ foreground: "text-foreground",
1286
+ muted: "text-muted-foreground",
1287
+ primary: "text-primary",
1288
+ success: "text-success",
1289
+ warning: "text-warning",
1290
+ danger: "text-danger",
1291
+ info: "text-info",
1292
+ };
1293
+
1294
+ const classes = computed(() =>
1295
+ cn(variantClasses[props.variant], colorClasses[props.color], props.class),
1296
+ );
1297
+ </script>
1298
+
1299
+ <template>
1300
+ <component :is="as" :class="classes" data-ds data-ds-component="text">
1301
+ <slot />
1302
+ </component>
1303
+ </template>
1304
+ `,
1305
+ },
1306
+ },
1307
+
1308
+ "laravel-blade": {
1309
+ files: {
1310
+ "vite.config.js": `import tailwindcss from "@tailwindcss/vite";
1311
+ import laravel from "laravel-vite-plugin";
1312
+ import { defineConfig } from "vite";
1313
+
1314
+ export default defineConfig({
1315
+ plugins: [
1316
+ laravel({
1317
+ input: ["resources/css/app.css", "resources/js/app.js"],
1318
+ refresh: true,
1319
+ }),
1320
+ tailwindcss(),
1321
+ ],
1322
+ });
1323
+ `,
1324
+ "resources/css/app.css": `@import "tailwindcss";
1325
+ @import "@work-rjkashyap/unified-ui/styles.css";
1326
+ `,
1327
+ "resources/js/app.js": `// Unified UI — Laravel Blade Starter
1328
+ // Design tokens are loaded via CSS. This file handles theme toggling.
1329
+
1330
+ function initTheme() {
1331
+ const stored = localStorage.getItem("theme");
1332
+ const prefersDark = window.matchMedia(
1333
+ "(prefers-color-scheme: dark)",
1334
+ ).matches;
1335
+ const theme = stored || (prefersDark ? "dark" : "light");
1336
+ document.documentElement.classList.toggle("dark", theme === "dark");
1337
+ }
1338
+
1339
+ function toggleTheme() {
1340
+ const isDark = document.documentElement.classList.toggle("dark");
1341
+ localStorage.setItem("theme", isDark ? "dark" : "light");
1342
+ }
1343
+
1344
+ // Initialize on load
1345
+ initTheme();
1346
+
1347
+ // Expose globally for Blade onclick handlers
1348
+ window.toggleTheme = toggleTheme;
1349
+ `,
1350
+ "resources/views/layouts/app.blade.php": `<!DOCTYPE html>
1351
+ <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
1352
+ <head>
1353
+ <meta charset="utf-8">
1354
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1355
+ <meta name="csrf-token" content="{{ csrf_token() }}">
1356
+ <title>{{ config('app.name', 'Unified UI') }}</title>
1357
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
1358
+ </head>
1359
+ <body class="min-h-svh bg-background text-foreground antialiased">
1360
+ @yield('content')
1361
+ </body>
1362
+ </html>
1363
+ `,
1364
+ "resources/views/welcome.blade.php": `@extends('layouts.app')
1365
+
1366
+ @section('content')
1367
+ <div class="flex min-h-svh flex-col items-center justify-center gap-8 p-8">
1368
+ <x-ui.card class="w-full max-w-lg">
1369
+ <x-ui.card-header>
1370
+ <x-ui.heading :level="2">Unified UI</x-ui.heading>
1371
+ <x-ui.text variant="bodySm" color="muted">
1372
+ Your Laravel project is ready with components. Start building!
1373
+ </x-ui.text>
1374
+ </x-ui.card-header>
1375
+
1376
+ <x-ui.card-body class="space-y-6">
1377
+ {{-- Buttons --}}
1378
+ <div class="space-y-2">
1379
+ <x-ui.text variant="label">Buttons</x-ui.text>
1380
+ <div class="flex flex-wrap items-center gap-2">
1381
+ <x-ui.button variant="primary">Primary</x-ui.button>
1382
+ <x-ui.button variant="secondary">Secondary</x-ui.button>
1383
+ <x-ui.button variant="ghost">Ghost</x-ui.button>
1384
+ <x-ui.button variant="danger" size="sm">Danger</x-ui.button>
1385
+ <x-ui.button variant="primary" :loading="true" size="sm">Loading</x-ui.button>
1386
+ </div>
1387
+ </div>
1388
+
1389
+ {{-- Badges --}}
1390
+ <div class="space-y-2">
1391
+ <x-ui.text variant="label">Badges</x-ui.text>
1392
+ <div class="flex flex-wrap items-center gap-2">
1393
+ <x-ui.badge variant="default">Default</x-ui.badge>
1394
+ <x-ui.badge variant="primary">Primary</x-ui.badge>
1395
+ <x-ui.badge variant="success">Success</x-ui.badge>
1396
+ <x-ui.badge variant="warning">Warning</x-ui.badge>
1397
+ <x-ui.badge variant="danger">Danger</x-ui.badge>
1398
+ <x-ui.badge variant="info">Info</x-ui.badge>
1399
+ <x-ui.badge variant="outline">Outline</x-ui.badge>
1400
+ </div>
1401
+ </div>
1402
+
1403
+ {{-- Input --}}
1404
+ <div class="space-y-2">
1405
+ <x-ui.text variant="label">Input</x-ui.text>
1406
+ <x-ui.input placeholder="you@example.com" />
1407
+ </div>
1408
+
1409
+ {{-- Alert --}}
1410
+ <x-ui.alert variant="info" title="All set!">
1411
+ Your design system components are working in Laravel.
1412
+ </x-ui.alert>
1413
+ </x-ui.card-body>
1414
+
1415
+ <x-ui.card-footer class="justify-between">
1416
+ <x-ui.button variant="primary">Get Started</x-ui.button>
1417
+ <x-ui.button variant="secondary" onclick="toggleTheme()">Toggle Theme</x-ui.button>
1418
+ </x-ui.card-footer>
1419
+ </x-ui.card>
1420
+ </div>
1421
+ @endsection
1422
+ `,
1423
+ "resources/views/components/ui/button.blade.php": `@props([
1424
+ 'variant' => 'primary',
1425
+ 'size' => 'md',
1426
+ 'as' => 'button',
1427
+ 'fullWidth' => false,
1428
+ 'iconOnly' => false,
1429
+ 'loading' => false,
1430
+ 'disabled' => false,
1431
+ ])
1432
+
1433
+ @php
1434
+ $variants = [
1435
+ 'primary' => 'bg-primary text-primary-foreground hover:bg-primary-hover active:bg-primary-active',
1436
+ 'secondary' => 'bg-secondary text-secondary-foreground border border-border hover:bg-secondary-hover active:bg-secondary-active',
1437
+ 'ghost' => 'bg-transparent text-foreground hover:bg-muted hover:text-foreground active:bg-secondary-active',
1438
+ 'danger' => 'bg-danger text-danger-foreground hover:bg-danger-hover active:bg-danger-active',
1439
+ ];
1440
+
1441
+ $sizes = [
1442
+ 'sm' => 'h-8 px-3 text-xs gap-1.5',
1443
+ 'md' => 'h-[var(--ds-control-height,36px)] px-[var(--ds-padding-button-x,16px)] text-sm gap-2',
1444
+ 'lg' => 'h-10 px-5 text-sm gap-2',
1445
+ ];
1446
+
1447
+ $iconOnlySizes = [
1448
+ 'sm' => 'w-8 !px-0',
1449
+ 'md' => 'w-9 !px-0',
1450
+ 'lg' => 'w-10 !px-0',
1451
+ ];
1452
+
1453
+ $classes = implode(' ', array_filter([
1454
+ 'inline-flex items-center justify-center gap-2 text-sm font-medium leading-5 rounded-md',
1455
+ '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))]',
1456
+ 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
1457
+ 'disabled:pointer-events-none disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed select-none',
1458
+ 'active:scale-[0.98] disabled:active:scale-100',
1459
+ $variants[$variant] ?? $variants['primary'],
1460
+ $sizes[$size] ?? $sizes['md'],
1461
+ $iconOnly ? ($iconOnlySizes[$size] ?? '') : '',
1462
+ $fullWidth ? 'w-full' : '',
1463
+ $loading ? 'pointer-events-none opacity-70' : '',
1464
+ ]));
1465
+ @endphp
1466
+
1467
+ <{{ $as }}
1468
+ {{ $attributes->merge(['class' => $classes, 'disabled' => $disabled || $loading]) }}
1469
+ data-ds
1470
+ data-ds-component="button"
1471
+ @if($loading) data-ds-loading @endif
1472
+ >
1473
+ @if($loading)
1474
+ <svg class="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
1475
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
1476
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
1477
+ </svg>
1478
+ @endif
1479
+ {{ $slot }}
1480
+ </{{ $as }}>
1481
+ `,
1482
+ "resources/views/components/ui/badge.blade.php": `@props([
1483
+ 'variant' => 'default',
1484
+ 'size' => 'md',
1485
+ 'dismissible' => false,
1486
+ ])
1487
+
1488
+ @php
1489
+ $variants = [
1490
+ 'default' => 'bg-muted text-foreground border border-transparent',
1491
+ 'primary' => 'bg-primary-muted text-primary-muted-foreground border border-transparent',
1492
+ 'secondary' => 'bg-secondary text-secondary-foreground border border-border',
1493
+ 'success' => 'bg-success-muted text-success-muted-foreground border border-transparent',
1494
+ 'warning' => 'bg-warning-muted text-warning-muted-foreground border border-transparent',
1495
+ 'danger' => 'bg-danger-muted text-danger-muted-foreground border border-transparent',
1496
+ 'info' => 'bg-info-muted text-info-muted-foreground border border-transparent',
1497
+ 'outline' => 'bg-transparent text-foreground border border-border',
1498
+ ];
1499
+
1500
+ $sizes = [
1501
+ 'sm' => 'px-2 py-0.5 text-[11px] gap-1',
1502
+ 'md' => 'px-2.5 py-1 text-xs gap-1.5',
1503
+ 'lg' => 'px-3 py-1.5 text-sm gap-2',
1504
+ ];
1505
+
1506
+ $classes = implode(' ', [
1507
+ 'inline-flex items-center gap-1.5 rounded-full font-medium leading-none whitespace-nowrap',
1508
+ '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))]',
1509
+ 'select-none shrink-0',
1510
+ $variants[$variant] ?? $variants['default'],
1511
+ $sizes[$size] ?? $sizes['md'],
1512
+ ]);
1513
+ @endphp
1514
+
1515
+ <span {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="badge">
1516
+ {{ $slot }}
1517
+ @if($dismissible)
1518
+ <button
1519
+ class="ml-0.5 inline-flex items-center justify-center rounded-full hover:bg-black/10 dark:hover:bg-white/10 {{ $size === 'sm' ? 'h-3 w-3' : ($size === 'md' ? 'h-3.5 w-3.5' : 'h-4 w-4') }}"
1520
+ onclick="this.closest('[data-ds-component=badge]').remove()"
1521
+ aria-label="Dismiss"
1522
+ >
1523
+ <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-full w-full"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1524
+ </button>
1525
+ @endif
1526
+ </span>
1527
+ `,
1528
+ "resources/views/components/ui/card.blade.php": `@props([
1529
+ 'variant' => 'default',
1530
+ 'padding' => 'compact',
1531
+ 'fullWidth' => false,
1532
+ 'as' => 'div',
1533
+ ])
1534
+
1535
+ @php
1536
+ $variants = [
1537
+ 'default' => 'bg-surface border border-border',
1538
+ 'outlined' => 'bg-transparent border border-border-strong',
1539
+ 'elevated' => 'bg-surface-raised border border-border-muted shadow-md',
1540
+ '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',
1541
+ ];
1542
+
1543
+ $classes = implode(' ', array_filter([
1544
+ 'flex flex-col rounded-md overflow-hidden text-sm text-foreground',
1545
+ $variants[$variant] ?? $variants['default'],
1546
+ $fullWidth ? 'w-full' : '',
1547
+ ]));
1548
+ @endphp
1549
+
1550
+ <{{ $as }} {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card" data-ds-padding="{{ $padding }}">
1551
+ {{ $slot }}
1552
+ </{{ $as }}>
1553
+ `,
1554
+ "resources/views/components/ui/card-header.blade.php": `@aware(['padding' => 'compact'])
1555
+
1556
+ @php
1557
+ $classes = $padding === 'comfortable'
1558
+ ? 'flex flex-col px-6 pt-6 gap-1.5'
1559
+ : 'flex flex-col px-[var(--ds-padding-card,16px)] pt-[var(--ds-padding-card,16px)] gap-1';
1560
+ @endphp
1561
+
1562
+ <div {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card-header">
1563
+ {{ $slot }}
1564
+ </div>
1565
+ `,
1566
+ "resources/views/components/ui/card-body.blade.php": `@aware(['padding' => 'compact'])
1567
+
1568
+ @php
1569
+ $classes = $padding === 'comfortable'
1570
+ ? 'flex flex-col flex-1 px-6 py-4 gap-4'
1571
+ : 'flex flex-col flex-1 px-[var(--ds-padding-card,16px)] py-3 gap-[var(--ds-gap-default,0.75rem)]';
1572
+ @endphp
1573
+
1574
+ <div {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card-body">
1575
+ {{ $slot }}
1576
+ </div>
1577
+ `,
1578
+ "resources/views/components/ui/card-footer.blade.php": `@aware(['padding' => 'compact'])
1579
+
1580
+ @php
1581
+ $classes = $padding === 'comfortable'
1582
+ ? 'flex items-center px-6 pb-6 gap-3'
1583
+ : 'flex items-center px-[var(--ds-padding-card,16px)] pb-[var(--ds-padding-card,16px)] gap-2';
1584
+ @endphp
1585
+
1586
+ <div {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="card-footer">
1587
+ {{ $slot }}
1588
+ </div>
1589
+ `,
1590
+ "resources/views/components/ui/input.blade.php": `@props([
1591
+ 'variant' => 'default',
1592
+ 'size' => 'md',
1593
+ 'disabled' => false,
1594
+ ])
1595
+
1596
+ @php
1597
+ $variants = [
1598
+ 'default' => 'border-input hover:border-border-strong focus-visible:border-border-strong',
1599
+ 'error' => 'border-danger text-foreground focus-visible:border-danger placeholder:text-input-placeholder',
1600
+ 'success' => 'border-success text-foreground focus-visible:border-success placeholder:text-input-placeholder',
1601
+ ];
1602
+
1603
+ $sizes = [
1604
+ 'sm' => 'h-8 px-2.5 text-xs',
1605
+ 'md' => 'h-[var(--ds-control-height,36px)] px-3 text-sm',
1606
+ 'lg' => 'h-10 px-3.5 text-sm',
1607
+ ];
1608
+
1609
+ $classes = implode(' ', [
1610
+ 'flex w-full text-sm leading-5 rounded-md border bg-background text-input-foreground',
1611
+ 'placeholder:text-input-placeholder',
1612
+ '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))]',
1613
+ 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
1614
+ 'disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-foreground',
1615
+ 'read-only:bg-muted read-only:cursor-default',
1616
+ $variants[$variant] ?? $variants['default'],
1617
+ $sizes[$size] ?? $sizes['md'],
1618
+ ]);
1619
+ @endphp
1620
+
1621
+ <input {{ $attributes->merge(['class' => $classes, 'disabled' => $disabled, 'type' => 'text']) }} data-ds data-ds-component="input" />
1622
+ `,
1623
+ "resources/views/components/ui/alert.blade.php": `@props([
1624
+ 'variant' => 'info',
1625
+ 'title' => null,
1626
+ 'dismissible' => false,
1627
+ ])
1628
+
1629
+ @php
1630
+ $variants = [
1631
+ 'info' => 'bg-info-muted text-info-muted-foreground border-info/20',
1632
+ 'success' => 'bg-success-muted text-success-muted-foreground border-success/20',
1633
+ 'warning' => 'bg-warning-muted text-warning-muted-foreground border-warning/20',
1634
+ 'danger' => 'bg-danger-muted text-danger-muted-foreground border-danger/20',
1635
+ 'default' => 'bg-muted text-muted-foreground border-border',
1636
+ ];
1637
+
1638
+ $iconColors = [
1639
+ 'info' => 'text-info',
1640
+ 'success' => 'text-success',
1641
+ 'warning' => 'text-warning',
1642
+ 'danger' => 'text-danger',
1643
+ 'default' => 'text-muted-foreground',
1644
+ ];
1645
+
1646
+ $iconPaths = [
1647
+ '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',
1648
+ 'success' => 'M9 12l2 2 4-4m6 2a10 10 0 11-20 0 10 10 0 0120 0z',
1649
+ 'warning' => 'M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z',
1650
+ 'danger' => 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a10 10 0 11-20 0 10 10 0 0120 0z',
1651
+ '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',
1652
+ ];
1653
+
1654
+ $classes = implode(' ', [
1655
+ 'relative flex gap-3 rounded-md p-4 text-sm leading-5 border',
1656
+ 'transition-colors duration-[var(--duration-fast,150ms)]',
1657
+ $variants[$variant] ?? $variants['info'],
1658
+ ]);
1659
+ @endphp
1660
+
1661
+ <div {{ $attributes->merge(['class' => $classes]) }} role="alert" data-ds data-ds-component="alert">
1662
+ <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'] }}">
1663
+ <path d="{{ $iconPaths[$variant] ?? $iconPaths['info'] }}"/>
1664
+ </svg>
1665
+ <div class="flex-1 space-y-1">
1666
+ @if($title)
1667
+ <p class="font-medium leading-5">{{ $title }}</p>
1668
+ @endif
1669
+ <div class="text-sm leading-5">{{ $slot }}</div>
1670
+ </div>
1671
+ @if($dismissible)
1672
+ <button
1673
+ class="absolute top-3 right-3 inline-flex items-center justify-center rounded-md h-6 w-6 hover:bg-black/10 dark:hover:bg-white/10"
1674
+ onclick="this.closest('[data-ds-component=alert]').remove()"
1675
+ aria-label="Dismiss"
1676
+ >
1677
+ <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"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1678
+ </button>
1679
+ @endif
1680
+ </div>
1681
+ `,
1682
+ "resources/views/components/ui/heading.blade.php": `@props([
1683
+ 'level' => 1,
1684
+ 'color' => 'default',
1685
+ ])
1686
+
1687
+ @php
1688
+ $levels = [
1689
+ 1 => 'text-[30px] leading-[36px] font-bold tracking-tight',
1690
+ 2 => 'text-[24px] leading-[32px] font-semibold tracking-tight',
1691
+ 3 => 'text-[20px] leading-[28px] font-semibold tracking-normal',
1692
+ 4 => 'text-[18px] leading-[28px] font-medium tracking-normal',
1693
+ ];
1694
+
1695
+ $colors = [
1696
+ 'default' => 'text-foreground',
1697
+ 'foreground' => 'text-foreground',
1698
+ 'muted' => 'text-muted-foreground',
1699
+ 'primary' => 'text-primary',
1700
+ ];
1701
+
1702
+ $classes = implode(' ', [
1703
+ $levels[$level] ?? $levels[1],
1704
+ $colors[$color] ?? $colors['default'],
1705
+ ]);
1706
+
1707
+ $tag = 'h' . min(max((int)$level, 1), 6);
1708
+ @endphp
1709
+
1710
+ <{{ $tag }} {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="heading">
1711
+ {{ $slot }}
1712
+ </{{ $tag }}>
1713
+ `,
1714
+ "resources/views/components/ui/text.blade.php": `@props([
1715
+ 'variant' => 'body',
1716
+ 'color' => 'default',
1717
+ 'as' => 'p',
1718
+ ])
1719
+
1720
+ @php
1721
+ $variants = [
1722
+ 'body' => 'text-[16px] leading-[24px] font-normal tracking-normal',
1723
+ 'bodySm' => 'text-[14px] leading-[20px] font-normal tracking-normal',
1724
+ 'caption' => 'text-[12px] leading-[16px] font-normal tracking-wide text-muted-foreground',
1725
+ 'label' => 'text-[14px] leading-[20px] font-medium tracking-normal',
1726
+ 'overline' => 'text-[12px] leading-[16px] font-semibold tracking-wider uppercase text-muted-foreground',
1727
+ 'code' => 'text-[14px] leading-[20px] font-normal tracking-normal font-mono',
1728
+ ];
1729
+
1730
+ $colors = [
1731
+ 'default' => 'text-foreground',
1732
+ 'foreground' => 'text-foreground',
1733
+ 'muted' => 'text-muted-foreground',
1734
+ 'primary' => 'text-primary',
1735
+ 'success' => 'text-success',
1736
+ 'warning' => 'text-warning',
1737
+ 'danger' => 'text-danger',
1738
+ 'info' => 'text-info',
1739
+ ];
1740
+
1741
+ $classes = implode(' ', [
1742
+ $variants[$variant] ?? $variants['body'],
1743
+ $colors[$color] ?? $colors['default'],
1744
+ ]);
1745
+ @endphp
1746
+
1747
+ <{{ $as }} {{ $attributes->merge(['class' => $classes]) }} data-ds data-ds-component="text">
1748
+ {{ $slot }}
1749
+ </{{ $as }}>
1750
+ `,
1751
+ },
1752
+ },
1753
+ };
1754
+
1755
+ // ---------------------------------------------------------------------------
1756
+ // Starter kit scaffolding command
1757
+ // ---------------------------------------------------------------------------
1758
+
1759
+ async function cmdInitWithTemplate(positional, flags) {
1760
+ log();
1761
+ log(` ${c("bold", "Unified UI")} ${c("dim", "— Create a new project")}`);
1762
+ log();
1763
+
1764
+ // 1. Pick framework
1765
+ let framework;
1766
+ const templateFlag = typeof flags.template === "string" ? flags.template : null;
1767
+
1768
+ if (templateFlag) {
1769
+ framework = FRAMEWORKS.find((f) => f.name === templateFlag);
1770
+ if (!framework) {
1771
+ logError(
1772
+ `Unknown template: "${templateFlag}". Available: ${FRAMEWORKS.map((f) => f.name).join(", ")}`,
1773
+ );
1774
+ process.exit(1);
1775
+ }
1776
+ } else {
1777
+ framework = await promptSelect(
1778
+ "Which framework do you want to use?",
1779
+ FRAMEWORKS,
1780
+ );
1781
+ if (!framework) {
1782
+ logError(
1783
+ `Invalid selection. Available: ${FRAMEWORKS.map((f) => f.name).join(", ")}`,
1784
+ );
1785
+ process.exit(1);
1786
+ }
1787
+ log();
1788
+ }
1789
+
1790
+ logStep("✓", `Framework: ${c("cyan", framework.label)}`);
1791
+
1792
+ // 2. Get project name
1793
+ let projectName = positional[0];
1794
+ if (!projectName) {
1795
+ projectName = await promptText("Project name:", "my-unified-app");
1796
+ }
1797
+
1798
+ const targetDir = resolve(process.cwd(), projectName);
1799
+ logStep("✓", `Project: ${c("cyan", projectName)}`);
1800
+ log();
1801
+
1802
+ // 3. Run the official scaffolding command
1803
+ logStep("📦", `Scaffolding ${c("cyan", framework.label)} project...`);
1804
+ log();
1805
+
1806
+ const scaffoldCmd = framework.scaffoldCmd(projectName);
1807
+ logStep(" ", c("dim", `> ${scaffoldCmd}`));
1808
+ log();
1809
+
1810
+ const scaffoldOk = runCmd(scaffoldCmd, process.cwd());
1811
+ if (!scaffoldOk) {
1812
+ logError(
1813
+ `Scaffolding failed. Make sure the required tool is installed.\n` +
1814
+ (framework.name === "laravel-blade"
1815
+ ? ` Requires: ${c("cyan", "composer")} (https://getcomposer.org)\n`
1816
+ : ` Requires: ${c("cyan", "node >= 20")} and ${c("cyan", "npm")}\n`),
1817
+ );
1818
+ process.exit(1);
1819
+ }
1820
+
1821
+ if (!existsSync(targetDir)) {
1822
+ logError(`Expected directory "${projectName}" was not created by the scaffolding tool.`);
1823
+ process.exit(1);
1824
+ }
1825
+
1826
+ log();
1827
+ logStep("✓", c("green", `${framework.label} project scaffolded`));
1828
+
1829
+ // 4. Install Unified UI + extra deps
1830
+ logStep("📦", "Installing Unified UI design system...");
1831
+
1832
+ const pm = await detectPackageManager(targetDir);
1833
+ const allDeps = [...framework.deps];
1834
+ const allDevDeps = [...framework.devDeps];
1835
+
1836
+ if (allDeps.length > 0) {
1837
+ const depCmd = getInstallCommand(pm, allDeps);
1838
+ logStep(" ", c("dim", depCmd));
1839
+ runCmd(depCmd, targetDir, "pipe");
1840
+ }
1841
+
1842
+ if (allDevDeps.length > 0) {
1843
+ const devDepCmd = getInstallCommand(pm, allDevDeps).replace(" add ", " add -D ").replace(" install ", " install -D ");
1844
+ logStep(" ", c("dim", devDepCmd));
1845
+ runCmd(devDepCmd, targetDir, "pipe");
1846
+ }
1847
+
1848
+ logStep("✓", c("green", "Dependencies installed"));
1849
+
1850
+ // 5. Apply overlay files
1851
+ log();
1852
+ logStep("✏️ ", "Applying Unified UI starter files...");
1853
+
1854
+ const overlay = OVERLAYS[framework.name];
1855
+ if (overlay) {
1856
+ for (const [filePath, content] of Object.entries(overlay.files)) {
1857
+ const fullPath = join(targetDir, filePath);
1858
+ writeOverlay(fullPath, content);
1859
+ logStep("✓", c("green", filePath));
1860
+ }
1861
+ }
1862
+
1863
+ // 6. Git commit (if git was initialized by the scaffold tool)
1864
+ const gitDir = join(targetDir, ".git");
1865
+ if (existsSync(gitDir)) {
1866
+ runCmd("git add -A", targetDir, "pipe");
1867
+ runCmd('git commit -m "chore: add Unified UI design system" --no-verify', targetDir, "pipe");
1868
+ logStep("✓", c("green", "Committed Unified UI changes"));
1869
+ } else {
1870
+ // Initialize git if it wasn't done by the scaffold tool
1871
+ if (runCmd("git init", targetDir, "pipe")) {
1872
+ runCmd("git add -A", targetDir, "pipe");
1873
+ runCmd('git commit -m "chore: initial commit with Unified UI" --no-verify', targetDir, "pipe");
1874
+ logStep("✓", c("green", "Initialized git repository"));
1875
+ }
1876
+ }
1877
+
1878
+ // 7. Print success
1879
+ log();
1880
+ logStep("🎉", c("green", `Project "${projectName}" is ready!`));
1881
+ log();
1882
+ log(` ${c("dim", "Next steps:")}`);
1883
+ log();
1884
+ log(` ${c("cyan", `cd ${projectName}`)}`);
1885
+
1886
+ if (framework.name === "laravel-blade") {
1887
+ log(` ${c("cyan", "npm run dev")}`);
1888
+ log(` ${c("cyan", "php artisan serve")}`);
1889
+ } else {
1890
+ log(` ${c("cyan", "npm run dev")}`);
1891
+ }
1892
+
1893
+ log();
1894
+
1895
+ if (framework.name === "vuejs" || framework.name === "laravel-blade") {
1896
+ log(` ${c("dim", "Note: This template includes design tokens (CSS variables + Tailwind")}`);
1897
+ log(` ${c("dim", "utilities) only. React components are not available in this framework.")}`);
1898
+ log(` ${c("dim", "See: https://www.unified-ui.space/docs/tokens")}`);
1899
+ log();
1900
+ } else {
1901
+ log(` ${c("dim", "Start adding components:")}`);
1902
+ log(` ${c("cyan", "npx @work-rjkashyap/unified-ui add button card badge")}`);
1903
+ log();
1904
+ }
1905
+ }
1906
+
1907
+ async function cmdInit(positional = [], flags = {}) {
1908
+ // If --template flag is present, run the full scaffolding flow
1909
+ if (flags.template) {
1910
+ return cmdInitWithTemplate(positional, flags);
1911
+ }
1912
+
310
1913
  log();
311
1914
  log(` ${c("bold", "Unified UI")} ${c("dim", "— Initialize project")}`);
312
1915
  log();
@@ -617,20 +2220,35 @@ function cmdHelp() {
617
2220
  log(` ${c("cyan", "npx @work-rjkashyap/unified-ui")} ${c("green", "<command>")} [options]`);
618
2221
  log();
619
2222
  log(" Commands:");
620
- log(` ${c("green", "init")} Initialize project config & base utils`);
2223
+ log(` ${c("green", "init")} Initialize existing project (copy-paste mode)`);
2224
+ log(` ${c("green", "init")} -t <framework> Scaffold a new project with full setup`);
621
2225
  log(` ${c("green", "add")} <component...> Add component(s) with dependencies`);
622
2226
  log(` ${c("green", "add")} --all Add all components`);
623
2227
  log(` ${c("green", "list")} List all available components`);
624
2228
  log(` ${c("green", "diff")} <component...> Compare local files with registry`);
625
2229
  log(` ${c("green", "help")} Show this help message`);
626
2230
  log();
2231
+ log(" Templates (for init -t):");
2232
+ log(` ${c("cyan", "vite-react")} Vite + React 19 SPA with full component library`);
2233
+ log(` ${c("cyan", "nextjs")} Next.js App Router with SSR + full component library`);
2234
+ log(` ${c("cyan", "vuejs")} Vue 3 + Vite with UI components & Tailwind theme`);
2235
+ log(` ${c("cyan", "laravel-blade")} Laravel with Blade UI components & Tailwind theme`);
2236
+ log();
627
2237
  log(" Options:");
2238
+ log(` ${c("yellow", "--template, -t")} Framework template (with 'init')`);
628
2239
  log(` ${c("yellow", "--yes, -y")} Skip confirmation prompts`);
629
2240
  log(` ${c("yellow", "--overwrite")} Overwrite existing files`);
630
2241
  log(` ${c("yellow", "--all")} Add all components (with 'add')`);
631
2242
  log();
632
2243
  log(" Examples:");
633
- log(` ${c("dim", "# Initialize project")} `);
2244
+ log(` ${c("dim", "# Scaffold a new project (interactive)")} `);
2245
+ log(` npx @work-rjkashyap/unified-ui init -t`);
2246
+ log();
2247
+ log(` ${c("dim", "# Scaffold with specific framework")}`);
2248
+ log(` npx @work-rjkashyap/unified-ui init -t nextjs my-app`);
2249
+ log(` npx @work-rjkashyap/unified-ui init --template vite-react my-app`);
2250
+ log();
2251
+ log(` ${c("dim", "# Initialize existing project (copy-paste mode)")} `);
634
2252
  log(` npx @work-rjkashyap/unified-ui init`);
635
2253
  log();
636
2254
  log(` ${c("dim", "# Add specific components")}`);
@@ -664,6 +2282,19 @@ function parseArgs(argv) {
664
2282
  flags.overwrite = true;
665
2283
  } else if (arg === "--all") {
666
2284
  flags.all = true;
2285
+ } else if (arg === "--template" || arg === "-t") {
2286
+ // Next arg is the template name (or true if none given)
2287
+ const next = args[i + 1];
2288
+ if (next && !next.startsWith("-")) {
2289
+ flags.template = next;
2290
+ i++;
2291
+ } else {
2292
+ flags.template = true; // Will trigger interactive picker
2293
+ }
2294
+ } else if (arg.startsWith("--template=")) {
2295
+ flags.template = arg.split("=")[1];
2296
+ } else if (arg.startsWith("-t=")) {
2297
+ flags.template = arg.split("=")[1];
667
2298
  } else if (arg.startsWith("--registry=")) {
668
2299
  flags.registryUrl = arg.split("=")[1];
669
2300
  } else if (!arg.startsWith("-")) {
@@ -691,7 +2322,7 @@ async function main() {
691
2322
 
692
2323
  switch (command) {
693
2324
  case "init":
694
- await cmdInit();
2325
+ await cmdInit(positional, flags);
695
2326
  break;
696
2327
  case "add":
697
2328
  await cmdAdd(positional, flags);