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,398 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import gsap from "gsap";
5
+ import { ScrollTrigger } from "gsap/ScrollTrigger";
6
+ import { SiteHeader } from "@/components/site/site-header";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Card, CardContent } from "@/components/ui/card";
10
+ import { MEMORY_LANE_THEME, NITRO_PUBLIC_THEMES, NITRO_ALL_THEMES, type ThemeId } from "@/components/theme/theme-engine";
11
+ import { useThemeContext } from "@/components/theme/theme-context";
12
+ import { useToast } from "@/components/ui/toast";
13
+ import { Switch } from "@/components/ui/switch";
14
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
15
+ import {
16
+ Image,
17
+ Grid,
18
+ List,
19
+ Sparkles,
20
+ Check,
21
+ Eye,
22
+ Search,
23
+ Filter,
24
+ Download,
25
+ Copy,
26
+ Sun,
27
+ Moon,
28
+ Palette
29
+ } from "lucide-react";
30
+ import { MagneticButton } from "@/components/ui/gsap-animated";
31
+ import { exportTokens, downloadFile, copyToClipboard, exportFormats, generateAllThemesCSS, type ExportFormat } from "@/components/theme/token-export-utils";
32
+ import { cn } from "@/lib/utils";
33
+
34
+ if (typeof window !== "undefined") {
35
+ gsap.registerPlugin(ScrollTrigger);
36
+ }
37
+
38
+ const galleryThemes = [...NITRO_PUBLIC_THEMES, MEMORY_LANE_THEME];
39
+
40
+ export default function GalleryPage() {
41
+ const { themeId, setThemeId, colorMode, setColorMode, memoryLaneUnlocked } = useThemeContext();
42
+ const { showToast } = useToast();
43
+ const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
44
+ const [searchQuery, setSearchQuery] = useState("");
45
+ const [filter, setFilter] = useState<"all" | "public" | "secret">("all");
46
+ const [selectedExportFormat, setSelectedExportFormat] = useState<ExportFormat>("css");
47
+ const [copyStatus, setCopyStatus] = useState<"idle" | "copying" | "copied" | "error">("idle");
48
+ const gridRef = useRef<HTMLDivElement>(null);
49
+
50
+ const isLight = colorMode === "light";
51
+
52
+ const handleCopy = async (content: string) => {
53
+ setCopyStatus("copying");
54
+ const success = await copyToClipboard(content);
55
+ setCopyStatus(success ? "copied" : "error");
56
+ setTimeout(() => setCopyStatus("idle"), 1500);
57
+ };
58
+
59
+ const handleDownload = () => {
60
+ const result = exportTokens({
61
+ format: selectedExportFormat,
62
+ themeId,
63
+ colorMode
64
+ });
65
+ downloadFile(result);
66
+ showToast(`Downloaded ${result.filename}`, "success");
67
+ };
68
+
69
+ const handleDownloadAll = () => {
70
+ const result = generateAllThemesCSS();
71
+ downloadFile(result);
72
+ showToast(`Downloaded ${result.filename}`, "success");
73
+ };
74
+
75
+ useEffect(() => {
76
+ const grid = gridRef.current;
77
+ if (!grid) return;
78
+
79
+ const cards = grid.querySelectorAll(".theme-card");
80
+
81
+ gsap.fromTo(
82
+ cards,
83
+ { opacity: 0, y: 30, scale: 0.9 },
84
+ {
85
+ opacity: 1,
86
+ y: 0,
87
+ scale: 1,
88
+ duration: 0.5,
89
+ stagger: {
90
+ amount: 0.3,
91
+ from: "start",
92
+ },
93
+ ease: "back.out(1.4)",
94
+ scrollTrigger: {
95
+ trigger: grid,
96
+ start: "top 85%",
97
+ },
98
+ }
99
+ );
100
+
101
+ return () => {
102
+ ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
103
+ };
104
+ }, [viewMode, filter, searchQuery]);
105
+
106
+ const handleThemeSelect = (id: ThemeId) => {
107
+ if (id === MEMORY_LANE_THEME.id && !memoryLaneUnlocked) {
108
+ showToast("Unlock Memory Lane by viewing all public themes!", "info");
109
+ return;
110
+ }
111
+ setThemeId(id);
112
+ showToast(`Applied ${galleryThemes.find(t => t.id === id)?.label} theme`, "success");
113
+ };
114
+
115
+ const isActive = (id: string) => themeId === id;
116
+ const isLocked = (id: string) => id === MEMORY_LANE_THEME.id && !memoryLaneUnlocked;
117
+
118
+ const filteredThemes = galleryThemes.filter((theme) => {
119
+ const matchesSearch = theme.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
120
+ theme.id.toLowerCase().includes(searchQuery.toLowerCase());
121
+ const matchesFilter = filter === "all" ||
122
+ (filter === "secret" && theme.id === MEMORY_LANE_THEME.id) ||
123
+ (filter === "public" && theme.id !== MEMORY_LANE_THEME.id);
124
+ return matchesSearch && matchesFilter;
125
+ });
126
+
127
+ return (
128
+ <main className="min-h-screen px-3 sm:px-4 lg:px-6 xl:px-8 pb-12 sm:pb-20 pt-8 sm:pt-12">
129
+ <section className="mx-auto flex w-full max-w-7xl flex-col gap-6 sm:gap-8">
130
+ <SiteHeader />
131
+
132
+ {/* Header */}
133
+ <div className="flex flex-col gap-3">
134
+ <Badge variant="glass" className="w-fit gap-1 text-xs sm:text-sm">
135
+ <Image className="h-3 w-3 sm:h-4 sm:w-4" />
136
+ Gallery
137
+ </Badge>
138
+ <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 sm:gap-4">
139
+ <div>
140
+ <h1 className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-semibold">
141
+ Theme Gallery
142
+ </h1>
143
+ <p className="max-w-2xl text-xs sm:text-sm text-muted-foreground mt-1 sm:mt-2">
144
+ Preview every gradient preset with consistent shadcn surfaces. Use these palettes as-is or remix them
145
+ to match your brand.
146
+ </p>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ {/* Search and Filter Bar */}
152
+ <div className="flex flex-col sm:flex-row gap-3 sm:gap-4 p-3 sm:p-4 rounded-xl sm:rounded-2xl border border-border/50 bg-background/60">
153
+ {/* Search */}
154
+ <div className="relative flex-1">
155
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
156
+ <input
157
+ type="text"
158
+ placeholder="Search themes..."
159
+ value={searchQuery}
160
+ onChange={(e) => setSearchQuery(e.target.value)}
161
+ className="w-full pl-9 sm:pl-10 pr-3 sm:pr-4 py-2 rounded-lg border border-border/50 bg-background text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
162
+ />
163
+ </div>
164
+
165
+ {/* Filter */}
166
+ <div className="flex items-center gap-2">
167
+ <Filter className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
168
+ <div className="flex rounded-lg border border-border/50 bg-background overflow-hidden">
169
+ {["all", "public", "secret"].map((f) => (
170
+ <button
171
+ key={f}
172
+ onClick={() => setFilter(f as typeof filter)}
173
+ className={`px-2 sm:px-3 py-1.5 text-[10px] sm:text-xs capitalize transition-colors ${
174
+ filter === f
175
+ ? "bg-primary text-primary-foreground"
176
+ : "hover:bg-muted"
177
+ }`}
178
+ >
179
+ {f}
180
+ </button>
181
+ ))}
182
+ </div>
183
+ </div>
184
+
185
+ {/* View Mode */}
186
+ <div className="flex items-center gap-1">
187
+ <Button
188
+ variant={viewMode === "grid" ? "default" : "ghost"}
189
+ size="sm"
190
+ onClick={() => setViewMode("grid")}
191
+ className="h-8 sm:h-9 px-2 sm:px-3"
192
+ >
193
+ <Grid className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
194
+ </Button>
195
+ <Button
196
+ variant={viewMode === "list" ? "default" : "ghost"}
197
+ size="sm"
198
+ onClick={() => setViewMode("list")}
199
+ className="h-8 sm:h-9 px-2 sm:px-3"
200
+ >
201
+ <List className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
202
+ </Button>
203
+ </div>
204
+ </div>
205
+
206
+ {/* Stats Bar */}
207
+ <div className="flex flex-wrap items-center gap-2 sm:gap-4 p-2 sm:p-3 rounded-lg sm:rounded-xl border border-border/50 bg-background/60">
208
+ <div className="flex items-center gap-2">
209
+ <Badge variant="glass" className="text-[10px] sm:text-xs">{NITRO_PUBLIC_THEMES.length} Public</Badge>
210
+ <Badge variant="outline" className="text-[10px] sm:text-xs">1 Secret</Badge>
211
+ </div>
212
+ <div className="h-3 sm:h-4 w-px bg-border" />
213
+ <p className="text-[10px] sm:text-xs text-muted-foreground">
214
+ Current: <span className="font-semibold text-foreground">{galleryThemes.find(t => t.id === themeId)?.label}</span>
215
+ </p>
216
+ <div className="flex-1" />
217
+ <p className="text-[10px] sm:text-xs text-muted-foreground">
218
+ Showing {filteredThemes.length} of {galleryThemes.length}
219
+ </p>
220
+ </div>
221
+
222
+ {/* Export Controls */}
223
+ <div className="flex flex-wrap items-center gap-3 p-3 sm:p-4 rounded-xl sm:rounded-2xl border border-border/50 bg-background/60">
224
+ <div className="flex items-center gap-2">
225
+ <Palette className="h-4 w-4 text-primary" />
226
+ <span className="text-sm font-medium">Export Theme</span>
227
+ </div>
228
+
229
+ <div className="h-4 w-px bg-border" />
230
+
231
+ <Select value={selectedExportFormat} onValueChange={(v: string) => setSelectedExportFormat(v as ExportFormat)}>
232
+ <SelectTrigger className="w-[130px] h-8">
233
+ <SelectValue placeholder="Format" />
234
+ </SelectTrigger>
235
+ <SelectContent>
236
+ {exportFormats.map((format) => (
237
+ <SelectItem key={format.value} value={format.value}>
238
+ {format.label}
239
+ </SelectItem>
240
+ ))}
241
+ </SelectContent>
242
+ </Select>
243
+
244
+ <Button
245
+ variant="outline"
246
+ size="sm"
247
+ className="h-8 gap-1.5"
248
+ onClick={() => handleCopy(exportTokens({ format: selectedExportFormat, themeId, colorMode }).content)}
249
+ disabled={copyStatus === "copying"}
250
+ >
251
+ <Copy className="h-3.5 w-3.5" />
252
+ {copyStatus === "copying" ? "Copying..." : copyStatus === "copied" ? "Copied!" : "Copy"}
253
+ </Button>
254
+
255
+ <Button
256
+ size="sm"
257
+ className="h-8 gap-1.5"
258
+ onClick={handleDownload}
259
+ >
260
+ <Download className="h-3.5 w-3.5" />
261
+ Download
262
+ </Button>
263
+
264
+ <div className="h-4 w-px bg-border" />
265
+
266
+ <div className="flex items-center gap-2 bg-muted/50 rounded-lg px-2 py-1">
267
+ <Sun className={cn("h-4 w-4", isLight ? "text-foreground" : "text-muted-foreground")} />
268
+ <Switch
269
+ checked={!isLight}
270
+ onCheckedChange={(checked) => setColorMode(checked ? "dark" : "light")}
271
+ label="Toggle dark mode"
272
+ />
273
+ <Moon className={cn("h-4 w-4", !isLight ? "text-foreground" : "text-muted-foreground")} />
274
+ </div>
275
+
276
+ <div className="flex-1" />
277
+
278
+ <Button
279
+ variant="secondary"
280
+ size="sm"
281
+ className="h-8 gap-1.5"
282
+ onClick={handleDownloadAll}
283
+ >
284
+ <Download className="h-3.5 w-3.5" />
285
+ Download All ({NITRO_ALL_THEMES.length})
286
+ </Button>
287
+ </div>
288
+
289
+ {/* Theme Grid/List */}
290
+ <section className="space-y-3 sm:space-y-4">
291
+ <div
292
+ ref={gridRef}
293
+ className={viewMode === "grid"
294
+ ? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4"
295
+ : "flex flex-col gap-2 sm:gap-3"
296
+ }
297
+ >
298
+ {filteredThemes.map((theme) => {
299
+ const active = isActive(theme.id);
300
+ const locked = isLocked(theme.id);
301
+
302
+ return (
303
+ <div
304
+ key={theme.id}
305
+ className={`theme-card ${locked ? "cursor-not-allowed" : "cursor-pointer"}`}
306
+ onClick={() => !locked && handleThemeSelect(theme.id as ThemeId)}
307
+ >
308
+ <Card className={`
309
+ overflow-hidden border-border/50 bg-background/60 transition-all duration-300
310
+ ${active ? "ring-2 ring-primary ring-offset-1 sm:ring-offset-2" : ""}
311
+ ${!locked && !active ? "hover:shadow-lg hover:shadow-primary/10 hover:-translate-y-1" : ""}
312
+ ${locked ? "opacity-60" : ""}
313
+ ${viewMode === "list" ? "flex flex-row items-center" : ""}
314
+ `}>
315
+ <div className={`relative ${viewMode === "list" ? "w-24 sm:w-32 shrink-0" : ""}`}>
316
+ <div
317
+ className={viewMode === "grid" ? "h-24 sm:h-28" : "h-16 sm:h-20"}
318
+ style={{ backgroundImage: theme.preview }}
319
+ />
320
+
321
+ {/* Locked overlay */}
322
+ {locked && (
323
+ <div className="absolute inset-0 bg-black/60 flex items-center justify-center">
324
+ <Badge variant="outline" className="gap-1 text-[10px]">
325
+ <Sparkles className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
326
+ Locked
327
+ </Badge>
328
+ </div>
329
+ )}
330
+
331
+ {/* Active indicator */}
332
+ {active && (
333
+ <div className="absolute top-2 right-2">
334
+ <Badge variant="default" className="gap-1 text-[10px]">
335
+ <Check className="h-2.5 w-2.5" />
336
+ Active
337
+ </Badge>
338
+ </div>
339
+ )}
340
+ </div>
341
+
342
+ <CardContent className={`
343
+ flex items-center justify-between gap-2 sm:gap-3
344
+ ${viewMode === "grid" ? "py-2.5 sm:py-4 px-3 sm:px-4" : "py-2 sm:py-3 px-3 sm:px-4 flex-1"}
345
+ `}>
346
+ <div className="min-w-0">
347
+ <p className="text-xs sm:text-sm font-semibold truncate">{theme.label}</p>
348
+ <p className="truncate text-[10px] sm:text-xs text-muted-foreground">{theme.id}</p>
349
+ </div>
350
+ {theme.id === MEMORY_LANE_THEME.id ? (
351
+ <Badge variant="outline" className="shrink-0 text-[10px]">Secret</Badge>
352
+ ) : (
353
+ <Badge variant="glass" className="shrink-0 text-[10px]">Public</Badge>
354
+ )}
355
+ </CardContent>
356
+ </Card>
357
+ </div>
358
+ );
359
+ })}
360
+ </div>
361
+
362
+ {filteredThemes.length === 0 && (
363
+ <div className="text-center py-8 sm:py-12">
364
+ <p className="text-sm sm:text-base text-muted-foreground">No themes found matching your search.</p>
365
+ <Button
366
+ variant="outline"
367
+ size="sm"
368
+ className="mt-3 sm:mt-4"
369
+ onClick={() => { setSearchQuery(""); setFilter("all"); }}
370
+ >
371
+ Clear filters
372
+ </Button>
373
+ </div>
374
+ )}
375
+ </section>
376
+
377
+ {/* Quick Tips */}
378
+ <div className="rounded-xl sm:rounded-2xl border border-border/50 bg-background/60 p-4 sm:p-6">
379
+ <h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Pro Tips</h3>
380
+ <div className="grid gap-2 sm:gap-3 sm:grid-cols-2 lg:grid-cols-3">
381
+ {[
382
+ { num: 1, text: "Click any theme to apply it instantly" },
383
+ { num: 2, text: "View all 20 public themes to unlock Memory Lane" },
384
+ { num: 3, text: "Use the Studio for detailed component preview" },
385
+ ].map((tip) => (
386
+ <div key={tip.num} className="flex items-start gap-2 sm:gap-3 text-xs sm:text-sm text-muted-foreground">
387
+ <span className="flex h-5 w-5 sm:h-6 sm:w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] sm:text-xs font-semibold text-primary">
388
+ {tip.num}
389
+ </span>
390
+ {tip.text}
391
+ </div>
392
+ ))}
393
+ </div>
394
+ </div>
395
+ </section>
396
+ </main>
397
+ );
398
+ }