Add live design configurator for marketing surface

Users can now compose their marketing site by selecting:
- Mode (dark/light), Background style (gradient/beams/meteors/etc.),
  Nav style, Hero header layout, which Sections appear, and Font.

All 4 marketing scaffolds (DaisyUI, HeroUI, Aceternity, Tailwind)
respond live to config changes. Library capability cards + style
options data defined per library. Aceternity shows actual
background effects (beams, meteors, sparkles, wavy, dot-grid).

Made-with: Cursor
This commit is contained in:
2026-03-05 20:15:59 -08:00
parent a980354da6
commit 9c8e1a5f34
4 changed files with 1167 additions and 186 deletions

View File

@@ -2,7 +2,10 @@
import { use, useState, useEffect } from "react";
import { toast } from "sonner";
import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor } from "@/components/design-scaffolds";
import {
SCAFFOLD_REGISTRY, THEME_REGISTRY,
type ThemeColor, type DesignConfig, type LibraryStyleOptions,
} from "@/components/design-scaffolds";
// ---------------------------------------------------------------------------
// Surface definitions
@@ -201,6 +204,310 @@ const ALL_SURFACES: Surface[] = [
},
];
// ---------------------------------------------------------------------------
// Library style configurator options — per library, for the Marketing surface
// (other surfaces can adopt this pattern as needed)
// ---------------------------------------------------------------------------
const LIBRARY_STYLE_OPTIONS: Record<string, LibraryStyleOptions> = {
"daisy-ui": {
modes: [{ id: "light", label: "Light" }, { id: "dark", label: "Dark" }],
backgrounds: [
{ id: "solid", label: "Solid", description: "Clean theme background" },
{ id: "gradient", label: "Gradient", description: "Radial glow from primary color" },
{ id: "pattern", label: "Dot grid", description: "Subtle repeating dot pattern" },
{ id: "noise", label: "Noise", description: "Fine grain texture" },
],
navStyles: [
{ id: "standard", label: "Standard", description: "Bordered top bar" },
{ id: "transparent", label: "Transparent", description: "Overlaps the hero section" },
{ id: "pill", label: "Pill", description: "Floating centered nav" },
],
headerStyles: [
{ id: "centered", label: "Centered", description: "Title and CTAs centred" },
{ id: "split", label: "Split", description: "Left text, right mockup" },
{ id: "stats", label: "With stats", description: "Metrics row below CTAs" },
],
components: [
{ id: "logos", label: "Logo strip" },
{ id: "features", label: "Feature cards" },
{ id: "steps", label: "How it works" },
{ id: "testimonials", label: "Testimonials" },
{ id: "pricing", label: "Pricing table" },
{ id: "faq", label: "FAQ" },
{ id: "cta", label: "CTA banner" },
],
fonts: [
{ id: "system", label: "System UI" },
{ id: "plus-jakarta", label: "Plus Jakarta" },
{ id: "dm-sans", label: "DM Sans" },
{ id: "geist", label: "Geist" },
],
defaultConfig: { mode: "dark", background: "solid", nav: "standard", header: "centered", components: ["features", "pricing"], font: "system" },
},
"hero-ui": {
modes: [{ id: "light", label: "Light" }, { id: "dark", label: "Dark" }],
backgrounds: [
{ id: "clean", label: "Clean", description: "Solid background" },
{ id: "gradient-mesh", label: "Gradient mesh", description: "Soft multi-colour mesh" },
{ id: "glass", label: "Glassmorphism", description: "Frosted glass feel" },
{ id: "aurora", label: "Aurora", description: "Animated colour glow" },
],
navStyles: [
{ id: "blur", label: "Blur backdrop", description: "Frosted glass sticky navbar" },
{ id: "standard", label: "Standard", description: "Solid bordered bar" },
{ id: "minimal", label: "Minimal", description: "No border, clean" },
],
headerStyles: [
{ id: "animated-badge", label: "Animated", description: "Chip badge + gradient headline" },
{ id: "split", label: "Split", description: "Text + dashboard preview" },
{ id: "gradient", label: "Gradient", description: "Full gradient headline" },
],
components: [
{ id: "features", label: "Feature cards" },
{ id: "metrics", label: "Metrics grid" },
{ id: "avatars", label: "Social proof" },
{ id: "pricing", label: "Pricing" },
{ id: "testimonials", label: "Testimonials" },
{ id: "cta", label: "CTA section" },
],
fonts: [
{ id: "system", label: "System UI" },
{ id: "inter", label: "Inter" },
{ id: "geist", label: "Geist" },
{ id: "nunito", label: "Nunito" },
],
defaultConfig: { mode: "light", background: "clean", nav: "blur", header: "animated-badge", components: ["features", "metrics"], font: "system" },
},
"aceternity": {
modes: [{ id: "dark", label: "Dark" }, { id: "light", label: "Light (limited)" }],
backgrounds: [
{ id: "beams", label: "Beams", description: "Signature vertical beam lines" },
{ id: "meteors", label: "Meteors", description: "Shooting particle streaks" },
{ id: "sparkles", label: "Sparkles", description: "Scattered star particles" },
{ id: "wavy", label: "Wavy", description: "Smooth wave shapes" },
{ id: "dot-grid", label: "Dot grid", description: "Perspective dot matrix" },
{ id: "gradient", label: "Glow only", description: "Pure radial glow" },
],
navStyles: [
{ id: "minimal", label: "Minimal", description: "Barely-there top bar" },
{ id: "floating", label: "Floating", description: "Centred floating pill" },
],
headerStyles: [
{ id: "gradient-text", label: "Gradient text", description: "Fade-to-transparent heading" },
{ id: "lamp", label: "Lamp effect", description: "Spotlight cone from above" },
{ id: "typewriter", label: "Typewriter", description: "Animated text reveal" },
],
components: [
{ id: "badge", label: "Glow badge" },
{ id: "features", label: "Feature cards" },
{ id: "moving-cards", label: "Moving cards" },
{ id: "bento", label: "Bento grid" },
{ id: "pricing", label: "Pricing" },
{ id: "cta", label: "CTA section" },
],
fonts: [
{ id: "system", label: "System UI" },
{ id: "geist", label: "Geist" },
{ id: "plus-jakarta", label: "Plus Jakarta" },
],
defaultConfig: { mode: "dark", background: "beams", nav: "minimal", header: "gradient-text", components: ["features", "moving-cards"], font: "system" },
},
"tailwind-only": {
modes: [{ id: "light", label: "Light" }, { id: "dark", label: "Dark" }],
backgrounds: [
{ id: "clean", label: "Clean", description: "Pure white or dark" },
{ id: "dot-grid", label: "Dot grid", description: "Subtle dot pattern" },
{ id: "lines", label: "Grid lines", description: "Faint gridlines" },
{ id: "noise", label: "Noise", description: "Fine grain texture" },
],
navStyles: [
{ id: "minimal", label: "Minimal", description: "Logo + links, no decoration" },
{ id: "bordered", label: "Bordered", description: "With bottom border" },
{ id: "spaced", label: "Spread", description: "Logo left, CTA right" },
],
headerStyles: [
{ id: "editorial", label: "Editorial", description: "Big bold display typography" },
{ id: "split", label: "Split", description: "Text left + terminal preview" },
{ id: "centered", label: "Centered", description: "Minimal centred layout" },
],
components: [
{ id: "badge", label: "Release badge" },
{ id: "logos", label: "Logo strip" },
{ id: "features", label: "Features grid" },
{ id: "stats", label: "Stats row" },
{ id: "testimonials", label: "Testimonials" },
{ id: "pricing", label: "Pricing" },
{ id: "faq", label: "FAQ" },
{ id: "cta", label: "CTA" },
],
fonts: [
{ id: "system", label: "System UI" },
{ id: "inter", label: "Inter" },
{ id: "plus-jakarta", label: "Plus Jakarta" },
{ id: "dm-sans", label: "DM Sans" },
],
defaultConfig: { mode: "light", background: "clean", nav: "minimal", header: "editorial", components: ["features", "stats", "pricing"], font: "system" },
},
};
// ---------------------------------------------------------------------------
// DesignConfigurator — shows mode/bg/nav/header/components/font pickers
// ---------------------------------------------------------------------------
function ConfigRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ display: "flex", alignItems: "flex-start", gap: 10, paddingBottom: 10, borderBottom: "1px solid #f0ece4" }}>
<span style={{ width: 88, flexShrink: 0, fontSize: "0.65rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.08em", fontFamily: "Outfit", paddingTop: 5 }}>{label}</span>
<div style={{ flex: 1, display: "flex", gap: 6, flexWrap: "wrap" }}>
{children}
</div>
</div>
);
}
function OptionChip({
label, description, active, onClick, multi, checked,
}: {
label: string; description?: string; active: boolean; onClick: () => void;
multi?: boolean; checked?: boolean;
}) {
return (
<button
onClick={onClick}
title={description}
style={{
display: "flex", alignItems: "center", gap: 5,
padding: multi ? "4px 9px" : "4px 11px",
borderRadius: 5, border: "1px solid",
fontSize: "0.72rem", fontFamily: "Outfit", cursor: "pointer",
transition: "all 0.1s",
borderColor: active ? "#1a1a1a" : "#e0dcd4",
background: active ? "#1a1a1a" : "#fff",
color: active ? "#fff" : "#4a4640",
}}
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.borderColor = "#c5c0b8"; }}
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.borderColor = "#e0dcd4"; }}
>
{multi && (
<span style={{ width: 11, height: 11, borderRadius: 2, border: `1.5px solid ${active ? "#fff" : "#c5c0b8"}`, background: checked ? (active ? "#fff" : "#1a1a1a") : "transparent", flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 7, color: checked ? (active ? "#1a1a1a" : "#fff") : "transparent" }}></span>
)}
{label}
</button>
);
}
function ModeToggle({ value, onChange }: { value: string; onChange: (v: "dark" | "light") => void }) {
return (
<div style={{ display: "inline-flex", borderRadius: 6, overflow: "hidden", border: "1px solid #e0dcd4", background: "#f8f5f0" }}>
{(["Light", "Dark"] as const).map(m => {
const id = m.toLowerCase() as "light" | "dark";
const active = value === id;
return (
<button
key={m}
onClick={() => onChange(id)}
style={{
padding: "4px 14px", border: "none", fontSize: "0.72rem", fontFamily: "Outfit",
cursor: "pointer", fontWeight: active ? 600 : 400,
background: active ? "#1a1a1a" : "transparent",
color: active ? "#fff" : "#8a8478",
transition: "all 0.1s",
}}
>
{m === "Light" ? "☀ Light" : "◗ Dark"}
</button>
);
})}
</div>
);
}
function DesignConfigurator({
libraryId,
config,
onChange,
}: {
libraryId: string;
config: DesignConfig;
onChange: (patch: Partial<DesignConfig>) => void;
}) {
const opts = LIBRARY_STYLE_OPTIONS[libraryId];
if (!opts) return null;
const toggleComponent = (id: string) => {
const next = config.components.includes(id)
? config.components.filter(c => c !== id)
: [...config.components, id];
onChange({ components: next });
};
return (
<div style={{ display: "flex", flexDirection: "column", gap: 10, padding: "14px 0 4px" }}>
{/* Mode */}
<ConfigRow label="Mode">
<ModeToggle value={config.mode} onChange={v => onChange({ mode: v })} />
</ConfigRow>
{/* Background */}
<ConfigRow label="Background">
{opts.backgrounds.map(bg => (
<OptionChip
key={bg.id} label={bg.label} description={bg.description}
active={config.background === bg.id}
onClick={() => onChange({ background: bg.id })}
/>
))}
</ConfigRow>
{/* Nav */}
<ConfigRow label="Nav style">
{opts.navStyles.map(n => (
<OptionChip
key={n.id} label={n.label} description={n.description}
active={config.nav === n.id}
onClick={() => onChange({ nav: n.id })}
/>
))}
</ConfigRow>
{/* Header */}
<ConfigRow label="Hero header">
{opts.headerStyles.map(h => (
<OptionChip
key={h.id} label={h.label} description={h.description}
active={config.header === h.id}
onClick={() => onChange({ header: h.id })}
/>
))}
</ConfigRow>
{/* Sections */}
<ConfigRow label="Sections">
{opts.components.map(c => (
<OptionChip
key={c.id} label={c.label} multi
active={config.components.includes(c.id)}
checked={config.components.includes(c.id)}
onClick={() => toggleComponent(c.id)}
/>
))}
</ConfigRow>
{/* Font */}
<ConfigRow label="Font">
{opts.fonts.map(f => (
<OptionChip
key={f.id} label={f.label}
active={config.font === f.id}
onClick={() => onChange({ font: f.id })}
/>
))}
</ConfigRow>
</div>
);
}
// ---------------------------------------------------------------------------
// Surface section
// ---------------------------------------------------------------------------
@@ -232,6 +539,22 @@ function SurfaceSection({
const [selectedColorTheme, setSelectedColorTheme] = useState<ThemeColor | null>(null);
const activeColorTheme = selectedColorTheme ?? availableColorThemes[0] ?? null;
// Design config — per-library style choices (mode, background, nav, header, sections, font)
const defaultForLibrary = previewId ? LIBRARY_STYLE_OPTIONS[previewId]?.defaultConfig : undefined;
const [designConfig, setDesignConfig] = useState<DesignConfig>(
defaultForLibrary ?? { mode: "light", background: "solid", nav: "standard", header: "centered", components: ["features", "pricing"], font: "system" }
);
// Reset config when library changes
const [lastPreviewId, setLastPreviewId] = useState<string | null>(previewId);
if (previewId !== lastPreviewId) {
setLastPreviewId(previewId);
const def = previewId ? LIBRARY_STYLE_OPTIONS[previewId]?.defaultConfig : undefined;
if (def) setDesignConfig(def);
}
const patchConfig = (patch: Partial<DesignConfig>) => setDesignConfig(prev => ({ ...prev, ...patch }));
const hasConfigurator = !!previewId && !!LIBRARY_STYLE_OPTIONS[previewId];
const isLocked = !!lockedThemeId;
return (
@@ -267,7 +590,7 @@ function SurfaceSection({
{/* Scaffold */}
<div style={{ flex: 1, overflow: "hidden" }}>
{ScaffoldComponent
? <ScaffoldComponent themeColor={activeColorTheme ?? undefined} />
? <ScaffoldComponent themeColor={activeColorTheme ?? undefined} config={designConfig} />
: (
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#b5b0a6", fontSize: "0.82rem", fontFamily: "Outfit" }}>
Select a library below to preview
@@ -369,7 +692,16 @@ function SurfaceSection({
})}
</div>
{/* Color swatches — shown below the cards when available */}
{/* Design configurator — mode, background, nav, header, sections, font */}
{hasConfigurator && !isLocked && (
<DesignConfigurator
libraryId={previewId!}
config={designConfig}
onChange={patchConfig}
/>
)}
{/* Palette (colour) — shown after configurator */}
{availableColorThemes.length > 0 && (
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ fontSize: "0.68rem", fontWeight: 600, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.08em", fontFamily: "Outfit" }}>Colour</span>