gradient-forge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.eslintrc.json +3 -0
  2. package/.github/FUNDING.yml +2 -0
  3. package/README.md +140 -0
  4. package/app/docs/page.tsx +417 -0
  5. package/app/gallery/page.tsx +398 -0
  6. package/app/globals.css +1155 -0
  7. package/app/layout.tsx +36 -0
  8. package/app/page.tsx +600 -0
  9. package/app/showcase/page.tsx +730 -0
  10. package/app/studio/page.tsx +1310 -0
  11. package/cli/index.mjs +1141 -0
  12. package/cli/templates/theme-context.tsx +120 -0
  13. package/cli/templates/theme-engine.ts +237 -0
  14. package/cli/templates/themes.css +512 -0
  15. package/components/site/component-showcase.tsx +623 -0
  16. package/components/site/site-data.ts +103 -0
  17. package/components/site/site-header.tsx +270 -0
  18. package/components/templates/blog.tsx +198 -0
  19. package/components/templates/components-showcase.tsx +298 -0
  20. package/components/templates/dashboard.tsx +246 -0
  21. package/components/templates/ecommerce.tsx +199 -0
  22. package/components/templates/mail.tsx +275 -0
  23. package/components/templates/saas-landing.tsx +169 -0
  24. package/components/theme/studio-code-panel.tsx +485 -0
  25. package/components/theme/theme-context.tsx +120 -0
  26. package/components/theme/theme-engine.ts +237 -0
  27. package/components/theme/theme-exporter.tsx +369 -0
  28. package/components/theme/theme-panel.tsx +268 -0
  29. package/components/theme/token-export-utils.ts +1211 -0
  30. package/components/ui/animated.tsx +55 -0
  31. package/components/ui/avatar.tsx +38 -0
  32. package/components/ui/badge.tsx +32 -0
  33. package/components/ui/button.tsx +65 -0
  34. package/components/ui/card.tsx +56 -0
  35. package/components/ui/checkbox.tsx +19 -0
  36. package/components/ui/command-palette.tsx +245 -0
  37. package/components/ui/gsap-animated.tsx +436 -0
  38. package/components/ui/input.tsx +17 -0
  39. package/components/ui/select.tsx +176 -0
  40. package/components/ui/skeleton.tsx +102 -0
  41. package/components/ui/switch.tsx +43 -0
  42. package/components/ui/tabs.tsx +115 -0
  43. package/components/ui/toast.tsx +119 -0
  44. package/gradient-forge/theme-context.tsx +119 -0
  45. package/gradient-forge/theme-engine.ts +236 -0
  46. package/gradient-forge/themes.css +556 -0
  47. package/lib/animations.ts +50 -0
  48. package/lib/gsap.ts +426 -0
  49. package/lib/utils.ts +6 -0
  50. package/next-env.d.ts +6 -0
  51. package/next.config.mjs +6 -0
  52. package/package.json +53 -0
  53. package/postcss.config.mjs +5 -0
  54. package/tailwind.config.ts +15 -0
  55. package/tsconfig.json +43 -0
  56. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,623 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Switch } from "@/components/ui/switch";
