// Gustai Lab design tokens + shared primitives const GL = { // Palette teal: '#59DFE8', tealDim: '#2FB9C2', ink: '#0A0A0A', inkSoft: '#111A1C', darkBg: '#0E1B1D', darkSurface: '#162628', darkBorder: 'rgba(255,255,255,0.08)', offwhite: '#F6F8F8', paper: '#FFFFFF', line: '#E8ECEC', muted: '#8A9698', mutedDark: 'rgba(255,255,255,0.55)', // Type display: "'Archivo Black', 'Arial Black', Impact, sans-serif", body: "'Inter', -apple-system, system-ui, sans-serif", mono: "'JetBrains Mono', ui-monospace, Menlo, monospace", }; // Inject fonts + base function GLFonts() { return ( ); } // Placeholder image: striped SVG with monospace caption function GLImage({ src, alt, label, ratio = '4/5', radius = 12, dark = false, style = {} }) { const [err, setErr] = React.useState(false); if (src && !err) { return (
{alt setErr(true)} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}/>
); } const stripe = dark ? 'rgba(255,255,255,0.04)' : 'rgba(10,10,10,0.04)'; return (
{label || alt || 'IMG'}
); } // Primary button (dark, pill-less rectangular iOS-ish) function GLButton({ children, onClick, variant = 'primary', icon, style = {}, disabled }) { const base = { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, width: '100%', height: 56, borderRadius: 8, fontFamily: GL.body, fontWeight: 800, fontSize: 14, letterSpacing: '0.18em', textTransform: 'uppercase', transition: 'transform .12s ease, opacity .15s', opacity: disabled ? 0.4 : 1, cursor: disabled ? 'not-allowed' : 'pointer', }; const variants = { primary: { background: GL.ink, color: '#fff' }, teal: { background: GL.teal, color: GL.ink }, outline: { background: 'transparent', color: GL.ink, border: `1.5px solid ${GL.ink}` }, ghost: { background: 'transparent', color: GL.ink }, darkPrimary: { background: '#fff', color: GL.ink }, }; return ( ); } // Tab bar — middle slot swaps with shop mode: // alc → "Cart" (shopping-cart icon, one-time order) // sub → "Box" (box icon, recurring standing-order template) // Both ids are 'box' so routing/active state stays unified — only the visual // presentation changes per mode. Same for the count badge. function GLTabBar({ active, onNav, cartCount = 0, dark = false, shopMode = 'alc' }) { const middleTab = shopMode === 'sub' ? { id: 'box', label: 'Box', icon: 'M3 7l9-4 9 4v10l-9 4-9-4V7zM3 7l9 4 9-4M12 11v10' } : { id: 'box', label: 'Cart', icon: 'M6 6h15l-1.5 9h-13zM6 6L5 3H2M9 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM18 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2z' }; const tabs = [ { id: 'home', label: 'Home', icon: 'M3 11l9-8 9 8v10a1 1 0 0 1-1 1h-5v-7h-6v7H4a1 1 0 0 1-1-1V11z' }, { id: 'lab', label: 'Lab', icon: 'M9 2v6.5L3.5 18.3A2 2 0 0 0 5.2 21.5h13.6a2 2 0 0 0 1.7-3.2L15 8.5V2M8 2h8M9 14h6' }, middleTab, // Mario AI chat replaces Profile in the bottom nav. Profile lives in the topbar now. { id: 'mario', label: 'Mario', icon: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z' }, ]; const bg = dark ? 'rgba(14,27,29,0.94)' : 'rgba(255,255,255,0.94)'; const border = dark ? GL.darkBorder : GL.line; return (
{tabs.map(t => { const on = t.id === active; const color = on ? (dark ? GL.teal : GL.ink) : (dark ? GL.mutedDark : GL.muted); return ( ); })}
); } // Top bar with logo/back function GLTopBar({ title = 'Gustai Lab', onBack, cartCount = 0, onCart, dark = false, right, center }) { const fg = dark ? '#fff' : GL.ink; const border = dark ? GL.darkBorder : GL.line; const bg = dark ? 'rgba(14,27,29,0.92)' : 'rgba(255,255,255,0.92)'; return (
{onBack && ( )}
{center || (
{title}
)}
{right} {onCart && ( )}
); } // Diet label registry — used by DietBadges + filtering // label = display text, color = chip background (light), fg = text color const GL_DIET_LABELS = { 'vegetarian': { label: 'Vegetarian', color: '#E8F5E9', fg: '#1B5E20', kind: 'positive' }, 'vegan': { label: 'Vegan', color: '#E8F5E9', fg: '#1B5E20', kind: 'positive' }, 'gluten-free': { label: 'Gluten-Free', color: '#E8F5E9', fg: '#1B5E20', kind: 'positive' }, 'dairy-free': { label: 'Dairy-Free', color: '#E8F5E9', fg: '#1B5E20', kind: 'positive' }, 'contains-gluten': { label: 'Gluten', color: '#FFF3E0', fg: '#B45309', kind: 'allergen' }, 'contains-dairy': { label: 'Dairy', color: '#FFF3E0', fg: '#B45309', kind: 'allergen' }, 'contains-eggs': { label: 'Eggs', color: '#FFF3E0', fg: '#B45309', kind: 'allergen' }, 'contains-nuts': { label: 'Nuts', color: '#FFF3E0', fg: '#B45309', kind: 'allergen' }, 'contains-shellfish': { label: 'Shellfish', color: '#FFF3E0', fg: '#B45309', kind: 'allergen' }, 'contains-pork': { label: 'Pork', color: '#FFF3E0', fg: '#B45309', kind: 'allergen' }, }; // Compact diet badge row. mode: 'compact' (max 3, dots overflow) | 'full' function GLDietBadges({ labels = [], mode = 'compact', dark = false }) { if (!labels || labels.length === 0) return null; const items = mode === 'compact' ? labels.slice(0, 3) : labels; const overflow = mode === 'compact' && labels.length > 3 ? labels.length - 3 : 0; return (
{items.map(key => { const meta = GL_DIET_LABELS[key]; if (!meta) return null; const bg = dark ? (meta.kind === 'positive' ? 'rgba(89,223,232,0.12)' : 'rgba(255,255,255,0.08)') : meta.color; const fg = dark ? (meta.kind === 'positive' ? GL.teal : 'rgba(255,255,255,0.85)') : meta.fg; return ( {meta.label} ); })} {overflow > 0 && ( +{overflow} )}
); } // Product/set data // All items are category 'sets' for launch (single-category UX). diet_labels drive badge UI. const GL_PASTA = [ { id: 'truffle', name: 'Truffle Mafaldine', pair: 'Creamy Truffle Sauce', price: 350, cat: 'sets', tags: ['BESTSELLER'], diet_labels: ['vegetarian','contains-gluten','contains-eggs','contains-dairy'], img: 'https://images.unsplash.com/photo-1622973536968-3ead9e780960?w=800&q=80', short_desc: 'Hand-rolled mafaldine ribbons drenched in black truffle cream.', long_desc: 'Made in small batches each morning. Pairs beautifully with our house-made focaccia and a chilled glass of Vermentino.', ingredients: '• Semolina flour (Imported from Italy)\n• Organic eggs\n• Fresh black summer truffle\n• White truffle oil\n• Sea salt', method: 'Bronze-extruded through custom dies for a rough, porous surface. Cold-pressed at under 40°C to preserve protein structure. Slow-dried for 24 hours minimum.', cooking_instr: '1. Bring a large pot of salted water to a rolling boil.\n2. Add pasta and cook 3–4 minutes until al dente.\n3. Reserve ½ cup pasta water, drain, and toss with sauce.', allergens: 'Contains: Gluten (wheat), Eggs. May contain traces of nuts. Dairy in sauce.', storage_info: 'Keep refrigerated below 4°C. Best consumed within 3 days of delivery. Can be frozen for up to 1 month.', subscribe_note: 'Cancel anytime. Free delivery over ฿1000.' }, { id: 'squid', name: 'Squid Ink Tagliolini', pair: 'Spicy Seafood Sauce', price: 320, cat: 'sets', tags: ['NEW'], diet_labels: ['contains-gluten','contains-eggs','contains-shellfish'], img: 'https://images.unsplash.com/photo-1551183053-bf91a1d81141?w=800&q=80', short_desc: 'Jet-black tagliolini with a bold spicy seafood sauce.', long_desc: 'Our squid ink is sourced fresh daily from Phuket fishermen. Intense, briny, and utterly addictive.', ingredients: '• Semolina flour\n• Organic eggs\n• Fresh squid ink\n• Sea salt', method: 'Hand-rolled to 1.5mm thickness then cut on a chitarra frame. Squid ink folded in during kneading for colour depth. Air-dried at room temperature for 4 hours.', cooking_instr: '1. Boil salted water. Cook pasta 2–3 minutes.\n2. Warm sauce separately.\n3. Toss together and serve immediately.', allergens: 'Contains: Gluten (wheat), Eggs, Molluscs (squid). May contain traces of shellfish and nuts.', storage_info: 'Keep refrigerated below 4°C. Best consumed within 2 days.', subscribe_note: 'Cancel anytime. Free delivery over ฿1000.' }, { id: 'rigatoni', name: 'Classic Rigatoni', pair: 'Wagyu Bolognese', price: 280, cat: 'sets', tags: ['POPULAR'], diet_labels: ['contains-gluten','contains-eggs'], img: 'https://images.unsplash.com/photo-1587740908075-9e245311e158?w=800&q=80', short_desc: 'Bronze-extruded rigatoni built for thick, hearty sauces.', long_desc: 'Our ridged rigatoni grips every drop of the slow-braised Wagyu bolognese. A Gustai Lab cornerstone.', ingredients: '• Durum wheat semolina\n• Organic eggs\n• Sea salt', method: 'Extruded through bronze dies under high pressure for maximum surface roughness. Short-cut and ridged to grip thick sauces. Dried slowly over 36 hours at 38°C.', cooking_instr: '1. Boil generously salted water. Cook 5–6 minutes.\n2. Reserve pasta water.\n3. Toss with bolognese over low heat.', allergens: 'Contains: Gluten (wheat), Eggs. May contain traces of nuts and dairy.', storage_info: 'Refrigerate below 4°C. Best within 3 days. Freezes well.', subscribe_note: 'Cancel anytime. Free delivery over ฿1000.' }, { id: 'burrata', name: 'Burrata & Pomodoro', pair: 'Tomato Basil Sauce', price: 400, cat: 'sets', tags: ['FRESH'], diet_labels: ['vegetarian','contains-gluten','contains-eggs','contains-dairy'], img: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=800&q=80', short_desc: 'Pillowy paccheri with Phuket heirloom tomatoes and creamy burrata.', long_desc: 'Made in small batches each morning. Pairs beautifully with our house-made focaccia and a chilled glass of Vermentino.', ingredients: '• Semolina flour\n• Organic eggs\n• Heirloom tomatoes\n• Fresh basil\n• Sea salt', method: 'Large-format paccheri shaped in a smooth bronze die and scored for sauce adhesion. Short-dried at ambient temperature to retain a silky interior. Paired with a cold-crush tomato sauce.', cooking_instr: '1. Cook pasta 4–5 minutes in salted water.\n2. Warm sauce gently — do not boil.\n3. Plate pasta, add sauce, top with burrata.', allergens: 'Contains: Gluten (wheat), Eggs, Milk (burrata). Suitable for vegetarians. May contain traces of nuts.', storage_info: 'Refrigerate below 4°C. Best consumed within 2 days.', subscribe_note: 'Cancel anytime. Free delivery over ฿1000.' }, { id: 'pesto', name: 'Pesto alla Genovese', pair: 'Pine Nut & Basil', price: 300, cat: 'sets', tags: ['CLASSIC'], diet_labels: ['vegetarian','contains-gluten','contains-eggs','contains-dairy','contains-nuts'], img: 'https://images.unsplash.com/photo-1473093226795-af9932fe5856?w=800&q=80', short_desc: 'Trofie-style pasta with a vivid cold-pressed Genovese pesto.', long_desc: 'Our basil is sourced from a single organic farm in Chiang Rai. The pesto is stone-ground the same morning.', ingredients: '• Semolina flour\n• Organic eggs\n• Fresh basil\n• Pine nuts\n• Parmigiano Reggiano\n• Extra virgin olive oil\n• Sea salt', method: 'Hand-twisted trofie rolled individually to create the spiral that traps pesto in each groove. Pesto stone-ground to order — never blended — for a coarser, more aromatic texture.', cooking_instr: '1. Cook pasta 3–4 minutes. Reserve ¼ cup pasta water.\n2. Never heat pesto. Toss cold with pasta off the heat.\n3. Loosen with pasta water as needed.', allergens: 'Contains: Gluten (wheat), Eggs, Milk, Tree nuts (pine nuts). May contain traces of other nuts.', storage_info: 'Refrigerate below 4°C. Consume within 3 days. Do not freeze pesto sauce.', subscribe_note: 'Cancel anytime. Free delivery over ฿1000.' }, { id: 'cacio', name: 'Cacio e Pepe', pair: 'Pecorino Romano', price: 290, cat: 'sets', tags: ['CLASSIC'], diet_labels: ['vegetarian','contains-gluten','contains-eggs','contains-dairy'], img: 'https://images.unsplash.com/photo-1556761223-4c4282c73f77?w=800&q=80', short_desc: 'Three ingredients. Zero compromise. The Roman original.', long_desc: 'Spaghetti alla chitarra with aged Pecorino Romano and cracked Tellicherry pepper. Simplicity perfected.', ingredients: '• Semolina flour\n• Organic eggs\n• Aged Pecorino Romano\n• Tellicherry black pepper\n• Sea salt', method: 'Cut on a traditional chitarra (guitar) wire frame to achieve square cross-section spaghetti. Dried at 36°C for 20 hours. The cheese sauce is emulsified tableside with only pasta water — no cream.', cooking_instr: '1. Cook pasta 3–4 minutes. Reserve ½ cup pasta water.\n2. Bloom pepper in a dry pan.\n3. Toss pasta with pepper, cheese, and pasta water off heat until creamy.', allergens: 'Contains: Gluten (wheat), Eggs, Milk (Pecorino Romano). May contain traces of nuts. Suitable for vegetarians.', storage_info: 'Refrigerate below 4°C. Best consumed within 3 days.', subscribe_note: 'Cancel anytime. Free delivery over ฿1000.' }, ]; // ── Weekly menu mock data (used by subscription screens) ────────────────────── const GL_WEEKLY_MENU = { week_start: '2026-04-21', week_label: 'Week of 21 Apr', theme: 'Truffle Season', dispatch_date: '2026-04-24', cutoff_at: '2026-04-22T18:00:00+07:00', items: [ { id:'wmi-1', product_id:'truffle', name:'Truffle Mafaldine', variant:'Single (200g)', price:380, available_qty:24, reserved_qty:8, feature_badge:'CHEF PICK', weight_g:200, img:'https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=800&auto=format&fit=crop' }, { id:'wmi-2', product_id:'squid', name:'Squid Ink Tagliolini', variant:'Double (400g)', price:640, available_qty:8, reserved_qty:4, feature_badge:'LIMITED', weight_g:400, img:'https://images.unsplash.com/photo-1563379926898-05f4575a45d8?w=800&auto=format&fit=crop' }, { id:'wmi-3', product_id:'rigatoni', name:'Classic Rigatoni', variant:'Single (200g)', price:280, available_qty:30, reserved_qty:6, feature_badge:'BESTSELLER', weight_g:200, img:'https://images.unsplash.com/photo-1555949258-eb67b1ef0ceb?w=800&auto=format&fit=crop' }, { id:'wmi-4', product_id:'burrata', name:'Burrata & Pomodoro', variant:'Family (1kg)', price:1240,available_qty:12, reserved_qty:3, feature_badge:'NEW', weight_g:1000, img:'https://images.unsplash.com/photo-1598866594230-a7c12756260f?w=800&auto=format&fit=crop' }, { id:'wmi-5', product_id:'pesto', name:'Pesto alla Genovese', variant:'Single (200g)', price:300, available_qty:20, reserved_qty:5, feature_badge:null, weight_g:200, img:'https://images.unsplash.com/photo-1473093295043-cdd812d0e601?w=800&auto=format&fit=crop' }, { id:'wmi-6', product_id:'cacio', name:'Cacio e Pepe', variant:'Double (400g)', price:580, available_qty:15, reserved_qty:4, feature_badge:'CLASSIC', weight_g:400, img:'https://images.unsplash.com/photo-1612874742237-6526221588e3?w=800&auto=format&fit=crop' }, ], }; Object.assign(window, { GL, GLFonts, GLImage, GLButton, GLTabBar, GLTopBar, GLDietBadges, GL_DIET_LABELS, GL_PASTA, GL_WEEKLY_MENU });