// app/screen-settings.jsx — Avatar ▸ Settings. Unit system, default planning
// servings, and the ingredient-price DB (the prices the cost engine uses).
const setLabel = { fontFamily: 'Bricolage Grotesque', fontSize: 10, fontWeight: 600,
letterSpacing: '0.24em', textTransform: 'uppercase', color: 'var(--ink-3)' };
const setInput = { width: '100%', padding: '11px 13px', borderRadius: 9, background: 'rgba(0,0,0,0.3)',
border: '1px solid var(--hairline)', color: 'var(--ink)', fontFamily: 'Bricolage Grotesque',
fontSize: 14, outline: 'none', boxSizing: 'border-box' };
function Segmented({ options, value, onChange }) {
return (
{options.map(([v, label]) => {
const on = v === value;
return (
);
})}
);
}
function SettingsCard({ children, style }) {
return {children}
;
}
function SettingsRow({ title, sub, children }) {
return (
);
}
const blankItem = () => ({ key: '', name: '', unit: 'g', packSize: '', packCost: '', isStaple: false, aliases: '', source: 'You' });
function IngredientEditor({ item, onSave, onCancel, isMobile }) {
const [f, setF] = React.useState(() => ({
...item, packSize: item.packSize ?? '', packCost: item.packCost ?? '',
aliases: Array.isArray(item.aliases) ? item.aliases.join(', ') : (item.aliases || '') }));
const upd = (k, v) => setF((p) => ({ ...p, [k]: v }));
const valid = f.name.trim() && Number(f.packSize) > 0 && Number(f.packCost) >= 0;
const save = () => onSave({
key: f.key || undefined, name: f.name.trim(), unit: f.unit,
packSize: Number(f.packSize), packCost: Number(f.packCost), isStaple: !!f.isStaple,
aliases: f.aliases.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
source: f.source || 'You' });
return (
upd('aliases', e.target.value)}
placeholder="olive oil, evoo" style={{ ...setInput, marginTop: 6 }} />
{!isMobile && }
{Number(f.packSize) > 0 && Number(f.packCost) >= 0 && (
= {window.LCDomain.fmtMoney(Number(f.packCost) / Number(f.packSize))} / {f.unit}
)}
Save
Cancel
);
}
function IngredientRow({ item, onEdit, onDelete, selected, onToggleSelect, isMobile }) {
const unitCost = item.packSize ? item.packCost / item.packSize : (item.unitCost || 0);
const checkbox = (
);
const nameLine = (
{item.name}
{item.isStaple && staple}
);
const sub = (
{window.LCDomain.fmtMoney(item.packCost)} / {item.packSize}{item.unit} · {item.source || 'AI'}{item.date ? ` · ${item.date}` : ''}
);
const unit = (
{window.LCDomain.fmtMoney(unitCost)}/{item.unit}
);
const editBtn = (
);
const delBtn = (
);
if (isMobile) {
return (
{checkbox}
{nameLine}
{unit}
);
}
return (
{checkbox}
{nameLine}{sub}
{unit}{editBtn}{delBtn}
);
}
function MergeDialog({ items, onConfirm, onClose, isMobile }) {
const [primaryKey, setPrimaryKey] = React.useState(items[0] ? items[0].key : null);
return (
e.stopPropagation()} style={{ width: '100%', maxWidth: isMobile ? '100%' : 460,
maxHeight: isMobile ? '92%' : '90%', overflowY: 'auto',
background: 'rgba(20,20,24,0.98)', border: '1px solid var(--hairline)',
borderRadius: isMobile ? '22px 22px 0 0' : 20,
boxShadow: '0 40px 100px rgba(0,0,0,0.7)', padding: isMobile ? '22px 20px 28px' : 26, boxSizing: 'border-box' }}>
Merge {items.length} ingredients
Keep one — fold the rest in.
The others' names and aliases become aliases of the one you keep, then they're removed. Recipes re-match to the kept item's price.
{items.map((it) => {
const on = it.key === primaryKey;
const unitCost = it.packSize ? it.packCost / it.packSize : (it.unitCost || 0);
return (
);
})}
Cancel
onConfirm(primaryKey)} style={isMobile ? { flex: 1.4 } : {}}>Merge into one
);
}
function SettingsScreen() {
const { layout, back, settings, setSettings, ingredientCosts, saveIngredientCost, deleteIngredientCost } = useApp();
const isMobile = layout === 'mobile';
const [q, setQ] = React.useState('');
const [editing, setEditing] = React.useState(null); // key being edited, or '__new__'
const [selected, setSelected] = React.useState(() => new Set());
const [merging, setMerging] = React.useState(false);
const list = (ingredientCosts || []).filter((it) =>
!q || (it.name + ' ' + (it.aliases || []).join(' ')).toLowerCase().includes(q.toLowerCase()))
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
const onSave = async (item) => { await saveIngredientCost(item); setEditing(null); };
const toggleSel = (key) => setSelected((s) => { const n = new Set(s); n.has(key) ? n.delete(key) : n.add(key); return n; });
const clearSel = () => setSelected(new Set());
const selectedItems = (ingredientCosts || []).filter((it) => selected.has(it.key));
const doMerge = async (primaryKey) => {
const primary = selectedItems.find((i) => i.key === primaryKey);
if (!primary) { setMerging(false); return; }
const others = selectedItems.filter((i) => i.key !== primaryKey);
const aliasSet = new Set((primary.aliases || []).map((a) => a.toLowerCase()));
others.forEach((o) => {
(o.aliases || []).forEach((a) => aliasSet.add(a.toLowerCase()));
if (o.name) aliasSet.add(o.name.toLowerCase());
});
await saveIngredientCost({ ...primary, aliases: [...aliasSet] });
for (const o of others) { await deleteIngredientCost(o.key); } // eslint-disable-line no-await-in-loop
clearSel(); setMerging(false);
};
const pad = isMobile ? '24px 18px 60px' : '40px 48px 80px';
return (
Settings
Settings.
{/* Preferences */}
Preferences
setSettings({ unitSystem: v })} />
setSettings({ defaultPlanningServings: n })} />
{/* Ingredient prices */}
Ingredient prices
The prices the cost estimator uses · {(ingredientCosts || []).length} items
setEditing('__new__')}
style={isMobile ? { width: '100%' } : {}}>Add ingredient
{editing === '__new__' && (
setEditing(null)} isMobile={isMobile} />
)}
setQ(e.target.value)} placeholder="search ingredients"
style={{ border: 'none', background: 'transparent', outline: 'none', flex: 1,
fontFamily: 'Bricolage Grotesque', fontSize: 14, color: 'var(--ink)' }} />
{selected.size > 0 && (
{selected.size} selected
{selected.size < 2 ? 'pick one more to merge' : 'merge duplicates into one'}
setMerging(true)}
style={selected.size < 2 ? { opacity: 0.5, pointerEvents: 'none' } : {}}>Merge…
)}
{list.length === 0 && (
No ingredients match “{q}”.
)}
{list.map((it) => editing === it.key ? (
setEditing(null)} isMobile={isMobile} />
) : (
toggleSel(it.key)}
onEdit={() => setEditing(it.key)} onDelete={() => deleteIngredientCost(it.key)} />
))}
{merging && setMerging(false)} isMobile={isMobile} />}
);
}
Object.assign(window, { SettingsScreen });