2026-04-05 18:31:16 -07:00
|
|
|
import React, { useCallback } from 'react';
|
|
|
|
|
import { useEditor } from '@craftjs/core';
|
|
|
|
|
import {
|
|
|
|
|
SPACING_PRESETS,
|
|
|
|
|
} from '../../../constants/presets';
|
|
|
|
|
import {
|
|
|
|
|
StylePanelProps,
|
|
|
|
|
SectionLabel,
|
|
|
|
|
PresetButtonGrid,
|
|
|
|
|
CollapsibleSection,
|
|
|
|
|
ColorPickerField,
|
2026-07-10 13:10:46 -07:00
|
|
|
navColorFields,
|
|
|
|
|
btnActiveStyle,
|
2026-04-05 18:31:16 -07:00
|
|
|
labelStyle,
|
|
|
|
|
inputStyle,
|
|
|
|
|
smallInputStyle,
|
|
|
|
|
sectionGap,
|
2026-07-12 15:13:35 -07:00
|
|
|
useNodeProp,
|
2026-04-05 18:31:16 -07:00
|
|
|
} from './shared';
|
2026-07-12 13:00:10 -07:00
|
|
|
import { AssetPicker } from '../../../ui/AssetPicker';
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
/* ---------- NAV / MENU / LOGO ---------- */
|
|
|
|
|
export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
|
|
|
|
const { actions } = useEditor();
|
2026-07-12 15:13:35 -07:00
|
|
|
const { setProp, setPropStyle } = useNodeProp(selectedId);
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
const links: any[] = nodeProps.links || [];
|
|
|
|
|
|
|
|
|
|
const updateLink = useCallback((index: number, field: string, value: any) => {
|
|
|
|
|
actions.setProp(selectedId, (props: any) => {
|
|
|
|
|
const updated = [...(props.links || [])];
|
|
|
|
|
updated[index] = { ...updated[index], [field]: value };
|
|
|
|
|
props.links = updated;
|
|
|
|
|
});
|
|
|
|
|
}, [actions, selectedId]);
|
|
|
|
|
|
|
|
|
|
const addLink = useCallback(() => {
|
|
|
|
|
actions.setProp(selectedId, (props: any) => {
|
|
|
|
|
props.links = [...(props.links || []), { text: 'New Link', href: '#' }];
|
|
|
|
|
});
|
|
|
|
|
}, [actions, selectedId]);
|
|
|
|
|
|
|
|
|
|
const removeLink = useCallback((index: number) => {
|
|
|
|
|
actions.setProp(selectedId, (props: any) => {
|
|
|
|
|
const updated = [...(props.links || [])];
|
|
|
|
|
updated.splice(index, 1);
|
|
|
|
|
props.links = updated;
|
|
|
|
|
});
|
|
|
|
|
}, [actions, selectedId]);
|
|
|
|
|
|
|
|
|
|
/* Detect standalone Logo vs Navbar/Menu */
|
|
|
|
|
const isStandaloneLogo = nodeProps.type !== undefined && (nodeProps.type === 'text' || nodeProps.type === 'image') && nodeProps.logoText === undefined;
|
|
|
|
|
|
2026-07-10 13:10:46 -07:00
|
|
|
/* 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 }));
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* Standalone Logo component settings */}
|
|
|
|
|
{isStandaloneLogo && (
|
|
|
|
|
<CollapsibleSection title="Logo" defaultOpen={true}>
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={{ ...labelStyle, fontWeight: 600, fontSize: 12, marginBottom: 8 }}>Logo Type</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setProp('type', 'text')}
|
|
|
|
|
style={{ padding: '4px 10px', fontSize: 11, background: nodeProps.type === 'text' ? '#3b82f6' : '#27272a', color: nodeProps.type === 'text' ? '#fff' : '#a1a1aa', border: `1px solid ${nodeProps.type === 'text' ? '#3b82f6' : '#3f3f46'}`, borderRadius: 4, cursor: 'pointer' }}
|
|
|
|
|
>
|
|
|
|
|
<i className="fa fa-font" style={{ marginRight: 4 }} />Text
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setProp('type', 'image')}
|
|
|
|
|
style={{ padding: '4px 10px', fontSize: 11, background: nodeProps.type === 'image' ? '#3b82f6' : '#27272a', color: nodeProps.type === 'image' ? '#fff' : '#a1a1aa', border: `1px solid ${nodeProps.type === 'image' ? '#3b82f6' : '#3f3f46'}`, borderRadius: 4, cursor: 'pointer' }}
|
|
|
|
|
>
|
|
|
|
|
<i className="fa fa-image" style={{ marginRight: 4 }} />Image
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{nodeProps.type === 'text' && (
|
|
|
|
|
<>
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Logo Text</label>
|
|
|
|
|
<input type="text" value={nodeProps.text || ''} onChange={(e) => setProp('text', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Font Size</label>
|
|
|
|
|
<input type="text" value={nodeProps.fontSize || '20px'} onChange={(e) => setProp('fontSize', e.target.value)} placeholder="20px" style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Font Weight</label>
|
|
|
|
|
<select value={nodeProps.fontWeight || '700'} onChange={(e) => setProp('fontWeight', e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
|
|
|
|
|
<option value="300">Light</option>
|
|
|
|
|
<option value="400">Normal</option>
|
|
|
|
|
<option value="500">Medium</option>
|
|
|
|
|
<option value="600">Semi</option>
|
|
|
|
|
<option value="700">Bold</option>
|
|
|
|
|
<option value="800">Extra Bold</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<ColorPickerField label="Text Color" value={nodeProps.color || '#1f2937'} onChange={(v) => setProp('color', v)} />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.type === 'image' && (
|
|
|
|
|
<>
|
|
|
|
|
<div style={sectionGap}>
|
2026-07-12 13:00:10 -07:00
|
|
|
<label style={labelStyle}>Logo Image</label>
|
|
|
|
|
<AssetPicker value={nodeProps.imageSrc || ''} onChange={(url) => setProp('imageSrc', url)} variant="full" />
|
2026-04-05 18:31:16 -07:00
|
|
|
</div>
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Image Width</label>
|
|
|
|
|
<input type="text" value={nodeProps.imageWidth || '120px'} onChange={(e) => setProp('imageWidth', e.target.value)} placeholder="120px" style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Link URL</label>
|
|
|
|
|
<input type="text" value={nodeProps.href || '/'} onChange={(e) => setProp('href', e.target.value)} placeholder="/" style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleSection>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Navbar Logo settings */}
|
|
|
|
|
{nodeProps.logoText !== undefined && (
|
|
|
|
|
<CollapsibleSection title="Logo">
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Logo Text</label>
|
|
|
|
|
<input type="text" value={nodeProps.logoText || ''} onChange={(e) => setProp('logoText', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
{nodeProps.logoImage !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
2026-07-12 13:00:10 -07:00
|
|
|
<label style={labelStyle}>Logo Image</label>
|
|
|
|
|
<AssetPicker value={nodeProps.logoImage || ''} onChange={(url) => setProp('logoImage', url)} variant="full" />
|
2026-04-05 18:31:16 -07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.logoUrl !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Logo Link URL</label>
|
|
|
|
|
<input type="text" value={nodeProps.logoUrl || ''} onChange={(e) => setProp('logoUrl', e.target.value)} placeholder="/" style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CollapsibleSection>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Links (not shown for standalone Logo) */}
|
|
|
|
|
{!isStandaloneLogo && (
|
|
|
|
|
<CollapsibleSection title="Links">
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
|
|
|
{links.map((link, i) => (
|
|
|
|
|
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 6, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
|
|
|
<input type="text" value={link.text || ''} onChange={(e) => updateLink(i, 'text', e.target.value)} placeholder="Text" style={{ ...smallInputStyle, flex: 1 }} />
|
|
|
|
|
<button onClick={() => removeLink(i)} style={{ padding: '2px 6px', fontSize: 10, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
|
|
|
|
|
<i className="fa fa-times" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<input type="text" value={link.href || ''} onChange={(e) => updateLink(i, 'href', e.target.value)} placeholder="URL" style={{ ...smallInputStyle, color: '#71717a' }} />
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<button onClick={addLink} style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}>
|
|
|
|
|
+ Add Link
|
|
|
|
|
</button>
|
|
|
|
|
</CollapsibleSection>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-07-10 13:10:46 -07:00
|
|
|
{/* 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 && (
|
2026-04-05 18:31:16 -07:00
|
|
|
<CollapsibleSection title="Colors">
|
2026-07-10 13:10:46 -07:00
|
|
|
{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.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>
|
2026-04-05 18:31:16 -07:00
|
|
|
)}
|
2026-07-10 13:10:46 -07:00
|
|
|
{nodeProps.gap !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Gap</label>
|
|
|
|
|
<PresetButtonGrid presets={GAP_PRESETS} activeValue={nodeProps.gap} onSelect={(v) => setProp('gap', v)} />
|
|
|
|
|
</div>
|
2026-04-05 18:31:16 -07:00
|
|
|
)}
|
2026-07-10 13:10:46 -07:00
|
|
|
{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>
|
2026-04-05 18:31:16 -07:00
|
|
|
)}
|
|
|
|
|
</CollapsibleSection>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Style overrides */}
|
|
|
|
|
<CollapsibleSection title="Spacing" defaultOpen={false}>
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<SectionLabel>Padding</SectionLabel>
|
|
|
|
|
<PresetButtonGrid presets={SPACING_PRESETS} activeValue={(nodeProps.style || {}).padding as string} onSelect={(v) => setPropStyle('padding', v)} />
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleSection>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|