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:
@@ -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>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* 4. Add it to ALL_SURFACES in the design page
|
||||
*/
|
||||
|
||||
export type { ThemeColor } from "./types";
|
||||
export type { ThemeColor, DesignConfig, StyleOption, LibraryStyleOptions } from "./types";
|
||||
export {
|
||||
SHADCN_THEMES, MANTINE_THEMES, HEROUI_THEMES, TREMOR_THEMES,
|
||||
DAISY_THEMES, HEROUI_MARKETING_THEMES,
|
||||
@@ -21,7 +21,7 @@ import { MobileNativewind, MobileGluestack } from "./mobile";
|
||||
import { EmailReactEmail } from "./email";
|
||||
import { DocsNextra, DocsShadcnCustom } from "./docs";
|
||||
|
||||
import type { ThemeColor } from "./types";
|
||||
import type { ThemeColor, DesignConfig } from "./types";
|
||||
import {
|
||||
SHADCN_THEMES, MANTINE_THEMES, HEROUI_THEMES, TREMOR_THEMES,
|
||||
DAISY_THEMES, HEROUI_MARKETING_THEMES,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
// SCAFFOLD_REGISTRY — surface → library → preview component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const SCAFFOLD_REGISTRY: Record<string, Record<string, React.ComponentType<{ themeColor?: ThemeColor }>>> = {
|
||||
export const SCAFFOLD_REGISTRY: Record<string, Record<string, React.ComponentType<{ themeColor?: ThemeColor; config?: DesignConfig }>>> = {
|
||||
"web-app": {
|
||||
shadcn: WebAppShadcn,
|
||||
mantine: WebAppMantine,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,35 @@ export interface ThemeColor {
|
||||
mutedText?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Design configurator types — used by the surface configurator and scaffolds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface StyleOption {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface LibraryStyleOptions {
|
||||
modes: StyleOption[];
|
||||
backgrounds: StyleOption[];
|
||||
navStyles: StyleOption[];
|
||||
headerStyles: StyleOption[];
|
||||
components: StyleOption[];
|
||||
fonts: StyleOption[];
|
||||
defaultConfig: DesignConfig;
|
||||
}
|
||||
|
||||
export interface DesignConfig {
|
||||
mode: "dark" | "light";
|
||||
background: string;
|
||||
nav: string;
|
||||
header: string;
|
||||
components: string[];
|
||||
font: string;
|
||||
}
|
||||
|
||||
// Shared mock data
|
||||
export const TABLE_ROWS = [
|
||||
{ name: "Alice Martin", email: "alice@co.com", role: "Admin", status: "Active", date: "Jan 12" },
|
||||
|
||||
Reference in New Issue
Block a user