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,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
|
+
}
|