import React, { CSSProperties, useState } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { useSiteDesign } from '../../state/SiteDesignContext'; import { escapeHtml, escapeAttr, safeUrl, safeImageUrl, cssValue, scopeId } from '../../utils/escape'; import { tokenRef } from '../../utils/tokens'; import { NavLink, DROPDOWN_TOGGLE_ONCLICK, hasDropdown } from '../../utils/nav-links'; /* ---------- Types ---------- */ interface NavbarProps { logoType?: 'text' | 'image'; logoText?: string; logoImage?: string; logoWidth?: string; logoUrl?: string; logoFontFamily?: string; logoFontSize?: string; logoColor?: string; links?: NavLink[]; /** #53: links follow the site's pages (materialised by PageContext). */ followPages?: boolean; backgroundColor?: string; textColor?: string; hoverColor?: string; ctaColor?: string; ctaTextColor?: string; padding?: string; navAlignment?: 'left' | 'center' | 'right' | 'space-between'; isSticky?: boolean; showMobileMenu?: boolean; style?: CSSProperties; hideOnDesktop?: boolean; hideOnTablet?: boolean; hideOnMobile?: boolean; animation?: string; animationDelay?: string; } /* ---------- Defaults ---------- */ const defaultLinks: NavLink[] = [ { text: 'Home', href: '/' }, { text: 'About', href: '#about' }, { text: 'Services', href: '#services' }, { text: 'Contact', href: '#contact', isCta: true }, ]; const PADDING_PRESETS = [ { label: 'Compact', value: '8px 16px' }, { label: 'Normal', value: '16px 24px' }, { label: 'Relaxed', value: '20px 32px' }, { label: 'Spacious', value: '24px 48px' }, ]; function isLightNavBg(bg: string): boolean { const v = (bg || '').trim().toLowerCase(); return v === tokenRef('bg') || v === '#ffffff' || v === '#f8fafc' || v === '#f9fafb'; } /* ---------- Component ---------- */ export const Navbar: UserComponent = ({ logoType = 'text', logoText = 'MySite', logoImage = '', logoWidth = '120px', logoUrl = '/', logoFontFamily = 'Inter, sans-serif', logoFontSize = '20px', logoColor, links = defaultLinks, backgroundColor = tokenRef('bg'), textColor = tokenRef('text'), hoverColor = tokenRef('primary'), ctaColor = tokenRef('primary'), ctaTextColor = tokenRef('bg'), padding = '16px 24px', navAlignment = 'space-between', isSticky = false, showMobileMenu = false, style = {}, }) => { const { connectors: { connect, drag }, selected, } = useNode((node) => ({ selected: node.events.selected, })); const { design } = useSiteDesign(); // Light-background heuristic: the bg token counts as light (it is the page // background); legacy stored hex values keep the old hex test. White/zinc on // an explicitly dark bar is "text on a dark custom colour" -- no token fits. const lightBg = isLightNavBg(backgroundColor); const resolvedLogoColor = logoColor || (lightBg ? design.textColor : '#ffffff'); const resolvedTextColor = textColor || (lightBg ? tokenRef('text') : '#e4e4e7'); const [hoveredLink, setHoveredLink] = useState(null); const [openDropdown, setOpenDropdown] = useState(null); return ( ); }; /* ---------- Craft config ---------- */ Navbar.craft = { displayName: 'Navbar', props: { logoType: 'text', logoText: 'MySite', logoImage: '', logoWidth: '120px', logoUrl: '/', logoFontFamily: 'Inter, sans-serif', logoFontSize: '20px', logoColor: undefined, links: defaultLinks, followPages: false, backgroundColor: tokenRef('bg'), textColor: tokenRef('text'), hoverColor: tokenRef('primary'), ctaColor: tokenRef('primary'), ctaTextColor: tokenRef('bg'), padding: '16px 24px', navAlignment: 'space-between', isSticky: false, showMobileMenu: false, style: { borderBottom: `1px solid ${tokenRef('border')}`, }, hideOnDesktop: false, hideOnTablet: false, hideOnMobile: false, animation: 'none', animationDelay: '0', } as NavbarProps, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, }; /* ---------- HTML export ---------- */ (Navbar as any).toHtml = (props: NavbarProps, _childrenHtml: string, nodeId?: string) => { // Sanitized once here -- these are raw string-interpolation sinks below // (hoverCol/bgColor go into a // breakout -> arbitrary