// src/product.jsx — Product detail with customization. // Customize pattern variants: 'modal' | 'sheet' | 'inline' (controlled via tweaks). // For the sandwich builder, uses DELI_BUILDER spec. For non-customizable products, shows a simpler detail. function Price({ value, size = 22 }) { return {money(value)}; } function BuilderPanel({ builder, value, setValue, helpMode }) { const isMobile = useMedia("(max-width: 720px)"); // value = { bread: 'hero', protein: 'turkey', ... , qty: 1, notes: '' } const toggleMulti = (sec, id) => { const arr = value[sec.id] || []; if (arr.includes(id)) setValue({ ...value, [sec.id]: arr.filter(x => x !== id) }); else if (!sec.max || arr.length < sec.max) setValue({ ...value, [sec.id]: [...arr, id] }); }; const setSingle = (secId, id) => setValue({ ...value, [secId]: id }); // Accordion: single-open. Click to open replaces, click open header to close. const firstRequired = builder.sections.find(s => s.required)?.id; const [openId, setOpenId] = React.useState(firstRequired || null); const toggleOpen = (id) => setOpenId(prev => prev === id ? null : id); // Summary of the current selection in a section — shown when it's collapsed. const summarize = (sec) => { if (sec.multi) { const ids = value[sec.id] || []; if (ids.length === 0) return "None"; return ids.map(id => sec.options.find(o => o.id === id)?.label).filter(Boolean).join(", "); } const v = value[sec.id]; if (!v) return "—"; return sec.options.find(o => o.id === v)?.label || "—"; }; return (
{builder.sections.map(sec => { const isOpen = openId === sec.id; const recommended = helpMode && sec.options.find(o => o.popular); return (
{isOpen && (
{sec.options.map(opt => { const selected = sec.multi ? (value[sec.id] || []).includes(opt.id) : value[sec.id] === opt.id; const isRec = helpMode && recommended && opt.id === recommended.id && !selected; return ( ); })}
)}
); })} {/* AI notes field — always visible, below the accordion */}

Anything else?

AI understands "make it spicy, no mayo"