diff --git a/public/app.css b/public/app.css index 4a082a1..b0dd01b 100644 --- a/public/app.css +++ b/public/app.css @@ -1706,6 +1706,11 @@ body.show-fids .field[data-fid]::after { display: grid; gap: 8px; } +.plan-list-row { + display: flex; + align-items: stretch; + gap: 6px; +} .plan-list-item { width: 100%; display: grid; @@ -1719,6 +1724,10 @@ body.show-fids .field[data-fid]::after { font: inherit; cursor: pointer; } +.plan-list-delete { + flex-shrink: 0; + align-self: center; +} .plan-list-item:hover, .plan-list-item.active { border-color: var(--ember); @@ -2603,7 +2612,8 @@ body.show-fids .field[data-fid]::after { flex: 1 1 auto; justify-content: center; } - .header-actions #btn-print { + .header-actions #btn-print, + .header-actions #btn-expand-why { flex: 0 0 auto; min-height: 44px; padding: 8px 16px; diff --git a/public/cupping.html b/public/cupping.html index d8c49f3..e6ec5b2 100644 --- a/public/cupping.html +++ b/public/cupping.html @@ -88,6 +88,9 @@ >Not saved yet + @@ -160,7 +163,7 @@
-
+
Why the scale starts at 6

The 6–10 range is a specialty-grading convention: @@ -186,7 +189,7 @@

Flavors

-
+
Tag what you can point at

A tag is only useful if you could defend it to @@ -206,7 +209,7 @@

Notes

-
+
Notes outlive scores

Numbers compare roasts; sentences explain them. Write diff --git a/public/index.html b/public/index.html index 4b05dd1..baa0230 100644 --- a/public/index.html +++ b/public/index.html @@ -129,6 +129,9 @@ >Not saved yet + @@ -372,7 +375,7 @@

The Coffee

-
+
Why cultivar sets the clock

A roast plan is three durations laid end to end: drying + @@ -543,7 +546,7 @@

-
+
Why blends combine times, never temperatures

The drum has one heat setting and one clock — you cannot @@ -615,7 +618,7 @@

Roast Target

-
+
Why processing moves development, not first crack

Processing changed the chemistry that browning starts @@ -743,7 +746,7 @@

Bean Condition

-
+
Why bean condition is worth seconds, not a new plan

Moisture, density and screen size change how much energy the @@ -826,7 +829,7 @@

Machine Plan

-
+
Why the rate of rise must fall, and never touch zero @@ -994,7 +997,7 @@ during the roast
-
+
Record what you observed, never what you planned

A planned milestone written into the log as though it @@ -1136,7 +1139,7 @@ after cupping

-
+
One change per batch — arithmetic, not patience

Move two variables and there are four possible diff --git a/public/js/main.js b/public/js/main.js index dd532b9..372c107 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -16,10 +16,11 @@ import { initPrefillPanel } from "./prefill-ui.js?v=__ASSET_VERSION__"; import { initAlogPanel } from "./alog-ui.js?v=__ASSET_VERSION__"; import { initPrint } from "./print.js?v=__ASSET_VERSION__"; import { initLotPicker } from "./lot-picker.js?v=__ASSET_VERSION__"; -import { protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__"; +import { api, protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__"; import { initSideNav } from "./nav.js?v=__ASSET_VERSION__"; import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__"; import { initFieldHelp } from "./field-help.js?v=__ASSET_VERSION__"; +import { showToast } from "./toast.js?v=__ASSET_VERSION__"; const FIELD_ID_SET = new Set(FIELD_IDS); const STORAGE_PREFIX = "roastPlannerPlan.v2"; @@ -823,6 +824,7 @@ async function loadPlans() { list.replaceChildren( ...plans.map((plan) => { const item = document.createElement("li"); + item.className = "plan-list-row"; const button = document.createElement("button"); button.type = "button"; button.className = "plan-list-item"; @@ -833,7 +835,20 @@ async function loadPlans() { updated.textContent = `Updated ${new Date(plan.updated_at).toLocaleDateString()}`; button.append(title, updated); button.addEventListener("click", () => selectPlan(plan)); - item.append(button); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "icon-btn plan-list-delete"; + deleteBtn.setAttribute("aria-label", `Delete ${title.textContent}`); + deleteBtn.textContent = "✕"; + // A sibling of the select button, not nested inside it — stopPropagation alone + // wouldn't be enough to keep a click here from also selecting the plan otherwise. + deleteBtn.addEventListener("click", (event) => { + event.stopPropagation(); + deletePlan(plan); + }); + + item.append(button, deleteBtn); return item; }), ); @@ -860,10 +875,9 @@ async function selectPlan(plan) { lotPicker?.refreshRefineSuggestion(); document.querySelector("[data-close-drawer]")?.click(); } -async function newPlan() { - if (!confirm("Start a new plan? Your current plan is already saved locally.")) - return; - if (!(await flushCurrentPlan())) return; +// Shared by newPlan() and deletePlan() (when the deleted plan was the one on screen) — resets +// the whole app to a fresh, unsynced plan. +function resetToBlankPlan() { remotePlanId = null; draftSyncedAt = null; state.plan = blankPlan(); @@ -875,6 +889,28 @@ async function newPlan() { lotPicker?.renderOptions(); lotPicker?.refreshRefineSuggestion(); } +async function newPlan() { + if (!confirm("Start a new plan? Your current plan is already saved locally.")) + return; + if (!(await flushCurrentPlan())) return; + resetToBlankPlan(); + // Without this, the Plans drawer stays open over the now-blank form — the reset happens but + // is invisible, so it doesn't read as "starting something new" until the user closes it themselves. + document.querySelector("[data-close-drawer]")?.click(); +} +async function deletePlan(plan) { + const name = plan.plan?.fields?.["0.1"] || "Untitled plan"; + if (!confirm(`Delete "${name}"? This cannot be undone.`)) return; + try { + await api(`/api/plans/${plan.id}`, { method: "DELETE" }); + } catch (error) { + showToast(error.message, "fail"); + return; + } + showToast("Plan deleted."); + if (plan.id === remotePlanId) resetToBlankPlan(); + await loadPlans(); +} function wireToolbar() { document.getElementById("btn-toggle-fids").addEventListener("click", (e) => { diff --git a/public/js/why-panels.js b/public/js/why-panels.js index 5838f72..379c80d 100644 --- a/public/js/why-panels.js +++ b/public/js/why-panels.js @@ -1,25 +1,42 @@ // Shared teaching-layer collapse memory for every page that uses `.why-panel` (`

` // with a `data-why` id): the planner and the cupping session view. One localStorage key across -// both so a user's dismissals carry over between pages. -const WHY_COLLAPSED_KEY = "roastPlannerWhyCollapsed.v1"; +// both so a user's choices carry over between pages. +// +// Panels are collapsed by default (see index.html/cupping.html) so the form isn't wall-to-wall +// explanatory text on first load; this tracks which ones a user has explicitly opened, rather +// than which they've closed, so it stays meaningful against a closed-by-default baseline. +const WHY_EXPANDED_KEY = "roastPlannerWhyExpanded.v1"; export function wireWhyPanels() { - let collapsed; + let expanded; try { - collapsed = new Set(JSON.parse(localStorage.getItem(WHY_COLLAPSED_KEY)) || []); + expanded = new Set(JSON.parse(localStorage.getItem(WHY_EXPANDED_KEY)) || []); } catch { - collapsed = new Set(); + expanded = new Set(); } - for (const panel of document.querySelectorAll(".why-panel")) { - if (collapsed.has(panel.dataset.why)) panel.open = false; + const persist = () => { + try { + localStorage.setItem(WHY_EXPANDED_KEY, JSON.stringify([...expanded])); + } catch { + /* unavailable storage */ + } + }; + + const panels = [...document.querySelectorAll(".why-panel")]; + for (const panel of panels) { + if (expanded.has(panel.dataset.why)) panel.open = true; panel.addEventListener("toggle", () => { - if (panel.open) collapsed.delete(panel.dataset.why); - else collapsed.add(panel.dataset.why); - try { - localStorage.setItem(WHY_COLLAPSED_KEY, JSON.stringify([...collapsed])); - } catch { - /* unavailable storage */ - } + if (panel.open) expanded.add(panel.dataset.why); + else expanded.delete(panel.dataset.why); + persist(); }); } + + document.getElementById("btn-expand-why")?.addEventListener("click", () => { + for (const panel of panels) { + panel.open = true; + expanded.add(panel.dataset.why); + } + persist(); + }); }