9
+
10
+ type ComponentFile = {
11
+ path: string;
12
+ code: string;
13
+ };
14
+
15
+ type ShowcaseComponent = {
16
+ id: string;
17
+ name: string;
18
+ category: "Dashboard" | "Auth" | "Commerce" | "Settings" | "Messaging";
19
+ description: string;
20
+ tags: string[];
21
+ preview: ReactNode;
22
+ files: ComponentFile[];
23
+ };
24
+
25
+ const showcaseComponents: ShowcaseComponent[] = [
26
+ {
27
+ id: "analytics-grid",
28
+ name: "Analytics Grid",
29
+ category: "Dashboard",
30
+ description: "Hero dashboard block with KPIs, growth badges, and action row.",
31
+ tags: ["kpi", "stats", "dashboard"],
32
+ preview: (
33
+ <Card className="border-border/40 bg-card/70">
34
+ <CardHeader className="pb-2">
35
+ <div className="flex items-center justify-between gap-2">
36
+ <CardTitle className="text-base">Revenue Snapshot</CardTitle>
37
+ <Badge variant="glass">Live</Badge>
38
+ </div>
39
+ </CardHeader>
40
+ <CardContent className="space-y-4">
41
+ <div className="grid grid-cols-3 gap-3">
42
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">
43
+ <p className="text-xs text-muted-foreground">MRR</p>
44
+ <p className="text-lg font-semibold">$92k</p>
45
+ </div>
46
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">
47
+ <p className="text-xs text-muted-foreground">Growth</p>
48
+ <p className="text-lg font-semibold">+18%</p>
49
+ </div>
50
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">
51
+ <p className="text-xs text-muted-foreground">Churn</p>
52
+ <p className="text-lg font-semibold">1.4%</p>
53
+ </div>
54
+ </div>
55
+ <div className="flex flex-wrap gap-2">
56
+ <Button size="sm">Open Report</Button>
57
+ <Button size="sm" variant="outline">Export CSV</Button>
58
+ </div>
59
+ </CardContent>
60
+ </Card>
61
+ ),
62
+ files: [
63
+ {
64
+ path: "components/analytics-grid.tsx",
65
+ code: `import { Badge } from "@/components/ui/badge";
66
+ import { Button } from "@/components/ui/button";
67
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
68
+
69
+ export function AnalyticsGrid() {
70
+ return (
71
+ <Card className="border-border/40 bg-card/70">
72
+ <CardHeader className="pb-2">
73
+ <div className="flex items-center justify-between gap-2">
74
+ <CardTitle className="text-base">Revenue Snapshot</CardTitle>
75
+ <Badge variant="glass">Live</Badge>
76
+ </div>
77
+ </CardHeader>
78
+ <CardContent className="space-y-4">
79
+ <div className="grid grid-cols-3 gap-3">
80
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">
81
+ <p className="text-xs text-muted-foreground">MRR</p>
82
+ <p className="text-lg font-semibold">$92k</p>
83
+ </div>
84
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">
85
+ <p className="text-xs text-muted-foreground">Growth</p>
86
+ <p className="text-lg font-semibold">+18%</p>
87
+ </div>
88
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">
89
+ <p className="text-xs text-muted-foreground">Churn</p>
90
+ <p className="text-lg font-semibold">1.4%</p>
91
+ </div>
92
+ </div>
93
+ <div className="flex flex-wrap gap-2">
94
+ <Button size="sm">Open Report</Button>
95
+ <Button size="sm" variant="outline">Export CSV</Button>
96
+ </div>
97
+ </CardContent>
98
+ </Card>
99
+ );
100
+ }`,
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ id: "checkout-pricing",
106
+ name: "Checkout Pricing",
107
+ category: "Commerce",
108
+ description: "Pricing card with highlighted plan and conversion-focused CTA.",
109
+ tags: ["pricing", "billing", "checkout"],
110
+ preview: (
111
+ <Card className="border-border/40 bg-card/70">
112
+ <CardHeader className="pb-2">
113
+ <div className="flex items-center justify-between">
114
+ <CardTitle className="text-base">Scale Plan</CardTitle>
115
+ <Badge>Popular</Badge>
116
+ </div>
117
+ <p className="text-xs text-muted-foreground">Best for teams shipping weekly.</p>
118
+ </CardHeader>
119
+ <CardContent className="space-y-4">
120
+ <div className="flex items-end gap-2">
121
+ <p className="text-3xl font-semibold">$49</p>
122
+ <p className="pb-1 text-xs text-muted-foreground">/month</p>
123
+ </div>
124
+ <div className="space-y-2 text-xs text-muted-foreground">
125
+ <p>Unlimited palettes</p>
126
+ <p>Advanced export presets</p>
127
+ <p>Theme audit tools</p>
128
+ </div>
129
+ <Button className="w-full">Start 14-day trial</Button>
130
+ </CardContent>
131
+ </Card>
132
+ ),
133
+ files: [
134
+ {
135
+ path: "components/checkout-pricing.tsx",
136
+ code: `import { Badge } from "@/components/ui/badge";
137
+ import { Button } from "@/components/ui/button";
138
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
139
+
140
+ export function CheckoutPricing() {
141
+ return (
142
+ <Card className="border-border/40 bg-card/70">
143
+ <CardHeader className="pb-2">
144
+ <div className="flex items-center justify-between">
145
+ <CardTitle className="text-base">Scale Plan</CardTitle>
146
+ <Badge>Popular</Badge>
147
+ </div>
148
+ <p className="text-xs text-muted-foreground">Best for teams shipping weekly.</p>
149
+ </CardHeader>
150
+ <CardContent className="space-y-4">
151
+ <div className="flex items-end gap-2">
152
+ <p className="text-3xl font-semibold">$49</p>
153
+ <p className="pb-1 text-xs text-muted-foreground">/month</p>
154
+ </div>
155
+ <div className="space-y-2 text-xs text-muted-foreground">
156
+ <p>Unlimited palettes</p>
157
+ <p>Advanced export presets</p>
158
+ <p>Theme audit tools</p>
159
+ </div>
160
+ <Button className="w-full">Start 14-day trial</Button>
161
+ </CardContent>
162
+ </Card>
163
+ );
164
+ }`,
165
+ },
166
+ ],
167
+ },
168
+ {
169
+ id: "auth-split",
170
+ name: "Auth Split",
171
+ category: "Auth",
172
+ description: "Modern sign-in panel with alternate auth actions.",
173
+ tags: ["auth", "login", "form"],
174
+ preview: (
175
+ <Card className="border-border/40 bg-card/70">
176
+ <CardHeader className="pb-2">
177
+ <CardTitle className="text-base">Welcome back</CardTitle>
178
+ <p className="text-xs text-muted-foreground">Sign in to continue to Gradient Forge.</p>
179
+ </CardHeader>
180
+ <CardContent className="space-y-3">
181
+ <Input placeholder="name@company.com" type="email" />
182
+ <Input placeholder="Password" type="password" />
183
+ <Button className="w-full">Continue</Button>
184
+ <Button variant="ghost" className="w-full">Sign in with GitHub</Button>
185
+ </CardContent>
186
+ </Card>
187
+ ),
188
+ files: [
189
+ {
190
+ path: "components/auth-split.tsx",
191
+ code: `import { Button } from "@/components/ui/button";
192
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
193
+ import { Input } from "@/components/ui/input";
194
+
195
+ export function AuthSplit() {
196
+ return (
197
+ <Card className="border-border/40 bg-card/70">
198
+ <CardHeader className="pb-2">
199
+ <CardTitle className="text-base">Welcome back</CardTitle>
200
+ <p className="text-xs text-muted-foreground">Sign in to continue to Gradient Forge.</p>
201
+ </CardHeader>
202
+ <CardContent className="space-y-3">
203
+ <Input placeholder="name@company.com" type="email" />
204
+ <Input placeholder="Password" type="password" />
205
+ <Button className="w-full">Continue</Button>
206
+ <Button variant="ghost" className="w-full">Sign in with GitHub</Button>
207
+ </CardContent>
208
+ </Card>
209
+ );
210
+ }`,
211
+ },
212
+ ],
213
+ },
214
+ {
215
+ id: "security-settings",
216
+ name: "Security Settings",
217
+ category: "Settings",
218
+ description: "Security preferences card with switch controls and device policy.",
219
+ tags: ["settings", "security", "switch"],
220
+ preview: (
221
+ <Card className="border-border/40 bg-card/70">
222
+ <CardHeader className="pb-2">
223
+ <CardTitle className="text-base">Security</CardTitle>
224
+ <p className="text-xs text-muted-foreground">Harden your workspace defaults.</p>
225
+ </CardHeader>
226
+ <CardContent className="space-y-3">
227
+ <div className="flex items-center justify-between rounded-2xl border border-border/40 bg-background/50 p-3">
228
+ <div>
229
+ <p className="text-sm font-semibold">Two-factor auth</p>
230
+ <p className="text-xs text-muted-foreground">Require 2FA for all members</p>
231
+ </div>
232
+ <Switch checked onCheckedChange={() => {}} />
233
+ </div>
234
+ <div className="flex items-center justify-between rounded-2xl border border-border/40 bg-background/50 p-3">
235
+ <div>
236
+ <p className="text-sm font-semibold">Unknown device alerts</p>
237
+ <p className="text-xs text-muted-foreground">Notify on suspicious sign-ins</p>
238
+ </div>
239
+ <Switch checked={false} onCheckedChange={() => {}} />
240
+ </div>
241
+ </CardContent>
242
+ </Card>
243
+ ),
244
+ files: [
245
+ {
246
+ path: "components/security-settings.tsx",
247
+ code: `import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
248
+ import { Switch } from "@/components/ui/switch";
249
+
250
+ export function SecuritySettings() {
251
+ return (
252
+ <Card className="border-border/40 bg-card/70">
253
+ <CardHeader className="pb-2">
254
+ <CardTitle className="text-base">Security</CardTitle>
255
+ <p className="text-xs text-muted-foreground">Harden your workspace defaults.</p>
256
+ </CardHeader>
257
+ <CardContent className="space-y-3">
258
+ <div className="flex items-center justify-between rounded-2xl border border-border/40 bg-background/50 p-3">
259
+ <div>
260
+ <p className="text-sm font-semibold">Two-factor auth</p>
261
+ <p className="text-xs text-muted-foreground">Require 2FA for all members</p>
262
+ </div>
263
+ <Switch checked onCheckedChange={() => {}} />
264
+ </div>
265
+ <div className="flex items-center justify-between rounded-2xl border border-border/40 bg-background/50 p-3">
266
+ <div>
267
+ <p className="text-sm font-semibold">Unknown device alerts</p>
268
+ <p className="text-xs text-muted-foreground">Notify on suspicious sign-ins</p>
269
+ </div>
270
+ <Switch checked={false} onCheckedChange={() => {}} />
271
+ </div>
272
+ </CardContent>
273
+ </Card>
274
+ );
275
+ }`,
276
+ },
277
+ ],
278
+ },
279
+ {
280
+ id: "team-invites",
281
+ name: "Team Invites",
282
+ category: "Settings",
283
+ description: "Role assignment block with email invite flow and seat usage.",
284
+ tags: ["team", "invite", "admin"],
285
+ preview: (
286
+ <Card className="border-border/40 bg-card/70">
287
+ <CardHeader className="pb-2">
288
+ <CardTitle className="text-base">Invite Team</CardTitle>
289
+ <p className="text-xs text-muted-foreground">8 / 15 seats in use</p>
290
+ </CardHeader>
291
+ <CardContent className="space-y-3">
292
+ <Input placeholder="designer@company.com" type="email" />
293
+ <div className="flex flex-wrap gap-2">
294
+ <Badge variant="outline">Admin</Badge>
295
+ <Badge variant="glass">Editor</Badge>
296
+ <Badge variant="outline">Viewer</Badge>
297
+ </div>
298
+ <Button className="w-full">Send invite</Button>
299
+ </CardContent>
300
+ </Card>
301
+ ),
302
+ files: [
303
+ {
304
+ path: "components/team-invites.tsx",
305
+ code: `import { Badge } from "@/components/ui/badge";
306
+ import { Button } from "@/components/ui/button";
307
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
308
+ import { Input } from "@/components/ui/input";
309
+
310
+ export function TeamInvites() {
311
+ return (
312
+ <Card className="border-border/40 bg-card/70">
313
+ <CardHeader className="pb-2">
314
+ <CardTitle className="text-base">Invite Team</CardTitle>
315
+ <p className="text-xs text-muted-foreground">8 / 15 seats in use</p>
316
+ </CardHeader>
317
+ <CardContent className="space-y-3">
318
+ <Input placeholder="designer@company.com" type="email" />
319
+ <div className="flex flex-wrap gap-2">
320
+ <Badge variant="outline">Admin</Badge>
321
+ <Badge variant="glass">Editor</Badge>
322
+ <Badge variant="outline">Viewer</Badge>
323
+ </div>
324
+ <Button className="w-full">Send invite</Button>
325
+ </CardContent>
326
+ </Card>
327
+ );
328
+ }`,
329
+ },
330
+ ],
331
+ },
332
+ {
333
+ id: "activity-timeline",
334
+ name: "Activity Timeline",
335
+ category: "Dashboard",
336
+ description: "Readable timeline panel for system events and deploy actions.",
337
+ tags: ["timeline", "activity", "logs"],
338
+ preview: (
339
+ <Card className="border-border/40 bg-card/70">
340
+ <CardHeader className="pb-2">
341
+ <CardTitle className="text-base">Recent Activity</CardTitle>
342
+ </CardHeader>
343
+ <CardContent className="space-y-3 text-xs text-muted-foreground">
344
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Theme changed to Aurora - 2m ago</div>
345
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Tokens exported to app/web - 10m ago</div>
346
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Studio published on preview - 31m ago</div>
347
+ </CardContent>
348
+ </Card>
349
+ ),
350
+ files: [
351
+ {
352
+ path: "components/activity-timeline.tsx",
353
+ code: `import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
354
+
355
+ export function ActivityTimeline() {
356
+ return (
357
+ <Card className="border-border/40 bg-card/70">
358
+ <CardHeader className="pb-2">
359
+ <CardTitle className="text-base">Recent Activity</CardTitle>
360
+ </CardHeader>
361
+ <CardContent className="space-y-3 text-xs text-muted-foreground">
362
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Theme changed to Aurora - 2m ago</div>
363
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Tokens exported to app/web - 10m ago</div>
364
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Studio published on preview - 31m ago</div>
365
+ </CardContent>
366
+ </Card>
367
+ );
368
+ }`,
369
+ },
370
+ ],
371
+ },
372
+ {
373
+ id: "notification-center",
374
+ name: "Notification Center",
375
+ category: "Messaging",
376
+ description: "Compact inbox list for updates, mentions, and warnings.",
377
+ tags: ["notifications", "inbox", "alerts"],
378
+ preview: (
379
+ <Card className="border-border/40 bg-card/70">
380
+ <CardHeader className="pb-2">
381
+ <div className="flex items-center justify-between">
382
+ <CardTitle className="text-base">Inbox</CardTitle>
383
+ <Badge variant="outline">3 unread</Badge>
384
+ </div>
385
+ </CardHeader>
386
+ <CardContent className="space-y-2 text-xs">
387
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Build passed on main branch</div>
388
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">New comment on docs install guide</div>
389
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Palette pack v2.1 available</div>
390
+ </CardContent>
391
+ </Card>
392
+ ),
393
+ files: [
394
+ {
395
+ path: "components/notification-center.tsx",
396
+ code: `import { Badge } from "@/components/ui/badge";
397
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
398
+
399
+ export function NotificationCenter() {
400
+ return (
401
+ <Card className="border-border/40 bg-card/70">
402
+ <CardHeader className="pb-2">
403
+ <div className="flex items-center justify-between">
404
+ <CardTitle className="text-base">Inbox</CardTitle>
405
+ <Badge variant="outline">3 unread</Badge>
406
+ </div>
407
+ </CardHeader>
408
+ <CardContent className="space-y-2 text-xs">
409
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Build passed on main branch</div>
410
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">New comment on docs install guide</div>
411
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-3">Palette pack v2.1 available</div>
412
+ </CardContent>
413
+ </Card>
414
+ );
415
+ }`,
416
+ },
417
+ ],
418
+ },
419
+ ];
420
+
421
+ const getAllFilesContent = (files: ComponentFile[]) => {
422
+ return files.map((file) => `// ${file.path}\n${file.code}`).join("\n\n");
423
+ };
424
+
425
+ const categories = [
426
+ "All",
427
+ ...new Set(showcaseComponents.map((component) => component.category)),
428
+ ] as const;
429
+
430
+ type CategoryFilter = (typeof categories)[number];
431
+
432
+ export function ComponentShowcase() {
433
+ const [activeId, setActiveId] = useState<string>(showcaseComponents[0].id);
434
+ const [activeTab, setActiveTab] = useState<"preview" | "code">("preview");
435
+ const [activeFileIndex, setActiveFileIndex] = useState<number>(0);
436
+ const [activeCategory, setActiveCategory] = useState<CategoryFilter>("All");
437
+ const [query, setQuery] = useState<string>("");
438
+ const [copyState, setCopyState] = useState<"idle" | "copied" | "error">("idle");
439
+
440
+ const filteredComponents = useMemo(() => {
441
+ const normalizedQuery = query.trim().toLowerCase();
442
+
443
+ return showcaseComponents.filter((component) => {
444
+ const categoryMatch =
445
+ activeCategory === "All" || component.category === activeCategory;
446
+
447
+ if (!normalizedQuery) return categoryMatch;
448
+
449
+ const searchBase = [component.name, component.description, ...component.tags]
450
+ .join(" ")
451
+ .toLowerCase();
452
+
453
+ return categoryMatch && searchBase.includes(normalizedQuery);
454
+ });
455
+ }, [activeCategory, query]);
456
+
457
+ useEffect(() => {
458
+ if (filteredComponents.length === 0) return;
459
+
460
+ const stillVisible = filteredComponents.some(
461
+ (component) => component.id === activeId,
462
+ );
463
+
464
+ if (!stillVisible) {
465
+ setActiveId(filteredComponents[0].id);
466
+ setActiveTab("preview");
467
+ setActiveFileIndex(0);
468
+ }
469
+ }, [filteredComponents, activeId]);
470
+
471
+ const activeComponent = useMemo(() => {
472
+ return (
473
+ filteredComponents.find((component) => component.id === activeId) ??
474
+ filteredComponents[0] ??
475
+ showcaseComponents[0]
476
+ );
477
+ }, [filteredComponents, activeId]);
478
+
479
+ const activeFiles = activeComponent.files;
480
+ const activeFile = activeFiles[activeFileIndex] ?? activeFiles[0];
481
+
482
+ const handleCopy = async (content: string) => {
483
+ try {
484
+ await navigator.clipboard.writeText(content);
485
+ setCopyState("copied");
486
+ window.setTimeout(() => setCopyState("idle"), 1500);
487
+ } catch {
488
+ setCopyState("error");
489
+ window.setTimeout(() => setCopyState("idle"), 1500);
490
+ }
491
+ };
492
+
493
+ return (
494
+ <section className="grid gap-6 xl:grid-cols-[280px_minmax(0,1fr)]">
495
+ <Card className="border-border/50 bg-background/60">
496
+ <CardHeader>
497
+ <CardTitle>Component Packs</CardTitle>
498
+ <p className="text-sm text-muted-foreground">
499
+ Filter by category, preview instantly, and copy full source files.
500
+ </p>
501
+ </CardHeader>
502
+ <CardContent className="space-y-4">
503
+ <Input
504
+ placeholder="Search components..."
505
+ value={query}
506
+ onChange={(event) => setQuery(event.target.value)}
507
+ />
508
+ <div className="flex flex-wrap gap-2">
509
+ {categories.map((category) => (
510
+ <Button
511
+ key={category}
512
+ size="sm"
513
+ variant={category === activeCategory ? "default" : "ghost"}
514
+ onClick={() => setActiveCategory(category)}
515
+ >
516
+ {category}
517
+ </Button>
518
+ ))}
519
+ </div>
520
+ <div className="space-y-2">
521
+ {filteredComponents.map((component) => (
522
+ <button
523
+ key={component.id}
524
+ type="button"
525
+ className={`w-full rounded-2xl border px-3 py-3 text-left transition-all ${
526
+ component.id === activeId
527
+ ? "border-primary/50 bg-primary/10"
528
+ : "border-border/40 bg-background/50 hover:bg-background/70"
529
+ }`}
530
+ onClick={() => {
531
+ setActiveId(component.id);
532
+ setActiveTab("preview");
533
+ setActiveFileIndex(0);
534
+ }}
535
+ >
536
+ <p className="text-sm font-semibold">{component.name}</p>
537
+ <p className="text-xs text-muted-foreground">{component.category}</p>
538
+ </button>
539
+ ))}
540
+ {filteredComponents.length === 0 && (
541
+ <div className="rounded-2xl border border-border/40 bg-background/50 p-4 text-sm text-muted-foreground">
542
+ No components match your filter. Clear the search and try again.
543
+ </div>
544
+ )}
545
+ </div>
546
+ </CardContent>
547
+ </Card>
548
+
549
+ <Card className="min-w-0 border-border/50 bg-background/60">
550
+ <CardHeader className="space-y-3">
551
+ <div className="flex flex-wrap items-center justify-between gap-3">
552
+ <div>
553
+ <CardTitle>{activeComponent.name}</CardTitle>
554
+ <p className="text-sm text-muted-foreground">{activeComponent.description}</p>
555
+ </div>
556
+ <div className="flex items-center gap-2">
557
+ <Button
558
+ size="sm"
559
+ variant={activeTab === "preview" ? "default" : "ghost"}
560
+ onClick={() => setActiveTab("preview")}
561
+ >
562
+ Preview
563
+ </Button>
564
+ <Button
565
+ size="sm"
566
+ variant={activeTab === "code" ? "default" : "ghost"}
567
+ onClick={() => setActiveTab("code")}
568
+ >
569
+ Code
570
+ </Button>
571
+ </div>
572
+ </div>
573
+ <div className="flex flex-wrap gap-2">
574
+ {activeComponent.tags.map((tag) => (
575
+ <Badge key={tag} variant="outline">{tag}</Badge>
576
+ ))}
577
+ </div>
578
+ </CardHeader>
579
+ <CardContent>
580
+ {activeTab === "preview" ? (
581
+ <div className="rounded-3xl border border-border/40 bg-background/50 p-4 sm:p-6">
582
+ <div className="mx-auto max-w-2xl">{activeComponent.preview}</div>
583
+ </div>
584
+ ) : (
585
+ <div className="space-y-4">
586
+ <div className="flex flex-wrap items-center gap-2">
587
+ {activeFiles.map((file, index) => (
588
+ <Button
589
+ key={file.path}
590
+ size="sm"
591
+ variant={index === activeFileIndex ? "default" : "ghost"}
592
+ className="max-w-full truncate"
593
+ title={file.path}
594
+ onClick={() => setActiveFileIndex(index)}
595
+ >
596
+ {file.path.split("/").pop() ?? file.path}
597
+ </Button>
598
+ ))}
599
+ <Button
600
+ size="sm"
601
+ variant="outline"
602
+ onClick={() => handleCopy(activeFile.code)}
603
+ >
604
+ {copyState === "copied" ? "Copied" : copyState === "error" ? "Copy failed" : "Copy file"}
605
+ </Button>
606
+ <Button
607
+ size="sm"
608
+ variant="outline"
609
+ onClick={() => handleCopy(getAllFilesContent(activeFiles))}
610
+ >
611
+ Copy all files
612
+ </Button>
613
+ </div>
614
+ <pre className="max-h-[520px] overflow-x-auto overflow-y-auto rounded-2xl border border-border/40 bg-black/80 p-4 text-xs text-white/90">
615
+ <code>{activeFile.code}</code>
616
+ </pre>
617
+ </div>
618
+ )}
619
+ </CardContent>
620
+ </Card>
621
+ </section>
622
+ );
623
+ }