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.
- package/.eslintrc.json +3 -0
- package/.github/FUNDING.yml +2 -0
- package/README.md +140 -0
- package/app/docs/page.tsx +417 -0
- package/app/gallery/page.tsx +398 -0
- package/app/globals.css +1155 -0
- package/app/layout.tsx +36 -0
- package/app/page.tsx +600 -0
- package/app/showcase/page.tsx +730 -0
- package/app/studio/page.tsx +1310 -0
- package/cli/index.mjs +1141 -0
- package/cli/templates/theme-context.tsx +120 -0
- package/cli/templates/theme-engine.ts +237 -0
- package/cli/templates/themes.css +512 -0
- package/components/site/component-showcase.tsx +623 -0
- package/components/site/site-data.ts +103 -0
- package/components/site/site-header.tsx +270 -0
- package/components/templates/blog.tsx +198 -0
- package/components/templates/components-showcase.tsx +298 -0
- package/components/templates/dashboard.tsx +246 -0
- package/components/templates/ecommerce.tsx +199 -0
- package/components/templates/mail.tsx +275 -0
- package/components/templates/saas-landing.tsx +169 -0
- package/components/theme/studio-code-panel.tsx +485 -0
- package/components/theme/theme-context.tsx +120 -0
- package/components/theme/theme-engine.ts +237 -0
- package/components/theme/theme-exporter.tsx +369 -0
- package/components/theme/theme-panel.tsx +268 -0
- package/components/theme/token-export-utils.ts +1211 -0
- package/components/ui/animated.tsx +55 -0
- package/components/ui/avatar.tsx +38 -0
- package/components/ui/badge.tsx +32 -0
- package/components/ui/button.tsx +65 -0
- package/components/ui/card.tsx +56 -0
- package/components/ui/checkbox.tsx +19 -0
- package/components/ui/command-palette.tsx +245 -0
- package/components/ui/gsap-animated.tsx +436 -0
- package/components/ui/input.tsx +17 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/skeleton.tsx +102 -0
- package/components/ui/switch.tsx +43 -0
- package/components/ui/tabs.tsx +115 -0
- package/components/ui/toast.tsx +119 -0
- package/gradient-forge/theme-context.tsx +119 -0
- package/gradient-forge/theme-engine.ts +236 -0
- package/gradient-forge/themes.css +556 -0
- package/lib/animations.ts +50 -0
- package/lib/gsap.ts +426 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +6 -0
- package/package.json +53 -0
- package/postcss.config.mjs +5 -0
- package/tailwind.config.ts +15 -0
- package/tsconfig.json +43 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useThemeContext } from "@/components/theme/theme-context";
|
|
4
|
+
import { MEMORY_LANE_THEME, NITRO_PUBLIC_THEMES, NITRO_ALL_THEMES, type ThemeId } from "@/components/theme/theme-engine";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
import { Switch } from "@/components/ui/switch";
|
|
9
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
import { Lock, Moon, Sun, Download, Copy, Check, FileCode, Palette } from "lucide-react";
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { exportTokens, downloadFile, copyToClipboard, exportFormats, generateAllThemesCSS, type ExportFormat } from "./token-export-utils";
|
|
14
|
+
|
|
15
|
+
export function ThemePanel() {
|
|
16
|
+
const {
|
|
17
|
+
themeId,
|
|
18
|
+
colorMode,
|
|
19
|
+
availableThemes,
|
|
20
|
+
memoryLaneUnlocked,
|
|
21
|
+
remainingForUnlock,
|
|
22
|
+
setThemeId,
|
|
23
|
+
setColorMode,
|
|
24
|
+
} = useThemeContext();
|
|
25
|
+
|
|
26
|
+
const isLight = colorMode === "light";
|
|
27
|
+
const totalThemes = NITRO_PUBLIC_THEMES.length;
|
|
28
|
+
const seenThemes = totalThemes - remainingForUnlock;
|
|
29
|
+
const progress = Math.round((seenThemes / totalThemes) * 100);
|
|
30
|
+
|
|
31
|
+
const [selectedExportFormat, setSelectedExportFormat] = useState<ExportFormat>("css");
|
|
32
|
+
const [copyStatus, setCopyStatus] = useState<"idle" | "copying" | "copied" | "error">("idle");
|
|
33
|
+
|
|
34
|
+
const handleCopy = async (content: string) => {
|
|
35
|
+
setCopyStatus("copying");
|
|
36
|
+
const success = await copyToClipboard(content);
|
|
37
|
+
setCopyStatus(success ? "copied" : "error");
|
|
38
|
+
setTimeout(() => setCopyStatus("idle"), 1500);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleDownload = () => {
|
|
42
|
+
const result = exportTokens({
|
|
43
|
+
format: selectedExportFormat,
|
|
44
|
+
themeId,
|
|
45
|
+
colorMode
|
|
46
|
+
});
|
|
47
|
+
downloadFile(result);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleDownloadAll = () => {
|
|
51
|
+
const result = generateAllThemesCSS();
|
|
52
|
+
downloadFile(result);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const currentTheme = NITRO_ALL_THEMES.find(t => t.id === themeId);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Card className="border-border/50 bg-background/60">
|
|
59
|
+
<CardHeader>
|
|
60
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
61
|
+
<div>
|
|
62
|
+
<CardTitle>Gradient Studio</CardTitle>
|
|
63
|
+
<CardDescription>
|
|
64
|
+
Preview every theme to unlock the secret Memory Lane palette.
|
|
65
|
+
</CardDescription>
|
|
66
|
+
</div>
|
|
67
|
+
<Badge variant="glass">Shadcn Ready</Badge>
|
|
68
|
+
</div>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent className="space-y-6">
|
|
71
|
+
{/* Theme Selection */}
|
|
72
|
+
<div className="flex flex-col gap-3 rounded-2xl border border-border/40 bg-background/50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
73
|
+
<div className="space-y-1 flex-1">
|
|
74
|
+
<p className="text-sm font-semibold flex items-center gap-2">
|
|
75
|
+
<Palette className="h-4 w-4" />
|
|
76
|
+
Select Theme
|
|
77
|
+
</p>
|
|
78
|
+
<p className="text-xs text-muted-foreground">
|
|
79
|
+
Choose a theme to export.
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<Select value={themeId} onValueChange={(v: string) => setThemeId(v as ThemeId)}>
|
|
83
|
+
<SelectTrigger className="w-full sm:w-[220px]">
|
|
84
|
+
<SelectValue placeholder="Select theme" />
|
|
85
|
+
</SelectTrigger>
|
|
86
|
+
<SelectContent>
|
|
87
|
+
{NITRO_ALL_THEMES.map((theme) => (
|
|
88
|
+
<SelectItem key={theme.id} value={theme.id}>
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<div
|
|
91
|
+
className="w-4 h-4 rounded-md border border-border/30"
|
|
92
|
+
style={{ background: theme.preview }}
|
|
93
|
+
/>
|
|
94
|
+
<span>{theme.label}</span>
|
|
95
|
+
</div>
|
|
96
|
+
</SelectItem>
|
|
97
|
+
))}
|
|
98
|
+
</SelectContent>
|
|
99
|
+
</Select>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Color Mode Toggle */}
|
|
103
|
+
<div className="flex flex-col gap-3 rounded-2xl border border-border/40 bg-background/50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
104
|
+
<div className="space-y-1">
|
|
105
|
+
<p className="text-sm font-semibold">Color Mode</p>
|
|
106
|
+
<p className="text-xs text-muted-foreground">
|
|
107
|
+
Switch between light and dark surfaces.
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex items-center gap-3 sm:justify-end">
|
|
111
|
+
<Sun className={cn("h-4 w-4", isLight ? "text-foreground" : "text-muted-foreground")} />
|
|
112
|
+
<Switch
|
|
113
|
+
checked={!isLight}
|
|
114
|
+
onCheckedChange={(checked) => setColorMode(checked ? "dark" : "light")}
|
|
115
|
+
label="Toggle dark mode"
|
|
116
|
+
/>
|
|
117
|
+
<Moon className={cn("h-4 w-4", !isLight ? "text-foreground" : "text-muted-foreground")} />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Export Section */}
|
|
122
|
+
<div className="flex flex-col gap-3 rounded-2xl border border-border/40 bg-background/50 px-4 py-3">
|
|
123
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
124
|
+
<div>
|
|
125
|
+
<p className="text-sm font-semibold flex items-center gap-2">
|
|
126
|
+
<Download className="h-4 w-4" />
|
|
127
|
+
Export Theme
|
|
128
|
+
</p>
|
|
129
|
+
<p className="text-xs text-muted-foreground">
|
|
130
|
+
Download theme files for your project.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="flex flex-wrap gap-2">
|
|
136
|
+
<Select value={selectedExportFormat} onValueChange={(v: string) => setSelectedExportFormat(v as ExportFormat)}>
|
|
137
|
+
<SelectTrigger className="w-[140px] h-8">
|
|
138
|
+
<SelectValue placeholder="Format" />
|
|
139
|
+
</SelectTrigger>
|
|
140
|
+
<SelectContent>
|
|
141
|
+
{exportFormats.map((format) => (
|
|
142
|
+
<SelectItem key={format.value} value={format.value}>
|
|
143
|
+
{format.label}
|
|
144
|
+
</SelectItem>
|
|
145
|
+
))}
|
|
146
|
+
</SelectContent>
|
|
147
|
+
</Select>
|
|
148
|
+
|
|
149
|
+
<Button
|
|
150
|
+
variant="outline"
|
|
151
|
+
size="sm"
|
|
152
|
+
className="h-8 gap-1.5"
|
|
153
|
+
onClick={() => handleCopy(exportTokens({ format: selectedExportFormat, themeId, colorMode }).content)}
|
|
154
|
+
disabled={copyStatus === "copying"}
|
|
155
|
+
>
|
|
156
|
+
{copyStatus === "copied" ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
157
|
+
{copyStatus === "copying" ? "Copying..." : copyStatus === "copied" ? "Copied!" : "Copy"}
|
|
158
|
+
</Button>
|
|
159
|
+
|
|
160
|
+
<Button
|
|
161
|
+
size="sm"
|
|
162
|
+
className="h-8 gap-1.5"
|
|
163
|
+
onClick={handleDownload}
|
|
164
|
+
>
|
|
165
|
+
<Download className="h-3.5 w-3.5" />
|
|
166
|
+
Download
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div className="pt-2 border-t border-border/30">
|
|
171
|
+
<Button
|
|
172
|
+
variant="secondary"
|
|
173
|
+
size="sm"
|
|
174
|
+
className="w-full h-8 gap-1.5"
|
|
175
|
+
onClick={handleDownloadAll}
|
|
176
|
+
>
|
|
177
|
+
<FileCode className="h-3.5 w-3.5" />
|
|
178
|
+
Download All Themes ({NITRO_ALL_THEMES.length} themes)
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="space-y-3 rounded-2xl border border-border/40 bg-background/50 px-4 py-3">
|
|
184
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
185
|
+
<div>
|
|
186
|
+
<p className="text-sm font-semibold">Secret Theme Unlock</p>
|
|
187
|
+
<p className="text-xs text-muted-foreground">
|
|
188
|
+
Preview all {totalThemes} public themes to reveal Memory Lane.
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
<Badge variant={memoryLaneUnlocked ? "default" : "outline"}>
|
|
192
|
+
{memoryLaneUnlocked ? "Unlocked" : `${remainingForUnlock} remaining`}
|
|
193
|
+
</Badge>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
197
|
+
<span>{seenThemes} of {totalThemes} previewed</span>
|
|
198
|
+
<span>{progress}%</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="h-2 rounded-full bg-border/60">
|
|
201
|
+
<div
|
|
202
|
+
className="h-full rounded-full bg-primary/80 transition-all"
|
|
203
|
+
style={{ width: `${progress}%` }}
|
|
204
|
+
aria-hidden
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
211
|
+
{availableThemes.map((theme) => {
|
|
212
|
+
const isActive = themeId === theme.id;
|
|
213
|
+
const isSecret = theme.id === MEMORY_LANE_THEME.id;
|
|
214
|
+
const isLocked = isSecret && !memoryLaneUnlocked;
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div
|
|
218
|
+
key={theme.id}
|
|
219
|
+
role="button"
|
|
220
|
+
tabIndex={isLocked ? -1 : 0}
|
|
221
|
+
onClick={() => !isLocked && setThemeId(theme.id)}
|
|
222
|
+
onKeyDown={(event) => {
|
|
223
|
+
if (isLocked) return;
|
|
224
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
setThemeId(theme.id);
|
|
227
|
+
}
|
|
228
|
+
}}
|
|
229
|
+
className={cn(
|
|
230
|
+
"group flex flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/40 text-left transition-all",
|
|
231
|
+
isActive && "border-primary/60 shadow-[0_0_25px_hsl(var(--primary)_/_0.35)]",
|
|
232
|
+
isLocked && "cursor-not-allowed opacity-70",
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
<div
|
|
236
|
+
className="relative h-24 w-full"
|
|
237
|
+
style={{ backgroundImage: theme.preview }}
|
|
238
|
+
>
|
|
239
|
+
<div className="absolute inset-0 bg-black/10" />
|
|
240
|
+
{isLocked && (
|
|
241
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/40 text-white">
|
|
242
|
+
<Lock className="h-5 w-5" />
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex flex-wrap items-center justify-between gap-2 px-3 py-3">
|
|
247
|
+
<div className="min-w-0">
|
|
248
|
+
<p className="text-sm font-semibold">{theme.label}</p>
|
|
249
|
+
<p className="text-xs text-muted-foreground">
|
|
250
|
+
{isActive ? "Active" : "Preview"}
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
<Button
|
|
254
|
+
variant={isActive ? "glow" : "ghost"}
|
|
255
|
+
size="sm"
|
|
256
|
+
className="h-7 px-3"
|
|
257
|
+
>
|
|
258
|
+
{isActive ? "Live" : "Set"}
|
|
259
|
+
</Button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
})}
|
|
264
|
+
</div>
|
|
265
|
+
</CardContent>
|
|
266
|
+
</Card>
|
|
267
|
+
);
|
|
268
|
+
}
|