@work-rjkashyap/unified-ui 0.2.4 → 0.3.1

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