v0.1.29 - Mix Editor % edits no longer rebalance other ingredients

Editing one ingredient's % now converts only that row to kg against the
Total mix anchor; other ingredients are left untouched (no proportional
redistribution). Removing a row likewise leaves the rest as-is. The hard
"percentages must total 100%" save guard is relaxed (kg is canonical and
the backend does not require 100%); the % chip remains as live feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 13:49:15 +12:00
co-authored by Claude Opus 4.8
parent 1dd48bc771
commit e7a7b11589
4 changed files with 18 additions and 55 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "hunter-app",
"version": "0.1.28",
"version": "0.1.29",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hunter-app",
"version": "0.1.28",
"version": "0.1.29",
"dependencies": {
"@fontsource/inter": "^5.2.8",
"lucide-svelte": "^1.0.1"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hunter-app",
"version": "0.1.28",
"version": "0.1.29",
"private": true,
"type": "module",
"scripts": {
+14 -51
View File
@@ -103,57 +103,24 @@
ingredientBaseline = ingredientDrafts.map((row) => ({ ...row }));
}
// kg overrides %: recompute the reference total from the kg column, then
// re-derive every row's percentage so they always sum to 100.
function applyKgEdit() {
const total = ingredientDrafts.reduce((sum, row) => sum + Number(row.quantity_kg || 0), 0);
totalReference = round4(total);
ingredientDrafts = ingredientDrafts.map((row) => ({
...row,
percentage: total > 0 ? round4((Number(row.quantity_kg || 0) / total) * 100) : 0
}));
}
// Editing a row's % sets it to that share of the mix and spreads the rest
// across the other rows in their existing proportions, so the column always
// re-totals 100 and kg stays the canonical value. The total mix kg is held
// constant; only the split moves. Without this rebalance a single % edit
// would leave the total off 100 and the save guard would block the change.
// Editing a row's % converts only that row to kilograms against the Total mix
// anchor. Every other ingredient is left exactly as it is — changing one
// ingredient's share never rebalances or recalculates the rest of the recipe.
// Percentages are free to sum to anything; the chip reports the running total
// and kg stays the canonical saved value.
function applyPercentEdit(index: number) {
const total = Number(totalReference || 0);
const rows = ingredientDrafts;
const count = rows.length;
// Clamp to a sane 0100 share.
let target = Number(rows[index].percentage || 0);
// Clamp to a sane 0100 share for the edited row only.
let target = Number(ingredientDrafts[index].percentage || 0);
if (!Number.isFinite(target) || target < 0) target = 0;
if (target > 100) target = 100;
// A single ingredient is always the whole mix.
if (count === 1) {
ingredientDrafts = rows.map((row) => ({
...row,
percentage: 100,
quantity_kg: round4(total)
}));
return;
}
const remaining = 100 - target;
const otherOldSum = rows.reduce(
(sum, row, rowIndex) => (rowIndex === index ? sum : sum + Number(row.percentage || 0)),
0
ingredientDrafts = ingredientDrafts.map((row, rowIndex) =>
rowIndex === index
? { ...row, percentage: round4(target), quantity_kg: round4((target / 100) * total) }
: row
);
ingredientDrafts = rows.map((row, rowIndex) => {
if (rowIndex === index) {
return { ...row, percentage: round4(target), quantity_kg: round4((target / 100) * total) };
}
// Keep the other rows' relative split; if they were all at 0, share evenly.
const share =
otherOldSum > 0 ? (Number(row.percentage || 0) / otherOldSum) * remaining : remaining / (count - 1);
return { ...row, percentage: round4(share), quantity_kg: round4((share / 100) * total) };
});
}
// Editing the Total mix (kg) rescales every row's kg from its current %.
@@ -293,9 +260,10 @@
}
function removeIngredient(index: number) {
// Drop the row and leave the remaining ingredients' % and kg exactly as they
// are — removing one ingredient never recalculates the others.
ingredientDrafts = ingredientDrafts.filter((_, rowIndex) => rowIndex !== index);
if (!ingredientDrafts.length) ingredientDrafts = [emptyIngredient()];
applyKgEdit();
}
function ingredientWarnings() {
@@ -312,11 +280,6 @@
if (Number(row.quantity_kg) <= 0) return [`Ingredient row ${index + 1} needs a quantity greater than zero.`];
}
// Percentages must add up to 100 before a change can be saved.
if (Math.abs(percentTotal - 100) > 0.1) {
return [`Percentages must total 100% (currently ${percentTotal.toFixed(2)}%).`];
}
return [];
}
@@ -687,7 +650,7 @@
<div class="ingredient-footer">
<span class="footer-total">Total {ingredientTotalKg.toFixed(2)} kg</span>
<button class="clear-button" type="button" onclick={addIngredient}>Add ingredient</button>
<button class="apply-button" type="button" disabled={savingKey === `mix-save:${row.id}` || !percentBalanced} onclick={saveIngredients}>
<button class="apply-button" type="button" disabled={savingKey === `mix-save:${row.id}`} onclick={saveIngredients}>
{savingKey === `mix-save:${row.id}` ? 'Saving...' : 'Save ingredients'}
</button>
</div>