import { useRef, useState } from 'react'; import type { HoopConfig, MachineHoop, MachinePreset } from '../data.ts'; import { ExampleBrowser } from './ExampleBrowser.tsx'; import { MachineMenu } from './MachineMenu.tsx'; import type { ActiveMachine } from './MachineMenu.tsx'; import { HoopIcon } from './HoopDialog.tsx'; import styles from './Header.module.css'; import { buttonVariants } from '@/components/ui/button.tsx'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuLabel, } from '@/components/ui/dropdown-menu.tsx'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip.tsx'; import { MenuIcon, ChevronDownIcon, DownloadIcon, SearchIcon, Share2Icon, UploadIcon, } from 'lucide-react'; import { cn } from '@/utils.ts'; export type ExportFormat = 'dst' | 'pes' | 'exp' | 'svg'; interface Props { hoop: HoopConfig; onOpenHoopDialog: () => void; onSVGImport: (mode: 'quick' | 'options') => void; onBitmapImport: () => void; onExampleSelect: (key: string) => void; onRun: () => void; onDownload: (format: ExportFormat) => void; onShare: () => Promise; onOpenReference: () => void; activeMachine: ActiveMachine | null; machineBudgetMode: boolean; onApplyMachine: (machine: MachinePreset, hoop: MachineHoop) => void; onApplyFabric: (fabric: string) => void; onMachineBudgetModeChange: (enabled: boolean) => void; onRemoveMachine: () => void; defaultExportFormat: Exclude; } // ── Inline logo SVG ──────────────────────────────────────────────────────────── function LogoIcon({ size = 22 }: { size?: number }) { return ( ); } // Shared sizing token for all header buttons const hdrBtn = 'h-[30px] text-ui font-mono cursor-pointer flex-shrink-0'; // Red primary style — Run and Export share this look const redBtn = cn( hdrBtn, 'px-3.5 relative rounded-[6px] border', 'inline-flex items-center font-semibold select-none', 'bg-run border-run-dark text-on-run', 'hover:bg-run-hi', 'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:border-ring', 'after:absolute after:inset-[3px] after:border after:border-dashed after:border-white/55 after:rounded-[3px] after:pointer-events-none', 'disabled:pointer-events-none disabled:opacity-50', ); // Warm secondary style — Hoop, Import SVG, Share const blueBtn = cn( hdrBtn, 'px-2.5 rounded-[6px] border', 'inline-flex items-center gap-1.5', 'bg-warm-btn border-warm-btn-edge text-gold', 'hover:bg-warm-btn-hi hover:border-warm-btn-edge-hi hover:text-gold-light', 'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:border-ring', 'disabled:pointer-events-none disabled:opacity-50', ); // ── Export dropdown (visible at lg+, also in hamburger) ─────────────────────── function ExportDropdown({ onDownload, defaultFormat, }: { onDownload: (fmt: ExportFormat) => void; defaultFormat: Exclude; }) { return ( Download .{defaultFormat.toUpperCase()}{' '} {(['dst', 'pes', 'exp'] as ExportFormat[]).map((fmt) => ( onDownload(fmt)}> {fmt === 'dst' ? '.DST · Tajima' : fmt === 'pes' ? '.PES · Brother' : '.EXP · Melco'} ))} onDownload('svg')}> .SVG · Print ); } // ── Share button (visible at lg+, also in hamburger) ────────────────────────── function ShareButton({ onShare }: { onShare: () => Promise }) { const [state, setState] = useState<'idle' | 'pending' | 'copied' | 'error'>('idle'); const timerRef = useRef | null>(null); async function handleClick() { if (state === 'pending') return; if (timerRef.current) clearTimeout(timerRef.current); setState('pending'); try { await onShare(); setState('copied'); } catch { setState('error'); } timerRef.current = setTimeout(() => setState('idle'), 2000); } const label = state === 'pending' ? '…' : state === 'copied' ? 'Copied!' : state === 'error' ? 'Failed' : 'Share'; return ( ); } // ── Hamburger menu ───────────────────────────────────────────────────────────── interface HamburgerProps { hoop: HoopConfig; onOpenHoopDialog: () => void; onOpenExamples: () => void; onSVGImport: (mode: 'quick' | 'options') => void; onBitmapImport: () => void; onDownload: (fmt: ExportFormat) => void; onShare: () => Promise; } function HamburgerMenu({ hoop, onOpenHoopDialog, onOpenExamples, onSVGImport, onBitmapImport, onDownload, onShare, }: HamburgerProps) { return ( {/* ── Design group: shown when < md ── */} Design Hoop {hoop.label} Browse examples… {/* ── Tools group: shown when < lg ── */} Import onSVGImport('quick')}> Quick import onSVGImport('options')}> Import with options… Import bitmap data… {/* ── Output group: shown when < lg ── */} Export & Share {(['dst', 'pes', 'exp'] as ExportFormat[]).map((fmt) => ( onDownload(fmt)}> {fmt === 'dst' ? '.DST · Tajima' : fmt === 'pes' ? '.PES · Brother' : '.EXP · Melco'} ))} onDownload('svg')}> .SVG · Print { void onShare(); }} > Copy share link ); } // ── Main header ──────────────────────────────────────────────────────────────── export default function Header({ hoop, onOpenHoopDialog, onSVGImport, onBitmapImport, onExampleSelect, onRun, onDownload, onShare, onOpenReference, activeMachine, machineBudgetMode, onApplyMachine, onApplyFabric, onMachineBudgetModeChange, onRemoveMachine, defaultExportFormat, }: Props) { const [examplesOpen, setExamplesOpen] = useState(false); return (
{/* ══ BRAND ════════════════════════════════════════════════════════════ */}

NeedleScript

Logo inspired programming language for generative embroidery  
{/* ══ DESIGN GROUP (Hoop + Examples at md+) ════════════════════════════ */}
{hoop.label} Change hoop size and shape {/* Kept mounted in code for an easy return, but hidden while the compact header is prioritised. Machine settings remain available on right-click. */}
{/* ══ IMPORT (lg+ only) ═════════════════════════════════════════════════ */}
Import onSVGImport('quick')}> Quick import onSVGImport('options')}> Import with options… Import bitmap data…
{/* ══ FLEX SPACER ══════════════════════════════════════════════════════ */}
{/* ══ ACTION GROUP ═════════════════════════════════════════════════════ */}
{/* ══ META GROUP (Help + Hamburger) ════════════════════════════════════ */} ? Language reference setExamplesOpen(true)} onSVGImport={onSVGImport} onBitmapImport={onBitmapImport} onDownload={onDownload} onShare={onShare} />
); }