Design panel: correct order + fix Lock In saving
- Right panel order now: Lock → Library → Mode → Colour → Font → Background → Nav → Hero → Sections - Lock In was always disabled because selectedThemeId was null until user explicitly clicked a library button; now uses previewId (which defaults to first theme) for the disabled check - Added useEffect to notify parent of the default library selection on mount so handleLock always has a theme to save - handleLock also falls back to first theme as double safety net Made-with: Cursor
This commit is contained in:
@@ -555,10 +555,24 @@ function SurfaceSection({
|
|||||||
}
|
}
|
||||||
const patchConfig = (patch: Partial<DesignConfig>) => setDesignConfig(prev => ({ ...prev, ...patch }));
|
const patchConfig = (patch: Partial<DesignConfig>) => setDesignConfig(prev => ({ ...prev, ...patch }));
|
||||||
|
|
||||||
const hasConfigurator = !!previewId && !!LIBRARY_STYLE_OPTIONS[previewId];
|
const opts = previewId ? LIBRARY_STYLE_OPTIONS[previewId] : null;
|
||||||
|
const hasConfigurator = !!previewId && !!opts;
|
||||||
const isLocked = !!lockedThemeId;
|
const isLocked = !!lockedThemeId;
|
||||||
|
|
||||||
|
// Ensure parent always knows the currently-displayed theme (even before user clicks)
|
||||||
|
// so Lock In works immediately without requiring an explicit library click first.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedThemeId && previewId) onSelect(previewId);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [previewId]);
|
||||||
|
|
||||||
|
const toggleComponent = (id: string) => {
|
||||||
|
const next = designConfig.components.includes(id)
|
||||||
|
? designConfig.components.filter(c => c !== id)
|
||||||
|
: [...designConfig.components, id];
|
||||||
|
patchConfig({ components: next });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "row", height: "100%", gap: 0 }}>
|
<div style={{ display: "flex", flexDirection: "row", height: "100%", gap: 0 }}>
|
||||||
|
|
||||||
@@ -605,7 +619,6 @@ function SurfaceSection({
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>{/* end center scaffold wrapper */}
|
</div>{/* end center scaffold wrapper */}
|
||||||
|
|
||||||
{/* Right — controls panel */}
|
{/* Right — controls panel */}
|
||||||
@@ -616,7 +629,7 @@ function SurfaceSection({
|
|||||||
}}>
|
}}>
|
||||||
<div style={{ padding: "14px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
|
<div style={{ padding: "14px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
|
|
||||||
{/* Lock / unlock — top of panel */}
|
{/* 1. Lock / unlock */}
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
|
||||||
{isLocked ? (
|
{isLocked ? (
|
||||||
<button
|
<button
|
||||||
@@ -632,16 +645,16 @@ function SurfaceSection({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={onLock}
|
onClick={onLock}
|
||||||
disabled={!selectedThemeId || saving}
|
disabled={!previewId || saving}
|
||||||
style={{
|
style={{
|
||||||
flex: 1, padding: "7px 14px", borderRadius: 7, border: "1px solid #1a1a1a",
|
flex: 1, padding: "7px 14px", borderRadius: 7, border: `1px solid ${previewId && !saving ? "#1a1a1a" : "#e0dcd4"}`,
|
||||||
background: !selectedThemeId || saving ? "#e0dcd4" : "#1a1a1a",
|
background: previewId && !saving ? "#1a1a1a" : "#e0dcd4",
|
||||||
color: !selectedThemeId || saving ? "#b5b0a6" : "#fff",
|
color: previewId && !saving ? "#fff" : "#b5b0a6",
|
||||||
fontSize: "0.76rem", fontWeight: 600, fontFamily: "Outfit",
|
fontSize: "0.76rem", fontWeight: 600, fontFamily: "Outfit",
|
||||||
cursor: !selectedThemeId || saving ? "not-allowed" : "pointer",
|
cursor: !previewId || saving ? "not-allowed" : "pointer",
|
||||||
transition: "opacity 0.15s",
|
transition: "opacity 0.15s",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={e => { if (selectedThemeId && !saving) (e.currentTarget.style.opacity = "0.8"); }}
|
onMouseEnter={e => { if (previewId && !saving) (e.currentTarget.style.opacity = "0.8"); }}
|
||||||
onMouseLeave={e => { (e.currentTarget.style.opacity = "1"); }}
|
onMouseLeave={e => { (e.currentTarget.style.opacity = "1"); }}
|
||||||
>{saving ? "Saving…" : "🔒 Lock in"}</button>
|
>{saving ? "Saving…" : "🔒 Lock in"}</button>
|
||||||
)}
|
)}
|
||||||
@@ -654,37 +667,7 @@ function SurfaceSection({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Palette (colour) — top of mind, shown right after Lock */}
|
{/* 2. Library */}
|
||||||
{availableColorThemes.length > 0 && (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
|
|
||||||
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Colour</span>
|
|
||||||
<div style={{ display: "flex", gap: 7, flexWrap: "wrap", alignItems: "center" }}>
|
|
||||||
{availableColorThemes.map(ct => (
|
|
||||||
<button
|
|
||||||
key={ct.id}
|
|
||||||
title={ct.label}
|
|
||||||
onClick={() => !isLocked && setSelectedColorTheme(ct)}
|
|
||||||
disabled={isLocked}
|
|
||||||
style={{
|
|
||||||
width: 22, height: 22, borderRadius: "50%", border: "none", cursor: isLocked ? "not-allowed" : "pointer",
|
|
||||||
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
|
|
||||||
outline: activeColorTheme?.id === ct.id ? `2.5px solid ${ct.primary}` : "2px solid transparent",
|
|
||||||
outlineOffset: 2, transition: "transform 0.12s",
|
|
||||||
opacity: isLocked ? 0.4 : 1,
|
|
||||||
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
|
|
||||||
}}
|
|
||||||
onMouseEnter={e => { if (!isLocked) (e.currentTarget as HTMLElement).style.transform = "scale(1.18)"; }}
|
|
||||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{activeColorTheme && (
|
|
||||||
<span style={{ fontSize: "0.72rem", color: "#a09a90", fontFamily: "Outfit" }}>{activeColorTheme.label}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Library — simple name buttons */}
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
|
||||||
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Library</span>
|
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Library</span>
|
||||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
||||||
@@ -717,13 +700,112 @@ function SurfaceSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Design configurator — mode, background, nav, header, sections, font */}
|
{hasConfigurator && !isLocked && opts && (<>
|
||||||
{hasConfigurator && !isLocked && (
|
|
||||||
<DesignConfigurator
|
{/* 3. Mode */}
|
||||||
libraryId={previewId!}
|
<ConfigRow label="Mode">
|
||||||
config={designConfig}
|
<ModeToggle value={designConfig.mode} onChange={v => patchConfig({ mode: v })} />
|
||||||
onChange={patchConfig}
|
</ConfigRow>
|
||||||
|
|
||||||
|
{/* 4. Colour */}
|
||||||
|
{availableColorThemes.length > 0 && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
|
||||||
|
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Colour</span>
|
||||||
|
<div style={{ display: "flex", gap: 7, flexWrap: "wrap", alignItems: "center" }}>
|
||||||
|
{availableColorThemes.map(ct => (
|
||||||
|
<button
|
||||||
|
key={ct.id}
|
||||||
|
title={ct.label}
|
||||||
|
onClick={() => setSelectedColorTheme(ct)}
|
||||||
|
style={{
|
||||||
|
width: 22, height: 22, borderRadius: "50%", border: "none", cursor: "pointer",
|
||||||
|
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
|
||||||
|
outline: activeColorTheme?.id === ct.id ? `2.5px solid ${ct.primary}` : "2px solid transparent",
|
||||||
|
outlineOffset: 2, transition: "transform 0.12s",
|
||||||
|
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1.18)"; }}
|
||||||
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; }}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{activeColorTheme && (
|
||||||
|
<span style={{ fontSize: "0.72rem", color: "#a09a90", fontFamily: "Outfit" }}>{activeColorTheme.label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 5. Font */}
|
||||||
|
<ConfigRow label="Font">
|
||||||
|
{opts.fonts.map(f => (
|
||||||
|
<OptionChip key={f.id} label={f.label}
|
||||||
|
active={designConfig.font === f.id}
|
||||||
|
onClick={() => patchConfig({ font: f.id })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ConfigRow>
|
||||||
|
|
||||||
|
{/* 6. Background */}
|
||||||
|
<ConfigRow label="Background">
|
||||||
|
{opts.backgrounds.map(bg => (
|
||||||
|
<OptionChip key={bg.id} label={bg.label} description={bg.description}
|
||||||
|
active={designConfig.background === bg.id}
|
||||||
|
onClick={() => patchConfig({ background: bg.id })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ConfigRow>
|
||||||
|
|
||||||
|
{/* 7. Nav */}
|
||||||
|
<ConfigRow label="Nav style">
|
||||||
|
{opts.navStyles.map(n => (
|
||||||
|
<OptionChip key={n.id} label={n.label} description={n.description}
|
||||||
|
active={designConfig.nav === n.id}
|
||||||
|
onClick={() => patchConfig({ nav: n.id })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ConfigRow>
|
||||||
|
|
||||||
|
{/* 8. Hero */}
|
||||||
|
<ConfigRow label="Hero header">
|
||||||
|
{opts.headerStyles.map(h => (
|
||||||
|
<OptionChip key={h.id} label={h.label} description={h.description}
|
||||||
|
active={designConfig.header === h.id}
|
||||||
|
onClick={() => patchConfig({ header: h.id })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ConfigRow>
|
||||||
|
|
||||||
|
{/* 9. Sections */}
|
||||||
|
<ConfigRow label="Sections">
|
||||||
|
{opts.components.map(c => (
|
||||||
|
<OptionChip key={c.id} label={c.label} multi
|
||||||
|
active={designConfig.components.includes(c.id)}
|
||||||
|
checked={designConfig.components.includes(c.id)}
|
||||||
|
onClick={() => toggleComponent(c.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ConfigRow>
|
||||||
|
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{/* Colour swatches when locked (read-only) */}
|
||||||
|
{isLocked && availableColorThemes.length > 0 && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
|
||||||
|
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Colour</span>
|
||||||
|
<div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}>
|
||||||
|
{availableColorThemes.map(ct => (
|
||||||
|
<button key={ct.id} title={ct.label} disabled
|
||||||
|
style={{
|
||||||
|
width: 22, height: 22, borderRadius: "50%", border: "none",
|
||||||
|
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
|
||||||
|
outline: activeColorTheme?.id === ct.id ? `2.5px solid ${ct.primary}` : "2px solid transparent",
|
||||||
|
outlineOffset: 2, opacity: 0.4, cursor: "not-allowed",
|
||||||
|
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>{/* end inner padding div */}
|
</div>{/* end inner padding div */}
|
||||||
@@ -931,7 +1013,8 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLock = async (surfaceId: string) => {
|
const handleLock = async (surfaceId: string) => {
|
||||||
const themeId = selectedThemes[surfaceId];
|
const surface = ALL_SURFACES.find(s => s.id === surfaceId);
|
||||||
|
const themeId = selectedThemes[surfaceId] ?? surface?.themes[0]?.id;
|
||||||
if (!themeId) return;
|
if (!themeId) return;
|
||||||
setSavingLock(surfaceId);
|
setSavingLock(surfaceId);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user