diff --git a/src/app.js b/src/app.js index 36dca93..064fc5f 100644 --- a/src/app.js +++ b/src/app.js @@ -258,7 +258,7 @@ function addEvent(type, note, at, photoId, weight) { const events = loadAll(); const now = Date.now(); - events.push({ + const ev = { id: uuid(), type, at: Number.isFinite(at) ? at : now, @@ -266,10 +266,12 @@ photoId: photoId || "", weight: Number.isFinite(weight) ? weight : undefined, updatedAt: now, - }); + }; + events.push(ev); saveAll(events); scheduleSync(); render(); + return ev; } function updateEvent(id, patch) { @@ -1602,9 +1604,59 @@ } }); + // ---------- quick-log snackbar ---------- + // A single tap on a quick-action logs the event at the current instant, then + // shows a brief snackbar to undo it or add detail — so the two-tap dialog flow + // is never required for a plain pee/poo/meal/sleep boundary. Weigh-ins still + // open the dialog since they need a value. + const snackbar = document.getElementById("snackbar"); + const snackbarMsg = document.getElementById("snackbar-msg"); + const snackbarUndo = document.getElementById("snackbar-undo"); + const snackbarNote = document.getElementById("snackbar-note"); + let snackbarEvent = null; + let snackbarTimer = null; + + function hideSnackbar() { + clearTimeout(snackbarTimer); + snackbarTimer = null; + snackbarEvent = null; + snackbar.classList.remove("show"); + snackbar.hidden = true; + } + + function showSnackbar(msg, ev) { + snackbarEvent = ev; + snackbarMsg.textContent = msg; + snackbar.hidden = false; + void snackbar.offsetWidth; // reflow so the fade-in transition runs + snackbar.classList.add("show"); + clearTimeout(snackbarTimer); + snackbarTimer = setTimeout(hideSnackbar, 5000); + } + + function quickLog(type) { + const ev = addEvent(type, "", Date.now()); + showSnackbar(`${EVENT_LABELS[type]} logged`, ev); + } + + snackbarUndo.addEventListener("click", () => { + if (snackbarEvent) deleteEvent(snackbarEvent.id); + hideSnackbar(); + }); + snackbarNote.addEventListener("click", () => { + const ev = snackbarEvent; + hideSnackbar(); + if (ev) openEditDialog(ev); + }); + // ---------- wiring ---------- document.querySelectorAll("button.action").forEach(btn => { - btn.addEventListener("click", () => openNoteDialog(btn.dataset.type)); + btn.addEventListener("click", () => { + const type = btn.dataset.type; + // Weigh-ins need a typed value, so they keep the full dialog. + if (type === "weight") { openNoteDialog(type); return; } + quickLog(type); + }); }); dayPicker.value = ymd(new Date()); diff --git a/src/index.html b/src/index.html index 8b89353..445b6c3 100644 --- a/src/index.html +++ b/src/index.html @@ -303,6 +303,13 @@ + + + diff --git a/src/style.css b/src/style.css index 9ab0578..e0d2cb9 100644 --- a/src/style.css +++ b/src/style.css @@ -696,3 +696,46 @@ input.switch:checked::after { transform: translateX(18px); } border: 1px solid rgba(255, 255, 255, 0.6); } .update-banner-btn:hover { filter: brightness(0.97); } + +/* ---------- quick-log snackbar ---------- */ +.snackbar { + position: fixed; + left: 50%; + bottom: calc(16px + env(safe-area-inset-bottom, 0)); + transform: translate(-50%, 12px); + z-index: 60; + display: flex; + align-items: center; + gap: 8px; + max-width: calc(100% - 32px); + padding: 8px 8px 8px 16px; + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); + border-radius: 999px; + box-shadow: var(--shadow); + opacity: 0; + pointer-events: none; + transition: opacity 0.18s ease, transform 0.18s ease; +} +.snackbar[hidden] { display: none; } +.snackbar.show { + opacity: 1; + transform: translate(-50%, 0); + pointer-events: auto; +} +.snackbar-msg { + font-size: 0.9rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.snackbar-action { + background: transparent; + color: var(--accent); + font-weight: 700; + padding: 6px 10px; + flex-shrink: 0; +} +.snackbar-action:hover { filter: none; background: var(--accent-soft); }