site-builder: fix Menu guided panel (empty Colors) + surface layout/nav colors
The shared NavStylePanel gated its Colors controls on the Navbar's prop names (backgroundColor/textColor/ctaColor), so a Menu -- whose color props are linkColor/linkHoverColor/ctaBgColor/ctaTextColor -- rendered an empty Colors section (customer report: 'nothing to select'). - Add navColorFields() helper: derives the visible color controls from the props actually present, covering both the Navbar and Menu schemas (+ Navbar's hoverColor, which render/toHtml consume but had no control). - Add a Menu Layout section (alignment/orientation/gap/font size), guarded on Menu's own props so it never leaks into Navbar or a standalone Logo. - Unit test (navColorFields.test.ts) locks each component to its real props. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
||||
PresetButtonGrid,
|
||||
CollapsibleSection,
|
||||
ColorPickerField,
|
||||
navColorFields,
|
||||
btnActiveStyle,
|
||||
labelStyle,
|
||||
inputStyle,
|
||||
smallInputStyle,
|
||||
@@ -56,6 +58,21 @@ export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps
|
||||
/* Detect standalone Logo vs Navbar/Menu */
|
||||
const isStandaloneLogo = nodeProps.type !== undefined && (nodeProps.type === 'text' || nodeProps.type === 'image') && nodeProps.logoText === undefined;
|
||||
|
||||
/* Color controls, derived from the props the selected component actually has */
|
||||
const colorFields = navColorFields(nodeProps);
|
||||
|
||||
/* Menu layout controls (alignment/orientation/gap/font size). These props are
|
||||
unique to the Menu component — Navbar uses navAlignment and has no
|
||||
orientation/gap/fontSize — so guarding on their presence scopes this section
|
||||
to the Menu without leaking into Navbar or a standalone Logo. */
|
||||
const hasMenuLayout = !isStandaloneLogo && (
|
||||
nodeProps.alignment !== undefined ||
|
||||
nodeProps.orientation !== undefined ||
|
||||
nodeProps.gap !== undefined ||
|
||||
nodeProps.fontSize !== undefined
|
||||
);
|
||||
const GAP_PRESETS = ['8px', '16px', '24px', '32px', '40px'].map((g) => ({ label: g, value: g }));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Standalone Logo component settings */}
|
||||
@@ -165,17 +182,61 @@ export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Colors (not shown for standalone Logo - it has its own color picker) */}
|
||||
{!isStandaloneLogo && (
|
||||
{/* Colors (not shown for standalone Logo - it has its own color picker).
|
||||
Fields are derived from whichever color props the selected component
|
||||
actually has, so Menu (linkColor/ctaBg/…) and Navbar (backgroundColor/…)
|
||||
each get the right controls instead of an empty section. */}
|
||||
{!isStandaloneLogo && colorFields.length > 0 && (
|
||||
<CollapsibleSection title="Colors">
|
||||
{nodeProps.backgroundColor !== undefined && (
|
||||
<ColorPickerField label="Background" value={nodeProps.backgroundColor || '#ffffff'} onChange={(v) => setProp('backgroundColor', v)} />
|
||||
{colorFields.map((f) => (
|
||||
<ColorPickerField
|
||||
key={f.key}
|
||||
label={f.label}
|
||||
value={nodeProps[f.key] || f.fallback}
|
||||
onChange={(v) => setProp(f.key, v)}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Menu layout (alignment / orientation / gap / font size) */}
|
||||
{hasMenuLayout && (
|
||||
<CollapsibleSection title="Layout">
|
||||
{nodeProps.alignment !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Alignment</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['left', 'center', 'right'] as const).map((a) => (
|
||||
<button key={a} onClick={() => setProp('alignment', a)} style={btnActiveStyle((nodeProps.alignment || 'right') === a)}>
|
||||
{a.charAt(0).toUpperCase() + a.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.textColor !== undefined && (
|
||||
<ColorPickerField label="Text Color" value={nodeProps.textColor || '#18181b'} onChange={(v) => setProp('textColor', v)} />
|
||||
{nodeProps.orientation !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Orientation</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['horizontal', 'vertical'] as const).map((o) => (
|
||||
<button key={o} onClick={() => setProp('orientation', o)} style={btnActiveStyle((nodeProps.orientation || 'horizontal') === o)}>
|
||||
{o.charAt(0).toUpperCase() + o.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.ctaColor !== undefined && (
|
||||
<ColorPickerField label="CTA Color" value={nodeProps.ctaColor || '#3b82f6'} onChange={(v) => setProp('ctaColor', v)} />
|
||||
{nodeProps.gap !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Gap</label>
|
||||
<PresetButtonGrid presets={GAP_PRESETS} activeValue={nodeProps.gap} onSelect={(v) => setProp('gap', v)} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.fontSize !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Font Size</label>
|
||||
<input type="text" value={nodeProps.fontSize || '14px'} onChange={(e) => setProp('fontSize', e.target.value)} placeholder="14px" style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user