// Screens.jsx — Home, Recipe, Weigh const { useState: useStateS, useEffect: useEffectS, useMemo: useMemoS, useRef: useRefS } = React; // ────────────────────────────────────────────────────────── // Shared atoms // ────────────────────────────────────────────────────────── function IconBack({ color, size = 24 }) { return ( ); } function IconCheck({ color, size = 22 }) { return ( ); } function IconPlus({ color, size = 22 }) { return ( ); } function IconMinus({ color, size = 22 }) { return ( ); } function IconArrowRight({ color, size = 22 }) { return ( ); } function TopBar({ theme, title, leading, trailing }) { return (
{leading}
{title}
{trailing}
); } function PillButton({ theme, onClick, children, style = {}, variant = 'primary', disabled }) { const isPrimary = variant === 'primary'; const bg = disabled ? theme.accentSoft : (isPrimary ? theme.accent : theme.surface); const fg = disabled ? theme.inkMute : (isPrimary ? (theme.accent === '#FFD429' ? '#1A1A1A' : '#fff') : theme.ink); return ( ); } // ────────────────────────────────────────────────────────── // HOME // ────────────────────────────────────────────────────────── function HomeScreen({ theme, onOpen, qtys, session, onMenu }) { const grouped = useMemoS(() => { const out = {}; for (const r of RECIPES) { // don't show companions as separate entries const isCompanion = RECIPES.some(x => x.companions?.some(c => c.id === r.id)); if (isCompanion) continue; if (!out[r.category]) out[r.category] = []; out[r.category].push(r); } return out; }, []); return ( <>
CELEBREAD · 2026{session ? ` · ${session.display_name}` : ''}

配方清單

選擇麵包 → 輸入顆數 → 開始秤量
{onMenu && ( )}
{CATEGORIES.map(cat => grouped[cat] && (
{cat}
{grouped[cat].map((r, idx) => { const qty = qtys[r.id]; return ( ); })}
))}
); } // ────────────────────────────────────────────────────────── // RECIPE — quantity input + live preview // ────────────────────────────────────────────────────────── function Stepper({ theme, value, onChange, suffix = '顆', min = 0, max = 999 }) { const dec = () => onChange(Math.max(min, (value || 0) - 1)); const inc = () => onChange(Math.min(max, (value || 0) + 1)); return (
{value || 0}
{suffix}
); } function NumPad({ theme, onPress }) { const keys = ['1','2','3','4','5','6','7','8','9','C','0','⌫']; return (
{keys.map(k => ( ))}
); } function RecipeScreen({ theme, recipes, qtys, setQty, onBack, onGo }) { const main = recipes[0]; const totalFlour = recipes.reduce((s, r) => s + r.perPieceFlour * (qtys[r.id] || 0), 0); const totalPieces = recipes.reduce((s, r) => s + (qtys[r.id] || 0), 0); const hasQty = recipes.some(r => (qtys[r.id] || 0) > 0); const press = (targetId) => (k) => { const cur = String(qtys[targetId] || 0); let next = cur; if (k === 'C') next = '0'; else if (k === '⌫') next = cur.slice(0, -1) || '0'; else if (/\d/.test(k)) next = (cur === '0' ? k : cur + k); const n = parseInt(next, 10) || 0; setQty(targetId, Math.min(999, n)); }; const [focused, setFocused] = useStateS(main.id); const titleStr = recipes.length > 1 ? `${main.name} + ${recipes.length - 1}款` : main.name; return ( <> } />
今天要做幾顆?{recipes.length > 1 && '(不需要的就放0)'}
{recipes.map((r, idx) => (
setFocused(r.id)} style={{ outline: focused === r.id ? `2px solid ${theme.accent}` : 'none', outlineOffset: 3, borderRadius: theme.cardRadius, marginBottom: 14, }}>
{r.emoji}
{r.name}
{r.absolute ? `1倍 ≈ ${r.batchUnit}g` : `每顆 ${r.perPieceFlour}g 粉${idx > 0 ? ' · 合併打' : ''}`}
setQty(r.id, v)} suffix={r.absolute ? '倍' : '顆'} />
))} {/* Numpad (acts on focused) */}
數字鍵盤 · 輸入 {recipes.find(r => r.id === focused)?.name}
{/* Summary tile */} {hasQty && !main.absolute && (
總粉量
{Math.round(totalFlour)}g
總顆數
{totalPieces}
)}
開始秤量
); } // ────────────────────────────────────────────────────────── // WEIGH — checklist // ────────────────────────────────────────────────────────── function WeighScreen({ theme, recipes, qtys, merged, checked, toggle, onBack, onDone }) { const allItems = merged.sections.flatMap(s => s.items.map(i => ({ ...i, section: s.title }))); const doneCount = allItems.filter(i => checked[i.section + '|' + i.name]).length; const total = allItems.length; const pct = total ? Math.round(doneCount / total * 100) : 0; const titleStr = recipes.map(r => `${r.name} ${qtys[r.id] || 0}${r.absolute ? '倍' : '顆'}`).join(' + '); return ( <> } /> {/* progress */}
已秤 {doneCount} / {total}
{pct}%
{merged.sections.map(section => (
{section.title} {section.totalLabel && ( {section.totalLabel} {Math.round(section.total)}g )}
{section.items.map((item, idx) => { const key = section.title + '|' + item.name; const isChecked = !!checked[key]; return ( ); })}
))} {/* breakdown footer */} {merged.recipeBreakdown && merged.recipeBreakdown.length > 1 && (
分割參考
{merged.recipeBreakdown.map(({ r, n }) => n > 0 && (
{r.name} × {n} {r.splitWeight ? `${r.splitWeight}g / 顆` : `${r.perPieceFlour}g 粉 / 顆`}
))}
)}
{pct === 100 ? '✓ 全部秤好,回配方列表' : '完成'}
); } Object.assign(window, { HomeScreen, RecipeScreen, WeighScreen });