react-principles-cli 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,4440 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+ var import_picocolors3 = __toESM(require("picocolors"));
29
+
30
+ // src/commands/init.ts
31
+ var import_fs4 = require("fs");
32
+ var import_path4 = require("path");
33
+ var import_prompts = __toESM(require("prompts"));
34
+ var import_picocolors = __toESM(require("picocolors"));
35
+
36
+ // src/utils/fs.ts
37
+ var import_fs = require("fs");
38
+ var import_path = require("path");
39
+ var CONFIG_FILE = "components.json";
40
+ var DEFAULT_CONFIG = {
41
+ framework: "next",
42
+ rsc: true,
43
+ tsx: true,
44
+ componentsDir: "src/components/ui",
45
+ hooksDir: "src/hooks",
46
+ libDir: "src/lib",
47
+ aliases: {
48
+ components: "@/components/ui",
49
+ hooks: "@/hooks",
50
+ lib: "@/lib"
51
+ }
52
+ };
53
+ function readConfig(cwd) {
54
+ const path = (0, import_path.join)(cwd, CONFIG_FILE);
55
+ if (!(0, import_fs.existsSync)(path)) return null;
56
+ try {
57
+ return JSON.parse((0, import_fs.readFileSync)(path, "utf8"));
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ function writeConfig(config, cwd) {
63
+ (0, import_fs.writeFileSync)((0, import_path.join)(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
64
+ }
65
+ function getDefaultConfig() {
66
+ return structuredClone(DEFAULT_CONFIG);
67
+ }
68
+ function detectTsconfigAliases(cwd) {
69
+ const candidates = ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"];
70
+ for (const file of candidates) {
71
+ const tsConfigPath = (0, import_path.join)(cwd, file);
72
+ if (!(0, import_fs.existsSync)(tsConfigPath)) continue;
73
+ try {
74
+ const raw = (0, import_fs.readFileSync)(tsConfigPath, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
75
+ const tsConfig = JSON.parse(raw);
76
+ const paths = tsConfig.compilerOptions?.paths ?? {};
77
+ const aliases = {};
78
+ for (const [alias] of Object.entries(paths)) {
79
+ const clean = alias.replace(/\/\*$/, "");
80
+ if (clean === "@") {
81
+ aliases.components = `${clean}/components/ui`;
82
+ aliases.hooks = `${clean}/hooks`;
83
+ aliases.lib = `${clean}/lib`;
84
+ }
85
+ }
86
+ return aliases;
87
+ } catch {
88
+ }
89
+ }
90
+ return {};
91
+ }
92
+ function detectFramework(cwd) {
93
+ const pkgPath = (0, import_path.join)(cwd, "package.json");
94
+ if (!(0, import_fs.existsSync)(pkgPath)) return "other";
95
+ try {
96
+ const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf8"));
97
+ const deps = {
98
+ ...pkg.dependencies,
99
+ ...pkg.devDependencies
100
+ };
101
+ if (deps["next"]) return "next";
102
+ if (deps["remix"] || deps["@remix-run/react"]) return "remix";
103
+ if (deps["vite"] || deps["@vitejs/plugin-react"]) return "vite";
104
+ return "other";
105
+ } catch {
106
+ return "other";
107
+ }
108
+ }
109
+ function resolveOutputPath(outputFile, target, config, cwd) {
110
+ const base = target === "components" ? config.componentsDir : target === "hooks" ? config.hooksDir : config.libDir;
111
+ return (0, import_path.join)(cwd, base, outputFile);
112
+ }
113
+ function writeFile(content, outputPath) {
114
+ const dir = (0, import_path.dirname)(outputPath);
115
+ if (!(0, import_fs.existsSync)(dir)) {
116
+ (0, import_fs.mkdirSync)(dir, { recursive: true });
117
+ }
118
+ if ((0, import_fs.existsSync)(outputPath)) return false;
119
+ (0, import_fs.writeFileSync)(outputPath, content, "utf8");
120
+ return true;
121
+ }
122
+
123
+ // src/registry/templates.ts
124
+ var TEMPLATES = {
125
+ "utils": `import { type ClassValue, clsx } from "clsx";
126
+ import { twMerge } from "tailwind-merge";
127
+
128
+ /**
129
+ * Merges class names using clsx and resolves Tailwind CSS conflicts
130
+ * with tailwind-merge. Accepts the same arguments as clsx.
131
+ *
132
+ * @example
133
+ * cn("px-2 py-1", "px-4") // => "px-4 py-1"
134
+ * cn("text-red-500", condition && "text-blue-500")
135
+ */
136
+ export function cn(...inputs: ClassValue[]): string {
137
+ return twMerge(clsx(inputs));
138
+ }`,
139
+ "use-animated-mount": `"use client";
140
+
141
+ import { useState, useEffect } from "react";
142
+
143
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
144
+
145
+ export interface AnimatedMountResult {
146
+ /** Whether the component should be in the DOM (stays true during exit animation) */
147
+ mounted: boolean;
148
+ /** Whether the "open" state is active \u2014 drives CSS enter/exit classes */
149
+ visible: boolean;
150
+ }
151
+
152
+ // \u2500\u2500\u2500 Hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
153
+
154
+ /**
155
+ * Manages mount/unmount timing for animated overlays.
156
+ *
157
+ * - \`mounted\` controls DOM presence. Stays \`true\` during the exit animation
158
+ * so the element can animate out before being removed.
159
+ * - \`visible\` drives CSS classes. Flip \`visible\` immediately on open/close;
160
+ * flip \`mounted\` only after the exit \`duration\` has elapsed.
161
+ *
162
+ * @example
163
+ * const { mounted, visible } = useAnimatedMount(open, 200);
164
+ * if (!mounted) return null;
165
+ * return (
166
+ * <div className={visible ? "animate-in fade-in" : "animate-out fade-out"}>
167
+ * {children}
168
+ * </div>
169
+ * );
170
+ */
171
+ export function useAnimatedMount(open: boolean, duration = 200): AnimatedMountResult {
172
+ const [mounted, setMounted] = useState(open);
173
+ const [visible, setVisible] = useState(false);
174
+
175
+ useEffect(() => {
176
+ if (open) {
177
+ setMounted(true);
178
+ // Double rAF ensures the element is painted before adding the visible
179
+ // class, guaranteeing the CSS transition fires on the first frame.
180
+ const raf = requestAnimationFrame(() => {
181
+ requestAnimationFrame(() => setVisible(true));
182
+ });
183
+ return () => cancelAnimationFrame(raf);
184
+ } else {
185
+ setVisible(false);
186
+ const t = setTimeout(() => setMounted(false), duration);
187
+ return () => clearTimeout(t);
188
+ }
189
+ }, [open, duration]);
190
+
191
+ return { mounted, visible };
192
+ }`,
193
+ "Accordion": `import {
194
+ createContext,
195
+ useContext,
196
+ useState,
197
+ type HTMLAttributes,
198
+ type ButtonHTMLAttributes,
199
+ type ReactNode,
200
+ } from "react";
201
+ import { cn } from "@/lib/utils";
202
+
203
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
204
+
205
+ export type AccordionType = "single" | "multiple";
206
+
207
+ export interface AccordionProps {
208
+ type?: AccordionType;
209
+ defaultValue?: string | string[];
210
+ value?: string | string[];
211
+ onChange?: (value: string | string[]) => void;
212
+ collapsible?: boolean;
213
+ children: ReactNode;
214
+ className?: string;
215
+ }
216
+
217
+ export interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {
218
+ value: string;
219
+ children: ReactNode;
220
+ }
221
+
222
+ export interface AccordionTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
223
+ children: ReactNode;
224
+ }
225
+
226
+ export interface AccordionContentProps extends HTMLAttributes<HTMLDivElement> {
227
+ children: ReactNode;
228
+ }
229
+
230
+ // \u2500\u2500\u2500 Context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
231
+
232
+ interface AccordionCtx {
233
+ isOpen: (value: string) => boolean;
234
+ toggle: (value: string) => void;
235
+ }
236
+
237
+ interface ItemCtx {
238
+ value: string;
239
+ open: boolean;
240
+ }
241
+
242
+ const AccordionContext = createContext<AccordionCtx | null>(null);
243
+ const ItemContext = createContext<ItemCtx | null>(null);
244
+
245
+ function useAccordion() {
246
+ const ctx = useContext(AccordionContext);
247
+ if (!ctx) throw new Error("AccordionTrigger/Content must be inside <AccordionItem>");
248
+ return ctx;
249
+ }
250
+
251
+ function useItem() {
252
+ const ctx = useContext(ItemContext);
253
+ if (!ctx) throw new Error("AccordionTrigger/Content must be inside <AccordionItem>");
254
+ return ctx;
255
+ }
256
+
257
+ // \u2500\u2500\u2500 Components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
258
+
259
+ export function Accordion({
260
+ type = "single",
261
+ defaultValue,
262
+ value: controlledValue,
263
+ onChange,
264
+ collapsible = true,
265
+ children,
266
+ className,
267
+ }: AccordionProps) {
268
+ const toSet = (v?: string | string[]): Set<string> => {
269
+ if (!v) return new Set();
270
+ return new Set(Array.isArray(v) ? v : [v]);
271
+ };
272
+
273
+ const [internal, setInternal] = useState<Set<string>>(() => toSet(defaultValue));
274
+ const isControlled = controlledValue !== undefined;
275
+ const active = isControlled ? toSet(controlledValue) : internal;
276
+
277
+ const toggle = (val: string) => {
278
+ let next: Set<string>;
279
+
280
+ if (type === "single") {
281
+ if (active.has(val)) {
282
+ next = collapsible ? new Set() : new Set([val]);
283
+ } else {
284
+ next = new Set([val]);
285
+ }
286
+ } else {
287
+ next = new Set(active);
288
+ if (next.has(val)) { next.delete(val); } else { next.add(val); }
289
+ }
290
+
291
+ if (!isControlled) setInternal(next);
292
+
293
+ if (onChange) {
294
+ const arr = [...next];
295
+ onChange(type === "single" ? (arr[0] ?? "") : arr);
296
+ }
297
+ };
298
+
299
+ const isOpen = (val: string) => active.has(val);
300
+
301
+ return (
302
+ <AccordionContext.Provider value={{ isOpen, toggle }}>
303
+ <div className={cn("w-full divide-y divide-slate-200 dark:divide-[#1f2937] rounded-xl border border-slate-200 dark:border-[#1f2937] overflow-hidden", className)}>
304
+ {children}
305
+ </div>
306
+ </AccordionContext.Provider>
307
+ );
308
+ }
309
+
310
+ Accordion.Item = function AccordionItem({ value, children, className, ...props }: AccordionItemProps) {
311
+ const { isOpen } = useAccordion();
312
+ const open = isOpen(value);
313
+
314
+ return (
315
+ <ItemContext.Provider value={{ value, open }}>
316
+ <div className={cn("bg-white dark:bg-[#161b22]", className)} {...props}>
317
+ {children}
318
+ </div>
319
+ </ItemContext.Provider>
320
+ );
321
+ }
322
+
323
+ Accordion.Trigger = function AccordionTrigger({ children, className, ...props }: AccordionTriggerProps) {
324
+ const { toggle } = useAccordion();
325
+ const { value, open } = useItem();
326
+
327
+ return (
328
+ <button
329
+ type="button"
330
+ aria-expanded={open}
331
+ onClick={() => toggle(value)}
332
+ className={cn(
333
+ "flex w-full items-center justify-between px-5 py-4 text-left text-sm font-medium",
334
+ "text-slate-900 dark:text-white",
335
+ "hover:bg-slate-50 dark:hover:bg-[#1f2937] transition-colors",
336
+ "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40",
337
+ className
338
+ )}
339
+ {...props}
340
+ >
341
+ <span>{children}</span>
342
+ <svg
343
+ className={cn("h-4 w-4 shrink-0 text-slate-400 transition-transform duration-200", open && "rotate-180")}
344
+ viewBox="0 0 16 16"
345
+ fill="none"
346
+ >
347
+ <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
348
+ </svg>
349
+ </button>
350
+ );
351
+ }
352
+
353
+ Accordion.Content = function AccordionContent({ children, className, ...props }: AccordionContentProps) {
354
+ const { open } = useItem();
355
+
356
+ return (
357
+ <div
358
+ style={{
359
+ display: "grid",
360
+ gridTemplateRows: open ? "1fr" : "0fr",
361
+ transition: "grid-template-rows 0.2s ease",
362
+ }}
363
+ >
364
+ <div style={{ overflow: "hidden" }}>
365
+ <div
366
+ className={cn("px-5 pb-4 text-sm text-slate-600 dark:text-slate-400 leading-relaxed", className)}
367
+ {...props}
368
+ >
369
+ {children}
370
+ </div>
371
+ </div>
372
+ </div>
373
+ );
374
+ }`,
375
+ "Alert": `import type { ButtonHTMLAttributes, HTMLAttributes } from "react";
376
+ import { cn } from "@/lib/utils";
377
+
378
+ export type AlertVariant = "default" | "success" | "warning" | "error" | "info";
379
+
380
+ export interface AlertProps extends HTMLAttributes<HTMLDivElement> {
381
+ variant?: AlertVariant;
382
+ }
383
+
384
+ const VARIANT_CLASSES: Record<AlertVariant, string> = {
385
+ default: "border-slate-200 bg-white dark:border-[#1f2937] dark:bg-[#161b22]",
386
+ success: "border-green-300 bg-green-50 dark:border-green-900 dark:bg-green-950/30",
387
+ warning: "border-amber-300 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/30",
388
+ error: "border-red-300 bg-red-50 dark:border-red-900 dark:bg-red-950/30",
389
+ info: "border-blue-300 bg-blue-50 dark:border-blue-900 dark:bg-blue-950/30",
390
+ };
391
+
392
+ export function Alert({ variant = "default", className, ...props }: AlertProps) {
393
+ return (
394
+ <div
395
+ role="alert"
396
+ className={cn("rounded-xl border p-4", VARIANT_CLASSES[variant], className)}
397
+ {...props}
398
+ />
399
+ );
400
+ }
401
+
402
+ Alert.Title = function AlertTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
403
+ return (
404
+ <h4
405
+ className={cn("text-sm font-semibold text-slate-900 dark:text-white", className)}
406
+ {...props}
407
+ />
408
+ );
409
+ }
410
+
411
+ Alert.Description = function AlertDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
412
+ return (
413
+ <p
414
+ className={cn("mt-1 text-xs leading-relaxed text-slate-600 dark:text-slate-400", className)}
415
+ {...props}
416
+ />
417
+ );
418
+ }
419
+
420
+ Alert.Footer = function AlertFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
421
+ return (
422
+ <div
423
+ className={cn("mt-3 flex items-center gap-2", className)}
424
+ {...props}
425
+ />
426
+ );
427
+ }
428
+
429
+ Alert.Action = function AlertAction({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
430
+ return (
431
+ <button
432
+ type="button"
433
+ className={cn(
434
+ "rounded-md bg-primary px-2.5 py-1.5 text-xs font-medium text-white transition-opacity hover:opacity-90",
435
+ className
436
+ )}
437
+ {...props}
438
+ />
439
+ );
440
+ }`,
441
+ "AlertDialog": `import { useEffect } from "react";
442
+ import { createPortal } from "react-dom";
443
+ import { cn } from "@/lib/utils";
444
+ import { useAnimatedMount } from "@/hooks/use-animated-mount";
445
+
446
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
447
+
448
+ export type AlertDialogVariant = "destructive" | "warning" | "default";
449
+
450
+ export interface AlertDialogProps {
451
+ open: boolean;
452
+ onClose: () => void;
453
+ onConfirm: () => void;
454
+ title: string;
455
+ description: string;
456
+ cancelLabel?: string;
457
+ confirmLabel?: string;
458
+ variant?: AlertDialogVariant;
459
+ isLoading?: boolean;
460
+ }
461
+
462
+ // \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
463
+
464
+ const CONFIRM_CLASSES: Record<AlertDialogVariant, string> = {
465
+ destructive: "bg-red-500 hover:bg-red-600 text-white focus-visible:ring-red-400/40",
466
+ warning: "bg-amber-500 hover:bg-amber-600 text-white focus-visible:ring-amber-400/40",
467
+ default: "bg-primary hover:bg-primary/90 text-white focus-visible:ring-primary/40",
468
+ };
469
+
470
+ const ICON_BG: Record<AlertDialogVariant, string> = {
471
+ destructive: "bg-red-100 dark:bg-red-900/30 text-red-500",
472
+ warning: "bg-amber-100 dark:bg-amber-900/30 text-amber-500",
473
+ default: "bg-primary/10 text-primary",
474
+ };
475
+
476
+ // \u2500\u2500\u2500 Icons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
477
+
478
+ function VariantIcon({ variant }: { variant: AlertDialogVariant }) {
479
+ if (variant === "destructive") {
480
+ return (
481
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="none">
482
+ <path d="M10 2a8 8 0 1 0 0 16A8 8 0 0 0 10 2zm0 5v4m0 2.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
483
+ </svg>
484
+ );
485
+ }
486
+ if (variant === "warning") {
487
+ return (
488
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="none">
489
+ <path d="M8.485 3.495a1.75 1.75 0 0 1 3.03 0l6.28 10.875A1.75 1.75 0 0 1 16.28 17H3.72a1.75 1.75 0 0 1-1.515-2.63L8.485 3.495zM10 8v3m0 2.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
490
+ </svg>
491
+ );
492
+ }
493
+ return (
494
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="none">
495
+ <circle cx="10" cy="10" r="8" stroke="currentColor" strokeWidth="1.5" />
496
+ <path d="M10 6v4m0 2.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
497
+ </svg>
498
+ );
499
+ }
500
+
501
+ // \u2500\u2500\u2500 Spinner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
502
+
503
+ function Spinner() {
504
+ return (
505
+ <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none">
506
+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeOpacity="0.25" strokeWidth="2" />
507
+ <path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
508
+ </svg>
509
+ );
510
+ }
511
+
512
+ // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
513
+
514
+ export function AlertDialog({
515
+ open,
516
+ onClose,
517
+ onConfirm,
518
+ title,
519
+ description,
520
+ cancelLabel = "Cancel",
521
+ confirmLabel = "Confirm",
522
+ variant = "default",
523
+ isLoading = false,
524
+ }: AlertDialogProps) {
525
+ const { mounted, visible } = useAnimatedMount(open, 200);
526
+
527
+ // Scroll lock only \u2014 no Escape dismiss (by design)
528
+ useEffect(() => {
529
+ if (!open) return;
530
+ document.body.style.overflow = "hidden";
531
+ return () => { document.body.style.overflow = ""; };
532
+ }, [open]);
533
+
534
+ if (!mounted) return null;
535
+
536
+ const panel = (
537
+ // Backdrop \u2014 clicking does NOT close (intentional)
538
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
539
+ <div
540
+ className={cn(
541
+ "absolute inset-0 bg-black/50 backdrop-blur-xs transition-opacity duration-200",
542
+ visible ? "opacity-100" : "opacity-0"
543
+ )}
544
+ />
545
+
546
+ <div
547
+ role="alertdialog"
548
+ aria-modal="true"
549
+ aria-labelledby="alert-title"
550
+ aria-describedby="alert-desc"
551
+ className={cn(
552
+ "relative w-full max-w-md rounded-2xl bg-white dark:bg-[#161b22] border border-slate-200 dark:border-[#1f2937] shadow-2xl shadow-black/20",
553
+ "transition-all duration-200",
554
+ visible ? "opacity-100 scale-100" : "opacity-0 scale-95"
555
+ )}
556
+ >
557
+ <div className="p-6">
558
+ {/* Icon + Title */}
559
+ <div className="flex items-start gap-4">
560
+ <div className={cn("mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-full", ICON_BG[variant])}>
561
+ <VariantIcon variant={variant} />
562
+ </div>
563
+ <div className="flex-1 min-w-0">
564
+ <h2 id="alert-title" className="text-base font-semibold text-slate-900 dark:text-white leading-snug">
565
+ {title}
566
+ </h2>
567
+ <p id="alert-desc" className="mt-1.5 text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
568
+ {description}
569
+ </p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+
574
+ {/* Footer */}
575
+ <div className="px-6 pb-6 flex items-center justify-end gap-3">
576
+ <button
577
+ onClick={onClose}
578
+ disabled={isLoading}
579
+ className="px-4 py-2 rounded-lg text-sm font-medium text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-[#1f2937] hover:bg-slate-50 dark:hover:bg-[#1f2937] disabled:opacity-50 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-primary/40"
580
+ >
581
+ {cancelLabel}
582
+ </button>
583
+ <button
584
+ onClick={onConfirm}
585
+ disabled={isLoading}
586
+ className={cn(
587
+ "inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-70 transition-colors focus-visible:outline-hidden focus-visible:ring-2",
588
+ CONFIRM_CLASSES[variant]
589
+ )}
590
+ >
591
+ {isLoading && <Spinner />}
592
+ {confirmLabel}
593
+ </button>
594
+ </div>
595
+ </div>
596
+ </div>
597
+ );
598
+
599
+ return createPortal(panel, document.body);
600
+ }`,
601
+ "Avatar": `"use client";
602
+ /* eslint-disable @next/next/no-img-element */
603
+
604
+ import { createContext, useContext, useState, type ImgHTMLAttributes, type ReactNode } from "react";
605
+ import { cn } from "@/lib/utils";
606
+
607
+ export type AvatarSize = "sm" | "md" | "lg" | "xl";
608
+
609
+ export interface AvatarProps {
610
+ size?: AvatarSize;
611
+ className?: string;
612
+ children?: ReactNode;
613
+ }
614
+
615
+ interface AvatarContextValue {
616
+ hasImageError: boolean;
617
+ setHasImageError: (value: boolean) => void;
618
+ }
619
+
620
+ const AvatarContext = createContext<AvatarContextValue | null>(null);
621
+
622
+ function useAvatarContext() {
623
+ const context = useContext(AvatarContext);
624
+ if (!context) throw new Error("Avatar sub-components must be used inside <Avatar>");
625
+ return context;
626
+ }
627
+
628
+ const SIZE_CLASSES: Record<AvatarSize, string> = {
629
+ sm: "h-8 w-8 text-xs",
630
+ md: "h-10 w-10 text-sm",
631
+ lg: "h-14 w-14 text-base",
632
+ xl: "h-20 w-20 text-lg",
633
+ };
634
+
635
+ export function Avatar({ size = "md", className, children }: AvatarProps) {
636
+ const [hasImageError, setHasImageError] = useState(false);
637
+
638
+ return (
639
+ <AvatarContext.Provider value={{ hasImageError, setHasImageError }}>
640
+ <span
641
+ className={cn(
642
+ "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-slate-100 text-slate-700 dark:bg-[#1f2937] dark:text-slate-200",
643
+ SIZE_CLASSES[size],
644
+ className
645
+ )}
646
+ >
647
+ {children}
648
+ </span>
649
+ </AvatarContext.Provider>
650
+ );
651
+ }
652
+
653
+ Avatar.Image = function AvatarImage({ className, onError, alt, ...props }: ImgHTMLAttributes<HTMLImageElement>) {
654
+ const { hasImageError, setHasImageError } = useAvatarContext();
655
+
656
+ if (hasImageError) return null;
657
+
658
+ return (
659
+ <img
660
+ className={cn("h-full w-full object-cover", className)}
661
+ alt={alt ?? ""}
662
+ onError={(event) => {
663
+ setHasImageError(true);
664
+ onError?.(event);
665
+ }}
666
+ {...props}
667
+ />
668
+ );
669
+ }
670
+
671
+ Avatar.Fallback = function AvatarFallback({ className, children }: { className?: string; children: ReactNode }) {
672
+ const { hasImageError } = useAvatarContext();
673
+ if (!hasImageError) return null;
674
+
675
+ return <span className={cn("font-semibold uppercase", className)}>{children}</span>;
676
+ }`,
677
+ "Badge": `import type { ReactNode } from "react";
678
+ import { cn } from "@/lib/utils";
679
+
680
+ export type BadgeVariant = "default" | "success" | "warning" | "error" | "info" | "outline";
681
+ export type BadgeSize = "sm" | "md" | "lg";
682
+
683
+ export interface BadgeProps {
684
+ variant?: BadgeVariant;
685
+ size?: BadgeSize;
686
+ children: ReactNode;
687
+ className?: string;
688
+ }
689
+
690
+ const VARIANT_CLASSES: Record<BadgeVariant, string> = {
691
+ default: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
692
+ success: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400",
693
+ warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400",
694
+ error: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400",
695
+ info: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400",
696
+ outline: "border border-slate-300 text-slate-600 dark:border-slate-600 dark:text-slate-400",
697
+ };
698
+
699
+ const SIZE_CLASSES: Record<BadgeSize, string> = {
700
+ sm: "text-[10px] px-2 py-0.5",
701
+ md: "text-xs px-2.5 py-0.5",
702
+ lg: "text-sm px-3 py-1",
703
+ };
704
+
705
+ export function Badge({ variant = "default", size = "md", children, className }: BadgeProps) {
706
+ return (
707
+ <span
708
+ className={cn(
709
+ "inline-flex items-center font-medium rounded-full",
710
+ VARIANT_CLASSES[variant],
711
+ SIZE_CLASSES[size],
712
+ className,
713
+ )}
714
+ >
715
+ {children}
716
+ </span>
717
+ );
718
+ }`,
719
+ "Breadcrumb": `import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
720
+ import { cn } from "@/lib/utils";
721
+
722
+ export function Breadcrumb({ className, ...props }: HTMLAttributes<HTMLElement>) {
723
+ return <nav aria-label="Breadcrumb" className={cn("w-full", className)} {...props} />;
724
+ }
725
+
726
+ Breadcrumb.List = function BreadcrumbList({ className, ...props }: HTMLAttributes<HTMLOListElement>) {
727
+ return <ol className={cn("flex items-center gap-1.5 text-sm", className)} {...props} />;
728
+ }
729
+
730
+ Breadcrumb.Item = function BreadcrumbItem({ className, ...props }: HTMLAttributes<HTMLLIElement>) {
731
+ return <li className={cn("inline-flex items-center gap-1.5", className)} {...props} />;
732
+ }
733
+
734
+ Breadcrumb.Link = function BreadcrumbLink({ className, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) {
735
+ return (
736
+ <a
737
+ className={cn(
738
+ "text-slate-500 transition-colors hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200",
739
+ className
740
+ )}
741
+ {...props}
742
+ />
743
+ );
744
+ }
745
+
746
+ Breadcrumb.Page = function BreadcrumbPage({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
747
+ return <span className={cn("font-medium text-slate-900 dark:text-white", className)} {...props} />;
748
+ }
749
+
750
+ Breadcrumb.Separator = function BreadcrumbSeparator({ className, children = <span aria-hidden="true">/</span> }: { className?: string; children?: ReactNode }) {
751
+ return <span className={cn("text-slate-400 dark:text-slate-500", className)}>{children}</span>;
752
+ }`,
753
+ "Button": `import type { ButtonHTMLAttributes, ReactNode } from "react";
754
+ import { cn } from "@/lib/utils";
755
+
756
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive" | "outline";
757
+ export type ButtonSize = "sm" | "md" | "lg";
758
+
759
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
760
+ variant?: ButtonVariant;
761
+ size?: ButtonSize;
762
+ isLoading?: boolean;
763
+ children: ReactNode;
764
+ }
765
+
766
+ const VARIANT_CLASSES: Record<ButtonVariant, string> = {
767
+ primary:
768
+ "bg-primary text-white hover:bg-primary/90 focus-visible:ring-primary/40",
769
+ secondary:
770
+ "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 focus-visible:ring-slate-400/40",
771
+ ghost:
772
+ "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 focus-visible:ring-slate-400/40",
773
+ destructive:
774
+ "bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600 focus-visible:ring-red-500/40",
775
+ outline:
776
+ "border border-slate-300 text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-800/50 focus-visible:ring-slate-400/40",
777
+ };
778
+
779
+ const SIZE_CLASSES: Record<ButtonSize, string> = {
780
+ sm: "text-xs px-3 py-1.5 h-7 gap-1.5",
781
+ md: "text-sm px-4 py-2 h-9 gap-2",
782
+ lg: "text-base px-6 py-2.5 h-11 gap-2",
783
+ };
784
+
785
+ function Spinner() {
786
+ return (
787
+ <svg className="animate-spin h-4 w-4 shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
788
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
789
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
790
+ </svg>
791
+ );
792
+ }
793
+
794
+ export function Button({
795
+ variant = "primary",
796
+ size = "md",
797
+ isLoading = false,
798
+ disabled,
799
+ children,
800
+ className,
801
+ ...props
802
+ }: ButtonProps) {
803
+ return (
804
+ <button
805
+ {...props}
806
+ disabled={disabled || isLoading}
807
+ className={cn(
808
+ "inline-flex items-center justify-center font-semibold rounded-lg transition-all",
809
+ "focus-visible:outline-hidden focus-visible:ring-2",
810
+ "disabled:opacity-50 disabled:cursor-not-allowed",
811
+ VARIANT_CLASSES[variant],
812
+ SIZE_CLASSES[size],
813
+ className,
814
+ )}
815
+ >
816
+ {isLoading && <Spinner />}
817
+ {children}
818
+ </button>
819
+ );
820
+ }`,
821
+ "Card": `import type { HTMLAttributes } from "react";
822
+ import { cn } from "@/lib/utils";
823
+
824
+ export type CardVariant = "default" | "elevated" | "flat";
825
+
826
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
827
+ variant?: CardVariant;
828
+ }
829
+
830
+ const CARD_VARIANT_CLASSES: Record<CardVariant, string> = {
831
+ default: "bg-white dark:bg-[#161b22] border border-slate-200 dark:border-[#1f2937]",
832
+ elevated: "bg-white dark:bg-[#161b22] border border-slate-200 dark:border-[#1f2937] shadow-lg shadow-slate-200/60 dark:shadow-black/30",
833
+ flat: "bg-slate-50 dark:bg-[#0d1117] border border-transparent",
834
+ };
835
+
836
+ export function Card({ variant = "default", className, children, ...props }: CardProps) {
837
+ return (
838
+ <div className={cn("rounded-xl", CARD_VARIANT_CLASSES[variant], className)} {...props}>
839
+ {children}
840
+ </div>
841
+ );
842
+ }
843
+
844
+ Card.Header = function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
845
+ return (
846
+ <div className={cn("p-6 pb-4", className)} {...props}>
847
+ {children}
848
+ </div>
849
+ );
850
+ }
851
+
852
+ Card.Title = function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
853
+ return (
854
+ <h3 className={cn("text-base font-bold text-slate-900 dark:text-white leading-snug", className)} {...props}>
855
+ {children}
856
+ </h3>
857
+ );
858
+ }
859
+
860
+ Card.Description = function CardDescription({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement>) {
861
+ return (
862
+ <p className={cn("mt-1 text-sm text-slate-500 dark:text-slate-400 leading-relaxed", className)} {...props}>
863
+ {children}
864
+ </p>
865
+ );
866
+ }
867
+
868
+ Card.Content = function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
869
+ return (
870
+ <div className={cn("px-6 pb-4", className)} {...props}>
871
+ {children}
872
+ </div>
873
+ );
874
+ }
875
+
876
+ Card.Footer = function CardFooter({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
877
+ return (
878
+ <div className={cn("px-6 pb-6 flex items-center gap-3", className)} {...props}>
879
+ {children}
880
+ </div>
881
+ );
882
+ }`,
883
+ "Checkbox": `import { useRef, useEffect } from "react";
884
+ import { cn } from "@/lib/utils";
885
+
886
+ export type CheckboxSize = "sm" | "md" | "lg";
887
+
888
+ export interface CheckboxProps {
889
+ checked?: boolean;
890
+ defaultChecked?: boolean;
891
+ indeterminate?: boolean;
892
+ disabled?: boolean;
893
+ size?: CheckboxSize;
894
+ label?: string;
895
+ description?: string;
896
+ id?: string;
897
+ name?: string;
898
+ onChange?: (checked: boolean) => void;
899
+ className?: string;
900
+ }
901
+
902
+ const BOX_SIZES: Record<CheckboxSize, string> = {
903
+ sm: "h-4 w-4",
904
+ md: "h-5 w-5",
905
+ lg: "h-6 w-6",
906
+ };
907
+
908
+ const ICON_SIZES: Record<CheckboxSize, string> = {
909
+ sm: "h-2.5 w-2.5",
910
+ md: "h-3 w-3",
911
+ lg: "h-3.5 w-3.5",
912
+ };
913
+
914
+ const LABEL_SIZES: Record<CheckboxSize, string> = {
915
+ sm: "text-xs",
916
+ md: "text-sm",
917
+ lg: "text-base",
918
+ };
919
+
920
+ function CheckIcon({ className }: { className?: string }) {
921
+ return (
922
+ <svg className={className} viewBox="0 0 12 12" fill="none">
923
+ <path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
924
+ </svg>
925
+ );
926
+ }
927
+
928
+ function MinusIcon({ className }: { className?: string }) {
929
+ return (
930
+ <svg className={className} viewBox="0 0 12 12" fill="none">
931
+ <path d="M2.5 6h7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
932
+ </svg>
933
+ );
934
+ }
935
+
936
+ export function Checkbox({
937
+ checked,
938
+ defaultChecked,
939
+ indeterminate = false,
940
+ disabled = false,
941
+ size = "md",
942
+ label,
943
+ description,
944
+ id,
945
+ name,
946
+ onChange,
947
+ className,
948
+ }: CheckboxProps) {
949
+ const inputRef = useRef<HTMLInputElement>(null);
950
+
951
+ useEffect(() => {
952
+ if (inputRef.current) {
953
+ inputRef.current.indeterminate = indeterminate;
954
+ }
955
+ }, [indeterminate]);
956
+
957
+ const isChecked = checked ?? false;
958
+ const isFilled = isChecked || indeterminate;
959
+
960
+ return (
961
+ <label
962
+ className={cn(
963
+ "inline-flex items-start gap-3",
964
+ disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
965
+ className,
966
+ )}
967
+ >
968
+ <div className="relative mt-0.5 shrink-0">
969
+ <input
970
+ ref={inputRef}
971
+ type="checkbox"
972
+ id={id}
973
+ name={name}
974
+ checked={checked}
975
+ defaultChecked={defaultChecked}
976
+ disabled={disabled}
977
+ onChange={(e) => onChange?.(e.target.checked)}
978
+ className="sr-only"
979
+ />
980
+ <div
981
+ className={cn(
982
+ "flex items-center justify-center rounded-sm border-2 transition-all",
983
+ BOX_SIZES[size],
984
+ isFilled
985
+ ? "bg-primary border-primary"
986
+ : "bg-white dark:bg-[#0d1117] border-slate-300 dark:border-slate-600",
987
+ !disabled && !isFilled && "hover:border-primary",
988
+ )}
989
+ >
990
+ {isChecked && <CheckIcon className={cn("text-white", ICON_SIZES[size])} />}
991
+ {indeterminate && !isChecked && <MinusIcon className={cn("text-white", ICON_SIZES[size])} />}
992
+ </div>
993
+ </div>
994
+
995
+ {(label ?? description) && (
996
+ <div className="min-w-0">
997
+ {label && (
998
+ <span className={cn("block font-medium text-slate-900 dark:text-white leading-tight", LABEL_SIZES[size])}>
999
+ {label}
1000
+ </span>
1001
+ )}
1002
+ {description && (
1003
+ <p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 leading-relaxed">
1004
+ {description}
1005
+ </p>
1006
+ )}
1007
+ </div>
1008
+ )}
1009
+ </label>
1010
+ );
1011
+ }`,
1012
+ "Combobox": `import { useEffect, useMemo, useRef, useState } from "react";
1013
+ import { cn } from "@/lib/utils";
1014
+
1015
+ export interface ComboboxOption {
1016
+ label: string;
1017
+ value: string;
1018
+ description?: string;
1019
+ disabled?: boolean;
1020
+ }
1021
+
1022
+ export interface ComboboxProps {
1023
+ options: ComboboxOption[];
1024
+ value?: string;
1025
+ defaultValue?: string;
1026
+ onValueChange?: (value: string) => void;
1027
+ placeholder?: string;
1028
+ emptyText?: string;
1029
+ label?: string;
1030
+ description?: string;
1031
+ className?: string;
1032
+ }
1033
+
1034
+ export function Combobox({
1035
+ options,
1036
+ value,
1037
+ defaultValue,
1038
+ onValueChange,
1039
+ placeholder = "Search...",
1040
+ emptyText = "No results",
1041
+ label,
1042
+ description,
1043
+ className,
1044
+ }: ComboboxProps) {
1045
+ const [query, setQuery] = useState("");
1046
+ const [open, setOpen] = useState(false);
1047
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
1048
+ const [internalValue, setInternalValue] = useState(defaultValue ?? "");
1049
+ const containerRef = useRef<HTMLDivElement>(null);
1050
+ const isControlled = value !== undefined;
1051
+ const selectedValue = isControlled ? value : internalValue;
1052
+
1053
+ const selectedOption = useMemo(
1054
+ () => options.find((option) => option.value === selectedValue),
1055
+ [options, selectedValue]
1056
+ );
1057
+
1058
+ const filtered = useMemo(() => {
1059
+ const q = query.trim().toLowerCase();
1060
+ if (!q) return options;
1061
+ return options.filter(
1062
+ (option) =>
1063
+ option.label.toLowerCase().includes(q) || option.description?.toLowerCase().includes(q)
1064
+ );
1065
+ }, [options, query]);
1066
+
1067
+ useEffect(() => {
1068
+ if (!open) return;
1069
+ const onPointerDown = (event: MouseEvent) => {
1070
+ if (!containerRef.current?.contains(event.target as Node)) {
1071
+ setOpen(false);
1072
+ }
1073
+ };
1074
+ window.addEventListener("mousedown", onPointerDown);
1075
+ return () => window.removeEventListener("mousedown", onPointerDown);
1076
+ }, [open]);
1077
+
1078
+ useEffect(() => {
1079
+ if (selectedOption && !open) {
1080
+ setQuery(selectedOption.label);
1081
+ }
1082
+ }, [selectedOption, open]);
1083
+
1084
+ const selectValue = (nextValue: string) => {
1085
+ if (!isControlled) setInternalValue(nextValue);
1086
+ onValueChange?.(nextValue);
1087
+ const nextOption = options.find((option) => option.value === nextValue);
1088
+ setQuery(nextOption?.label ?? "");
1089
+ setOpen(false);
1090
+ };
1091
+
1092
+ return (
1093
+ <div className={cn("flex flex-col gap-1.5", className)}>
1094
+ {label && <label className="text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}
1095
+
1096
+ <div ref={containerRef} className="relative">
1097
+ <input
1098
+ value={query}
1099
+ onChange={(event) => {
1100
+ setQuery(event.target.value);
1101
+ setOpen(true);
1102
+ setHighlightedIndex(0);
1103
+ }}
1104
+ onFocus={() => setOpen(true)}
1105
+ onKeyDown={(event) => {
1106
+ if (!open && event.key === "ArrowDown") {
1107
+ setOpen(true);
1108
+ return;
1109
+ }
1110
+ if (event.key === "ArrowDown") {
1111
+ event.preventDefault();
1112
+ setHighlightedIndex((index) => Math.min(index + 1, filtered.length - 1));
1113
+ }
1114
+ if (event.key === "ArrowUp") {
1115
+ event.preventDefault();
1116
+ setHighlightedIndex((index) => Math.max(index - 1, 0));
1117
+ }
1118
+ if (event.key === "Enter") {
1119
+ event.preventDefault();
1120
+ const option = filtered[highlightedIndex];
1121
+ if (option && !option.disabled) selectValue(option.value);
1122
+ }
1123
+ if (event.key === "Escape") {
1124
+ setOpen(false);
1125
+ }
1126
+ }}
1127
+ placeholder={placeholder}
1128
+ className={cn(
1129
+ "h-10 w-full rounded-lg border border-slate-200 bg-white px-3.5 text-sm text-slate-900 outline-hidden transition-all",
1130
+ "hover:border-slate-300 focus:border-primary focus:ring-2 focus:ring-primary/20",
1131
+ "dark:border-[#1f2937] dark:bg-[#0d1117] dark:text-white dark:hover:border-slate-600"
1132
+ )}
1133
+ />
1134
+ <span className="material-symbols-outlined pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[18px] text-slate-400">
1135
+ {open ? "expand_less" : "expand_more"}
1136
+ </span>
1137
+
1138
+ {open && (
1139
+ <div className="absolute z-40 mt-2 max-h-64 w-full overflow-y-auto rounded-xl border border-slate-200 bg-white p-1 shadow-lg dark:border-[#1f2937] dark:bg-[#161b22]">
1140
+ {filtered.length === 0 && (
1141
+ <p className="px-3 py-2 text-xs text-slate-500 dark:text-slate-400">{emptyText}</p>
1142
+ )}
1143
+ {filtered.map((option, index) => {
1144
+ const isSelected = selectedValue === option.value;
1145
+ const isHighlighted = index === highlightedIndex;
1146
+ return (
1147
+ <button
1148
+ key={option.value}
1149
+ type="button"
1150
+ disabled={option.disabled}
1151
+ onMouseEnter={() => setHighlightedIndex(index)}
1152
+ onClick={() => !option.disabled && selectValue(option.value)}
1153
+ className={cn(
1154
+ "flex w-full items-start gap-2 rounded-lg px-3 py-2 text-left transition-colors",
1155
+ isHighlighted && "bg-primary/10",
1156
+ !isHighlighted && "hover:bg-slate-50 dark:hover:bg-[#0d1117]",
1157
+ option.disabled && "cursor-not-allowed opacity-50"
1158
+ )}
1159
+ >
1160
+ <span
1161
+ className={cn(
1162
+ "mt-0.5 material-symbols-outlined text-[16px]",
1163
+ isSelected ? "text-primary" : "text-transparent"
1164
+ )}
1165
+ >
1166
+ check
1167
+ </span>
1168
+ <span className="min-w-0 flex-1">
1169
+ <span className="block truncate text-sm text-slate-900 dark:text-white">{option.label}</span>
1170
+ {option.description && (
1171
+ <span className="mt-0.5 block truncate text-xs text-slate-500 dark:text-slate-400">
1172
+ {option.description}
1173
+ </span>
1174
+ )}
1175
+ </span>
1176
+ </button>
1177
+ );
1178
+ })}
1179
+ </div>
1180
+ )}
1181
+ </div>
1182
+
1183
+ {description && <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>}
1184
+ </div>
1185
+ );
1186
+ }`,
1187
+ "Command": `import { createContext, useContext, useMemo, useState, type HTMLAttributes, type InputHTMLAttributes, type ReactNode } from "react";
1188
+ import { cn } from "@/lib/utils";
1189
+
1190
+ interface CommandContextValue {
1191
+ query: string;
1192
+ setQuery: (query: string) => void;
1193
+ }
1194
+
1195
+ const CommandContext = createContext<CommandContextValue | null>(null);
1196
+
1197
+ function useCommandContext() {
1198
+ const context = useContext(CommandContext);
1199
+ if (!context) throw new Error("Command sub-components must be used inside <Command>");
1200
+ return context;
1201
+ }
1202
+
1203
+ export function Command({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
1204
+ const [query, setQuery] = useState("");
1205
+
1206
+ return (
1207
+ <CommandContext.Provider value={{ query, setQuery }}>
1208
+ <div
1209
+ className={cn(
1210
+ "rounded-xl border border-slate-200 bg-white dark:border-[#1f2937] dark:bg-[#161b22]",
1211
+ className
1212
+ )}
1213
+ {...props}
1214
+ />
1215
+ </CommandContext.Provider>
1216
+ );
1217
+ }
1218
+
1219
+ Command.Input = function CommandInput({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
1220
+ const { query, setQuery } = useCommandContext();
1221
+
1222
+ return (
1223
+ <div className="flex items-center gap-2 border-b border-slate-100 px-3 py-2 dark:border-[#1f2937]">
1224
+ <span className="material-symbols-outlined text-[18px] text-slate-400">search</span>
1225
+ <input
1226
+ value={query}
1227
+ onChange={(event) => setQuery(event.target.value)}
1228
+ className={cn(
1229
+ "h-8 w-full bg-transparent text-sm text-slate-900 outline-hidden placeholder:text-slate-400 dark:text-white",
1230
+ className
1231
+ )}
1232
+ {...props}
1233
+ />
1234
+ </div>
1235
+ );
1236
+ }
1237
+
1238
+ Command.List = function CommandList({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
1239
+ return <div className={cn("max-h-64 overflow-y-auto p-1", className)} {...props} />;
1240
+ }
1241
+
1242
+ interface CommandItemProps extends HTMLAttributes<HTMLButtonElement> {
1243
+ value: string;
1244
+ keywords?: string[];
1245
+ children: ReactNode;
1246
+ }
1247
+
1248
+ Command.Item = function CommandItem({ value, keywords, className, children, ...props }: CommandItemProps) {
1249
+ const { query } = useCommandContext();
1250
+
1251
+ const visible = useMemo(() => {
1252
+ const normalized = query.trim().toLowerCase();
1253
+ if (!normalized) return true;
1254
+ const terms = [value, ...(keywords ?? [])].join(" ").toLowerCase();
1255
+ return terms.includes(normalized);
1256
+ }, [query, value, keywords]);
1257
+
1258
+ if (!visible) return null;
1259
+
1260
+ return (
1261
+ <button
1262
+ type="button"
1263
+ className={cn(
1264
+ "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-slate-700 transition-colors hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-[#0d1117]",
1265
+ className
1266
+ )}
1267
+ {...props}
1268
+ >
1269
+ {children}
1270
+ </button>
1271
+ );
1272
+ }
1273
+
1274
+ Command.Empty = function CommandEmpty({ className, children = "No results" }: { className?: string; children?: ReactNode }) {
1275
+ const { query } = useCommandContext();
1276
+ if (!query.trim()) return null;
1277
+ return <p className={cn("px-3 py-5 text-center text-xs text-slate-500 dark:text-slate-400", className)}>{children}</p>;
1278
+ }
1279
+
1280
+ Command.Group = function CommandGroup({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
1281
+ return <div className={cn("space-y-1", className)} {...props} />;
1282
+ }
1283
+
1284
+ Command.Label = function CommandLabel({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
1285
+ return (
1286
+ <p
1287
+ className={cn("px-3 py-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400", className)}
1288
+ {...props}
1289
+ />
1290
+ );
1291
+ }
1292
+
1293
+ Command.Separator = function CommandSeparator({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
1294
+ return <div className={cn("my-1 h-px bg-slate-200 dark:bg-[#1f2937]", className)} {...props} />;
1295
+ }`,
1296
+ "DatePicker": `import { forwardRef, type InputHTMLAttributes } from "react";
1297
+ import { cn } from "@/lib/utils";
1298
+
1299
+ export interface DatePickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
1300
+ label?: string;
1301
+ description?: string;
1302
+ error?: string;
1303
+ }
1304
+
1305
+ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function DatePickerRoot(
1306
+ { label, description, error, className, id, ...props },
1307
+ ref
1308
+ ) {
1309
+ const inputId = id ?? (label ? label.toLowerCase().replace(/\\s+/g, "-") : undefined);
1310
+
1311
+ return (
1312
+ <div className={cn("flex flex-col gap-1.5", className)}>
1313
+ {label && (
1314
+ <label htmlFor={inputId} className="text-sm font-medium text-slate-700 dark:text-slate-300">
1315
+ {label}
1316
+ </label>
1317
+ )}
1318
+ <input
1319
+ ref={ref}
1320
+ id={inputId}
1321
+ type="date"
1322
+ className={cn(
1323
+ "h-10 w-full rounded-lg border border-slate-200 bg-white px-3.5 text-sm text-slate-900 outline-hidden transition-all",
1324
+ "hover:border-slate-300 focus:border-primary focus:ring-2 focus:ring-primary/20",
1325
+ "dark:border-[#1f2937] dark:bg-[#0d1117] dark:text-white dark:hover:border-slate-600",
1326
+ error && "border-red-400 focus:border-red-400 focus:ring-red-400/20 dark:border-red-500"
1327
+ )}
1328
+ {...props}
1329
+ />
1330
+ {description && !error && <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>}
1331
+ {error && <p className="text-xs text-red-500 dark:text-red-400">{error}</p>}
1332
+ </div>
1333
+ );
1334
+ });`,
1335
+ "Dialog": `"use client";
1336
+
1337
+ import { useEffect, useRef, type HTMLAttributes, type ReactNode } from "react";
1338
+ import { createPortal } from "react-dom";
1339
+ import { cn } from "@/lib/utils";
1340
+ import { useAnimatedMount } from "@/hooks/use-animated-mount";
1341
+
1342
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1343
+
1344
+ export type DialogSize = "sm" | "md" | "lg" | "xl";
1345
+
1346
+ export interface DialogProps {
1347
+ open: boolean;
1348
+ onClose: () => void;
1349
+ size?: DialogSize;
1350
+ children: ReactNode;
1351
+ className?: string;
1352
+ }
1353
+
1354
+ export interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {
1355
+ children: ReactNode;
1356
+ }
1357
+
1358
+ export interface DialogTitleProps extends HTMLAttributes<HTMLHeadingElement> {
1359
+ children: ReactNode;
1360
+ }
1361
+
1362
+ export interface DialogDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {
1363
+ children: ReactNode;
1364
+ }
1365
+
1366
+ export interface DialogContentProps extends HTMLAttributes<HTMLDivElement> {
1367
+ children: ReactNode;
1368
+ }
1369
+
1370
+ export interface DialogFooterProps extends HTMLAttributes<HTMLDivElement> {
1371
+ children: ReactNode;
1372
+ }
1373
+
1374
+ // \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1375
+
1376
+ const SIZE_CLASSES: Record<DialogSize, string> = {
1377
+ sm: "max-w-sm",
1378
+ md: "max-w-md",
1379
+ lg: "max-w-lg",
1380
+ xl: "max-w-xl",
1381
+ };
1382
+
1383
+ // \u2500\u2500\u2500 Sub-components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1384
+
1385
+ Dialog.Header = function DialogHeader({ children, className, ...props }: DialogHeaderProps) {
1386
+ return (
1387
+ <div className={cn("px-6 pt-6 pb-4", className)} {...props}>
1388
+ {children}
1389
+ </div>
1390
+ );
1391
+ }
1392
+
1393
+ Dialog.Title = function DialogTitle({ children, className, ...props }: DialogTitleProps) {
1394
+ return (
1395
+ <h2 className={cn("text-lg font-semibold text-slate-900 dark:text-white pr-8", className)} {...props}>
1396
+ {children}
1397
+ </h2>
1398
+ );
1399
+ }
1400
+
1401
+ Dialog.Description = function DialogDescription({ children, className, ...props }: DialogDescriptionProps) {
1402
+ return (
1403
+ <p className={cn("mt-1.5 text-sm text-slate-500 dark:text-slate-400 leading-relaxed", className)} {...props}>
1404
+ {children}
1405
+ </p>
1406
+ );
1407
+ }
1408
+
1409
+ Dialog.Content = function DialogContent({ children, className, ...props }: DialogContentProps) {
1410
+ return (
1411
+ <div className={cn("px-6 py-2", className)} {...props}>
1412
+ {children}
1413
+ </div>
1414
+ );
1415
+ }
1416
+
1417
+ Dialog.Footer = function DialogFooter({ children, className, ...props }: DialogFooterProps) {
1418
+ return (
1419
+ <div className={cn("px-6 py-4 flex items-center justify-end gap-3 border-t border-slate-100 dark:border-[#1f2937]", className)} {...props}>
1420
+ {children}
1421
+ </div>
1422
+ );
1423
+ }
1424
+
1425
+ // \u2500\u2500\u2500 Dialog \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1426
+
1427
+ export function Dialog({ open, onClose, size = "md", children, className }: DialogProps) {
1428
+ const overlayRef = useRef<HTMLDivElement>(null);
1429
+ const { mounted, visible } = useAnimatedMount(open, 200);
1430
+
1431
+ useEffect(() => {
1432
+ if (!open) return;
1433
+
1434
+ const handleKey = (e: KeyboardEvent) => {
1435
+ if (e.key === "Escape") onClose();
1436
+ };
1437
+
1438
+ document.addEventListener("keydown", handleKey);
1439
+ document.body.style.overflow = "hidden";
1440
+
1441
+ return () => {
1442
+ document.removeEventListener("keydown", handleKey);
1443
+ document.body.style.overflow = "";
1444
+ };
1445
+ }, [open, onClose]);
1446
+
1447
+ if (!mounted) return null;
1448
+
1449
+ const panel = (
1450
+ <div
1451
+ ref={overlayRef}
1452
+ className="fixed inset-0 z-50 flex items-center justify-center p-4"
1453
+ onClick={(e) => {
1454
+ if (e.target === overlayRef.current) onClose();
1455
+ }}
1456
+ >
1457
+ {/* Backdrop */}
1458
+ <div
1459
+ className={cn(
1460
+ "absolute inset-0 bg-black/50 backdrop-blur-xs transition-opacity duration-200",
1461
+ visible ? "opacity-100" : "opacity-0"
1462
+ )}
1463
+ />
1464
+
1465
+ {/* Panel */}
1466
+ <div
1467
+ role="dialog"
1468
+ aria-modal="true"
1469
+ className={cn(
1470
+ "relative w-full rounded-2xl bg-white dark:bg-[#161b22] shadow-2xl shadow-black/20",
1471
+ "border border-slate-200 dark:border-[#1f2937]",
1472
+ "transition-all duration-200",
1473
+ visible ? "opacity-100 scale-100" : "opacity-0 scale-95",
1474
+ SIZE_CLASSES[size],
1475
+ className
1476
+ )}
1477
+ >
1478
+ {/* Close button */}
1479
+ <button
1480
+ onClick={onClose}
1481
+ className="absolute right-4 top-4 rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 dark:hover:bg-[#1f2937] hover:text-slate-600 dark:hover:text-slate-200 transition-colors"
1482
+ aria-label="Close dialog"
1483
+ >
1484
+ <svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
1485
+ <path d="M12 4L4 12M4 4l8 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
1486
+ </svg>
1487
+ </button>
1488
+
1489
+ {children}
1490
+ </div>
1491
+ </div>
1492
+ );
1493
+
1494
+ return createPortal(panel, document.body);
1495
+ }`,
1496
+ "Drawer": `"use client";
1497
+
1498
+ import { useEffect, useRef, type HTMLAttributes, type ReactNode } from "react";
1499
+ import { createPortal } from "react-dom";
1500
+ import { cn } from "@/lib/utils";
1501
+ import { useAnimatedMount } from "@/hooks/use-animated-mount";
1502
+
1503
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1504
+
1505
+ export type DrawerSide = "right" | "left";
1506
+ export type DrawerSize = "sm" | "md" | "lg" | "full";
1507
+
1508
+ export interface DrawerProps {
1509
+ open: boolean;
1510
+ onClose: () => void;
1511
+ side?: DrawerSide;
1512
+ size?: DrawerSize;
1513
+ children: ReactNode;
1514
+ className?: string;
1515
+ }
1516
+
1517
+ export interface DrawerHeaderProps extends HTMLAttributes<HTMLDivElement> {
1518
+ children: ReactNode;
1519
+ }
1520
+
1521
+ export interface DrawerTitleProps extends HTMLAttributes<HTMLHeadingElement> {
1522
+ children: ReactNode;
1523
+ }
1524
+
1525
+ export interface DrawerDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {
1526
+ children: ReactNode;
1527
+ }
1528
+
1529
+ export interface DrawerContentProps extends HTMLAttributes<HTMLDivElement> {
1530
+ children: ReactNode;
1531
+ }
1532
+
1533
+ export interface DrawerFooterProps extends HTMLAttributes<HTMLDivElement> {
1534
+ children: ReactNode;
1535
+ }
1536
+
1537
+ // \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1538
+
1539
+ const SIZE_CLASSES: Record<DrawerSize, string> = {
1540
+ sm: "w-80",
1541
+ md: "w-96",
1542
+ lg: "w-lg",
1543
+ full: "w-full",
1544
+ };
1545
+
1546
+ const SIDE_CLASSES: Record<DrawerSide, { panel: string; hidden: string }> = {
1547
+ right: { panel: "right-0 inset-y-0", hidden: "translate-x-full" },
1548
+ left: { panel: "left-0 inset-y-0", hidden: "-translate-x-full" },
1549
+ };
1550
+
1551
+ // \u2500\u2500\u2500 Sub-components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1552
+
1553
+ Drawer.Header = function DrawerHeader({ children, className, ...props }: DrawerHeaderProps) {
1554
+ return (
1555
+ <div className={cn("px-6 pt-6 pb-4 border-b border-slate-100 dark:border-[#1f2937]", className)} {...props}>
1556
+ {children}
1557
+ </div>
1558
+ );
1559
+ }
1560
+
1561
+ Drawer.Title = function DrawerTitle({ children, className, ...props }: DrawerTitleProps) {
1562
+ return (
1563
+ <h2 className={cn("text-lg font-semibold text-slate-900 dark:text-white pr-8", className)} {...props}>
1564
+ {children}
1565
+ </h2>
1566
+ );
1567
+ }
1568
+
1569
+ Drawer.Description = function DrawerDescription({ children, className, ...props }: DrawerDescriptionProps) {
1570
+ return (
1571
+ <p className={cn("mt-1 text-sm text-slate-500 dark:text-slate-400 leading-relaxed", className)} {...props}>
1572
+ {children}
1573
+ </p>
1574
+ );
1575
+ }
1576
+
1577
+ Drawer.Content = function DrawerContent({ children, className, ...props }: DrawerContentProps) {
1578
+ return (
1579
+ <div className={cn("flex-1 overflow-y-auto px-6 py-4", className)} {...props}>
1580
+ {children}
1581
+ </div>
1582
+ );
1583
+ }
1584
+
1585
+ Drawer.Footer = function DrawerFooter({ children, className, ...props }: DrawerFooterProps) {
1586
+ return (
1587
+ <div className={cn("px-6 py-4 border-t border-slate-100 dark:border-[#1f2937] flex items-center justify-end gap-3 shrink-0", className)} {...props}>
1588
+ {children}
1589
+ </div>
1590
+ );
1591
+ }
1592
+
1593
+ // \u2500\u2500\u2500 Drawer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1594
+
1595
+ export function Drawer({ open, onClose, side = "right", size = "md", children, className }: DrawerProps) {
1596
+ const backdropRef = useRef<HTMLDivElement>(null);
1597
+ const { mounted, visible } = useAnimatedMount(open, 300);
1598
+
1599
+ useEffect(() => {
1600
+ if (!open) return;
1601
+
1602
+ const handleKey = (e: KeyboardEvent) => {
1603
+ if (e.key === "Escape") onClose();
1604
+ };
1605
+
1606
+ document.addEventListener("keydown", handleKey);
1607
+ document.body.style.overflow = "hidden";
1608
+
1609
+ return () => {
1610
+ document.removeEventListener("keydown", handleKey);
1611
+ document.body.style.overflow = "";
1612
+ };
1613
+ }, [open, onClose]);
1614
+
1615
+ if (!mounted) return null;
1616
+
1617
+ const { panel, hidden } = SIDE_CLASSES[side];
1618
+
1619
+ const drawer = (
1620
+ <div
1621
+ ref={backdropRef}
1622
+ className="fixed inset-0 z-50 flex"
1623
+ onClick={(e) => {
1624
+ if (e.target === backdropRef.current) onClose();
1625
+ }}
1626
+ >
1627
+ {/* Backdrop */}
1628
+ <div
1629
+ className={cn(
1630
+ "absolute inset-0 bg-black/50 backdrop-blur-xs transition-opacity duration-300",
1631
+ visible ? "opacity-100" : "opacity-0"
1632
+ )}
1633
+ />
1634
+
1635
+ {/* Panel */}
1636
+ <div
1637
+ role="dialog"
1638
+ aria-modal="true"
1639
+ className={cn(
1640
+ "absolute flex flex-col h-full bg-white dark:bg-[#161b22]",
1641
+ "border-slate-200 dark:border-[#1f2937]",
1642
+ side === "right" ? "border-l" : "border-r",
1643
+ "shadow-2xl shadow-black/20",
1644
+ "transition-transform duration-300 ease-in-out",
1645
+ visible ? "translate-x-0" : hidden,
1646
+ SIZE_CLASSES[size],
1647
+ panel,
1648
+ className
1649
+ )}
1650
+ >
1651
+ {/* Close button */}
1652
+ <button
1653
+ onClick={onClose}
1654
+ className="absolute right-4 top-4 z-10 rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 dark:hover:bg-[#1f2937] hover:text-slate-600 dark:hover:text-slate-200 transition-colors"
1655
+ aria-label="Close drawer"
1656
+ >
1657
+ <svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
1658
+ <path d="M12 4L4 12M4 4l8 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
1659
+ </svg>
1660
+ </button>
1661
+
1662
+ {children}
1663
+ </div>
1664
+ </div>
1665
+ );
1666
+
1667
+ return createPortal(drawer, document.body);
1668
+ }`,
1669
+ "DropdownMenu": `import { createContext, useCallback, useContext, useEffect, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
1670
+ import { cn } from "@/lib/utils";
1671
+
1672
+ interface DropdownMenuContextValue {
1673
+ open: boolean;
1674
+ setOpen: (open: boolean) => void;
1675
+ }
1676
+
1677
+ const DropdownMenuContext = createContext<DropdownMenuContextValue | null>(null);
1678
+
1679
+ function useDropdownMenuContext() {
1680
+ const context = useContext(DropdownMenuContext);
1681
+ if (!context) throw new Error("DropdownMenu sub-components must be used inside <DropdownMenu>");
1682
+ return context;
1683
+ }
1684
+
1685
+ export interface DropdownMenuProps {
1686
+ open?: boolean;
1687
+ defaultOpen?: boolean;
1688
+ onOpenChange?: (open: boolean) => void;
1689
+ children: ReactNode;
1690
+ className?: string;
1691
+ }
1692
+
1693
+ export interface DropdownMenuTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
1694
+ children: ReactNode;
1695
+ }
1696
+
1697
+ export interface DropdownMenuContentProps extends HTMLAttributes<HTMLDivElement> {
1698
+ children: ReactNode;
1699
+ }
1700
+
1701
+ export interface DropdownMenuItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
1702
+ inset?: boolean;
1703
+ onSelect?: () => void;
1704
+ }
1705
+
1706
+ export function DropdownMenu({ open, defaultOpen = false, onOpenChange, children, className }: DropdownMenuProps) {
1707
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
1708
+ const containerRef = useRef<HTMLDivElement>(null);
1709
+ const isControlled = open !== undefined;
1710
+ const isOpen = isControlled ? open : internalOpen;
1711
+
1712
+ const setOpen = useCallback((next: boolean) => {
1713
+ if (!isControlled) setInternalOpen(next);
1714
+ onOpenChange?.(next);
1715
+ }, [isControlled, onOpenChange]);
1716
+
1717
+ useEffect(() => {
1718
+ if (!isOpen) return;
1719
+
1720
+ const onPointerDown = (event: MouseEvent) => {
1721
+ if (!containerRef.current?.contains(event.target as Node)) {
1722
+ setOpen(false);
1723
+ }
1724
+ };
1725
+
1726
+ const onKeyDown = (event: KeyboardEvent) => {
1727
+ if (event.key === "Escape") setOpen(false);
1728
+ };
1729
+
1730
+ window.addEventListener("mousedown", onPointerDown);
1731
+ window.addEventListener("keydown", onKeyDown);
1732
+
1733
+ return () => {
1734
+ window.removeEventListener("mousedown", onPointerDown);
1735
+ window.removeEventListener("keydown", onKeyDown);
1736
+ };
1737
+ }, [isOpen, setOpen]);
1738
+
1739
+ return (
1740
+ <DropdownMenuContext.Provider value={{ open: isOpen, setOpen }}>
1741
+ <div ref={containerRef} className={cn("relative inline-block", className)}>
1742
+ {children}
1743
+ </div>
1744
+ </DropdownMenuContext.Provider>
1745
+ );
1746
+ }
1747
+
1748
+ DropdownMenu.Trigger = function DropdownMenuTrigger({ children, className, onClick, ...props }: DropdownMenuTriggerProps) {
1749
+ const { open, setOpen } = useDropdownMenuContext();
1750
+
1751
+ return (
1752
+ <button
1753
+ type="button"
1754
+ onClick={(event) => {
1755
+ onClick?.(event);
1756
+ setOpen(!open);
1757
+ }}
1758
+ className={cn(
1759
+ "inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 transition-all",
1760
+ "hover:bg-slate-50 dark:border-[#1f2937] dark:bg-[#0d1117] dark:text-slate-200 dark:hover:bg-[#161b22]",
1761
+ className
1762
+ )}
1763
+ {...props}
1764
+ >
1765
+ {children}
1766
+ </button>
1767
+ );
1768
+ }
1769
+
1770
+ DropdownMenu.Content = function DropdownMenuContent({ children, className, ...props }: DropdownMenuContentProps) {
1771
+ const { open } = useDropdownMenuContext();
1772
+ if (!open) return null;
1773
+
1774
+ return (
1775
+ <div
1776
+ className={cn(
1777
+ "absolute right-0 top-[calc(100%+8px)] z-50 min-w-56 rounded-xl border border-slate-200 bg-white p-1 shadow-xl",
1778
+ "dark:border-[#1f2937] dark:bg-[#161b22]",
1779
+ className
1780
+ )}
1781
+ {...props}
1782
+ >
1783
+ {children}
1784
+ </div>
1785
+ );
1786
+ }
1787
+
1788
+ DropdownMenu.Label = function DropdownMenuLabel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
1789
+ return (
1790
+ <div
1791
+ className={cn("px-2.5 py-1.5 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400", className)}
1792
+ {...props}
1793
+ />
1794
+ );
1795
+ }
1796
+
1797
+ DropdownMenu.Separator = function DropdownMenuSeparator({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
1798
+ return <div className={cn("my-1 h-px bg-slate-200 dark:bg-[#1f2937]", className)} {...props} />;
1799
+ }
1800
+
1801
+ DropdownMenu.Item = function DropdownMenuItem({ inset = false, onSelect, onClick, className, disabled, ...props }: DropdownMenuItemProps) {
1802
+ const { setOpen } = useDropdownMenuContext();
1803
+
1804
+ return (
1805
+ <button
1806
+ type="button"
1807
+ disabled={disabled}
1808
+ onClick={(event) => {
1809
+ onClick?.(event);
1810
+ if (disabled) return;
1811
+ onSelect?.();
1812
+ setOpen(false);
1813
+ }}
1814
+ className={cn(
1815
+ "flex w-full items-center rounded-lg px-2.5 py-2 text-left text-sm text-slate-700 transition-all",
1816
+ "hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-[#0d1117]",
1817
+ inset && "pl-8",
1818
+ disabled && "cursor-not-allowed opacity-50",
1819
+ className
1820
+ )}
1821
+ {...props}
1822
+ />
1823
+ );
1824
+ }`,
1825
+ "FloatingLines": `import { useEffect, useRef } from "react";
1826
+ import {
1827
+ Scene,
1828
+ OrthographicCamera,
1829
+ WebGLRenderer,
1830
+ PlaneGeometry,
1831
+ Mesh,
1832
+ ShaderMaterial,
1833
+ Vector3,
1834
+ Vector2,
1835
+ Clock,
1836
+ } from "three";
1837
+
1838
+ const vertexShader = \`
1839
+ precision highp float;
1840
+
1841
+ void main() {
1842
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1843
+ }
1844
+ \`;
1845
+
1846
+ const fragmentShader = \`
1847
+ precision highp float;
1848
+
1849
+ uniform float iTime;
1850
+ uniform vec3 iResolution;
1851
+ uniform float animationSpeed;
1852
+
1853
+ uniform bool enableTop;
1854
+ uniform bool enableMiddle;
1855
+ uniform bool enableBottom;
1856
+
1857
+ uniform int topLineCount;
1858
+ uniform int middleLineCount;
1859
+ uniform int bottomLineCount;
1860
+
1861
+ uniform float topLineDistance;
1862
+ uniform float middleLineDistance;
1863
+ uniform float bottomLineDistance;
1864
+
1865
+ uniform vec3 topWavePosition;
1866
+ uniform vec3 middleWavePosition;
1867
+ uniform vec3 bottomWavePosition;
1868
+
1869
+ uniform vec2 iMouse;
1870
+ uniform bool interactive;
1871
+ uniform float bendRadius;
1872
+ uniform float bendStrength;
1873
+ uniform float bendInfluence;
1874
+
1875
+ uniform bool parallax;
1876
+ uniform float parallaxStrength;
1877
+ uniform vec2 parallaxOffset;
1878
+
1879
+ uniform vec3 lineGradient[8];
1880
+ uniform int lineGradientCount;
1881
+
1882
+ const vec3 BLACK = vec3(0.0);
1883
+ const vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;
1884
+ const vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;
1885
+
1886
+ mat2 rotate(float r) {
1887
+ return mat2(cos(r), sin(r), -sin(r), cos(r));
1888
+ }
1889
+
1890
+ vec3 background_color(vec2 uv) {
1891
+ vec3 col = vec3(0.0);
1892
+ float y = sin(uv.x - 0.2) * 0.3 - 0.1;
1893
+ float m = uv.y - y;
1894
+ col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));
1895
+ col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));
1896
+ return col * 0.5;
1897
+ }
1898
+
1899
+ vec3 getLineColor(float t, vec3 baseColor) {
1900
+ if (lineGradientCount <= 0) return baseColor;
1901
+
1902
+ vec3 gradientColor;
1903
+ if (lineGradientCount == 1) {
1904
+ gradientColor = lineGradient[0];
1905
+ } else {
1906
+ float clampedT = clamp(t, 0.0, 0.9999);
1907
+ float scaled = clampedT * float(lineGradientCount - 1);
1908
+ int idx = int(floor(scaled));
1909
+ float f = fract(scaled);
1910
+ int idx2 = min(idx + 1, lineGradientCount - 1);
1911
+ vec3 c1 = lineGradient[idx];
1912
+ vec3 c2 = lineGradient[idx2];
1913
+ gradientColor = mix(c1, c2, f);
1914
+ }
1915
+ return gradientColor * 0.5;
1916
+ }
1917
+
1918
+ float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {
1919
+ float time = iTime * animationSpeed;
1920
+ float x_offset = offset;
1921
+ float x_movement = time * 0.1;
1922
+ float amp = sin(offset + time * 0.2) * 0.3;
1923
+ float y = sin(uv.x + x_offset + x_movement) * amp;
1924
+
1925
+ if (shouldBend) {
1926
+ vec2 d = screenUv - mouseUv;
1927
+ float influence = exp(-dot(d, d) * bendRadius);
1928
+ float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;
1929
+ y += bendOffset;
1930
+ }
1931
+
1932
+ float m = uv.y - y;
1933
+ return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;
1934
+ }
1935
+
1936
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1937
+ vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
1938
+ baseUv.y *= -1.0;
1939
+
1940
+ if (parallax) baseUv += parallaxOffset;
1941
+
1942
+ vec3 col = vec3(0.0);
1943
+ vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);
1944
+
1945
+ vec2 mouseUv = vec2(0.0);
1946
+ if (interactive) {
1947
+ mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;
1948
+ mouseUv.y *= -1.0;
1949
+ }
1950
+
1951
+ if (enableBottom) {
1952
+ for (int i = 0; i < bottomLineCount; ++i) {
1953
+ float fi = float(i);
1954
+ float t = fi / max(float(bottomLineCount - 1), 1.0);
1955
+ vec3 lineCol = getLineColor(t, b);
1956
+ float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);
1957
+ vec2 ruv = baseUv * rotate(angle);
1958
+ col += lineCol * wave(
1959
+ ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),
1960
+ 1.5 + 0.2 * fi, baseUv, mouseUv, interactive
1961
+ ) * 0.2;
1962
+ }
1963
+ }
1964
+
1965
+ if (enableMiddle) {
1966
+ for (int i = 0; i < middleLineCount; ++i) {
1967
+ float fi = float(i);
1968
+ float t = fi / max(float(middleLineCount - 1), 1.0);
1969
+ vec3 lineCol = getLineColor(t, b);
1970
+ float angle = middleWavePosition.z * log(length(baseUv) + 1.0);
1971
+ vec2 ruv = baseUv * rotate(angle);
1972
+ col += lineCol * wave(
1973
+ ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),
1974
+ 2.0 + 0.15 * fi, baseUv, mouseUv, interactive
1975
+ );
1976
+ }
1977
+ }
1978
+
1979
+ if (enableTop) {
1980
+ for (int i = 0; i < topLineCount; ++i) {
1981
+ float fi = float(i);
1982
+ float t = fi / max(float(topLineCount - 1), 1.0);
1983
+ vec3 lineCol = getLineColor(t, b);
1984
+ float angle = topWavePosition.z * log(length(baseUv) + 1.0);
1985
+ vec2 ruv = baseUv * rotate(angle);
1986
+ ruv.x *= -1.0;
1987
+ col += lineCol * wave(
1988
+ ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),
1989
+ 1.0 + 0.2 * fi, baseUv, mouseUv, interactive
1990
+ ) * 0.1;
1991
+ }
1992
+ }
1993
+
1994
+ fragColor = vec4(col, 1.0);
1995
+ }
1996
+
1997
+ void main() {
1998
+ vec4 color = vec4(0.0);
1999
+ mainImage(color, gl_FragCoord.xy);
2000
+ gl_FragColor = color;
2001
+ }
2002
+ \`;
2003
+
2004
+ const MAX_GRADIENT_STOPS = 8;
2005
+
2006
+ function hexToVec3(hex: string): Vector3 {
2007
+ const value = hex.trim().replace(/^#/, "");
2008
+ let r = 255, g = 255, b = 255;
2009
+
2010
+ if (value.length === 3) {
2011
+ r = parseInt((value[0] ?? "f") + (value[0] ?? "f"), 16);
2012
+ g = parseInt((value[1] ?? "f") + (value[1] ?? "f"), 16);
2013
+ b = parseInt((value[2] ?? "f") + (value[2] ?? "f"), 16);
2014
+ } else if (value.length === 6) {
2015
+ r = parseInt(value.slice(0, 2), 16);
2016
+ g = parseInt(value.slice(2, 4), 16);
2017
+ b = parseInt(value.slice(4, 6), 16);
2018
+ }
2019
+
2020
+ return new Vector3(r / 255, g / 255, b / 255);
2021
+ }
2022
+
2023
+ export interface WavePosition {
2024
+ x?: number;
2025
+ y?: number;
2026
+ rotate?: number;
2027
+ }
2028
+
2029
+ export interface FloatingLinesProps {
2030
+ linesGradient?: string[];
2031
+ enabledWaves?: Array<"top" | "middle" | "bottom">;
2032
+ lineCount?: number | number[];
2033
+ lineDistance?: number | number[];
2034
+ topWavePosition?: WavePosition;
2035
+ middleWavePosition?: WavePosition;
2036
+ bottomWavePosition?: WavePosition;
2037
+ animationSpeed?: number;
2038
+ interactive?: boolean;
2039
+ bendRadius?: number;
2040
+ bendStrength?: number;
2041
+ mouseDamping?: number;
2042
+ parallax?: boolean;
2043
+ parallaxStrength?: number;
2044
+ mixBlendMode?: React.CSSProperties["mixBlendMode"];
2045
+ className?: string;
2046
+ }
2047
+
2048
+ type WaveType = "top" | "middle" | "bottom";
2049
+
2050
+ export function FloatingLines({
2051
+ linesGradient,
2052
+ enabledWaves = ["top", "middle", "bottom"],
2053
+ lineCount = 6,
2054
+ lineDistance = 5,
2055
+ topWavePosition,
2056
+ middleWavePosition,
2057
+ bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },
2058
+ animationSpeed = 1,
2059
+ interactive = true,
2060
+ bendRadius = 5.0,
2061
+ bendStrength = -0.5,
2062
+ mouseDamping = 0.05,
2063
+ parallax = true,
2064
+ parallaxStrength = 0.2,
2065
+ mixBlendMode = "screen",
2066
+ className,
2067
+ }: FloatingLinesProps) {
2068
+ const containerRef = useRef<HTMLDivElement>(null);
2069
+ const targetMouseRef = useRef(new Vector2(-1000, -1000));
2070
+ const currentMouseRef = useRef(new Vector2(-1000, -1000));
2071
+ const targetInfluenceRef = useRef(0);
2072
+ const currentInfluenceRef = useRef(0);
2073
+ const targetParallaxRef = useRef(new Vector2(0, 0));
2074
+ const currentParallaxRef = useRef(new Vector2(0, 0));
2075
+
2076
+ const getLineCount = (waveType: WaveType): number => {
2077
+ if (typeof lineCount === "number") return lineCount;
2078
+ if (!enabledWaves.includes(waveType)) return 0;
2079
+ const index = enabledWaves.indexOf(waveType);
2080
+ return lineCount[index] ?? 6;
2081
+ };
2082
+
2083
+ const getLineDistance = (waveType: WaveType): number => {
2084
+ if (typeof lineDistance === "number") return lineDistance;
2085
+ if (!enabledWaves.includes(waveType)) return 0.1;
2086
+ const index = enabledWaves.indexOf(waveType);
2087
+ return lineDistance[index] ?? 0.1;
2088
+ };
2089
+
2090
+ const topLineCount = enabledWaves.includes("top") ? getLineCount("top") : 0;
2091
+ const middleLineCount = enabledWaves.includes("middle") ? getLineCount("middle") : 0;
2092
+ const bottomLineCount = enabledWaves.includes("bottom") ? getLineCount("bottom") : 0;
2093
+
2094
+ const topLineDistance = enabledWaves.includes("top") ? getLineDistance("top") * 0.01 : 0.01;
2095
+ const middleLineDistance = enabledWaves.includes("middle") ? getLineDistance("middle") * 0.01 : 0.01;
2096
+ const bottomLineDistance = enabledWaves.includes("bottom") ? getLineDistance("bottom") * 0.01 : 0.01;
2097
+
2098
+ useEffect(() => {
2099
+ if (!containerRef.current) return;
2100
+
2101
+ const scene = new Scene();
2102
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
2103
+ camera.position.z = 1;
2104
+
2105
+ const renderer = new WebGLRenderer({ antialias: true, alpha: false });
2106
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
2107
+ renderer.domElement.style.width = "100%";
2108
+ renderer.domElement.style.height = "100%";
2109
+ containerRef.current.appendChild(renderer.domElement);
2110
+
2111
+ const uniforms = {
2112
+ iTime: { value: 0 },
2113
+ iResolution: { value: new Vector3(1, 1, 1) },
2114
+ animationSpeed: { value: animationSpeed },
2115
+
2116
+ enableTop: { value: enabledWaves.includes("top") },
2117
+ enableMiddle: { value: enabledWaves.includes("middle") },
2118
+ enableBottom: { value: enabledWaves.includes("bottom") },
2119
+
2120
+ topLineCount: { value: topLineCount },
2121
+ middleLineCount: { value: middleLineCount },
2122
+ bottomLineCount: { value: bottomLineCount },
2123
+
2124
+ topLineDistance: { value: topLineDistance },
2125
+ middleLineDistance: { value: middleLineDistance },
2126
+ bottomLineDistance: { value: bottomLineDistance },
2127
+
2128
+ topWavePosition: {
2129
+ value: new Vector3(
2130
+ topWavePosition?.x ?? 10.0,
2131
+ topWavePosition?.y ?? 0.5,
2132
+ topWavePosition?.rotate ?? -0.4,
2133
+ ),
2134
+ },
2135
+ middleWavePosition: {
2136
+ value: new Vector3(
2137
+ middleWavePosition?.x ?? 5.0,
2138
+ middleWavePosition?.y ?? 0.0,
2139
+ middleWavePosition?.rotate ?? 0.2,
2140
+ ),
2141
+ },
2142
+ bottomWavePosition: {
2143
+ value: new Vector3(
2144
+ bottomWavePosition?.x ?? 2.0,
2145
+ bottomWavePosition?.y ?? -0.7,
2146
+ bottomWavePosition?.rotate ?? 0.4,
2147
+ ),
2148
+ },
2149
+
2150
+ iMouse: { value: new Vector2(-1000, -1000) },
2151
+ interactive: { value: interactive },
2152
+ bendRadius: { value: bendRadius },
2153
+ bendStrength: { value: bendStrength },
2154
+ bendInfluence: { value: 0 },
2155
+
2156
+ parallax: { value: parallax },
2157
+ parallaxStrength: { value: parallaxStrength },
2158
+ parallaxOffset: { value: new Vector2(0, 0) },
2159
+
2160
+ lineGradient: {
2161
+ value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1)),
2162
+ },
2163
+ lineGradientCount: { value: 0 },
2164
+ };
2165
+
2166
+ if (linesGradient && linesGradient.length > 0) {
2167
+ const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);
2168
+ uniforms.lineGradientCount.value = stops.length;
2169
+ stops.forEach((hex, i) => {
2170
+ const color = hexToVec3(hex);
2171
+ const gradientEntry = uniforms.lineGradient.value[i];
2172
+ if (gradientEntry) gradientEntry.set(color.x, color.y, color.z);
2173
+ });
2174
+ }
2175
+
2176
+ const material = new ShaderMaterial({ uniforms, vertexShader, fragmentShader });
2177
+ const geometry = new PlaneGeometry(2, 2);
2178
+ const mesh = new Mesh(geometry, material);
2179
+ scene.add(mesh);
2180
+
2181
+ const clock = new Clock();
2182
+
2183
+ const setSize = () => {
2184
+ const el = containerRef.current;
2185
+ if (!el) return;
2186
+ const width = el.clientWidth || 1;
2187
+ const height = el.clientHeight || 1;
2188
+ renderer.setSize(width, height, false);
2189
+ const cw = renderer.domElement.width;
2190
+ const ch = renderer.domElement.height;
2191
+ uniforms.iResolution.value.set(cw, ch, 1);
2192
+ };
2193
+
2194
+ setSize();
2195
+
2196
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(setSize) : null;
2197
+ if (ro && containerRef.current) ro.observe(containerRef.current);
2198
+
2199
+ const handlePointerMove = (event: PointerEvent) => {
2200
+ const rect = renderer.domElement.getBoundingClientRect();
2201
+ const x = event.clientX - rect.left;
2202
+ const y = event.clientY - rect.top;
2203
+ const dpr = renderer.getPixelRatio();
2204
+ targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);
2205
+ targetInfluenceRef.current = 1.0;
2206
+
2207
+ if (parallax) {
2208
+ const cx = rect.width / 2;
2209
+ const cy = rect.height / 2;
2210
+ targetParallaxRef.current.set(
2211
+ ((x - cx) / rect.width) * parallaxStrength,
2212
+ (-(y - cy) / rect.height) * parallaxStrength,
2213
+ );
2214
+ }
2215
+ };
2216
+
2217
+ const handlePointerLeave = () => {
2218
+ targetInfluenceRef.current = 0.0;
2219
+ };
2220
+
2221
+ if (interactive) {
2222
+ renderer.domElement.addEventListener("pointermove", handlePointerMove);
2223
+ renderer.domElement.addEventListener("pointerleave", handlePointerLeave);
2224
+ }
2225
+
2226
+ let raf = 0;
2227
+ const renderLoop = () => {
2228
+ uniforms.iTime.value = clock.getElapsedTime();
2229
+
2230
+ if (interactive) {
2231
+ currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);
2232
+ uniforms.iMouse.value.copy(currentMouseRef.current);
2233
+ currentInfluenceRef.current +=
2234
+ (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;
2235
+ uniforms.bendInfluence.value = currentInfluenceRef.current;
2236
+ }
2237
+
2238
+ if (parallax) {
2239
+ currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);
2240
+ uniforms.parallaxOffset.value.copy(currentParallaxRef.current);
2241
+ }
2242
+
2243
+ renderer.render(scene, camera);
2244
+ raf = requestAnimationFrame(renderLoop);
2245
+ };
2246
+ renderLoop();
2247
+
2248
+ return () => {
2249
+ cancelAnimationFrame(raf);
2250
+ ro?.disconnect();
2251
+
2252
+ if (interactive) {
2253
+ renderer.domElement.removeEventListener("pointermove", handlePointerMove);
2254
+ renderer.domElement.removeEventListener("pointerleave", handlePointerLeave);
2255
+ }
2256
+
2257
+ geometry.dispose();
2258
+ material.dispose();
2259
+ renderer.dispose();
2260
+ if (renderer.domElement.parentElement) {
2261
+ renderer.domElement.parentElement.removeChild(renderer.domElement);
2262
+ }
2263
+ };
2264
+ }, [
2265
+ linesGradient,
2266
+ enabledWaves,
2267
+ lineCount,
2268
+ lineDistance,
2269
+ topWavePosition,
2270
+ middleWavePosition,
2271
+ bottomWavePosition,
2272
+ animationSpeed,
2273
+ interactive,
2274
+ bendRadius,
2275
+ bendStrength,
2276
+ mouseDamping,
2277
+ parallax,
2278
+ parallaxStrength,
2279
+ topLineCount,
2280
+ middleLineCount,
2281
+ bottomLineCount,
2282
+ topLineDistance,
2283
+ middleLineDistance,
2284
+ bottomLineDistance,
2285
+ ]);
2286
+
2287
+ return (
2288
+ <div
2289
+ ref={containerRef}
2290
+ className={className}
2291
+ style={{ mixBlendMode, position: "absolute", inset: 0, width: "100%", height: "100%" }}
2292
+ />
2293
+ );
2294
+ }`,
2295
+ "Input": `import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
2296
+ import { cn } from "@/lib/utils";
2297
+
2298
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2299
+
2300
+ export type InputSize = "sm" | "md" | "lg";
2301
+ export type InputVariant = "default" | "filled" | "ghost";
2302
+
2303
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
2304
+ label?: string;
2305
+ description?: string;
2306
+ error?: string;
2307
+ size?: InputSize;
2308
+ variant?: InputVariant;
2309
+ leadingIcon?: ReactNode;
2310
+ trailingIcon?: ReactNode;
2311
+ }
2312
+
2313
+ // \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2314
+
2315
+ const SIZE_CLASSES: Record<InputSize, { input: string; label: string; icon: string }> = {
2316
+ sm: { input: "h-8 px-3 text-xs", label: "text-xs", icon: "h-3.5 w-3.5" },
2317
+ md: { input: "h-10 px-3.5 text-sm", label: "text-sm", icon: "h-4 w-4" },
2318
+ lg: { input: "h-12 px-4 text-base", label: "text-sm", icon: "h-4.5 w-4.5" },
2319
+ };
2320
+
2321
+ const VARIANT_BASE: Record<InputVariant, string> = {
2322
+ default:
2323
+ "border border-slate-200 dark:border-[#1f2937] bg-white dark:bg-[#0d1117] " +
2324
+ "hover:border-slate-300 dark:hover:border-slate-600 " +
2325
+ "focus-within:border-primary dark:focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20",
2326
+ filled:
2327
+ "border border-transparent bg-slate-100 dark:bg-[#161b22] " +
2328
+ "hover:bg-slate-150 dark:hover:bg-[#1f2937] " +
2329
+ "focus-within:border-primary dark:focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 focus-within:bg-white dark:focus-within:bg-[#0d1117]",
2330
+ ghost:
2331
+ "border border-transparent bg-transparent " +
2332
+ "hover:bg-slate-50 dark:hover:bg-[#161b22] " +
2333
+ "focus-within:border-primary dark:focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20",
2334
+ };
2335
+
2336
+ const ERROR_OVERRIDE =
2337
+ "border-red-400 dark:border-red-500 focus-within:border-red-400 dark:focus-within:border-red-500 focus-within:ring-red-400/20";
2338
+
2339
+ // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2340
+
2341
+ export const Input = forwardRef<HTMLInputElement, InputProps>(function InputRoot(
2342
+ {
2343
+ label,
2344
+ description,
2345
+ error,
2346
+ size = "md",
2347
+ variant = "default",
2348
+ leadingIcon,
2349
+ trailingIcon,
2350
+ disabled,
2351
+ className,
2352
+ id,
2353
+ ...rest
2354
+ },
2355
+ ref
2356
+ ) {
2357
+ const s = SIZE_CLASSES[size];
2358
+ const inputId = id ?? (label ? label.toLowerCase().replace(/\\s+/g, "-") : undefined);
2359
+
2360
+ return (
2361
+ <div className={cn("flex flex-col gap-1.5", className)}>
2362
+ {label && (
2363
+ <label
2364
+ htmlFor={inputId}
2365
+ className={cn(
2366
+ "font-medium text-slate-700 dark:text-slate-300",
2367
+ s.label,
2368
+ disabled && "opacity-50"
2369
+ )}
2370
+ >
2371
+ {label}
2372
+ </label>
2373
+ )}
2374
+
2375
+ <div
2376
+ className={cn(
2377
+ "relative flex items-center rounded-lg transition-all",
2378
+ VARIANT_BASE[variant],
2379
+ error && ERROR_OVERRIDE,
2380
+ disabled && "opacity-50 cursor-not-allowed pointer-events-none"
2381
+ )}
2382
+ >
2383
+ {leadingIcon && (
2384
+ <span className={cn("absolute left-3 flex shrink-0 items-center text-slate-400", s.icon)}>
2385
+ {leadingIcon}
2386
+ </span>
2387
+ )}
2388
+
2389
+ <input
2390
+ ref={ref}
2391
+ id={inputId}
2392
+ disabled={disabled}
2393
+ className={cn(
2394
+ "w-full bg-transparent outline-hidden text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500",
2395
+ s.input,
2396
+ leadingIcon && (size === "sm" ? "pl-8" : size === "lg" ? "pl-10" : "pl-9"),
2397
+ trailingIcon && (size === "sm" ? "pr-8" : size === "lg" ? "pr-10" : "pr-9")
2398
+ )}
2399
+ {...rest}
2400
+ />
2401
+
2402
+ {trailingIcon && (
2403
+ <span className={cn("absolute right-3 flex shrink-0 items-center text-slate-400", s.icon)}>
2404
+ {trailingIcon}
2405
+ </span>
2406
+ )}
2407
+ </div>
2408
+
2409
+ {description && !error && (
2410
+ <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>
2411
+ )}
2412
+ {error && (
2413
+ <p className="text-xs text-red-500 dark:text-red-400">{error}</p>
2414
+ )}
2415
+ </div>
2416
+ );
2417
+ });`,
2418
+ "Pagination": `import { cn } from "@/lib/utils";
2419
+
2420
+ type PaginationToken = number | "ellipsis";
2421
+
2422
+ export interface PaginationProps {
2423
+ page: number;
2424
+ totalPages: number;
2425
+ onPageChange: (page: number) => void;
2426
+ siblingCount?: number;
2427
+ className?: string;
2428
+ }
2429
+
2430
+ function buildRange(page: number, totalPages: number, siblingCount: number): PaginationToken[] {
2431
+ const pages: PaginationToken[] = [];
2432
+ const totalButtons = siblingCount * 2 + 5;
2433
+
2434
+ if (totalPages <= totalButtons) {
2435
+ for (let i = 1; i <= totalPages; i += 1) pages.push(i);
2436
+ return pages;
2437
+ }
2438
+
2439
+ const leftSibling = Math.max(page - siblingCount, 1);
2440
+ const rightSibling = Math.min(page + siblingCount, totalPages);
2441
+ const showLeftEllipsis = leftSibling > 2;
2442
+ const showRightEllipsis = rightSibling < totalPages - 1;
2443
+
2444
+ pages.push(1);
2445
+
2446
+ if (showLeftEllipsis) {
2447
+ pages.push("ellipsis");
2448
+ } else {
2449
+ for (let i = 2; i < leftSibling; i += 1) pages.push(i);
2450
+ }
2451
+
2452
+ for (let i = leftSibling; i <= rightSibling; i += 1) {
2453
+ if (i !== 1 && i !== totalPages) pages.push(i);
2454
+ }
2455
+
2456
+ if (showRightEllipsis) {
2457
+ pages.push("ellipsis");
2458
+ } else {
2459
+ for (let i = rightSibling + 1; i < totalPages; i += 1) pages.push(i);
2460
+ }
2461
+
2462
+ if (totalPages > 1) pages.push(totalPages);
2463
+
2464
+ return pages;
2465
+ }
2466
+
2467
+ export function Pagination({ page, totalPages, onPageChange, siblingCount = 1, className }: PaginationProps) {
2468
+ const clampedPage = Math.min(Math.max(page, 1), Math.max(totalPages, 1));
2469
+ const range = buildRange(clampedPage, totalPages, siblingCount);
2470
+
2471
+ return (
2472
+ <nav aria-label="Pagination" className={cn("inline-flex items-center gap-1", className)}>
2473
+ <button
2474
+ type="button"
2475
+ onClick={() => onPageChange(clampedPage - 1)}
2476
+ disabled={clampedPage <= 1}
2477
+ className="rounded-md border border-slate-200 px-2.5 py-1.5 text-xs text-slate-700 transition-all hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-[#1f2937] dark:text-slate-200 dark:hover:bg-[#161b22]"
2478
+ >
2479
+ Prev
2480
+ </button>
2481
+
2482
+ {range.map((token, index) =>
2483
+ token === "ellipsis" ? (
2484
+ <span key={\`ellipsis-\${index}\`} className="px-1 text-xs text-slate-400">
2485
+ ...
2486
+ </span>
2487
+ ) : (
2488
+ <button
2489
+ key={token}
2490
+ type="button"
2491
+ onClick={() => onPageChange(token)}
2492
+ aria-current={token === clampedPage ? "page" : undefined}
2493
+ className={cn(
2494
+ "min-w-8 rounded-md px-2.5 py-1.5 text-xs font-medium transition-all",
2495
+ token === clampedPage
2496
+ ? "bg-primary text-white"
2497
+ : "border border-slate-200 text-slate-700 hover:bg-slate-50 dark:border-[#1f2937] dark:text-slate-200 dark:hover:bg-[#161b22]"
2498
+ )}
2499
+ >
2500
+ {token}
2501
+ </button>
2502
+ )
2503
+ )}
2504
+
2505
+ <button
2506
+ type="button"
2507
+ onClick={() => onPageChange(clampedPage + 1)}
2508
+ disabled={clampedPage >= totalPages}
2509
+ className="rounded-md border border-slate-200 px-2.5 py-1.5 text-xs text-slate-700 transition-all hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-[#1f2937] dark:text-slate-200 dark:hover:bg-[#161b22]"
2510
+ >
2511
+ Next
2512
+ </button>
2513
+ </nav>
2514
+ );
2515
+ }`,
2516
+ "Popover": `import { createContext, useCallback, useContext, useEffect, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
2517
+ import { cn } from "@/lib/utils";
2518
+
2519
+ type PopoverSide = "top" | "bottom";
2520
+ type PopoverAlign = "start" | "center" | "end";
2521
+
2522
+ interface PopoverContextValue {
2523
+ open: boolean;
2524
+ setOpen: (open: boolean) => void;
2525
+ side: PopoverSide;
2526
+ align: PopoverAlign;
2527
+ }
2528
+
2529
+ const PopoverContext = createContext<PopoverContextValue | null>(null);
2530
+
2531
+ function usePopoverContext() {
2532
+ const context = useContext(PopoverContext);
2533
+ if (!context) throw new Error("Popover sub-components must be used inside <Popover>");
2534
+ return context;
2535
+ }
2536
+
2537
+ export interface PopoverProps {
2538
+ open?: boolean;
2539
+ defaultOpen?: boolean;
2540
+ onOpenChange?: (open: boolean) => void;
2541
+ side?: PopoverSide;
2542
+ align?: PopoverAlign;
2543
+ children: ReactNode;
2544
+ className?: string;
2545
+ }
2546
+
2547
+ export interface PopoverTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
2548
+ children: ReactNode;
2549
+ }
2550
+
2551
+ export interface PopoverContentProps extends HTMLAttributes<HTMLDivElement> {
2552
+ children: ReactNode;
2553
+ }
2554
+
2555
+ export interface PopoverCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
2556
+ children?: ReactNode;
2557
+ }
2558
+
2559
+ export function Popover({
2560
+ open,
2561
+ defaultOpen = false,
2562
+ onOpenChange,
2563
+ side = "bottom",
2564
+ align = "start",
2565
+ children,
2566
+ className,
2567
+ }: PopoverProps) {
2568
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
2569
+ const containerRef = useRef<HTMLDivElement>(null);
2570
+ const isControlled = open !== undefined;
2571
+ const isOpen = isControlled ? open : internalOpen;
2572
+
2573
+ const setOpen = useCallback((next: boolean) => {
2574
+ if (!isControlled) setInternalOpen(next);
2575
+ onOpenChange?.(next);
2576
+ }, [isControlled, onOpenChange]);
2577
+
2578
+ useEffect(() => {
2579
+ if (!isOpen) return;
2580
+
2581
+ const handleOutside = (event: MouseEvent) => {
2582
+ if (!containerRef.current?.contains(event.target as Node)) {
2583
+ setOpen(false);
2584
+ }
2585
+ };
2586
+
2587
+ const handleEscape = (event: KeyboardEvent) => {
2588
+ if (event.key === "Escape") setOpen(false);
2589
+ };
2590
+
2591
+ window.addEventListener("mousedown", handleOutside);
2592
+ window.addEventListener("keydown", handleEscape);
2593
+
2594
+ return () => {
2595
+ window.removeEventListener("mousedown", handleOutside);
2596
+ window.removeEventListener("keydown", handleEscape);
2597
+ };
2598
+ }, [isOpen, setOpen]);
2599
+
2600
+ return (
2601
+ <PopoverContext.Provider value={{ open: isOpen, setOpen, side, align }}>
2602
+ <div ref={containerRef} className={cn("relative inline-block", className)}>
2603
+ {children}
2604
+ </div>
2605
+ </PopoverContext.Provider>
2606
+ );
2607
+ }
2608
+
2609
+ Popover.Trigger = function PopoverTrigger({ children, className, onClick, ...props }: PopoverTriggerProps) {
2610
+ const { open, setOpen } = usePopoverContext();
2611
+
2612
+ return (
2613
+ <button
2614
+ type="button"
2615
+ onClick={(event) => {
2616
+ onClick?.(event);
2617
+ setOpen(!open);
2618
+ }}
2619
+ className={cn(
2620
+ "inline-flex items-center justify-center gap-2 rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 transition-all",
2621
+ "hover:bg-slate-50 dark:border-[#1f2937] dark:text-slate-200 dark:hover:bg-[#161b22]",
2622
+ className
2623
+ )}
2624
+ {...props}
2625
+ >
2626
+ {children}
2627
+ </button>
2628
+ );
2629
+ }
2630
+
2631
+ const SIDE_CLASS: Record<PopoverSide, string> = {
2632
+ top: "bottom-[calc(100%+8px)]",
2633
+ bottom: "top-[calc(100%+8px)]",
2634
+ };
2635
+
2636
+ const ALIGN_CLASS: Record<PopoverAlign, string> = {
2637
+ start: "left-0",
2638
+ center: "left-1/2 -translate-x-1/2",
2639
+ end: "right-0",
2640
+ };
2641
+
2642
+ Popover.Content = function PopoverContent({ children, className, ...props }: PopoverContentProps) {
2643
+ const { open, side, align } = usePopoverContext();
2644
+
2645
+ if (!open) return null;
2646
+
2647
+ return (
2648
+ <div
2649
+ className={cn(
2650
+ "absolute z-50 w-72 rounded-xl border border-slate-200 bg-white p-4 shadow-xl dark:border-[#1f2937] dark:bg-[#161b22]",
2651
+ SIDE_CLASS[side],
2652
+ ALIGN_CLASS[align],
2653
+ className
2654
+ )}
2655
+ {...props}
2656
+ >
2657
+ {children}
2658
+ </div>
2659
+ );
2660
+ }
2661
+
2662
+ Popover.Close = function PopoverClose({ children = "Close", className, onClick, ...props }: PopoverCloseProps) {
2663
+ const { setOpen } = usePopoverContext();
2664
+
2665
+ return (
2666
+ <button
2667
+ type="button"
2668
+ onClick={(event) => {
2669
+ onClick?.(event);
2670
+ setOpen(false);
2671
+ }}
2672
+ className={cn("text-sm font-medium text-primary hover:underline", className)}
2673
+ {...props}
2674
+ >
2675
+ {children}
2676
+ </button>
2677
+ );
2678
+ }`,
2679
+ "Progress": `import type { HTMLAttributes } from "react";
2680
+ import { cn } from "@/lib/utils";
2681
+
2682
+ export interface ProgressProps extends HTMLAttributes<HTMLDivElement> {
2683
+ value: number;
2684
+ max?: number;
2685
+ }
2686
+
2687
+ export function Progress({ value, max = 100, className, ...props }: ProgressProps) {
2688
+ const safeMax = max > 0 ? max : 100;
2689
+ const clamped = Math.min(Math.max(value, 0), safeMax);
2690
+ const percentage = (clamped / safeMax) * 100;
2691
+
2692
+ return (
2693
+ <div
2694
+ role="progressbar"
2695
+ aria-valuemin={0}
2696
+ aria-valuemax={safeMax}
2697
+ aria-valuenow={Math.round(clamped)}
2698
+ className={cn("h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-[#1f2937]", className)}
2699
+ {...props}
2700
+ >
2701
+ <div
2702
+ className="h-full rounded-full bg-primary transition-all duration-300"
2703
+ style={{ width: \`\${percentage}%\` }}
2704
+ />
2705
+ </div>
2706
+ );
2707
+ }`,
2708
+ "ProgressBar": `import { cn } from "@/lib/utils";
2709
+
2710
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2711
+
2712
+ export interface ProgressBarProps {
2713
+ /** Current progress 0\u2013100 */
2714
+ progress: number;
2715
+ /** Whether the bar is visible (false triggers fade-out) */
2716
+ visible: boolean;
2717
+ }
2718
+
2719
+ // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2720
+
2721
+ /**
2722
+ * Thin top-of-page progress bar for navigation feedback.
2723
+ * Pair with \`useProgressBar\` hook.
2724
+ */
2725
+ export function ProgressBar({ progress, visible }: ProgressBarProps) {
2726
+ return (
2727
+ <div
2728
+ role="progressbar"
2729
+ aria-valuemin={0}
2730
+ aria-valuemax={100}
2731
+ aria-valuenow={progress}
2732
+ aria-hidden="true"
2733
+ className={cn(
2734
+ "pointer-events-none fixed left-0 top-0 z-200 h-0.5 bg-primary",
2735
+ // Glow effect matching the primary color
2736
+ "shadow-[0_0_8px_1px_rgba(70,40,241,0.7)]",
2737
+ "transition-opacity duration-300",
2738
+ visible ? "opacity-100" : "opacity-0"
2739
+ )}
2740
+ style={{
2741
+ width: \`\${progress}%\`,
2742
+ // Instant reset to 0, ease-out for forward progress
2743
+ transition: \`width \${progress === 0 ? "0ms" : progress === 100 ? "150ms" : "350ms"} ease-out, opacity 300ms\`,
2744
+ }}
2745
+ />
2746
+ );
2747
+ }`,
2748
+ "RadioGroup": `import { createContext, useContext, useId, useState, type HTMLAttributes, type ReactNode } from "react";
2749
+ import { cn } from "@/lib/utils";
2750
+
2751
+ export interface RadioGroupProps extends HTMLAttributes<HTMLDivElement> {
2752
+ value?: string;
2753
+ defaultValue?: string;
2754
+ onValueChange?: (value: string) => void;
2755
+ name?: string;
2756
+ children: ReactNode;
2757
+ }
2758
+
2759
+ export interface RadioGroupItemProps extends HTMLAttributes<HTMLButtonElement> {
2760
+ value: string;
2761
+ disabled?: boolean;
2762
+ label?: string;
2763
+ description?: string;
2764
+ }
2765
+
2766
+ interface RadioGroupContextValue {
2767
+ value: string;
2768
+ name: string;
2769
+ setValue: (value: string) => void;
2770
+ }
2771
+
2772
+ const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
2773
+
2774
+ function useRadioGroupContext() {
2775
+ const context = useContext(RadioGroupContext);
2776
+ if (!context) {
2777
+ throw new Error("RadioGroup sub-components must be used inside <RadioGroup>");
2778
+ }
2779
+ return context;
2780
+ }
2781
+
2782
+ export function RadioGroup({
2783
+ value,
2784
+ defaultValue = "",
2785
+ onValueChange,
2786
+ name,
2787
+ children,
2788
+ className,
2789
+ ...props
2790
+ }: RadioGroupProps) {
2791
+ const [internalValue, setInternalValue] = useState(defaultValue);
2792
+ const autoName = useId();
2793
+ const isControlled = value !== undefined;
2794
+ const active = isControlled ? value : internalValue;
2795
+
2796
+ const setValue = (next: string) => {
2797
+ if (!isControlled) setInternalValue(next);
2798
+ onValueChange?.(next);
2799
+ };
2800
+
2801
+ return (
2802
+ <RadioGroupContext.Provider value={{ value: active, setValue, name: name ?? autoName }}>
2803
+ <div className={cn("space-y-2", className)} role="radiogroup" {...props}>
2804
+ {children}
2805
+ </div>
2806
+ </RadioGroupContext.Provider>
2807
+ );
2808
+ }
2809
+
2810
+ RadioGroup.Item = function RadioGroupItem({
2811
+ value,
2812
+ disabled = false,
2813
+ label,
2814
+ description,
2815
+ children,
2816
+ className,
2817
+ ...props
2818
+ }: RadioGroupItemProps) {
2819
+ const context = useRadioGroupContext();
2820
+ const checked = context.value === value;
2821
+
2822
+ return (
2823
+ <button
2824
+ type="button"
2825
+ role="radio"
2826
+ aria-checked={checked}
2827
+ disabled={disabled}
2828
+ onClick={() => !disabled && context.setValue(value)}
2829
+ className={cn(
2830
+ "flex w-full items-start gap-3 rounded-lg border p-3 text-left transition-all",
2831
+ checked
2832
+ ? "border-primary bg-primary/5"
2833
+ : "border-slate-200 bg-white hover:border-slate-300 dark:border-[#1f2937] dark:bg-[#0d1117] dark:hover:border-slate-600",
2834
+ disabled && "cursor-not-allowed opacity-50",
2835
+ className
2836
+ )}
2837
+ {...props}
2838
+ >
2839
+ <span
2840
+ className={cn(
2841
+ "mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border",
2842
+ checked ? "border-primary" : "border-slate-400 dark:border-slate-500"
2843
+ )}
2844
+ >
2845
+ {checked && <span className="h-2 w-2 rounded-full bg-primary" />}
2846
+ </span>
2847
+ <span className="min-w-0 flex-1">
2848
+ {label && <span className="block text-sm font-medium text-slate-900 dark:text-white">{label}</span>}
2849
+ {description && <span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">{description}</span>}
2850
+ {children}
2851
+ </span>
2852
+ <input readOnly tabIndex={-1} type="radio" name={context.name} value={value} checked={checked} className="sr-only" />
2853
+ </button>
2854
+ );
2855
+ }`,
2856
+ "SearchDialog": `import { useEffect, useRef, useState } from "react";
2857
+ import { useAnimatedMount } from "@/hooks/use-animated-mount";
2858
+ import { cn } from "@/lib/utils";
2859
+
2860
+ export interface SearchItem {
2861
+ title: string;
2862
+ href: string;
2863
+ description?: string;
2864
+ group: "Docs" | "Cookbook";
2865
+ section?: string;
2866
+ icon?: string;
2867
+ }
2868
+
2869
+ interface SearchDialogProps {
2870
+ open: boolean;
2871
+ items: SearchItem[];
2872
+ onClose: () => void;
2873
+ onNavigate: (href: string) => void;
2874
+ savedSlugs?: string[];
2875
+ }
2876
+
2877
+ export function SearchDialog({ open, items, onClose, onNavigate, savedSlugs = [] }: SearchDialogProps) {
2878
+ const [query, setQuery] = useState("");
2879
+ const [activeIndex, setActiveIndex] = useState(0);
2880
+ const inputRef = useRef<HTMLInputElement>(null);
2881
+ const { mounted, visible } = useAnimatedMount(open, 150);
2882
+
2883
+ const results = query.trim()
2884
+ ? items.filter(
2885
+ (item) =>
2886
+ item.title.toLowerCase().includes(query.toLowerCase()) ||
2887
+ item.description?.toLowerCase().includes(query.toLowerCase()),
2888
+ )
2889
+ : items.slice(0, 8);
2890
+
2891
+ const docsResults = results.filter((r) => r.group === "Docs");
2892
+ const cookbookResults = results.filter((r) => r.group === "Cookbook");
2893
+
2894
+ // Focus input and reset state on open
2895
+ useEffect(() => {
2896
+ if (open) {
2897
+ setQuery("");
2898
+ setActiveIndex(0);
2899
+ setTimeout(() => inputRef.current?.focus(), 50);
2900
+ }
2901
+ }, [open]);
2902
+
2903
+ // Reset active index when results change
2904
+ useEffect(() => {
2905
+ setActiveIndex(0);
2906
+ }, [query]);
2907
+
2908
+ // Keyboard navigation
2909
+ useEffect(() => {
2910
+ if (!open) return;
2911
+ const handler = (e: KeyboardEvent) => {
2912
+ if (e.key === "ArrowDown") {
2913
+ e.preventDefault();
2914
+ setActiveIndex((i) => Math.min(i + 1, results.length - 1));
2915
+ } else if (e.key === "ArrowUp") {
2916
+ e.preventDefault();
2917
+ setActiveIndex((i) => Math.max(i - 1, 0));
2918
+ } else if (e.key === "Enter") {
2919
+ const item = results[activeIndex];
2920
+ if (item) {
2921
+ onNavigate(item.href);
2922
+ onClose();
2923
+ }
2924
+ } else if (e.key === "Escape") {
2925
+ onClose();
2926
+ }
2927
+ };
2928
+ window.addEventListener("keydown", handler);
2929
+ return () => window.removeEventListener("keydown", handler);
2930
+ }, [open, results, activeIndex, onNavigate, onClose]);
2931
+
2932
+ if (!mounted) return null;
2933
+
2934
+ return (
2935
+ <div
2936
+ className={cn(
2937
+ "fixed inset-0 z-50 flex items-start justify-center pt-[15vh] transition-opacity duration-150",
2938
+ visible ? "opacity-100" : "opacity-0",
2939
+ )}
2940
+ >
2941
+ {/* Backdrop */}
2942
+ <div
2943
+ className="absolute inset-0 bg-black/50 backdrop-blur-xs"
2944
+ onClick={onClose}
2945
+ />
2946
+
2947
+ {/* Panel */}
2948
+ <div
2949
+ className={cn(
2950
+ "relative z-10 mx-4 w-full max-w-xl rounded-xl border border-slate-200 dark:border-[#1f2937] bg-white dark:bg-[#161b22] shadow-2xl transition-all duration-150",
2951
+ visible ? "scale-100 opacity-100" : "scale-95 opacity-0",
2952
+ )}
2953
+ >
2954
+ {/* Input */}
2955
+ <div className="flex items-center gap-3 border-b border-slate-100 dark:border-[#1f2937] px-4 py-3.5">
2956
+ <span className="material-symbols-outlined text-[20px] text-slate-400">
2957
+ search
2958
+ </span>
2959
+ <input
2960
+ ref={inputRef}
2961
+ value={query}
2962
+ onChange={(e) => setQuery(e.target.value)}
2963
+ placeholder="Search docs, components, patterns..."
2964
+ className="flex-1 bg-transparent text-sm text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-hidden"
2965
+ />
2966
+ {query ? (
2967
+ <button
2968
+ onClick={() => setQuery("")}
2969
+ className="text-slate-400 transition-colors hover:text-slate-600 dark:hover:text-slate-200"
2970
+ >
2971
+ <span className="material-symbols-outlined text-[18px]">close</span>
2972
+ </button>
2973
+ ) : (
2974
+ <kbd className="hidden rounded-sm border border-slate-200 dark:border-[#1f2937] px-1.5 py-0.5 text-[10px] font-medium text-slate-400 sm:inline-flex">
2975
+ ESC
2976
+ </kbd>
2977
+ )}
2978
+ </div>
2979
+
2980
+ {/* Results */}
2981
+ <div className="max-h-[55vh] overflow-y-auto p-2">
2982
+ {results.length === 0 ? (
2983
+ <div className="flex flex-col items-center py-12 text-slate-400">
2984
+ <span className="material-symbols-outlined mb-2 text-[40px]">
2985
+ search_off
2986
+ </span>
2987
+ <p className="text-sm">No results for &ldquo;{query}&rdquo;</p>
2988
+ </div>
2989
+ ) : (
2990
+ <>
2991
+ {docsResults.length > 0 && (
2992
+ <ResultGroup
2993
+ title="Docs"
2994
+ items={docsResults}
2995
+ allResults={results}
2996
+ query={query}
2997
+ activeIndex={activeIndex}
2998
+ savedSlugs={savedSlugs}
2999
+ onSelect={(href) => {
3000
+ onNavigate(href);
3001
+ onClose();
3002
+ }}
3003
+ onHover={setActiveIndex}
3004
+ />
3005
+ )}
3006
+ {cookbookResults.length > 0 && (
3007
+ <ResultGroup
3008
+ title="Cookbook"
3009
+ items={cookbookResults}
3010
+ allResults={results}
3011
+ query={query}
3012
+ activeIndex={activeIndex}
3013
+ savedSlugs={savedSlugs}
3014
+ onSelect={(href) => {
3015
+ onNavigate(href);
3016
+ onClose();
3017
+ }}
3018
+ onHover={setActiveIndex}
3019
+ />
3020
+ )}
3021
+ </>
3022
+ )}
3023
+ </div>
3024
+
3025
+ {/* Footer */}
3026
+ <div className="flex items-center justify-between border-t border-slate-100 dark:border-[#1f2937] px-4 py-2.5 text-[11px] text-slate-400">
3027
+ <div className="flex items-center gap-3">
3028
+ <span className="flex items-center gap-1">
3029
+ <kbd className="rounded-sm border border-slate-200 dark:border-[#2d3748] px-1 py-0.5 font-mono">
3030
+ \u2191
3031
+ </kbd>
3032
+ <kbd className="rounded-sm border border-slate-200 dark:border-[#2d3748] px-1 py-0.5 font-mono">
3033
+ \u2193
3034
+ </kbd>
3035
+ navigate
3036
+ </span>
3037
+ <span className="flex items-center gap-1">
3038
+ <kbd className="rounded-sm border border-slate-200 dark:border-[#2d3748] px-1 py-0.5 font-mono">
3039
+ \u21B5
3040
+ </kbd>
3041
+ select
3042
+ </span>
3043
+ </div>
3044
+ <span>
3045
+ {results.length} result{results.length !== 1 ? "s" : ""}
3046
+ </span>
3047
+ </div>
3048
+ </div>
3049
+ </div>
3050
+ );
3051
+ }
3052
+
3053
+ // --- helpers ---
3054
+
3055
+ function highlight(text: string, query: string) {
3056
+ if (!query.trim()) return text;
3057
+ const idx = text.toLowerCase().indexOf(query.toLowerCase());
3058
+ if (idx === -1) return text;
3059
+ return (
3060
+ <>
3061
+ {text.slice(0, idx)}
3062
+ <mark className="rounded-xs bg-primary/20 px-0.5 font-semibold text-primary not-italic">
3063
+ {text.slice(idx, idx + query.length)}
3064
+ </mark>
3065
+ {text.slice(idx + query.length)}
3066
+ </>
3067
+ );
3068
+ }
3069
+
3070
+ interface ResultGroupProps {
3071
+ title: string;
3072
+ items: SearchItem[];
3073
+ allResults: SearchItem[];
3074
+ query: string;
3075
+ activeIndex: number;
3076
+ savedSlugs: string[];
3077
+ onSelect: (href: string) => void;
3078
+ onHover: (index: number) => void;
3079
+ }
3080
+
3081
+ function ResultGroup({
3082
+ title,
3083
+ items,
3084
+ allResults,
3085
+ query,
3086
+ activeIndex,
3087
+ savedSlugs,
3088
+ onSelect,
3089
+ onHover,
3090
+ }: ResultGroupProps) {
3091
+ return (
3092
+ <div className="mb-1">
3093
+ <p className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">
3094
+ {title}
3095
+ </p>
3096
+ {items.map((item) => {
3097
+ const globalIdx = allResults.indexOf(item);
3098
+ const isActive = globalIdx === activeIndex;
3099
+ const cookbookMatch = item.href.match(/\\/(nextjs|vitejs)\\/cookbook\\/(.+)/);
3100
+ const slug = cookbookMatch ? cookbookMatch[2] : null;
3101
+ const isSaved = slug ? savedSlugs.includes(slug) : false;
3102
+ return (
3103
+ <button
3104
+ key={item.href}
3105
+ onMouseEnter={() => onHover(globalIdx)}
3106
+ onClick={() => onSelect(item.href)}
3107
+ className={cn(
3108
+ "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors",
3109
+ isActive
3110
+ ? "bg-primary/10 dark:bg-primary/20"
3111
+ : "hover:bg-slate-50 dark:hover:bg-[#0d1117]",
3112
+ )}
3113
+ >
3114
+ <span
3115
+ className={cn(
3116
+ "material-symbols-outlined shrink-0 text-[18px]",
3117
+ isActive ? "text-primary" : "text-slate-400",
3118
+ )}
3119
+ >
3120
+ {item.icon ?? (title === "Docs" ? "article" : "menu_book")}
3121
+ </span>
3122
+ <div className="min-w-0 flex-1">
3123
+ <p
3124
+ className={cn(
3125
+ "flex items-center gap-1.5 truncate text-sm font-medium",
3126
+ isActive ? "text-primary" : "text-slate-900 dark:text-white",
3127
+ )}
3128
+ >
3129
+ {highlight(item.title, query)}
3130
+ {isSaved && (
3131
+ <span
3132
+ className="material-symbols-outlined shrink-0 text-[13px] text-primary"
3133
+ style={{ fontVariationSettings: "'FILL' 1" }}
3134
+ >
3135
+ bookmark
3136
+ </span>
3137
+ )}
3138
+ </p>
3139
+ {item.description && (
3140
+ <p className="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
3141
+ {highlight(item.description, query)}
3142
+ </p>
3143
+ )}
3144
+ {item.section && !item.description && (
3145
+ <p className="mt-0.5 truncate text-xs text-slate-400 dark:text-slate-500">
3146
+ {item.section}
3147
+ </p>
3148
+ )}
3149
+ </div>
3150
+ {isActive && (
3151
+ <span className="material-symbols-outlined shrink-0 text-[16px] text-primary">
3152
+ arrow_forward
3153
+ </span>
3154
+ )}
3155
+ </button>
3156
+ );
3157
+ })}
3158
+ </div>
3159
+ );
3160
+ }`,
3161
+ "Select": `import { forwardRef, type SelectHTMLAttributes } from "react";
3162
+ import { cn } from "@/lib/utils";
3163
+
3164
+ export type SelectSize = "sm" | "md" | "lg";
3165
+
3166
+ export interface SelectOption {
3167
+ label: string;
3168
+ value: string;
3169
+ disabled?: boolean;
3170
+ }
3171
+
3172
+ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
3173
+ label?: string;
3174
+ description?: string;
3175
+ error?: string;
3176
+ size?: SelectSize;
3177
+ options?: SelectOption[];
3178
+ placeholder?: string;
3179
+ }
3180
+
3181
+ const SIZE_CLASSES: Record<SelectSize, string> = {
3182
+ sm: "h-8 px-3 pr-9 text-xs",
3183
+ md: "h-10 px-3.5 pr-10 text-sm",
3184
+ lg: "h-12 px-4 pr-11 text-base",
3185
+ };
3186
+
3187
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function SelectRoot(
3188
+ { label, description, error, size = "md", options, placeholder, className, id, children, ...props },
3189
+ ref
3190
+ ) {
3191
+ const inputId = id ?? (label ? label.toLowerCase().replace(/\\s+/g, "-") : undefined);
3192
+
3193
+ return (
3194
+ <div className={cn("flex flex-col gap-1.5", className)}>
3195
+ {label && <label htmlFor={inputId} className="text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}
3196
+
3197
+ <div className="relative">
3198
+ <select
3199
+ ref={ref}
3200
+ id={inputId}
3201
+ className={cn(
3202
+ "w-full appearance-none rounded-lg border bg-white outline-hidden transition-all",
3203
+ "border-slate-200 text-slate-900 hover:border-slate-300",
3204
+ "focus:border-primary focus:ring-2 focus:ring-primary/20",
3205
+ "dark:border-[#1f2937] dark:bg-[#0d1117] dark:text-white dark:hover:border-slate-600",
3206
+ error && "border-red-400 focus:border-red-400 focus:ring-red-400/20 dark:border-red-500",
3207
+ SIZE_CLASSES[size]
3208
+ )}
3209
+ {...props}
3210
+ >
3211
+ {placeholder && <option value="">{placeholder}</option>}
3212
+ {options?.map((option) => (
3213
+ <option key={option.value} value={option.value} disabled={option.disabled}>
3214
+ {option.label}
3215
+ </option>
3216
+ ))}
3217
+ {children}
3218
+ </select>
3219
+ <span className="material-symbols-outlined pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[18px] text-slate-400">
3220
+ expand_more
3221
+ </span>
3222
+ </div>
3223
+
3224
+ {description && !error && <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>}
3225
+ {error && <p className="text-xs text-red-500 dark:text-red-400">{error}</p>}
3226
+ </div>
3227
+ );
3228
+ });`,
3229
+ "Separator": `import type { HTMLAttributes } from "react";
3230
+ import { cn } from "@/lib/utils";
3231
+
3232
+ export type SeparatorOrientation = "horizontal" | "vertical";
3233
+
3234
+ export interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
3235
+ orientation?: SeparatorOrientation;
3236
+ decorative?: boolean;
3237
+ }
3238
+
3239
+ export function Separator({
3240
+ orientation = "horizontal",
3241
+ decorative = true,
3242
+ className,
3243
+ ...props
3244
+ }: SeparatorProps) {
3245
+ return (
3246
+ <div
3247
+ role={decorative ? "presentation" : "separator"}
3248
+ aria-orientation={orientation}
3249
+ className={cn(
3250
+ "shrink-0 bg-slate-200 dark:bg-[#1f2937]",
3251
+ orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
3252
+ className
3253
+ )}
3254
+ {...props}
3255
+ />
3256
+ );
3257
+ }`,
3258
+ "Skeleton": `import { cn } from "@/lib/utils";
3259
+
3260
+ export type SkeletonVariant = "line" | "rect" | "circle";
3261
+
3262
+ export interface SkeletonProps {
3263
+ variant?: SkeletonVariant;
3264
+ width?: number | string;
3265
+ height?: number | string;
3266
+ className?: string;
3267
+ }
3268
+
3269
+ export function Skeleton({ variant = "line", width, height, className }: SkeletonProps) {
3270
+ return (
3271
+ <span
3272
+ aria-hidden="true"
3273
+ className={cn(
3274
+ "inline-block animate-pulse bg-slate-200 dark:bg-[#1f2937]",
3275
+ variant === "line" && "h-4 w-40 rounded-md",
3276
+ variant === "rect" && "h-24 w-full rounded-xl",
3277
+ variant === "circle" && "h-10 w-10 rounded-full",
3278
+ className
3279
+ )}
3280
+ style={{ width, height }}
3281
+ />
3282
+ );
3283
+ }`,
3284
+ "Slider": `import { useState } from "react";
3285
+ import { cn } from "@/lib/utils";
3286
+
3287
+ export interface SliderProps {
3288
+ value?: number;
3289
+ defaultValue?: number;
3290
+ min?: number;
3291
+ max?: number;
3292
+ step?: number;
3293
+ onValueChange?: (value: number) => void;
3294
+ label?: string;
3295
+ showValue?: boolean;
3296
+ className?: string;
3297
+ }
3298
+
3299
+ export function Slider({
3300
+ value,
3301
+ defaultValue = 50,
3302
+ min = 0,
3303
+ max = 100,
3304
+ step = 1,
3305
+ onValueChange,
3306
+ label,
3307
+ showValue = true,
3308
+ className,
3309
+ }: SliderProps) {
3310
+ const [internalValue, setInternalValue] = useState(defaultValue);
3311
+ const isControlled = value !== undefined;
3312
+ const current = isControlled ? value : internalValue;
3313
+
3314
+ return (
3315
+ <div className={cn("w-full", className)}>
3316
+ {(label || showValue) && (
3317
+ <div className="mb-2 flex items-center justify-between gap-3">
3318
+ {label && <label className="text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}
3319
+ {showValue && <span className="text-xs text-slate-500 dark:text-slate-400">{Math.round(current)}</span>}
3320
+ </div>
3321
+ )}
3322
+
3323
+ <input
3324
+ type="range"
3325
+ min={min}
3326
+ max={max}
3327
+ step={step}
3328
+ value={current}
3329
+ onChange={(event) => {
3330
+ const next = Number(event.target.value);
3331
+ if (!isControlled) setInternalValue(next);
3332
+ onValueChange?.(next);
3333
+ }}
3334
+ className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-200 accent-primary dark:bg-[#1f2937]"
3335
+ />
3336
+ </div>
3337
+ );
3338
+ }`,
3339
+ "Switch": `import { useMemo, useState } from "react";
3340
+ import { cn } from "@/lib/utils";
3341
+
3342
+ export type SwitchSize = "sm" | "md" | "lg";
3343
+
3344
+ export interface SwitchProps {
3345
+ checked?: boolean;
3346
+ defaultChecked?: boolean;
3347
+ onChange?: (checked: boolean) => void;
3348
+ disabled?: boolean;
3349
+ size?: SwitchSize;
3350
+ label?: string;
3351
+ description?: string;
3352
+ className?: string;
3353
+ }
3354
+
3355
+ const TRACK_SIZES: Record<SwitchSize, string> = {
3356
+ sm: "h-5 w-9",
3357
+ md: "h-6 w-11",
3358
+ lg: "h-7 w-14",
3359
+ };
3360
+
3361
+ const THUMB_SIZES: Record<SwitchSize, string> = {
3362
+ sm: "h-4 w-4 data-[checked=true]:translate-x-4",
3363
+ md: "h-5 w-5 data-[checked=true]:translate-x-5",
3364
+ lg: "h-6 w-6 data-[checked=true]:translate-x-7",
3365
+ };
3366
+
3367
+ export function Switch({
3368
+ checked,
3369
+ defaultChecked = false,
3370
+ onChange,
3371
+ disabled = false,
3372
+ size = "md",
3373
+ label,
3374
+ description,
3375
+ className,
3376
+ }: SwitchProps) {
3377
+ const [internal, setInternal] = useState(defaultChecked);
3378
+ const isControlled = checked !== undefined;
3379
+ const isOn = isControlled ? checked : internal;
3380
+
3381
+ const stateLabel = useMemo(() => (isOn ? "enabled" : "disabled"), [isOn]);
3382
+
3383
+ const toggle = () => {
3384
+ if (disabled) return;
3385
+ const next = !isOn;
3386
+ if (!isControlled) setInternal(next);
3387
+ onChange?.(next);
3388
+ };
3389
+
3390
+ return (
3391
+ <div className={cn("inline-flex items-start gap-3", className)}>
3392
+ <button
3393
+ type="button"
3394
+ role="switch"
3395
+ aria-checked={isOn}
3396
+ aria-label={label ?? "Switch"}
3397
+ aria-disabled={disabled}
3398
+ data-state={isOn ? "checked" : "unchecked"}
3399
+ onClick={toggle}
3400
+ className={cn(
3401
+ "relative shrink-0 rounded-full p-0.5 transition-all outline-hidden",
3402
+ "focus-visible:ring-2 focus-visible:ring-primary/40",
3403
+ TRACK_SIZES[size],
3404
+ isOn ? "bg-primary" : "bg-slate-300 dark:bg-slate-700",
3405
+ disabled && "cursor-not-allowed opacity-50"
3406
+ )}
3407
+ >
3408
+ <span
3409
+ data-checked={isOn}
3410
+ className={cn(
3411
+ "block rounded-full bg-white shadow-sm transition-transform duration-200",
3412
+ THUMB_SIZES[size]
3413
+ )}
3414
+ />
3415
+ </button>
3416
+
3417
+ {(label ?? description) && (
3418
+ <div className="min-w-0">
3419
+ {label && <p className="text-sm font-medium text-slate-900 dark:text-white">{label}</p>}
3420
+ {description && (
3421
+ <p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
3422
+ {description} ({stateLabel})
3423
+ </p>
3424
+ )}
3425
+ </div>
3426
+ )}
3427
+ </div>
3428
+ );
3429
+ }`,
3430
+ "Tabs": `import {
3431
+ createContext,
3432
+ useContext,
3433
+ useState,
3434
+ type ButtonHTMLAttributes,
3435
+ type HTMLAttributes,
3436
+ type ReactNode,
3437
+ } from "react";
3438
+ import { cn } from "@/lib/utils";
3439
+
3440
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3441
+
3442
+ export type TabsVariant = "underline" | "pills";
3443
+
3444
+ export interface TabsProps {
3445
+ value?: string;
3446
+ defaultValue?: string;
3447
+ onChange?: (value: string) => void;
3448
+ variant?: TabsVariant;
3449
+ children: ReactNode;
3450
+ className?: string;
3451
+ }
3452
+
3453
+ export interface TabsListProps extends HTMLAttributes<HTMLDivElement> {
3454
+ children: ReactNode;
3455
+ }
3456
+
3457
+ export interface TabsTriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "value"> {
3458
+ value: string;
3459
+ children: ReactNode;
3460
+ }
3461
+
3462
+ export interface TabsContentProps extends HTMLAttributes<HTMLDivElement> {
3463
+ value: string;
3464
+ children: ReactNode;
3465
+ }
3466
+
3467
+ // \u2500\u2500\u2500 Context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3468
+
3469
+ interface TabsContextValue {
3470
+ active: string;
3471
+ setActive: (value: string) => void;
3472
+ variant: TabsVariant;
3473
+ }
3474
+
3475
+ const TabsContext = createContext<TabsContextValue | null>(null);
3476
+
3477
+ function useTabsContext() {
3478
+ const ctx = useContext(TabsContext);
3479
+ if (!ctx) throw new Error("Tabs sub-components must be used inside <Tabs> or <Tabs>");
3480
+ return ctx;
3481
+ }
3482
+
3483
+ // \u2500\u2500\u2500 Components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3484
+
3485
+ export function Tabs({
3486
+ value,
3487
+ defaultValue = "",
3488
+ onChange,
3489
+ variant = "underline",
3490
+ children,
3491
+ className,
3492
+ }: TabsProps) {
3493
+ const [internalValue, setInternalValue] = useState(defaultValue);
3494
+ const isControlled = value !== undefined;
3495
+ const active = isControlled ? value : internalValue;
3496
+
3497
+ const setActive = (next: string) => {
3498
+ if (!isControlled) setInternalValue(next);
3499
+ onChange?.(next);
3500
+ };
3501
+
3502
+ return (
3503
+ <TabsContext.Provider value={{ active, setActive, variant }}>
3504
+ <div className={cn("w-full", className)}>{children}</div>
3505
+ </TabsContext.Provider>
3506
+ );
3507
+ }
3508
+
3509
+ Tabs.List = function TabsList({ children, className, ...props }: TabsListProps) {
3510
+ const { variant } = useTabsContext();
3511
+
3512
+ return (
3513
+ <div
3514
+ role="tablist"
3515
+ className={cn(
3516
+ "flex",
3517
+ variant === "underline" && "border-b border-slate-200 dark:border-[#1f2937] gap-0",
3518
+ variant === "pills" && "gap-1 p-1 rounded-xl bg-slate-100 dark:bg-[#161b22] w-fit",
3519
+ className
3520
+ )}
3521
+ {...props}
3522
+ >
3523
+ {children}
3524
+ </div>
3525
+ );
3526
+ }
3527
+
3528
+ Tabs.Trigger = function TabsTrigger({ value, children, disabled, className, ...props }: TabsTriggerProps) {
3529
+ const { active, setActive, variant } = useTabsContext();
3530
+ const isActive = active === value;
3531
+
3532
+ return (
3533
+ <button
3534
+ role="tab"
3535
+ aria-selected={isActive}
3536
+ disabled={disabled}
3537
+ onClick={() => setActive(value)}
3538
+ className={cn(
3539
+ "text-sm font-medium transition-all outline-hidden focus-visible:ring-2 focus-visible:ring-primary/40 rounded-sm",
3540
+ disabled && "opacity-40 cursor-not-allowed pointer-events-none",
3541
+
3542
+ // underline variant
3543
+ variant === "underline" && [
3544
+ "px-4 py-2.5 -mb-px border-b-2",
3545
+ isActive
3546
+ ? "border-primary text-primary"
3547
+ : "border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 hover:border-slate-300 dark:hover:border-slate-600",
3548
+ ],
3549
+
3550
+ // pills variant
3551
+ variant === "pills" && [
3552
+ "px-4 py-1.5 rounded-lg",
3553
+ isActive
3554
+ ? "bg-white dark:bg-[#0d1117] text-slate-900 dark:text-white shadow-xs"
3555
+ : "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200",
3556
+ ],
3557
+
3558
+ className
3559
+ )}
3560
+ {...props}
3561
+ >
3562
+ {children}
3563
+ </button>
3564
+ );
3565
+ }
3566
+
3567
+ Tabs.Content = function TabsContent({ value, children, className, ...props }: TabsContentProps) {
3568
+ const { active } = useTabsContext();
3569
+
3570
+ if (active !== value) return null;
3571
+
3572
+ return (
3573
+ <div
3574
+ role="tabpanel"
3575
+ className={cn("mt-4 animate-fade-in", className)}
3576
+ {...props}
3577
+ >
3578
+ {children}
3579
+ </div>
3580
+ );
3581
+ }`,
3582
+ "Textarea": `import { forwardRef, type ReactNode, type TextareaHTMLAttributes } from "react";
3583
+ import { cn } from "@/lib/utils";
3584
+
3585
+ export type TextareaSize = "sm" | "md" | "lg";
3586
+ export type TextareaVariant = "default" | "filled" | "ghost";
3587
+
3588
+ export interface TextareaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "size"> {
3589
+ label?: string;
3590
+ description?: string;
3591
+ error?: string;
3592
+ size?: TextareaSize;
3593
+ variant?: TextareaVariant;
3594
+ children?: ReactNode;
3595
+ }
3596
+
3597
+ const SIZE_CLASSES: Record<TextareaSize, string> = {
3598
+ sm: "min-h-20 px-3 py-2 text-xs",
3599
+ md: "min-h-24 px-3.5 py-2.5 text-sm",
3600
+ lg: "min-h-32 px-4 py-3 text-base",
3601
+ };
3602
+
3603
+ const VARIANT_CLASSES: Record<TextareaVariant, string> = {
3604
+ default:
3605
+ "border border-slate-200 dark:border-[#1f2937] bg-white dark:bg-[#0d1117] hover:border-slate-300 dark:hover:border-slate-600",
3606
+ filled:
3607
+ "border border-transparent bg-slate-100 dark:bg-[#161b22] hover:bg-slate-200/80 dark:hover:bg-[#1f2937]",
3608
+ ghost:
3609
+ "border border-transparent bg-transparent hover:bg-slate-50 dark:hover:bg-[#161b22]",
3610
+ };
3611
+
3612
+ const ERROR_CLASSES =
3613
+ "border-red-400 dark:border-red-500 focus-within:border-red-400 dark:focus-within:border-red-500 focus-within:ring-red-400/20";
3614
+
3615
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function TextareaRoot(
3616
+ {
3617
+ label,
3618
+ description,
3619
+ error,
3620
+ size = "md",
3621
+ variant = "default",
3622
+ disabled,
3623
+ className,
3624
+ id,
3625
+ ...props
3626
+ },
3627
+ ref
3628
+ ) {
3629
+ const inputId = id ?? (label ? label.toLowerCase().replace(/\\s+/g, "-") : undefined);
3630
+
3631
+ return (
3632
+ <div className={cn("flex flex-col gap-1.5", className)}>
3633
+ {label && (
3634
+ <label
3635
+ htmlFor={inputId}
3636
+ className={cn("text-sm font-medium text-slate-700 dark:text-slate-300", disabled && "opacity-50")}
3637
+ >
3638
+ {label}
3639
+ </label>
3640
+ )}
3641
+
3642
+ <div
3643
+ className={cn(
3644
+ "rounded-lg transition-all focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary dark:focus-within:border-primary",
3645
+ VARIANT_CLASSES[variant],
3646
+ error && ERROR_CLASSES,
3647
+ disabled && "opacity-50"
3648
+ )}
3649
+ >
3650
+ <textarea
3651
+ ref={ref}
3652
+ id={inputId}
3653
+ disabled={disabled}
3654
+ className={cn(
3655
+ "w-full resize-y rounded-lg bg-transparent outline-hidden",
3656
+ "text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500",
3657
+ SIZE_CLASSES[size]
3658
+ )}
3659
+ {...props}
3660
+ />
3661
+ </div>
3662
+
3663
+ {description && !error && <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>}
3664
+ {error && <p className="text-xs text-red-500 dark:text-red-400">{error}</p>}
3665
+ </div>
3666
+ );
3667
+ });`,
3668
+ "Toast": `"use client";
3669
+
3670
+ import { createContext, useContext, useEffect, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
3671
+ import { createPortal } from "react-dom";
3672
+ import { useAnimatedMount } from "@/hooks/use-animated-mount";
3673
+ import { cn } from "@/lib/utils";
3674
+
3675
+ export type ToastVariant = "default" | "success" | "warning" | "error";
3676
+ export type ToastPosition = "top-right" | "bottom-right" | "top-left" | "bottom-left";
3677
+
3678
+ interface ToastContextValue {
3679
+ onClose: () => void;
3680
+ }
3681
+
3682
+ const ToastContext = createContext<ToastContextValue | null>(null);
3683
+
3684
+ function useToastContext() {
3685
+ const context = useContext(ToastContext);
3686
+ if (!context) throw new Error("Toast sub-components must be used inside <Toast>");
3687
+ return context;
3688
+ }
3689
+
3690
+ export interface ToastProps {
3691
+ open: boolean;
3692
+ onOpenChange: (open: boolean) => void;
3693
+ duration?: number;
3694
+ variant?: ToastVariant;
3695
+ position?: ToastPosition;
3696
+ children: ReactNode;
3697
+ className?: string;
3698
+ }
3699
+
3700
+ const VARIANT_CLASSES: Record<ToastVariant, string> = {
3701
+ default: "border-slate-200 bg-white dark:border-[#1f2937] dark:bg-[#161b22]",
3702
+ success: "border-green-300 bg-green-50 dark:border-green-900 dark:bg-green-950/30",
3703
+ warning: "border-amber-300 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/30",
3704
+ error: "border-red-300 bg-red-50 dark:border-red-900 dark:bg-red-950/30",
3705
+ };
3706
+
3707
+ const POSITION_CLASSES: Record<ToastPosition, string> = {
3708
+ "top-right": "right-4 top-4",
3709
+ "bottom-right": "right-4 bottom-4",
3710
+ "top-left": "left-4 top-4",
3711
+ "bottom-left": "left-4 bottom-4",
3712
+ };
3713
+
3714
+ export function Toast({
3715
+ open,
3716
+ onOpenChange,
3717
+ duration = 3000,
3718
+ variant = "default",
3719
+ position = "top-right",
3720
+ children,
3721
+ className,
3722
+ }: ToastProps) {
3723
+ const { mounted, visible } = useAnimatedMount(open, 180);
3724
+
3725
+ useEffect(() => {
3726
+ if (!open || duration <= 0) return;
3727
+ const timeout = setTimeout(() => onOpenChange(false), duration);
3728
+ return () => clearTimeout(timeout);
3729
+ }, [open, duration, onOpenChange]);
3730
+
3731
+ useEffect(() => {
3732
+ if (!open) return;
3733
+ const onKeyDown = (event: KeyboardEvent) => {
3734
+ if (event.key === "Escape") onOpenChange(false);
3735
+ };
3736
+ window.addEventListener("keydown", onKeyDown);
3737
+ return () => window.removeEventListener("keydown", onKeyDown);
3738
+ }, [open, onOpenChange]);
3739
+
3740
+ if (!mounted) return null;
3741
+
3742
+ return createPortal(
3743
+ <ToastContext.Provider value={{ onClose: () => onOpenChange(false) }}>
3744
+ <div className={cn("fixed z-[70] w-full max-w-sm", POSITION_CLASSES[position])}>
3745
+ <div
3746
+ role="status"
3747
+ className={cn(
3748
+ "rounded-xl border p-4 shadow-xl transition-all",
3749
+ VARIANT_CLASSES[variant],
3750
+ visible ? "translate-y-0 opacity-100" : "translate-y-2 opacity-0",
3751
+ className
3752
+ )}
3753
+ >
3754
+ {children}
3755
+ </div>
3756
+ </div>
3757
+ </ToastContext.Provider>,
3758
+ document.body
3759
+ );
3760
+ }
3761
+
3762
+ Toast.Title = function ToastTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
3763
+ return <h4 className={cn("text-sm font-semibold text-slate-900 dark:text-white", className)} {...props} />;
3764
+ }
3765
+
3766
+ Toast.Description = function ToastDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
3767
+ return <p className={cn("mt-1 text-xs text-slate-600 dark:text-slate-400", className)} {...props} />;
3768
+ }
3769
+
3770
+ Toast.Action = function ToastAction({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
3771
+ return (
3772
+ <button
3773
+ type="button"
3774
+ className={cn("rounded-md bg-primary px-2.5 py-1.5 text-xs font-medium text-white transition-opacity hover:opacity-90", className)}
3775
+ {...props}
3776
+ />
3777
+ );
3778
+ }
3779
+
3780
+ Toast.Close = function ToastClose({ className, onClick, children = "Dismiss", ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
3781
+ const { onClose } = useToastContext();
3782
+
3783
+ return (
3784
+ <button
3785
+ type="button"
3786
+ onClick={(event) => {
3787
+ onClick?.(event);
3788
+ onClose();
3789
+ }}
3790
+ className={cn("text-xs font-medium text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200", className)}
3791
+ {...props}
3792
+ >
3793
+ {children}
3794
+ </button>
3795
+ );
3796
+ }
3797
+
3798
+ Toast.Footer = function ToastFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
3799
+ return <div className={cn("mt-3 flex items-center justify-end gap-2", className)} {...props} />;
3800
+ }`,
3801
+ "Tooltip": `import { createContext, useContext, useState, type HTMLAttributes, type ReactNode } from "react";
3802
+ import { cn } from "@/lib/utils";
3803
+
3804
+ type TooltipSide = "top" | "bottom" | "left" | "right";
3805
+
3806
+ interface TooltipContextValue {
3807
+ open: boolean;
3808
+ setOpen: (open: boolean) => void;
3809
+ side: TooltipSide;
3810
+ }
3811
+
3812
+ const TooltipContext = createContext<TooltipContextValue | null>(null);
3813
+
3814
+ function useTooltipContext() {
3815
+ const context = useContext(TooltipContext);
3816
+ if (!context) throw new Error("Tooltip sub-components must be used inside <Tooltip>");
3817
+ return context;
3818
+ }
3819
+
3820
+ export interface TooltipProps {
3821
+ defaultOpen?: boolean;
3822
+ side?: TooltipSide;
3823
+ children: ReactNode;
3824
+ className?: string;
3825
+ }
3826
+
3827
+ export interface TooltipTriggerProps extends HTMLAttributes<HTMLSpanElement> {
3828
+ children: ReactNode;
3829
+ }
3830
+
3831
+ export interface TooltipContentProps extends HTMLAttributes<HTMLDivElement> {
3832
+ children: ReactNode;
3833
+ }
3834
+
3835
+ export function Tooltip({ defaultOpen = false, side = "top", children, className }: TooltipProps) {
3836
+ const [open, setOpen] = useState(defaultOpen);
3837
+
3838
+ return (
3839
+ <TooltipContext.Provider value={{ open, setOpen, side }}>
3840
+ <span className={cn("relative inline-flex", className)}>{children}</span>
3841
+ </TooltipContext.Provider>
3842
+ );
3843
+ }
3844
+
3845
+ Tooltip.Trigger = function TooltipTrigger({ children, className, ...props }: TooltipTriggerProps) {
3846
+ const { setOpen } = useTooltipContext();
3847
+
3848
+ return (
3849
+ <span
3850
+ className={cn("inline-flex", className)}
3851
+ onMouseEnter={() => setOpen(true)}
3852
+ onMouseLeave={() => setOpen(false)}
3853
+ onFocus={() => setOpen(true)}
3854
+ onBlur={() => setOpen(false)}
3855
+ {...props}
3856
+ >
3857
+ {children}
3858
+ </span>
3859
+ );
3860
+ }
3861
+
3862
+ const SIDE_CLASSES: Record<TooltipSide, string> = {
3863
+ top: "bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2",
3864
+ bottom: "top-[calc(100%+8px)] left-1/2 -translate-x-1/2",
3865
+ left: "right-[calc(100%+8px)] top-1/2 -translate-y-1/2",
3866
+ right: "left-[calc(100%+8px)] top-1/2 -translate-y-1/2",
3867
+ };
3868
+
3869
+ Tooltip.Content = function TooltipContent({ children, className, ...props }: TooltipContentProps) {
3870
+ const { open, side } = useTooltipContext();
3871
+
3872
+ return (
3873
+ <div
3874
+ role="tooltip"
3875
+ className={cn(
3876
+ "pointer-events-none absolute z-50 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs text-white shadow-lg transition-all",
3877
+ SIDE_CLASSES[side],
3878
+ open ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0",
3879
+ className
3880
+ )}
3881
+ {...props}
3882
+ >
3883
+ {children}
3884
+ </div>
3885
+ );
3886
+ }`
3887
+ };
3888
+
3889
+ // src/registry/index.ts
3890
+ var REGISTRY = [
3891
+ // ── Utilities ────────────────────────────────────────────────────────────
3892
+ {
3893
+ name: "utils",
3894
+ description: "cn() utility for merging Tailwind classes",
3895
+ templateKey: "utils",
3896
+ outputFile: "utils.ts",
3897
+ internalDeps: [],
3898
+ npmDeps: ["clsx", "tailwind-merge"],
3899
+ target: "lib"
3900
+ },
3901
+ {
3902
+ name: "use-animated-mount",
3903
+ description: "Hook for managing animated mount/unmount timing",
3904
+ templateKey: "use-animated-mount",
3905
+ outputFile: "use-animated-mount.ts",
3906
+ internalDeps: [],
3907
+ npmDeps: [],
3908
+ target: "hooks"
3909
+ },
3910
+ // ── Components ───────────────────────────────────────────────────────────
3911
+ {
3912
+ name: "accordion",
3913
+ description: "Collapsible content sections",
3914
+ templateKey: "Accordion",
3915
+ outputFile: "Accordion.tsx",
3916
+ internalDeps: ["utils"],
3917
+ npmDeps: [],
3918
+ target: "components"
3919
+ },
3920
+ {
3921
+ name: "alert",
3922
+ description: "Inline status messages",
3923
+ templateKey: "Alert",
3924
+ outputFile: "Alert.tsx",
3925
+ internalDeps: ["utils"],
3926
+ npmDeps: [],
3927
+ target: "components"
3928
+ },
3929
+ {
3930
+ name: "alert-dialog",
3931
+ description: "Confirmation dialog with destructive actions",
3932
+ templateKey: "AlertDialog",
3933
+ outputFile: "AlertDialog.tsx",
3934
+ internalDeps: ["utils", "use-animated-mount"],
3935
+ npmDeps: [],
3936
+ target: "components"
3937
+ },
3938
+ {
3939
+ name: "avatar",
3940
+ description: "User avatar with size variants",
3941
+ templateKey: "Avatar",
3942
+ outputFile: "Avatar.tsx",
3943
+ internalDeps: ["utils"],
3944
+ npmDeps: [],
3945
+ target: "components"
3946
+ },
3947
+ {
3948
+ name: "badge",
3949
+ description: "Status and label tags",
3950
+ templateKey: "Badge",
3951
+ outputFile: "Badge.tsx",
3952
+ internalDeps: ["utils"],
3953
+ npmDeps: [],
3954
+ target: "components"
3955
+ },
3956
+ {
3957
+ name: "breadcrumb",
3958
+ description: "Navigation breadcrumb trail",
3959
+ templateKey: "Breadcrumb",
3960
+ outputFile: "Breadcrumb.tsx",
3961
+ internalDeps: ["utils"],
3962
+ npmDeps: [],
3963
+ target: "components"
3964
+ },
3965
+ {
3966
+ name: "button",
3967
+ description: "Primary, secondary, ghost, outline, destructive variants",
3968
+ templateKey: "Button",
3969
+ outputFile: "Button.tsx",
3970
+ internalDeps: ["utils"],
3971
+ npmDeps: [],
3972
+ target: "components"
3973
+ },
3974
+ {
3975
+ name: "card",
3976
+ description: "Content container with variants",
3977
+ templateKey: "Card",
3978
+ outputFile: "Card.tsx",
3979
+ internalDeps: ["utils"],
3980
+ npmDeps: [],
3981
+ target: "components"
3982
+ },
3983
+ {
3984
+ name: "checkbox",
3985
+ description: "Controlled checkbox with label",
3986
+ templateKey: "Checkbox",
3987
+ outputFile: "Checkbox.tsx",
3988
+ internalDeps: ["utils"],
3989
+ npmDeps: [],
3990
+ target: "components"
3991
+ },
3992
+ {
3993
+ name: "combobox",
3994
+ description: "Searchable dropdown select",
3995
+ templateKey: "Combobox",
3996
+ outputFile: "Combobox.tsx",
3997
+ internalDeps: ["utils"],
3998
+ npmDeps: [],
3999
+ target: "components"
4000
+ },
4001
+ {
4002
+ name: "command",
4003
+ description: "Command palette container",
4004
+ templateKey: "Command",
4005
+ outputFile: "Command.tsx",
4006
+ internalDeps: ["utils"],
4007
+ npmDeps: [],
4008
+ target: "components"
4009
+ },
4010
+ {
4011
+ name: "date-picker",
4012
+ description: "Date input",
4013
+ templateKey: "DatePicker",
4014
+ outputFile: "DatePicker.tsx",
4015
+ internalDeps: ["utils"],
4016
+ npmDeps: [],
4017
+ target: "components"
4018
+ },
4019
+ {
4020
+ name: "dialog",
4021
+ description: "Modal dialog with compound sub-components",
4022
+ templateKey: "Dialog",
4023
+ outputFile: "Dialog.tsx",
4024
+ internalDeps: ["utils", "use-animated-mount"],
4025
+ npmDeps: [],
4026
+ target: "components"
4027
+ },
4028
+ {
4029
+ name: "drawer",
4030
+ description: "Slide-in panel (left/right)",
4031
+ templateKey: "Drawer",
4032
+ outputFile: "Drawer.tsx",
4033
+ internalDeps: ["utils", "use-animated-mount"],
4034
+ npmDeps: [],
4035
+ target: "components"
4036
+ },
4037
+ {
4038
+ name: "dropdown-menu",
4039
+ description: "Contextual dropdown",
4040
+ templateKey: "DropdownMenu",
4041
+ outputFile: "DropdownMenu.tsx",
4042
+ internalDeps: ["utils"],
4043
+ npmDeps: [],
4044
+ target: "components"
4045
+ },
4046
+ {
4047
+ name: "floating-lines",
4048
+ description: "Decorative animated background",
4049
+ templateKey: "FloatingLines",
4050
+ outputFile: "FloatingLines.tsx",
4051
+ internalDeps: ["utils"],
4052
+ npmDeps: [],
4053
+ target: "components"
4054
+ },
4055
+ {
4056
+ name: "input",
4057
+ description: "Text input with variants",
4058
+ templateKey: "Input",
4059
+ outputFile: "Input.tsx",
4060
+ internalDeps: ["utils"],
4061
+ npmDeps: [],
4062
+ target: "components"
4063
+ },
4064
+ {
4065
+ name: "pagination",
4066
+ description: "Page navigation",
4067
+ templateKey: "Pagination",
4068
+ outputFile: "Pagination.tsx",
4069
+ internalDeps: ["utils"],
4070
+ npmDeps: [],
4071
+ target: "components"
4072
+ },
4073
+ {
4074
+ name: "popover",
4075
+ description: "Floating content anchored to a trigger",
4076
+ templateKey: "Popover",
4077
+ outputFile: "Popover.tsx",
4078
+ internalDeps: ["utils"],
4079
+ npmDeps: [],
4080
+ target: "components"
4081
+ },
4082
+ {
4083
+ name: "progress",
4084
+ description: "Linear progress bar",
4085
+ templateKey: "Progress",
4086
+ outputFile: "Progress.tsx",
4087
+ internalDeps: ["utils"],
4088
+ npmDeps: [],
4089
+ target: "components"
4090
+ },
4091
+ {
4092
+ name: "progress-bar",
4093
+ description: "Animated top progress bar tied to navigation",
4094
+ templateKey: "ProgressBar",
4095
+ outputFile: "ProgressBar.tsx",
4096
+ internalDeps: ["utils"],
4097
+ npmDeps: [],
4098
+ target: "components"
4099
+ },
4100
+ {
4101
+ name: "radio-group",
4102
+ description: "Radio button group",
4103
+ templateKey: "RadioGroup",
4104
+ outputFile: "RadioGroup.tsx",
4105
+ internalDeps: ["utils"],
4106
+ npmDeps: [],
4107
+ target: "components"
4108
+ },
4109
+ {
4110
+ name: "search-dialog",
4111
+ description: "Full-screen search dialog",
4112
+ templateKey: "SearchDialog",
4113
+ outputFile: "SearchDialog.tsx",
4114
+ internalDeps: ["utils", "use-animated-mount"],
4115
+ npmDeps: [],
4116
+ target: "components"
4117
+ },
4118
+ {
4119
+ name: "select",
4120
+ description: "Native select with styling",
4121
+ templateKey: "Select",
4122
+ outputFile: "Select.tsx",
4123
+ internalDeps: ["utils"],
4124
+ npmDeps: [],
4125
+ target: "components"
4126
+ },
4127
+ {
4128
+ name: "separator",
4129
+ description: "Horizontal/vertical divider",
4130
+ templateKey: "Separator",
4131
+ outputFile: "Separator.tsx",
4132
+ internalDeps: ["utils"],
4133
+ npmDeps: [],
4134
+ target: "components"
4135
+ },
4136
+ {
4137
+ name: "skeleton",
4138
+ description: "Loading placeholder shapes",
4139
+ templateKey: "Skeleton",
4140
+ outputFile: "Skeleton.tsx",
4141
+ internalDeps: ["utils"],
4142
+ npmDeps: [],
4143
+ target: "components"
4144
+ },
4145
+ {
4146
+ name: "slider",
4147
+ description: "Range input",
4148
+ templateKey: "Slider",
4149
+ outputFile: "Slider.tsx",
4150
+ internalDeps: ["utils"],
4151
+ npmDeps: [],
4152
+ target: "components"
4153
+ },
4154
+ {
4155
+ name: "switch",
4156
+ description: "Toggle switch",
4157
+ templateKey: "Switch",
4158
+ outputFile: "Switch.tsx",
4159
+ internalDeps: ["utils"],
4160
+ npmDeps: [],
4161
+ target: "components"
4162
+ },
4163
+ {
4164
+ name: "tabs",
4165
+ description: "Tabbed content panels",
4166
+ templateKey: "Tabs",
4167
+ outputFile: "Tabs.tsx",
4168
+ internalDeps: ["utils"],
4169
+ npmDeps: [],
4170
+ target: "components"
4171
+ },
4172
+ {
4173
+ name: "textarea",
4174
+ description: "Multi-line text input",
4175
+ templateKey: "Textarea",
4176
+ outputFile: "Textarea.tsx",
4177
+ internalDeps: ["utils"],
4178
+ npmDeps: [],
4179
+ target: "components"
4180
+ },
4181
+ {
4182
+ name: "toast",
4183
+ description: "Transient notification",
4184
+ templateKey: "Toast",
4185
+ outputFile: "Toast.tsx",
4186
+ internalDeps: ["utils", "use-animated-mount"],
4187
+ npmDeps: [],
4188
+ target: "components"
4189
+ },
4190
+ {
4191
+ name: "tooltip",
4192
+ description: "Hover tooltip with positioning",
4193
+ templateKey: "Tooltip",
4194
+ outputFile: "Tooltip.tsx",
4195
+ internalDeps: ["utils"],
4196
+ npmDeps: [],
4197
+ target: "components"
4198
+ }
4199
+ ];
4200
+ var BY_NAME = new Map(REGISTRY.map((e) => [e.name, e]));
4201
+ function getEntry(name) {
4202
+ return BY_NAME.get(name);
4203
+ }
4204
+ function getAll() {
4205
+ return REGISTRY;
4206
+ }
4207
+ function resolve(name) {
4208
+ const entry = BY_NAME.get(name);
4209
+ if (!entry) return [];
4210
+ const visited = /* @__PURE__ */ new Set();
4211
+ const result = [];
4212
+ function walk(e) {
4213
+ if (visited.has(e.name)) return;
4214
+ visited.add(e.name);
4215
+ for (const dep of e.internalDeps) {
4216
+ const d = BY_NAME.get(dep);
4217
+ if (d) walk(d);
4218
+ }
4219
+ result.push(e);
4220
+ }
4221
+ walk(entry);
4222
+ return result;
4223
+ }
4224
+ function getTemplate(key) {
4225
+ return TEMPLATES[key] ?? "";
4226
+ }
4227
+
4228
+ // src/utils/pm.ts
4229
+ var import_child_process = require("child_process");
4230
+ var import_fs2 = require("fs");
4231
+ var import_path2 = require("path");
4232
+ function detectPackageManager(cwd) {
4233
+ if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
4234
+ if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, "yarn.lock"))) return "yarn";
4235
+ return "npm";
4236
+ }
4237
+ function installPackages(deps, cwd) {
4238
+ if (deps.length === 0) return;
4239
+ const pm = detectPackageManager(cwd);
4240
+ const cmd = pm === "yarn" ? "yarn add" : `${pm} install`;
4241
+ const list = deps.join(" ");
4242
+ (0, import_child_process.execSync)(`${cmd} ${list}`, { cwd, stdio: "inherit" });
4243
+ }
4244
+
4245
+ // src/utils/css.ts
4246
+ var import_fs3 = require("fs");
4247
+ var import_path3 = require("path");
4248
+ var GLOBALS_CANDIDATES = [
4249
+ "src/app/globals.css",
4250
+ "src/index.css",
4251
+ "src/styles/globals.css",
4252
+ "src/styles/index.css",
4253
+ "app/globals.css"
4254
+ ];
4255
+ var TAILWIND_IMPORT = '@import "tailwindcss";';
4256
+ function setupGlobalsCss(cwd, framework) {
4257
+ if (framework === "vite") return null;
4258
+ for (const candidate of GLOBALS_CANDIDATES) {
4259
+ const fullPath = (0, import_path3.join)(cwd, candidate);
4260
+ if (!(0, import_fs3.existsSync)(fullPath)) continue;
4261
+ const content = (0, import_fs3.readFileSync)(fullPath, "utf8");
4262
+ if (content.includes("@import") && content.includes("tailwindcss")) return null;
4263
+ const updated = `${TAILWIND_IMPORT}
4264
+ ${content}`.trimEnd() + "\n";
4265
+ (0, import_fs3.writeFileSync)(fullPath, updated, "utf8");
4266
+ return candidate;
4267
+ }
4268
+ return null;
4269
+ }
4270
+
4271
+ // src/commands/init.ts
4272
+ var FRAMEWORK_LABELS = {
4273
+ next: "Next.js",
4274
+ vite: "Vite",
4275
+ remix: "Remix",
4276
+ other: "Other"
4277
+ };
4278
+ async function init(cwd, frameworkFlag) {
4279
+ const existing = readConfig(cwd);
4280
+ if (existing) {
4281
+ console.log(import_picocolors.default.yellow("components.json already exists. Remove it to re-run init."));
4282
+ return;
4283
+ }
4284
+ console.log(import_picocolors.default.bold("\nInitializing react-principles components...\n"));
4285
+ const detectedFramework = frameworkFlag ?? detectFramework(cwd);
4286
+ const detectedAliases = detectTsconfigAliases(cwd);
4287
+ const defaults = getDefaultConfig();
4288
+ if (detectedAliases.components) defaults.aliases.components = detectedAliases.components;
4289
+ if (detectedAliases.hooks) defaults.aliases.hooks = detectedAliases.hooks;
4290
+ if (detectedAliases.lib) defaults.aliases.lib = detectedAliases.lib;
4291
+ const answers = await (0, import_prompts.default)(
4292
+ [
4293
+ {
4294
+ type: "select",
4295
+ name: "framework",
4296
+ message: "Which framework are you using?",
4297
+ choices: [
4298
+ { title: "Next.js", value: "next" },
4299
+ { title: "Vite", value: "vite" },
4300
+ { title: "Remix", value: "remix" },
4301
+ { title: "Other", value: "other" }
4302
+ ],
4303
+ initial: ["next", "vite", "remix", "other"].indexOf(detectedFramework),
4304
+ hint: detectedFramework !== "other" ? `detected: ${FRAMEWORK_LABELS[detectedFramework]}` : void 0
4305
+ },
4306
+ {
4307
+ type: "text",
4308
+ name: "componentsDir",
4309
+ message: "Where should components be installed?",
4310
+ initial: defaults.componentsDir
4311
+ },
4312
+ {
4313
+ type: "text",
4314
+ name: "hooksDir",
4315
+ message: "Where should hooks be installed?",
4316
+ initial: defaults.hooksDir
4317
+ },
4318
+ {
4319
+ type: "text",
4320
+ name: "libDir",
4321
+ message: "Where should utilities be installed?",
4322
+ initial: defaults.libDir
4323
+ }
4324
+ ],
4325
+ {
4326
+ onCancel: () => {
4327
+ console.log(import_picocolors.default.red("\nSetup cancelled."));
4328
+ process.exit(0);
4329
+ }
4330
+ }
4331
+ );
4332
+ const framework = answers.framework;
4333
+ const rsc = framework === "next";
4334
+ const config = {
4335
+ framework,
4336
+ rsc,
4337
+ tsx: true,
4338
+ componentsDir: answers.componentsDir,
4339
+ hooksDir: answers.hooksDir,
4340
+ libDir: answers.libDir,
4341
+ aliases: {
4342
+ components: `@/${answers.componentsDir.replace(/^src\//, "")}`,
4343
+ hooks: `@/${answers.hooksDir.replace(/^src\//, "")}`,
4344
+ lib: `@/${answers.libDir.replace(/^src\//, "")}`
4345
+ }
4346
+ };
4347
+ writeConfig(config, cwd);
4348
+ console.log(import_picocolors.default.green("\n\u2713") + " Created components.json");
4349
+ const utilsEntry = getEntry("utils");
4350
+ if (utilsEntry) {
4351
+ const outputPath = resolveOutputPath(utilsEntry.outputFile, utilsEntry.target, config, cwd);
4352
+ const written = writeFile(getTemplate(utilsEntry.templateKey), outputPath);
4353
+ const rel = (0, import_path4.join)(config.libDir, utilsEntry.outputFile);
4354
+ console.log(
4355
+ written ? import_picocolors.default.green("\u2713") + ` Created ${rel}` : import_picocolors.default.gray("\u2013") + ` Skipped ${rel} (already exists)`
4356
+ );
4357
+ const hasDeps = utilsEntry.npmDeps.some((d) => !(0, import_fs4.existsSync)((0, import_path4.join)(cwd, "node_modules", d)));
4358
+ if (hasDeps) {
4359
+ console.log(import_picocolors.default.cyan("\nInstalling dependencies..."));
4360
+ installPackages([...utilsEntry.npmDeps, "tailwind-animate"], cwd);
4361
+ }
4362
+ }
4363
+ const cssResult = setupGlobalsCss(cwd, framework);
4364
+ if (cssResult) console.log(import_picocolors.default.green("\u2713") + ` Updated ${cssResult}`);
4365
+ console.log(
4366
+ import_picocolors.default.bold(import_picocolors.default.green("\nDone!")) + ` Framework: ${FRAMEWORK_LABELS[framework]} | RSC: ${rsc ? "yes" : "no"}
4367
+ Run ${import_picocolors.default.cyan("npx react-principles-cli add <component>")} to add components.
4368
+ `
4369
+ );
4370
+ }
4371
+
4372
+ // src/commands/add.ts
4373
+ var import_picocolors2 = __toESM(require("picocolors"));
4374
+ async function add(names, cwd) {
4375
+ const config = readConfig(cwd);
4376
+ if (!config) {
4377
+ console.log(import_picocolors2.default.red("No components.json found. Run `npx react-principles-cli init` first."));
4378
+ process.exit(1);
4379
+ }
4380
+ const targets = names.length === 1 && names[0] === "--all" ? getAll().filter((e) => e.target === "components").map((e) => e.name) : names;
4381
+ const unknown = targets.filter((n) => !getEntry(n));
4382
+ if (unknown.length > 0) {
4383
+ console.log(import_picocolors2.default.red(`Unknown component(s): ${unknown.join(", ")}`));
4384
+ console.log(import_picocolors2.default.gray("Run `npx react-principles-cli list` to see available components."));
4385
+ process.exit(1);
4386
+ }
4387
+ const seen = /* @__PURE__ */ new Set();
4388
+ const toInstall = targets.flatMap((n) => resolve(n)).filter((e) => {
4389
+ if (seen.has(e.name)) return false;
4390
+ seen.add(e.name);
4391
+ return true;
4392
+ });
4393
+ const allNpmDeps = /* @__PURE__ */ new Set();
4394
+ const written = [];
4395
+ const skipped = [];
4396
+ for (const entry of toInstall) {
4397
+ const outputPath = resolveOutputPath(entry.outputFile, entry.target, config, cwd);
4398
+ const content = getTemplate(entry.templateKey);
4399
+ if (!content) {
4400
+ console.log(import_picocolors2.default.yellow(`\u26A0 No template found for ${entry.name}, skipping.`));
4401
+ continue;
4402
+ }
4403
+ const didWrite = writeFile(content, outputPath);
4404
+ const label = [config.componentsDir, config.hooksDir, config.libDir].find((d) => outputPath.includes(d))?.concat("/", entry.outputFile) ?? entry.outputFile;
4405
+ if (didWrite) {
4406
+ written.push(label);
4407
+ } else {
4408
+ skipped.push(label);
4409
+ }
4410
+ for (const dep of entry.npmDeps) allNpmDeps.add(dep);
4411
+ }
4412
+ for (const f of written) console.log(import_picocolors2.default.green("\u2713") + ` Created ${f}`);
4413
+ for (const f of skipped) console.log(import_picocolors2.default.gray("\u2013") + ` Skipped ${f} (already exists)`);
4414
+ if (allNpmDeps.size > 0) {
4415
+ console.log(import_picocolors2.default.cyan("\nInstalling dependencies..."));
4416
+ installPackages([...allNpmDeps], cwd);
4417
+ }
4418
+ if (written.length > 0) {
4419
+ console.log(import_picocolors2.default.bold(import_picocolors2.default.green("\nDone!\n")));
4420
+ }
4421
+ }
4422
+
4423
+ // src/index.ts
4424
+ var program = new import_commander.Command();
4425
+ program.name("react-principles").description("Add react-principles UI components to your project").version("0.0.1");
4426
+ program.command("init").description("Initialize components.json config and install the cn() utility").option("-t, --template <framework>", "Framework: next | vite | remix | other").action(async (opts) => {
4427
+ await init(process.cwd(), opts.template);
4428
+ });
4429
+ program.command("add <components...>").description("Add one or more components to your project").action(async (components) => {
4430
+ await add(components, process.cwd());
4431
+ });
4432
+ program.command("list").description("List all available components").action(() => {
4433
+ const entries = getAll().filter((e) => e.target === "components");
4434
+ console.log(import_picocolors3.default.bold("\nAvailable components:\n"));
4435
+ for (const e of entries) {
4436
+ console.log(` ${import_picocolors3.default.cyan(e.name.padEnd(20))} ${import_picocolors3.default.gray(e.description)}`);
4437
+ }
4438
+ console.log();
4439
+ });
4440
+ program.parse();