// 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 (
);
}
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
)}
>
);
}
// ──────────────────────────────────────────────────────────
// 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 });