import React, { CSSProperties, useState } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { usePages } from '../../state/PageContext'; /* ---------- Types ---------- */ interface MenuLink { text: string; href: string; isExternal?: boolean; isCta?: boolean; } interface MenuProps { links?: MenuLink[]; alignment?: 'left' | 'center' | 'right'; linkColor?: string; linkHoverColor?: string; ctaBgColor?: string; ctaTextColor?: string; gap?: string; orientation?: 'horizontal' | 'vertical'; fontSize?: string; style?: CSSProperties; } /* ---------- Defaults ---------- */ const defaultLinks: MenuLink[] = [ { text: 'Home', href: '/' }, { text: 'About', href: '#about' }, { text: 'Services', href: '#services' }, { text: 'Contact', href: '#contact', isCta: true }, ]; /* ---------- Helper: escape HTML ---------- */ function esc(str: string): string { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /* ---------- Component ---------- */ export const Menu: UserComponent = ({ links = defaultLinks, alignment = 'right', linkColor = '#3f3f46', linkHoverColor = '#3b82f6', ctaBgColor = '#3b82f6', ctaTextColor = '#ffffff', gap = '24px', orientation = 'horizontal', fontSize = '14px', style = {}, }) => { const { connectors: { connect, drag }, } = useNode(); const [hoveredLink, setHoveredLink] = useState(null); const justifyMap = { left: 'flex-start', center: 'center', right: 'flex-end' }; return ( ); }; /* ---------- Settings panel ---------- */ const MenuSettings: React.FC = () => { const { actions: { setProp }, props } = useNode((node) => ({ props: node.data.props as MenuProps, })); const { pages } = usePages(); const links = props.links || defaultLinks; /* Drag state for reordering */ const [dragIdx, setDragIdx] = useState(null); const [dragOverIdx, setDragOverIdx] = useState(null); /* ---- Link management ---- */ const updateLink = (index: number, field: keyof MenuLink, value: string | boolean) => { setProp((p: MenuProps) => { const updated = [...(p.links || defaultLinks)]; updated[index] = { ...updated[index], [field]: value }; p.links = updated; }); }; const addLink = (link?: Partial) => { setProp((p: MenuProps) => { p.links = [...(p.links || defaultLinks), { text: 'Link', href: '#', ...link }]; }); }; const removeLink = (index: number) => { setProp((p: MenuProps) => { const updated = [...(p.links || defaultLinks)]; updated.splice(index, 1); p.links = updated; }); }; const moveLink = (fromIdx: number, toIdx: number) => { if (fromIdx === toIdx) return; setProp((p: MenuProps) => { const updated = [...(p.links || defaultLinks)]; const [moved] = updated.splice(fromIdx, 1); updated.splice(toIdx, 0, moved); p.links = updated; }); }; /* ---- Shared styles ---- */ const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4 }; const inputStyle: CSSProperties = { width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, }; const sectionStyle: CSSProperties = { borderBottom: '1px solid #27272a', paddingBottom: 12, }; const btnSmall: CSSProperties = { padding: '2px 6px', fontSize: 11, background: '#27272a', color: '#a1a1aa', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', }; const btnActive: CSSProperties = { ...btnSmall, background: '#3b82f6', color: '#fff', borderColor: '#3b82f6', }; const textColorPresets = ['#1f2937', '#374151', '#3f3f46', '#6b7280', '#ffffff', '#e4e4e7', '#a1a1aa', '#3b82f6']; const gapPresets = ['8px', '16px', '24px', '32px', '40px']; return (
{/* ===== Style Section ===== */}
{/* Link color */}
{textColorPresets.map((c) => (
{/* Hover color */}
setProp((p: MenuProps) => { p.linkHoverColor = e.target.value; })} style={{ width: 28, height: 24, padding: 0, border: '1px solid #3f3f46', borderRadius: 3, cursor: 'pointer', background: 'none' }} /> {props.linkHoverColor || '#3b82f6'}
{/* CTA button colors */}
BG setProp((p: MenuProps) => { p.ctaBgColor = e.target.value; })} style={{ display: 'block', width: 28, height: 20, padding: 0, border: '1px solid #3f3f46', borderRadius: 3, cursor: 'pointer', background: 'none' }} />
Text setProp((p: MenuProps) => { p.ctaTextColor = e.target.value; })} style={{ display: 'block', width: 28, height: 20, padding: 0, border: '1px solid #3f3f46', borderRadius: 3, cursor: 'pointer', background: 'none' }} />
{/* Font size */}
setProp((p: MenuProps) => { p.fontSize = e.target.value; })} placeholder="14px" style={inputStyle} />
{/* Alignment */}
{(['left', 'center', 'right'] as const).map((a) => ( ))}
{/* Orientation */}
{(['horizontal', 'vertical'] as const).map((o) => ( ))}
{/* Gap */}
{gapPresets.map((g) => ( ))}
{/* ===== Links Section ===== */}
{links.map((link, i) => (
setDragIdx(i)} onDragOver={(e) => { e.preventDefault(); setDragOverIdx(i); }} onDragEnd={() => { if (dragIdx !== null && dragOverIdx !== null) { moveLink(dragIdx, dragOverIdx); } setDragIdx(null); setDragOverIdx(null); }} style={{ background: dragOverIdx === i && dragIdx !== null && dragIdx !== i ? '#1e293b' : '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4, border: dragOverIdx === i && dragIdx !== null && dragIdx !== i ? '1px solid #3b82f6' : '1px solid transparent', transition: 'background 0.1s, border-color 0.1s', }} > {/* Row 1: drag handle + text + delete */}
updateLink(i, 'text', e.target.value)} placeholder="Text" style={{ ...inputStyle, flex: 1 }} />
{/* Row 2: URL */} updateLink(i, 'href', e.target.value)} placeholder="URL (e.g. /about or https://...)" style={inputStyle} /> {/* Row 3: checkboxes */}
))}
{/* Add link button */} {/* Add page dropdown */}
); }; /* ---------- Craft config ---------- */ Menu.craft = { displayName: 'Menu', props: { links: defaultLinks, alignment: 'right', linkColor: '#3f3f46', linkHoverColor: '#3b82f6', ctaBgColor: '#3b82f6', ctaTextColor: '#ffffff', gap: '24px', orientation: 'horizontal', fontSize: '14px', style: {}, } as MenuProps, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, related: { settings: MenuSettings, }, }; /* ---------- HTML export ---------- */ (Menu as any).toHtml = (props: MenuProps, _childrenHtml: string) => { const linkCol = props.linkColor || '#3f3f46'; const hoverCol = props.linkHoverColor || '#3b82f6'; const ctaBg = props.ctaBgColor || '#3b82f6'; const ctaText = props.ctaTextColor || '#ffffff'; const gap = props.gap || '24px'; const orientation = props.orientation || 'horizontal'; const alignment = props.alignment || 'right'; const fSize = props.fontSize || '14px'; const justifyMap: Record = { left: 'flex-start', center: 'center', right: 'flex-end' }; const navStyle = cssPropsToString({ display: 'flex', flexDirection: orientation === 'vertical' ? 'column' : 'row', alignItems: orientation === 'vertical' ? (alignment === 'center' ? 'center' : alignment === 'right' ? 'flex-end' : 'flex-start') : 'center', justifyContent: orientation === 'horizontal' ? justifyMap[alignment] : undefined, gap, flexWrap: 'wrap', ...props.style, }); const links = props.links || defaultLinks; // Unique ID suffix for scoped CSS const scopeId = `menu-${Math.random().toString(36).slice(2, 8)}`; const linksHtml = links.map((link) => { const target = link.isExternal ? ' target="_blank" rel="noopener noreferrer"' : ''; const cls = link.isCta ? `${scopeId}-cta` : `${scopeId}-link`; const linkStyle = cssPropsToString({ textDecoration: 'none', fontSize: fSize, fontWeight: link.isCta ? '600' : '400', color: link.isCta ? ctaText : linkCol, backgroundColor: link.isCta ? ctaBg : 'transparent', padding: link.isCta ? '8px 20px' : '0', borderRadius: link.isCta ? '6px' : '0', transition: 'color 0.15s, background-color 0.15s', }); return `${esc(link.text)}`; }).join('\n '); const hoverCss = ``; return { html: `${hoverCss} ${linksHtml} `, }; };