Files
simple-pos-system/CLAUDE_DESIGN/order-drawer.jsx

921 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Order drawer — add/customize a product before sending to the kitchen.
// Mobile-portrait, ~90% height bottom sheet with horizontal tabs.
const { DIAVOLA } = window;
// --- utils -----------------------------------------------------------------
const fmt = (n) => {
const sign = n < 0 ? '' : '';
return sign + '€' + Math.abs(n).toFixed(2);
};
const fmtSigned = (n) => (n >= 0 ? '+' : '') + '€' + Math.abs(n).toFixed(2);
// --- stepper (big touch) ---------------------------------------------------
function Stepper({ value, onChange, min = 0, max = 99, size = 'md' }) {
const sizes = {
md: { btn: 40, font: 18, w: 108 },
lg: { btn: 48, font: 22, w: 132 },
};
const s = sizes[size];
return (
<div style={{
display: 'inline-flex', alignItems: 'center',
height: s.btn, width: s.w,
borderRadius: s.btn / 2,
background: 'white',
border: '1px solid var(--ink-200)',
overflow: 'hidden',
}} onClick={(e) => e.stopPropagation()}>
<button
onClick={() => onChange(Math.max(min, value - 1))}
disabled={value <= min}
style={{
width: s.btn, height: s.btn,
border: 'none', background: 'transparent',
fontSize: s.font, fontWeight: 500,
color: value <= min ? 'var(--ink-300)' : 'var(--ink-900)',
cursor: value <= min ? 'default' : 'pointer',
}}></button>
<div style={{
flex: 1, textAlign: 'center',
fontSize: s.font - 2, fontWeight: 600,
fontFamily: "'Geist Mono', monospace",
color: 'var(--ink-900)',
}}>{value}</div>
<button
onClick={() => onChange(Math.min(max, value + 1))}
disabled={value >= max}
style={{
width: s.btn, height: s.btn,
border: 'none', background: 'transparent',
fontSize: s.font, fontWeight: 500,
color: value >= max ? 'var(--ink-300)' : 'var(--ink-900)',
cursor: value >= max ? 'default' : 'pointer',
}}>+</button>
</div>
);
}
// --- checkmark icon --------------------------------------------------------
function Check({ size = 18 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M5 12.5L10 17.5L19 7.5" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
}
function Chevron({ open, size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
style={{ transform: `rotate(${open ? 180 : 0}deg)`, transition: 'transform 180ms ease' }}>
<path d="M6 9L12 15L18 9" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
}
// --- Row primitive (shared look across tabs) -------------------------------
function Row({ selected, onClick, left, right, children }) {
return (
<div
onClick={onClick}
style={{
padding: '14px 16px',
background: selected ? 'var(--brand-50)' : 'white',
border: '1px solid ' + (selected ? 'var(--brand-200)' : 'var(--ink-100)'),
borderRadius: 12,
display: 'flex', alignItems: 'center', gap: 12,
cursor: onClick ? 'pointer' : 'default',
transition: 'background 120ms ease, border-color 120ms ease',
minHeight: 60,
}}
>
{left && <div style={{ flexShrink: 0 }}>{left}</div>}
<div style={{ flex: 1, minWidth: 0 }}>{children}</div>
{right && <div style={{ flexShrink: 0 }}>{right}</div>}
</div>
);
}
// --- Checkbox circle (selected / not) --------------------------------------
function CheckCircle({ selected, size = 26 }) {
return (
<div style={{
width: size, height: size,
borderRadius: '50%',
border: '2px solid ' + (selected ? 'var(--brand-500)' : 'var(--ink-300)'),
background: selected ? 'var(--brand-500)' : 'white',
color: 'white',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 120ms ease',
flexShrink: 0,
}}>
{selected && <Check size={size * 0.65} />}
</div>
);
}
// --- Radio dot -------------------------------------------------------------
function RadioDot({ selected, size = 22 }) {
return (
<div style={{
width: size, height: size,
borderRadius: '50%',
border: '2px solid ' + (selected ? 'var(--brand-500)' : 'var(--ink-300)'),
background: 'white',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
transition: 'all 120ms ease',
}}>
{selected && (
<div style={{
width: size - 10, height: size - 10,
borderRadius: '50%',
background: 'var(--brand-500)',
}} />
)}
</div>
);
}
// ===========================================================================
// QUICK OPTIONS TAB
// ===========================================================================
function QuickOptionsTab({ options, state, setState }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '4px 0' }}>
{options.map(opt => {
const qty = state[opt.id] || 0;
const selected = qty > 0;
return (
<Row
key={opt.id}
selected={selected}
onClick={opt.multi ? undefined : () => setState({ ...state, [opt.id]: selected ? 0 : 1 })}
left={!opt.multi && <CheckCircle selected={selected} />}
right={
opt.multi ? (
selected ? (
<Stepper value={qty} onChange={(v) => setState({ ...state, [opt.id]: v })} />
) : (
<button
onClick={(e) => { e.stopPropagation(); setState({ ...state, [opt.id]: 1 }); }}
style={{
height: 40, padding: '0 18px',
borderRadius: 20,
background: 'white',
border: '1.5px solid var(--ink-200)',
color: 'var(--ink-900)',
fontSize: 15, fontWeight: 600,
cursor: 'pointer',
}}
>Add</button>
)
) : null
}
>
<div style={{ fontSize: 16, fontWeight: 500, color: 'var(--ink-900)' }}>{opt.label}</div>
{opt.price > 0 && (
<div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 2 }}>
+{opt.price.toFixed(2)} {opt.multi ? 'each' : ''}
</div>
)}
</Row>
);
})}
</div>
);
}
// ===========================================================================
// EXTRAS TAB — each row expands inline to pick a sub-option
// ===========================================================================
function ExtrasTab({ extras, state, setState, expandedId, setExpandedId }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '4px 0' }}>
{extras.map(ex => {
const selection = state[ex.id]; // { qty, subId } | undefined
const selected = !!selection;
const open = expandedId === ex.id;
const subLabel = selection
? ex.subOptions.find(s => s.id === selection.subId)?.label
: null;
const toggle = () => {
if (selected) {
setState({ ...state, [ex.id]: undefined });
if (open) setExpandedId(null);
} else {
// default to first sub-option, auto-expand so user can change it
setState({ ...state, [ex.id]: { qty: 1, subId: ex.subOptions[0].id } });
setExpandedId(ex.id);
}
};
return (
<div key={ex.id}>
<Row
selected={selected}
onClick={toggle}
left={<CheckCircle selected={selected} />}
right={
selected ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }} onClick={(e) => e.stopPropagation()}>
{ex.multi && (
<Stepper
value={selection.qty}
onChange={(v) => setState({ ...state, [ex.id]: v === 0 ? undefined : { ...selection, qty: v } })}
/>
)}
<button
onClick={(e) => { e.stopPropagation(); setExpandedId(open ? null : ex.id); }}
style={{
height: 40, width: 40,
borderRadius: '50%',
background: 'white',
border: '1px solid var(--ink-200)',
color: 'var(--ink-700)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer',
}}
>
<Chevron open={open} />
</button>
</div>
) : null
}
>
<div style={{ fontSize: 16, fontWeight: 500, color: 'var(--ink-900)' }}>{ex.label}</div>
<div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 2 }}>
{ex.price > 0 ? `+€${ex.price.toFixed(2)}${ex.multi ? ' each' : ''}` : 'Included'}
{subLabel && <span style={{ color: 'var(--brand-700)', fontWeight: 600 }}> · {subLabel}</span>}
</div>
</Row>
{/* Inline sub-option picker */}
{selected && open && (
<div style={{
margin: '6px 0 2px 16px',
paddingLeft: 14,
borderLeft: '2px solid var(--brand-200)',
display: 'flex', flexDirection: 'column', gap: 6,
}}>
<div style={{
fontSize: 12, fontWeight: 600, color: 'var(--ink-500)',
textTransform: 'uppercase', letterSpacing: 0.6,
padding: '6px 2px 2px',
}}>{ex.subLabel}</div>
{ex.subOptions.map(sub => {
const isSel = selection.subId === sub.id;
return (
<Row
key={sub.id}
selected={isSel}
onClick={() => setState({ ...state, [ex.id]: { ...selection, subId: sub.id } })}
left={<RadioDot selected={isSel} />}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ fontSize: 15, color: 'var(--ink-900)' }}>{sub.label}</div>
{sub.price > 0 && (
<div style={{ fontSize: 13, color: 'var(--ink-500)' }}>+{sub.price.toFixed(2)}</div>
)}
</div>
</Row>
);
})}
</div>
)}
</div>
);
})}
</div>
);
}
// ===========================================================================
// INGREDIENTS TAB — remove only
// ===========================================================================
function IngredientsTab({ ingredients, state, setState }) {
return (
<div>
<div style={{
padding: '10px 14px',
background: 'var(--ink-100)',
borderRadius: 10,
fontSize: 13, color: 'var(--ink-700)',
marginBottom: 12,
}}>
Tap to remove ingredients from this item.
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{ingredients.map(ing => {
const removed = !!state[ing.id];
return (
<Row
key={ing.id}
selected={false}
onClick={() => setState({ ...state, [ing.id]: !removed })}
right={
<div style={{
height: 36, padding: '0 14px',
borderRadius: 18,
background: removed ? 'var(--alert-500)' : 'white',
border: '1.5px solid ' + (removed ? 'var(--alert-500)' : 'var(--ink-200)'),
color: removed ? 'white' : 'var(--ink-700)',
fontSize: 14, fontWeight: 600,
display: 'inline-flex', alignItems: 'center', gap: 6,
transition: 'all 120ms ease',
}}>
{removed ? 'Removed' : 'Remove'}
</div>
}
>
<div style={{
fontSize: 16, fontWeight: 500,
color: removed ? 'var(--ink-400)' : 'var(--ink-900)',
textDecoration: removed ? 'line-through' : 'none',
}}>{ing.label}</div>
</Row>
);
})}
</div>
</div>
);
}
// ===========================================================================
// PREFERENCES TAB — radio groups
// ===========================================================================
function PreferencesTab({ preferences, state, setState }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{preferences.map(pref => {
const selected = state[pref.id];
return (
<div key={pref.id}>
<div style={{
display: 'flex', alignItems: 'baseline', gap: 8,
padding: '0 2px 10px',
}}>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--ink-900)' }}>{pref.label}</div>
{pref.required && (
<div style={{
fontSize: 11, fontWeight: 700,
color: 'var(--alert-500)',
textTransform: 'uppercase', letterSpacing: 0.6,
}}>Required</div>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{pref.subOptions.map(sub => {
const isSel = selected === sub.id;
return (
<Row
key={sub.id}
selected={isSel}
onClick={() => setState({ ...state, [pref.id]: sub.id })}
left={<RadioDot selected={isSel} />}
right={
sub.price !== 0 ? (
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--ink-500)' }}>
{fmtSigned(sub.price)}
</div>
) : null
}
>
<div style={{ fontSize: 16, fontWeight: 500, color: 'var(--ink-900)' }}>{sub.label}</div>
</Row>
);
})}
</div>
</div>
);
})}
</div>
);
}
// ===========================================================================
// NOTES TAB
// ===========================================================================
const QUICK_NOTES = ['No eye contact 😅', 'Table is in a hurry', 'Customer is allergic', 'Cut in smaller pieces', 'Leave box open'];
function NotesTab({ note, setNote }) {
return (
<div>
<div style={{
padding: '10px 14px',
background: 'var(--ink-100)',
borderRadius: 10,
fontSize: 13, color: 'var(--ink-700)',
marginBottom: 12,
}}>
Anything specific for the kitchen. Short and clear works best.
</div>
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="e.g. Please cut in 12 slices, extra napkins..."
rows={5}
style={{
width: '100%',
padding: 14,
fontSize: 16,
fontFamily: 'inherit',
color: 'var(--ink-900)',
background: 'white',
border: '1px solid var(--ink-200)',
borderRadius: 12,
resize: 'none',
outline: 'none',
boxSizing: 'border-box',
}}
/>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink-500)', textTransform: 'uppercase', letterSpacing: 0.6, marginTop: 20, marginBottom: 8 }}>
Quick notes
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{QUICK_NOTES.map(q => (
<button
key={q}
onClick={() => setNote(note ? `${note}\n${q}` : q)}
style={{
height: 38, padding: '0 14px',
borderRadius: 19,
background: 'white',
border: '1px solid var(--ink-200)',
color: 'var(--ink-700)',
fontSize: 14, fontWeight: 500,
cursor: 'pointer',
}}
>+ {q}</button>
))}
</div>
</div>
);
}
// ===========================================================================
// SUMMARY TAB — final config review
// ===========================================================================
function SummaryTab({ product, config, lines, onJumpTab }) {
const sectionStyle = { marginBottom: 22 };
const headerStyle = {
fontSize: 12, fontWeight: 700, color: 'var(--ink-500)',
textTransform: 'uppercase', letterSpacing: 0.8,
padding: '0 2px 8px',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
};
const editBtn = (tab) => (
<button onClick={() => onJumpTab(tab)} style={{
background: 'none', border: 'none',
fontSize: 12, fontWeight: 700, color: 'var(--brand-700)',
textTransform: 'uppercase', letterSpacing: 0.8,
cursor: 'pointer',
}}>Edit</button>
);
// Group summary lines
const groups = {
pref: lines.filter(l => l.group === 'pref'),
quick: lines.filter(l => l.group === 'quick'),
extra: lines.filter(l => l.group === 'extra'),
removed: lines.filter(l => l.group === 'removed'),
};
const LineItem = ({ l }) => (
<div style={{
padding: '10px 14px',
background: 'white',
border: '1px solid var(--ink-100)',
borderRadius: 10,
display: 'flex', alignItems: 'center', gap: 10,
minHeight: 48,
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 500, color: 'var(--ink-900)' }}>
{l.qty > 1 && <span style={{ fontFamily: "'Geist Mono', monospace", color: 'var(--ink-500)', marginRight: 6 }}>{l.qty}×</span>}
{l.label}
</div>
{l.detail && (
<div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 2 }}>{l.detail}</div>
)}
</div>
{l.price !== 0 && (
<div style={{
fontSize: 14, fontWeight: 600,
fontFamily: "'Geist Mono', monospace",
color: l.price < 0 ? 'var(--alert-500)' : 'var(--ink-900)',
}}>{fmtSigned(l.price)}</div>
)}
</div>
);
const isEmpty = lines.length === 0 && !config.note;
return (
<div>
{/* Product summary header */}
<div style={{
padding: '14px 16px',
background: 'var(--brand-50)',
border: '1px solid var(--brand-200)',
borderRadius: 12,
marginBottom: 20,
display: 'flex', alignItems: 'center', gap: 12,
}}>
<div style={{ fontSize: 32 }}>{product.emoji}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 16, fontWeight: 600, color: 'var(--ink-900)' }}>{product.name}</div>
<div style={{ fontSize: 13, color: 'var(--ink-700)', marginTop: 2 }}>Base {product.price.toFixed(2)}</div>
</div>
</div>
{isEmpty && (
<div style={{
padding: '28px 16px',
textAlign: 'center',
color: 'var(--ink-500)',
fontSize: 14,
}}>
No customization yet. Switch to other tabs to add options.
</div>
)}
{groups.pref.length > 0 && (
<div style={sectionStyle}>
<div style={headerStyle}>
<span>Preferences</span>
{editBtn('preferences')}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{groups.pref.map((l, i) => <LineItem key={i} l={l} />)}
</div>
</div>
)}
{groups.quick.length > 0 && (
<div style={sectionStyle}>
<div style={headerStyle}>
<span>Quick options</span>
{editBtn('quick')}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{groups.quick.map((l, i) => <LineItem key={i} l={l} />)}
</div>
</div>
)}
{groups.extra.length > 0 && (
<div style={sectionStyle}>
<div style={headerStyle}>
<span>Extras</span>
{editBtn('extras')}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{groups.extra.map((l, i) => <LineItem key={i} l={l} />)}
</div>
</div>
)}
{groups.removed.length > 0 && (
<div style={sectionStyle}>
<div style={headerStyle}>
<span>Removed</span>
{editBtn('ingredients')}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{groups.removed.map((l, i) => <LineItem key={i} l={l} />)}
</div>
</div>
)}
{config.note && (
<div style={sectionStyle}>
<div style={headerStyle}>
<span>Note</span>
{editBtn('notes')}
</div>
<div style={{
padding: '12px 14px',
background: '#fffbeb',
border: '1px solid #fde6a7',
borderRadius: 10,
fontSize: 15,
color: 'var(--ink-900)',
lineHeight: 1.4,
whiteSpace: 'pre-wrap',
}}>{config.note}</div>
</div>
)}
</div>
);
}
// ===========================================================================
// MAIN DRAWER
// ===========================================================================
function OrderDrawer({ product, isOpen, onClose, onAddToOrder }) {
// Tabs available for this product (hidden if empty)
const tabs = React.useMemo(() => {
const list = [];
if (product.quickOptions?.length) list.push({ id: 'quick', label: 'Quick' });
if (product.extras?.length) list.push({ id: 'extras', label: 'Extras' });
if (product.ingredients?.length) list.push({ id: 'ingredients', label: 'Ingredients' });
if (product.preferences?.length) list.push({ id: 'preferences', label: 'Preferences' });
list.push({ id: 'notes', label: 'Notes' });
list.push({ id: 'summary', label: 'Summary' });
return list;
}, [product]);
// --- STATE ---
const initialPrefs = () => {
const p = {};
(product.preferences || []).forEach(pref => {
const def = pref.subOptions.find(s => s.default);
if (def) p[pref.id] = def.id;
});
return p;
};
const [activeTab, setActiveTab] = React.useState(tabs[0].id);
const [quick, setQuick] = React.useState({}); // { id: qty }
const [extras, setExtras] = React.useState({}); // { id: {qty, subId} }
const [extraExpanded, setExtraExpanded] = React.useState(null);
const [removed, setRemoved] = React.useState({}); // { id: true }
const [prefs, setPrefs] = React.useState(initialPrefs());
const [note, setNote] = React.useState('');
const [qty, setQty] = React.useState(1);
// Reset when drawer opens with a (possibly different) product
React.useEffect(() => {
if (isOpen) {
setActiveTab(tabs[0].id);
setQuick({});
setExtras({});
setExtraExpanded(null);
setRemoved({});
setPrefs(initialPrefs());
setNote('');
setQty(1);
}
}, [isOpen, product.id]);
const config = { quick, extras, removed, prefs, note };
// --- Derived: summary lines -------------------------------------------
const { lines, itemPrice } = React.useMemo(() => {
const L = [];
let p = product.price;
// Preferences
(product.preferences || []).forEach(pref => {
const selId = prefs[pref.id];
if (!selId) return;
const sub = pref.subOptions.find(s => s.id === selId);
if (!sub) return;
if (!sub.default || sub.price !== 0) {
L.push({
group: 'pref',
label: `${pref.label}: ${sub.label}`,
qty: 1,
price: sub.price,
});
}
p += sub.price;
});
// Quick
(product.quickOptions || []).forEach(opt => {
const q = quick[opt.id] || 0;
if (q === 0) return;
const linePrice = opt.price * q;
L.push({
group: 'quick',
label: opt.label,
qty: q,
price: linePrice,
});
p += linePrice;
});
// Extras
(product.extras || []).forEach(ex => {
const sel = extras[ex.id];
if (!sel) return;
const sub = ex.subOptions.find(s => s.id === sel.subId);
const linePrice = (ex.price + (sub?.price || 0)) * sel.qty;
L.push({
group: 'extra',
label: ex.label,
detail: sub?.label,
qty: sel.qty,
price: linePrice,
});
p += linePrice;
});
// Removed ingredients
(product.ingredients || []).forEach(ing => {
if (removed[ing.id]) {
L.push({
group: 'removed',
label: `No ${ing.label.toLowerCase()}`,
qty: 1,
price: 0,
});
}
});
return { lines: L, itemPrice: p };
}, [prefs, quick, extras, removed, product]);
const total = itemPrice * qty;
const customizationCount = lines.length + (note ? 1 : 0);
// Backdrop click to close
const handleBackdrop = (e) => {
if (e.target === e.currentTarget) onClose();
};
// --- Render ---
return (
<>
{/* Backdrop */}
<div
onClick={handleBackdrop}
style={{
position: 'absolute', inset: 0,
background: 'rgba(16, 20, 24, 0.45)',
opacity: isOpen ? 1 : 0,
pointerEvents: isOpen ? 'auto' : 'none',
transition: 'opacity 260ms ease',
zIndex: 10,
}}
/>
{/* Sheet */}
<div style={{
position: 'absolute',
left: 0, right: 0, bottom: 0,
height: '90%',
background: 'var(--surface)',
borderRadius: '20px 20px 0 0',
transform: isOpen ? 'translateY(0)' : 'translateY(100%)',
transition: 'transform 320ms cubic-bezier(0.32, 0.72, 0, 1)',
zIndex: 11,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
boxShadow: '0 -8px 32px rgba(16, 20, 24, 0.18)',
}}>
{/* Grab handle */}
<div style={{ padding: '8px 0 4px', display: 'flex', justifyContent: 'center' }}>
<div style={{ width: 40, height: 4, borderRadius: 2, background: 'var(--ink-200)' }} />
</div>
{/* Header */}
<div style={{
padding: '8px 16px 0',
display: 'flex', alignItems: 'flex-start', gap: 12,
}}>
<div style={{
width: 56, height: 56,
borderRadius: 14,
background: 'var(--brand-50)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 32,
flexShrink: 0,
}}>{product.emoji}</div>
<div style={{ flex: 1, minWidth: 0, paddingTop: 2 }}>
<div style={{ fontSize: 19, fontWeight: 700, color: 'var(--ink-900)', lineHeight: 1.2 }}>{product.name}</div>
<div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 4, lineHeight: 1.4 }}>{product.desc}</div>
</div>
<button
onClick={onClose}
style={{
width: 36, height: 36,
borderRadius: '50%',
background: 'var(--ink-100)',
border: 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer',
flexShrink: 0,
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M6 6L18 18M6 18L18 6" stroke="var(--ink-700)" strokeWidth="2.2" strokeLinecap="round"/>
</svg>
</button>
</div>
{/* Tabs */}
<div style={{
marginTop: 14,
borderBottom: '1px solid var(--ink-100)',
overflowX: 'auto',
scrollbarWidth: 'none',
WebkitOverflowScrolling: 'touch',
}}>
<div style={{ display: 'flex', padding: '0 16px', gap: 4, minWidth: 'max-content' }}>
{tabs.map(t => {
const active = activeTab === t.id;
const badge = (
t.id === 'quick' ? Object.values(quick).filter(v => v > 0).length :
t.id === 'extras' ? Object.values(extras).filter(Boolean).length :
t.id === 'ingredients' ? Object.values(removed).filter(Boolean).length :
t.id === 'notes' ? (note ? 1 : 0) :
t.id === 'summary' ? customizationCount :
0
);
return (
<button
key={t.id}
onClick={() => setActiveTab(t.id)}
style={{
padding: '14px 4px',
background: 'none',
border: 'none',
borderBottom: '2px solid ' + (active ? 'var(--brand-500)' : 'transparent'),
color: active ? 'var(--brand-700)' : 'var(--ink-500)',
fontSize: 15,
fontWeight: active ? 700 : 500,
fontFamily: 'inherit',
cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 6,
whiteSpace: 'nowrap',
marginRight: 10,
transition: 'color 120ms ease, border-color 120ms ease',
}}
>
{t.label}
{badge > 0 && (
<span style={{
minWidth: 20, height: 20, padding: '0 6px',
borderRadius: 10,
background: active ? 'var(--brand-500)' : 'var(--ink-200)',
color: active ? 'white' : 'var(--ink-700)',
fontSize: 12, fontWeight: 700,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontFamily: "'Geist Mono', monospace",
}}>{badge}</span>
)}
</button>
);
})}
</div>
</div>
{/* Scrollable content */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '16px 16px 20px',
background: 'var(--bg)',
WebkitOverflowScrolling: 'touch',
}}>
{activeTab === 'quick' && <QuickOptionsTab options={product.quickOptions} state={quick} setState={setQuick} />}
{activeTab === 'extras' && <ExtrasTab extras={product.extras} state={extras} setState={setExtras} expandedId={extraExpanded} setExpandedId={setExtraExpanded} />}
{activeTab === 'ingredients' && <IngredientsTab ingredients={product.ingredients} state={removed} setState={setRemoved} />}
{activeTab === 'preferences' && <PreferencesTab preferences={product.preferences} state={prefs} setState={setPrefs} />}
{activeTab === 'notes' && <NotesTab note={note} setNote={setNote} />}
{activeTab === 'summary' && <SummaryTab product={product} config={config} lines={lines} onJumpTab={setActiveTab} />}
</div>
{/* Footer — qty stepper + Add to Order */}
<div style={{
padding: '12px 16px 18px',
background: 'white',
borderTop: '1px solid var(--ink-100)',
display: 'flex', alignItems: 'center', gap: 12,
boxShadow: '0 -4px 12px rgba(16, 20, 24, 0.04)',
}}>
<Stepper value={qty} onChange={setQty} min={1} size="lg" />
<button
onClick={() => onAddToOrder({ product, config, qty, total })}
style={{
flex: 1, height: 56,
borderRadius: 28,
background: 'var(--brand-500)',
border: 'none',
color: 'white',
fontSize: 16, fontWeight: 700,
fontFamily: 'inherit',
cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0 22px',
transition: 'transform 100ms ease, background 120ms ease',
}}
onMouseDown={(e) => e.currentTarget.style.transform = 'scale(0.98)'}
onMouseUp={(e) => e.currentTarget.style.transform = 'scale(1)'}
onMouseLeave={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
<span>Add to order</span>
<span style={{ fontFamily: "'Geist Mono', monospace" }}>{total.toFixed(2)}</span>
</button>
</div>
</div>
</>
);
}
window.OrderDrawer = OrderDrawer;