Files
site-builder/craft/src/panels/right/styles/NavStylePanel.tsx
T

255 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useCallback } from 'react';
import { useEditor } from '@craftjs/core';
import {
SPACING_PRESETS,
} from '../../../constants/presets';
import {
StylePanelProps,
SectionLabel,
PresetButtonGrid,
CollapsibleSection,
ColorPickerField,
navColorFields,
btnActiveStyle,
labelStyle,
inputStyle,
smallInputStyle,
sectionGap,
} from './shared';
import { AssetPicker } from '../../../ui/AssetPicker';
/* ---------- NAV / MENU / LOGO ---------- */
export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor();
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
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;
/* 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 */}
{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}>
<label style={labelStyle}>Logo Image</label>
<AssetPicker value={nodeProps.imageSrc || ''} onChange={(url) => setProp('imageSrc', url)} variant="full" />
</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}>
<label style={labelStyle}>Logo Image</label>
<AssetPicker value={nodeProps.logoImage || ''} onChange={(url) => setProp('logoImage', url)} variant="full" />
</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>
)}
{/* 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">
{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>
)}
{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>
)}
{/* 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>
</>
);
};