// Shared atoms used across screens. Magazine aesthetic. const Icon = ({ name, size = 20, stroke = 1.6 }) => { const paths = { search: 'M21 21l-4.3-4.3M10.5 18a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15z', plus: 'M12 5v14M5 12h14', x: 'M18 6 6 18M6 6l12 12', clock: 'M12 7v5l3 2M12 21a9 9 0 1 1 0-18 9 9 0 0 1 0 18z', flame: 'M8.5 14.5C8.5 17 10 19 12 19s3.5-1.5 3.5-4c0-1.5-1-2.5-2-3 0-2 .5-3.5-1.5-5 0 2-2 3-3 5-1 1.5-1 3.5 0 5z M12 19c-3 0-5.5-2.5-5.5-5.5 0-3 2-5 3.5-7C12 9 14 10 14 13', star: 'M12 3l2.6 5.7 6.4.7-4.8 4.4 1.4 6.2L12 17l-5.6 3 1.4-6.2L3 9.4l6.4-.7L12 3z', users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M22 21v-2a4 4 0 0 0-3-3.9 M16 3.1A4 4 0 0 1 16 11', bookmark: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z', print: 'M6 9V2h12v7 M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2 M6 14h12v8H6z', share: 'M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8 M16 6l-4-4-4 4 M12 2v13', chevronRight: 'M9 18l6-6-6-6', chevronDown: 'M6 9l6 6 6-6', chevronLeft: 'M15 18l-6-6 6-6', sparkles: 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z M19 14l.7 2.1L22 17l-2.3.9L19 20l-.7-2.1L16 17l2.3-.9L19 14z', mic: 'M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z M19 11a7 7 0 0 1-14 0 M12 18v4 M8 22h8', filter: 'M3 5h18 M6 12h12 M10 19h4', grid: 'M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z', fridge: 'M5 3h14a1 1 0 0 1 1 1v17a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z M4 10h16 M8 6v2 M8 14v3', cart: 'M3 3h2l2.4 11.4a2 2 0 0 0 2 1.6h7.7a2 2 0 0 0 2-1.6L21 7H6 M9 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2z M19 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2z', calendar: 'M3 6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6z M3 10h18 M8 2v4 M16 2v4', check: 'M5 12l5 5L20 7', play: 'M5 3l14 9-14 9V3z', menu: 'M3 6h18 M3 12h18 M3 18h18', arrowRight: 'M5 12h14 M13 5l7 7-7 7', }; return ( {paths[name].split(' M').map((d, i) => ( ))} ); }; // Magazine sticky header const Header = ({ active = 'home', onNav = () => {}, mobile = false }) => { const items = [ { id: 'home', label: '食材搜索' }, { id: 'recipes', label: '所有食譜' }, { id: 'category', label: '分類' }, { id: 'mealplan', label: '膳食計劃' }, { id: 'about', label: '關於' }, ]; if (mobile) { return (
onNav('home')}>AIRecipeLive
); } return (
onNav('home')}> AIRecipeLive 試刊號 · 01
); }; // Match-degree badge const MatchBadge = ({ pct }) => { const cls = pct >= 100 ? 'match--full' : pct >= 80 ? 'match--mid' : 'match--low'; const label = pct >= 100 ? '完美匹配' : pct >= 80 ? `匹配 ${pct}%` : `匹配 ${pct}%`; return {label}; }; // Recipe card — editorial 16:9 const RecipeCard = ({ r, showMatch = false, size = 'm', onClick }) => { return (
{r.title} {showMatch && } {r.cat} {r.editor === 'pick' && EDITOR'S PICK}

{r.title}

{size !== 's' &&

{r.subtitle}

}
{r.time} 分鐘 {r.cal} 卡 {r.rating}
{showMatch && r.miss && (
缺:{r.miss.join('、')} {r.sub && · 可替代}
)}
); }; // Ingredient tag input — the centerpiece interaction const TagInput = ({ tags, setTags, max = 4, placeholder = '輸入食材,按 Enter 加入…' }) => { const [v, setV] = React.useState(''); const [focus, setFocus] = React.useState(false); const ref = React.useRef(null); const suggestions = React.useMemo(() => { if (!v) return []; const k = v[0]; const list = window.SUGGEST[k] || []; return list.filter(s => !tags.includes(s) && s.includes(v)).slice(0, 5); }, [v, tags]); const add = (t) => { if (!t || tags.length >= max || tags.includes(t)) return; setTags([...tags, t]); setV(''); }; const remove = (t) => setTags(tags.filter(x => x !== t)); return (
{tags.map(t => ( {t} ))} setV(e.target.value)} onFocus={() => setFocus(true)} onBlur={() => setTimeout(() => setFocus(false), 150)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); add(v.trim()); } if (e.key === 'Backspace' && !v && tags.length) remove(tags[tags.length - 1]); }} placeholder={tags.length === 0 ? placeholder : ''} disabled={tags.length >= max} />
{tags.length}/{max}
{focus && suggestions.length > 0 && (
{suggestions.map(s => ( ))}
)}
); }; const Eyebrow = ({ children }) => {children}; const Divider = ({ label }) => (
{label && {label}}
); Object.assign(window, { Icon, Header, MatchBadge, RecipeCard, TagInput, Eyebrow, Divider });