Skip to content

Commit 5bc2480

Browse files
feat(themes) : add interactive customization studio with real-time preview and export configuration
1 parent 9b64cab commit 5bc2480

9 files changed

Lines changed: 199 additions & 16 deletions

File tree

app/components/CustomizeCTA.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function CustomizeCTA() {
2121

2222
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-8 px-8 py-10">
2323
<div className="flex-1 text-center md:text-left">
24-
<p className="text-xs font-bold uppercase tracking-[0.25em] text-emerald-400 mb-3">
24+
<p className="text-xs font-bold uppercase tracking-[0.25em] text-emerald-600 dark:text-emerald-400 mb-3">
2525
Customization Studio
2626
</p>
2727
<h2 className="text-2xl md:text-3xl font-extrabold text-black dark:text-white tracking-tight mb-3 leading-snug">

app/components/navbar.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const NAV_LINKS = [
2222

2323
export default function Navbar() {
2424
const [open, setOpen] = useState(false);
25+
const [mounted, setMounted] = useState(false);
2526

2627
const [isDark, setIsDark] = useState(() => {
2728
if (typeof window === 'undefined') return true;
@@ -32,6 +33,11 @@ export default function Navbar() {
3233
const { shellRef, shellVars, handleMouseEnter, handleMouseMove, handleMouseLeave } =
3334
useGlowEffect();
3435

36+
useEffect(() => {
37+
// eslint-disable-next-line react-hooks/set-state-in-effect
38+
setMounted(true);
39+
}, []);
40+
3541
useEffect(() => {
3642
document.documentElement.classList.toggle('dark', isDark);
3743
localStorage.setItem('theme', isDark ? 'dark' : 'light');
@@ -124,7 +130,15 @@ export default function Navbar() {
124130
className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-white/15 bg-white/5 text-white transition hover:bg-white/10"
125131
aria-label="Toggle theme"
126132
>
127-
{isDark ? <Sun size={18} /> : <Moon size={18} />}
133+
{mounted ? (
134+
isDark ? (
135+
<Sun size={18} />
136+
) : (
137+
<Moon size={18} />
138+
)
139+
) : (
140+
<span className="w-[18px] h-[18px]" />
141+
)}
128142
</button>
129143

130144
{NAV_LINKS.map((link) => (

app/customize/components/ControlsPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export function ControlsPanel({
183183

184184
return (
185185
<div>
186-
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-400 mb-4">
186+
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-600 dark:text-emerald-400 mb-4">
187187
Controls
188188
</p>
189189

app/customize/components/ExportPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function ExportPanel({
124124
<div className="bg-white/70 backdrop-blur-xl border border-black/10 dark:bg-black/35 dark:border-white/10 rounded-[1.75rem] p-6 shadow-[0_20px_60px_rgba(0,0,0,0.15)]">
125125
<div className="flex flex-col gap-4 mb-5 md:flex-row md:items-center md:justify-between">
126126
<div>
127-
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-400">
127+
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-600 dark:text-emerald-400">
128128
{formatLabel} Export Snippet
129129
</p>
130130
<p className="mt-1 text-[11px] text-gray-500 dark:text-white/25">
@@ -145,7 +145,7 @@ export function ExportPanel({
145145
aria-pressed={format === option.value}
146146
className={`rounded-lg px-3 py-1.5 text-xs font-bold transition-all ${
147147
format === option.value
148-
? 'bg-emerald-500/15 text-emerald-300 shadow-[0_0_24px_rgba(16,185,129,0.16)]'
148+
? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 shadow-[0_0_24px_rgba(16,185,129,0.16)]'
149149
: 'text-gray-600 hover:text-black bg-gray-100/70 dark:bg-transparent dark:text-white/35 dark:hover:text-white'
150150
}`}
151151
>

app/customize/components/ThemeQuickPresets.tsx

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ function IconSynthwave({ bg, text, accent }: IC): ReactElement {
305305
);
306306
}
307307

308-
function IconGruvbox({ text, accent }: IC): ReactElement {
308+
function IconGruvbox({ bg, text, accent }: IC): ReactElement {
309309
return (
310310
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" aria-hidden="true">
311311
{/* top handle */}
@@ -327,7 +327,7 @@ function IconGruvbox({ text, accent }: IC): ReactElement {
327327
{/* lantern body */}
328328
<rect x="8" y="11" width="12" height="11" rx="2.5" fill={text} opacity="0.82" />
329329
{/* glass chamber */}
330-
<rect x="10.2" y="13" width="7.6" height="6.8" rx="1.5" fill="#1d2021" opacity="0.9" />
330+
<rect x="10.2" y="13" width="7.6" height="6.8" rx="1.5" fill={bg} opacity="0.9" />
331331
{/* warm glow */}
332332
<ellipse cx="14" cy="16.5" rx="2.8" ry="3.2" fill={accent} opacity="0.95" />
333333
{/* flame */}
@@ -353,7 +353,145 @@ function IconHighcontrast({ text, accent }: IC): ReactElement {
353353
);
354354
}
355355

356-
const ICON_MAP: Record<ThemeKey, (c: IC) => ReactElement> = {
356+
function IconCatppuccinLatte({ text, accent }: IC): ReactElement {
357+
return (
358+
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" aria-hidden="true">
359+
{/* Cup body */}
360+
<path d="M6 10 C6 17, 7 20, 14 20 C21 20, 22 17, 22 10 Z" fill={accent} opacity="0.9" />
361+
{/* Cup handle */}
362+
<path
363+
d="M22 12 C24.5 12, 24.5 16, 22 16"
364+
stroke={accent}
365+
strokeWidth="2.2"
366+
strokeLinecap="round"
367+
fill="none"
368+
/>
369+
{/* Latte art / foam */}
370+
<ellipse cx="14" cy="10" rx="6.5" ry="1.8" fill={text} opacity="0.9" />
371+
{/* Steam lines */}
372+
<path
373+
d="M11 7 C11 5, 13 5, 13 3"
374+
stroke={text}
375+
strokeWidth="1.2"
376+
strokeLinecap="round"
377+
fill="none"
378+
opacity="0.6"
379+
/>
380+
<path
381+
d="M15 7 C15 5, 17 5, 17 3"
382+
stroke={text}
383+
strokeWidth="1.2"
384+
strokeLinecap="round"
385+
fill="none"
386+
opacity="0.6"
387+
/>
388+
</svg>
389+
);
390+
}
391+
392+
function IconSolarizedLight({ text, accent }: IC): ReactElement {
393+
return (
394+
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" aria-hidden="true">
395+
<circle cx="14" cy="14" r="5" stroke={accent} strokeWidth="2" opacity="0.95" />
396+
<circle cx="14" cy="14" r="2" fill={text} opacity="0.9" />
397+
{/* Solar/Astronomical clean geometric rays */}
398+
<line x1="14" y1="3" x2="14" y2="6" stroke={accent} strokeWidth="1.8" strokeLinecap="round" />
399+
<line
400+
x1="14"
401+
y1="22"
402+
x2="14"
403+
y2="25"
404+
stroke={accent}
405+
strokeWidth="1.8"
406+
strokeLinecap="round"
407+
/>
408+
<line x1="3" y1="14" x2="6" y2="14" stroke={accent} strokeWidth="1.8" strokeLinecap="round" />
409+
<line
410+
x1="22"
411+
y1="14"
412+
x2="25"
413+
y2="14"
414+
stroke={accent}
415+
strokeWidth="1.8"
416+
strokeLinecap="round"
417+
/>
418+
<line
419+
x1="6.2"
420+
y1="6.2"
421+
x2="8.3"
422+
y2="8.3"
423+
stroke={accent}
424+
strokeWidth="1.5"
425+
strokeLinecap="round"
426+
opacity="0.7"
427+
/>
428+
<line
429+
x1="19.7"
430+
y1="19.7"
431+
x2="21.8"
432+
y2="21.8"
433+
stroke={accent}
434+
strokeWidth="1.5"
435+
strokeLinecap="round"
436+
opacity="0.7"
437+
/>
438+
<line
439+
x1="19.7"
440+
y1="6.2"
441+
x2="17.6"
442+
y2="8.3"
443+
stroke={accent}
444+
strokeWidth="1.5"
445+
strokeLinecap="round"
446+
opacity="0.7"
447+
/>
448+
<line
449+
x1="6.2"
450+
y1="19.7"
451+
x2="8.3"
452+
y2="17.6"
453+
stroke={accent}
454+
strokeWidth="1.5"
455+
strokeLinecap="round"
456+
opacity="0.7"
457+
/>
458+
</svg>
459+
);
460+
}
461+
462+
function IconAuroraCyberpunk({ text, accent }: IC): ReactElement {
463+
return (
464+
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" aria-hidden="true">
465+
{/* Cyberpunk double triangles */}
466+
<polygon
467+
points="14,4 24,20 4,20"
468+
stroke={accent}
469+
strokeWidth="2.2"
470+
strokeLinejoin="round"
471+
opacity="0.9"
472+
/>
473+
<polygon
474+
points="14,10 20,20 8,20"
475+
stroke={text}
476+
strokeWidth="1.2"
477+
strokeLinejoin="round"
478+
opacity="0.7"
479+
/>
480+
<line
481+
x1="14"
482+
y1="4"
483+
x2="14"
484+
y2="20"
485+
stroke={accent}
486+
strokeWidth="1"
487+
strokeDasharray="2 2"
488+
opacity="0.5"
489+
/>
490+
</svg>
491+
);
492+
}
493+
494+
const ICON_MAP: Record<string, (c: IC) => ReactElement> = {
357495
dark: (c) => <IconDark {...c} />,
358496
light: (c) => <IconLight {...c} />,
359497
neon: (c) => <IconNeon {...c} />,
@@ -367,6 +505,11 @@ const ICON_MAP: Record<ThemeKey, (c: IC) => ReactElement> = {
367505
synthwave: (c) => <IconSynthwave {...c} />,
368506
gruvbox: (c) => <IconGruvbox {...c} />,
369507
highcontrast: (c) => <IconHighcontrast {...c} />,
508+
aurora_cyberpunk: (c) => <IconAuroraCyberpunk {...c} />,
509+
catppuccin_latte: (c) => <IconCatppuccinLatte {...c} />,
510+
solarized_light: (c) => <IconSolarizedLight {...c} />,
511+
gruvbox_light: (c) => <IconGruvbox {...c} />,
512+
nord_light: (c) => <IconNord {...c} />,
370513
};
371514

372515
export function ThemeQuickPresets({ theme, onThemeChange }: ThemeQuickPresetsProps): ReactElement {

app/customize/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export default function CustomizePage(): ReactElement {
227227
<div className="h-4 w-px bg-white/10" />
228228

229229
<div>
230-
<span className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-400">
230+
<span className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-600 dark:text-emerald-400">
231231
Customization Studio
232232
</span>
233233
</div>
@@ -316,7 +316,7 @@ export default function CustomizePage(): ReactElement {
316316
>
317317
{/* Live Preview */}
318318
<div className="bg-white/70 backdrop-blur-xl border border-black/10 dark:bg-black/35 dark:border-white/10 rounded-[1.75rem] p-6 shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
319-
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-400 mb-5">
319+
<p className="text-xs font-bold uppercase tracking-[0.22em] text-emerald-600 dark:text-emerald-400 mb-5">
320320
Live Preview
321321
</p>
322322

@@ -404,7 +404,9 @@ export default function CustomizePage(): ReactElement {
404404
>
405405
<span className="text-purple-400">{decodeURIComponent(k)}</span>
406406
<span className="text-gray-400 dark:text-white/20">=</span>
407-
<span className="text-emerald-400">{decodeURIComponent(v)}</span>
407+
<span className="text-emerald-600 dark:text-emerald-400">
408+
{decodeURIComponent(v)}
409+
</span>
408410
</span>
409411
);
410412
}

app/documentation/page.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,26 @@ const themeDescriptions: Record<
165165
name: 'High Contrast',
166166
vibe: 'Ultra-high contrast dark theme with bold orange-red highlights for maximum visibility.',
167167
},
168+
169+
catppuccin_latte: {
170+
name: 'Catppuccin Latte',
171+
vibe: 'A cozy, warm light theme from the popular Catppuccin palette.',
172+
},
173+
174+
solarized_light: {
175+
name: 'Solarized Light',
176+
vibe: 'The classic Solarized light palette with excellent contrast and retro aesthetics.',
177+
},
178+
179+
gruvbox_light: {
180+
name: 'Gruvbox Light',
181+
vibe: 'Earthy, warm retro cream background with high-contrast charcoal text.',
182+
},
183+
184+
nord_light: {
185+
name: 'Nord Light',
186+
vibe: 'Ice-cold and minimal light gray palette with signature arctic-blue accents.',
187+
},
168188
};
169189

170190
const allthemes = Object.entries(themePalette).map(([slug, palette]) => ({
@@ -196,7 +216,7 @@ export default function DocumentationPage() {
196216

197217
<div className="relative mx-auto flex min-h-screen max-w-[1600px] flex-col px-6 pb-10 pt-6 md:px-8">
198218
<section className="mb-10 rounded-[2rem] border border-black/10 bg-white px-6 py-10 shadow-[0_20px_60px_rgba(0,0,0,0.06)] backdrop-blur dark:border-white/10 dark:bg-white/[0.03] dark:shadow-[0_30px_100px_rgba(0,0,0,0.45)] md:px-10">
199-
<div className="mb-6 inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-500/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-emerald-300">
219+
<div className="mb-6 inline-flex items-center rounded-full border border-emerald-300/30 bg-emerald-50 dark:border-emerald-400/20 dark:bg-emerald-500/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-emerald-700 dark:text-emerald-300">
200220
Documentation
201221
</div>
202222
<div className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:items-end">
@@ -306,7 +326,7 @@ export default function DocumentationPage() {
306326
key={parameter.name}
307327
className="border-t border-black/10 align-top dark:border-white/8"
308328
>
309-
<td className="px-4 py-4 font-mono text-sm text-emerald-300">
329+
<td className="px-4 py-4 font-mono text-sm text-emerald-700 dark:text-emerald-300">
310330
{parameter.name}
311331
</td>
312332
<td className="px-4 py-4 text-sm text-gray-700 dark:text-white/70">
@@ -401,7 +421,7 @@ export default function DocumentationPage() {
401421
key={note}
402422
className="flex gap-3 rounded-[1.25rem] border border-black/10 bg-gray-50 px-4 py-4 dark:border-white/8 dark:bg-black/35"
403423
>
404-
<span className="mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-400 shadow-[0_0_18px_rgba(52,211,153,0.9)]" />
424+
<span className="mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-500 dark:bg-emerald-400 shadow-[0_0_18px_rgba(52,211,153,0.9)]" />
405425
<p className="text-sm leading-7 text-gray-600 dark:text-white/60">{note}</p>
406426
</div>
407427
))}
@@ -457,7 +477,7 @@ function Panel({
457477
}) {
458478
return (
459479
<section className="rounded-[2rem] border border-black/10 bg-white p-6 shadow-[0_20px_60px_rgba(0,0,0,0.06)] backdrop-blur dark:border-white/10 dark:bg-white/[0.03] dark:shadow-[0_30px_80px_rgba(0,0,0,0.32)] md:p-8">
460-
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-300">
480+
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-600 dark:text-emerald-300">
461481
{eyebrow}
462482
</p>
463483
<h2 className="mt-3 text-2xl font-bold tracking-tight text-black dark:text-white md:text-3xl">

components/ReturnToTop.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default function ReturnToTop() {
4242
exit={{ opacity: 0, y: 20 }}
4343
transition={{ duration: 0.3 }}
4444
onClick={scrollToTop}
45-
className="fixed bottom-8 right-8 p-3 bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-600 hover:to-blue-600 text-white rounded-full shadow-lg hover:shadow-xl transition-shadow duration-300 z-50 flex items-center justify-center"
45+
className="fixed bottom-8 right-8 p-3 rounded-full border border-emerald-500/20 bg-white text-emerald-600 hover:bg-emerald-500 hover:text-white hover:border-emerald-500 dark:border-emerald-400/20 dark:bg-black dark:text-emerald-400 dark:hover:bg-emerald-400 dark:hover:text-black dark:hover:border-emerald-400 hover:scale-110 active:scale-95 shadow-[0_4px_20px_rgba(16,185,129,0.15)] dark:shadow-[0_4px_30px_rgba(16,185,129,0.3)] transition-all duration-300 z-50 flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00ffaa] focus-visible:ring-offset-2"
4646
aria-label="Return to top"
4747
>
4848
<ChevronUp size={24} />

lib/svg/themes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const themes: Record<string, BadgeTheme> = {
2525
gruvbox: makeTheme('282828', 'ebdbb2', 'fe8019'),
2626
aurora_cyberpunk: makeTheme('090B13', 'EAF2FF', '9D5CFF'),
2727
highcontrast: makeTheme('0a0a0a', '888888', 'ff4500'),
28+
catppuccin_latte: makeTheme('eff1f5', '4c4f69', '1e66f5'),
29+
solarized_light: makeTheme('fdf6e3', '586e75', '268bd2'),
30+
gruvbox_light: makeTheme('fbf1c7', '3c3836', 'd65d0e'),
31+
nord_light: makeTheme('eceff4', '2e3440', '5e81ac'),
2832
};
2933

3034
// Auto-theme pairs: the SVG switches between these two palettes

0 commit comments

Comments
 (0)