diff --git a/.beans/vibed-6uyy--bit-by-bit-backfill-missed-days-via-a-day-navigato.md b/.beans/vibed-6uyy--bit-by-bit-backfill-missed-days-via-a-day-navigato.md new file mode 100644 index 0000000..73e0f61 --- /dev/null +++ b/.beans/vibed-6uyy--bit-by-bit-backfill-missed-days-via-a-day-navigato.md @@ -0,0 +1,10 @@ +--- +# vibed-6uyy +title: 'bit-by-bit: backfill missed days via a day navigator on the Today card' +status: completed +type: feature +created_at: 2026-07-18T17:26:52Z +updated_at: 2026-07-18T17:26:52Z +--- + +Let users step back to an earlier day and check off habits they forgot, so a missed day no longer permanently breaks a streak. The Today card became a day navigator (‹/› arrows + a 'back to today' link); editable range is the active image's start through today. Reveal is derived from the whole log with each habit's streak counted backwards from its own day, so backfilling a missed day automatically re-bridges the streak and paints the pixels it earns. Updated index.html (selectedDay state, clampSelectedDay, nav wiring, per-day streak display, About copy), DESIGN.md (§2, §3.1, §9), and ARCHITECTURE.md. Validated with Playwright: backfilling one gap day bridged the streak and grew reveal 4→10 pixels, nav bounds and back-to-today work, no console errors or non-file requests. diff --git a/.beans/vibed-qk1k--bit-by-bit-changelog-tab-in-settings.md b/.beans/vibed-qk1k--bit-by-bit-changelog-tab-in-settings.md new file mode 100644 index 0000000..9217d5c --- /dev/null +++ b/.beans/vibed-qk1k--bit-by-bit-changelog-tab-in-settings.md @@ -0,0 +1,10 @@ +--- +# vibed-qk1k +title: 'bit-by-bit: Changelog tab in Settings' +status: completed +type: feature +created_at: 2026-07-18T17:26:57Z +updated_at: 2026-07-18T17:26:57Z +--- + +Added a Changelog tab to the Settings modal (after About) listing released changes as dated entries, one concise sentence each, newest first. Content is distilled from the project's beans issues (release dates from each bean's completion). Implemented as a CHANGELOG data array + renderChangelog() rendering into #panel-changelog, with styling matching the app. Updated DESIGN.md §2 settings sections list. Validated with Playwright + screenshot: tab switches, four date groups render, no console errors or non-file requests. diff --git a/bit-by-bit/docs/ARCHITECTURE.md b/bit-by-bit/docs/ARCHITECTURE.md index 82d01c6..cc9456f 100644 --- a/bit-by-bit/docs/ARCHITECTURE.md +++ b/bit-by-bit/docs/ARCHITECTURE.md @@ -5,10 +5,10 @@ Everything lives in one file, `index.html`, organized into sections: | Component | Responsibility | |---|---| | **State** | Single JSON document under localStorage key `bitbybit.v1`; load/save with corruption guards; versioned for future migration. | -| **Mechanics** | Habit check/uncheck logging, daily rollover, per-habit streak math, seeded (mulberry32) reveal-order permutation, and log-derived reveal: each habit reveals pixels equal to its own current streak length (1 the first day, 2 the next consecutive day, and so on), independent of any other habit. | +| **Mechanics** | Habit check/uncheck logging for the selected day (today or a backfilled past day, bounded by the active image's start), daily rollover, per-habit streak math, seeded (mulberry32) reveal-order permutation, and log-derived reveal: each habit reveals pixels equal to its own current streak length (1 the first day, 2 the next consecutive day, and so on), independent of any other habit. Because reveal recomputes from the whole log, backfilling a missed day re-bridges that habit's streak automatically. | | **Artwork** | Preset pixel-art definitions (palette + row strings); upload pipeline (offscreen canvas downsample → pixel grid); gallery of finished pieces. | | **Rendering** | Canvas renderer for the artwork (revealed pixels, checkerboard for hidden, pop-in animation for deltas); DOM renderer for checklist, stats, settings. | -| **UI wiring** | Onboarding, settings dialog (habits / artwork / data / about), theme cycling (auto→light→dark via `data-theme` + CSS custom properties), export/import/wipe, confetti celebration, and a page footer disclosing authorship. | +| **UI wiring** | Onboarding, day navigator on the Today card (backfill past days), settings dialog (habits / artwork / data / about / changelog), theme cycling (auto→light→dark via `data-theme` + CSS custom properties), export/import/wipe, confetti celebration, and a page footer disclosing authorship. | Data flows one way: user action → mutate state → persist → re-render. The reveal permutation is derived from a stored seed, never persisted, so exports diff --git a/bit-by-bit/docs/DESIGN.md b/bit-by-bit/docs/DESIGN.md index a31302c..4c60586 100644 --- a/bit-by-bit/docs/DESIGN.md +++ b/bit-by-bit/docs/DESIGN.md @@ -33,7 +33,7 @@ stay revealed. │ progress: ▓▓▓░░ 42% │ progress bar + "142 / 340 pixels" │ │ ├──────────────────────────────┤ -│ Today · Sunday, July 13 │ +│ ‹ Sunday, July 13 › │ day navigator: step back to fix a missed day │ ┌──────────────────────┐ │ │ │ 💧 Drink water 🔥5[✓]│ │ today's habit checklist — each row shows │ │ 🏃 Go for a run [ ] │ │ its own streak; big, thumb-friendly rows @@ -47,8 +47,9 @@ stay revealed. - Max content width ~28rem, centered; scales up gracefully on desktop. - Settings (⚙️) opens a panel/modal with sections: **Habits**, **Artwork**, - **Data** (export/import plus a destructive "wipe all data" action), **About**. - Use `` or an equivalent overlay. + **Data** (export/import plus a destructive "wipe all data" action), **About**, + and **Changelog** (a dated list of released changes, one concise sentence each, + newest first). Use `` or an equivalent overlay. - A small page **footer** discloses authorship ("Written with Claude Code · Sonnet 5, Opus 4.8 & Fable 5"), plain text with no external link. - First visit (no saved state): a friendly 2-step onboarding — (1) add habits, @@ -62,8 +63,14 @@ stay revealed. emoji (offer a small emoji picker of ~24 curated options; default assigned cyclically if skipped). - Habits reset daily (local timezone). Checking a habit today logs it for today. -- Unchecking is allowed **same-day only** (undo mistaken taps). Past days are - immutable. +- **Backfilling past days:** the Today card is a day navigator (‹ / › arrows plus + a "back to today" link) so a user who forgot to check off on the day can step + back and fix it. Any day from the **active image's start** through **today** is + editable (checking or unchecking); you cannot edit days before the current + picture existed (they can't paint pixels) or into the future. Because reveal is + derived from the log with each habit's streak counted *backwards* from its own + day, backfilling a missed day automatically re-bridges the streak and paints + the pixels it earns — no separate reconciliation needed. - Habits can be added/renamed/deleted in settings at any time. Deleting a habit keeps its historical log entries (they still count toward revealed pixels). @@ -219,9 +226,12 @@ Single localStorage key: `bitbybit.v1`. Entire state is one JSON document: ## 9. Edge Cases (must handle) 1. First run / empty state → onboarding. -2. Same-day uncheck only shrinks that habit's contribution for today; - previous days' revealed pixels are never lost (reveal is derived from the log, - and each habit's streak counts backwards from the day it's evaluated on). +2. Editing any day (today or a backfilled past day within the active image's + range) recomputes reveal from the whole log: checking a missed day re-bridges + the affected habit's streak and paints the pixels it earns, unchecking removes + exactly those pixels. Because each habit's streak counts backwards from the + day it's evaluated on, editing one day never corrupts an unrelated day's + contribution. Days before the image's start or in the future are not editable. 3. Habit deleted after being checked today → today's log keeps the id; its streak/counts keep working off the log id even though it no longer appears in the habit list or UI. diff --git a/bit-by-bit/index.html b/bit-by-bit/index.html index 92e9615..bb0fcd8 100644 --- a/bit-by-bit/index.html +++ b/bit-by-bit/index.html @@ -124,8 +124,16 @@ /* ---- today card ---- */ .today-card{ background:var(--bg-elevated); border-radius:var(--radius-lg); padding:var(--space-4); box-shadow:var(--shadow); display:flex; flex-direction:column; gap:var(--space-3); } - .today-header{ display:flex; justify-content:space-between; align-items:baseline; flex-wrap:wrap; gap:var(--space-2); } - .today-label{ font-weight:700; } + .today-header{ display:flex; align-items:center; gap:var(--space-2); } + .today-label{ font-weight:700; flex:1; text-align:center; } + .today-label.past{ color:var(--accent-strong); } + .day-nav{ width:34px; height:34px; flex-shrink:0; display:flex; align-items:center; justify-content:center; + border-radius:var(--radius-md); border:1px solid var(--border); background:var(--bg-sunken); + color:var(--text); cursor:pointer; font-size:1.2rem; line-height:1; padding:0; } + .day-nav:hover{ filter:brightness(1.05); } + .day-nav:disabled{ opacity:0.3; cursor:not-allowed; filter:none; } + .today-back{ align-self:center; font-size:0.78rem; color:var(--accent-strong); background:none; border:none; + cursor:pointer; text-decoration:underline; padding:2px; } .habit-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--space-2); } .habit-row{ display:flex; align-items:center; gap:var(--space-3); border-radius:var(--radius-md); background:var(--bg-sunken); min-height:44px; transition:background .2s ease; } @@ -231,6 +239,11 @@ .gallery-tile{ text-align:center; font-size:0.68rem; color:var(--text-dim); } .gallery-tile canvas{ background:var(--canvas-bg); border-radius:var(--radius-sm); } .about-text p{ margin:0 0 var(--space-3); color:var(--text-dim); } + .changelog-entry{ margin-bottom:var(--space-4); } + .changelog-date{ font-weight:700; font-size:0.9rem; color:var(--accent-strong); margin-bottom:var(--space-2); } + .changelog-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--space-2); } + .changelog-list li{ color:var(--text-dim); font-size:0.9rem; line-height:1.5; + padding-left:var(--space-3); border-left:2px solid var(--accent-soft); } /* ---- celebration ---- */ .celebration-overlay{ position:fixed; inset:0; background:rgba(10,8,15,0.55); z-index:70; @@ -281,8 +294,11 @@
+ Today +
+
    @@ -366,6 +382,7 @@ +
    @@ -436,13 +453,21 @@ time you check it, but never erases pixels you've already earned.

    Every habit scores on its own — there's no bonus for doing them all in one day, just each one quietly climbing the longer you keep it up.

    +

    Forgot to check off yesterday? Use the ‹ / › arrows above your habit list to + step back to an earlier day and tick it off — the streak mends and the pixels you missed + paint right in.

    Everything lives only in this browser's local storage, on this device. Nothing is ever sent anywhere.

    Created by Elijah Voigt Made using Claude Code · Sonnet 5, Opus 4.8 & Fable 5 + Feedback? Email pop@elijah.run

    +
    @@ -1229,24 +1254,58 @@ } } + // The day currently shown in the Today card. Defaults to today, but the ‹/› + // nav lets the user step back to check off a day they forgot. Reveal is + // derived from the log with each habit's streak counted backwards from its + // day, so backfilling a missed day correctly re-bridges the streak and + // repaints the pixels it earns — no special-casing needed. + var selectedDay = todayStr(); + + // Earliest day the user may edit: the active image's start. Editing days + // before the current picture existed can't paint any pixels, so we don't + // allow wandering there; with no active image the only editable day is today. + function earliestEditableDay(){ + return (state.image && state.image.startedAt) ? state.image.startedAt : todayStr(); + } + + // Keep the viewed day inside [earliestEditableDay, today]. Called before any + // render so a rollover, image completion, or import can't strand the view on + // an out-of-range day. + function clampSelectedDay(){ + var today = todayStr(); + if (selectedDay > today) selectedDay = today; + var min = earliestEditableDay(); + if (selectedDay < min) selectedDay = min; + } + function renderTodayHeader(){ + clampSelectedDay(); var today = todayStr(); - var d = new Date(); - var label = 'Today · ' + d.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }); - if (isPerfectDay(state.log, today, state.habits)) label += ' ✨'; - q('todayLabel').textContent = label; + var isToday = selectedDay === today; + var d = parseDateLocal(selectedDay); + var dateText = d.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }); + var label = (isToday ? 'Today · ' : '') + dateText; + if (isPerfectDay(state.log, selectedDay, state.habits)) label += ' ✨'; + var labelEl = q('todayLabel'); + labelEl.textContent = label; + labelEl.classList.toggle('past', !isToday); + q('dayPrev').disabled = selectedDay <= earliestEditableDay(); + q('dayNext').disabled = isToday; + q('dayBackToday').hidden = isToday; } function renderHabits(){ + clampSelectedDay(); var list = q('habitList'); var today = todayStr(); - var checkedToday = state.log[today] || []; + var viewDay = selectedDay; + var checkedDay = state.log[viewDay] || []; list.innerHTML = ''; q('emptyHabits').hidden = state.habits.length > 0; state.habits.forEach(function(h){ var li = document.createElement('li'); li.className = 'habit-row'; - var isChecked = checkedToday.indexOf(h.id) !== -1; + var isChecked = checkedDay.indexOf(h.id) !== -1; if (isChecked) li.classList.add('checked'); var label = document.createElement('label'); @@ -1260,7 +1319,11 @@ nameSpan.textContent = h.name; label.appendChild(emojiSpan); label.appendChild(nameSpan); - var streak = currentHabitStreak(state.log, h.id, today); + // On today, show the "alive through yesterday" streak; on a past day, + // show the streak as it actually stood ending on that day. + var streak = (viewDay === today) + ? currentHabitStreak(state.log, h.id, viewDay) + : habitStreakEndingOn(state.log, h.id, viewDay); if (streak > 0) { var streakSpan = document.createElement('span'); streakSpan.className = 'habit-streak'; @@ -1290,9 +1353,12 @@ } function onHabitToggle(habitId, wantChecked, rowEl){ - var today = todayStr(); - if (!state.log[today]) state.log[today] = []; - var list = state.log[today]; + // Edits apply to the day being viewed (today, or a past day the user + // navigated to). Reveal is recomputed from the whole log, so the delta + // below correctly accounts for streaks a backfilled day bridges. + var day = selectedDay; + if (!state.log[day]) state.log[day] = []; + var list = state.log[day]; var idx = list.indexOf(habitId); var already = idx !== -1; @@ -1324,9 +1390,10 @@ } } else if (!wantChecked && already) { list.splice(idx, 1); - if (list.length === 0) delete state.log[today]; // don't keep empty day arrays around - // Reveal is derived from the log, so removing today's entry only shrinks - // that habit's contribution for today — past days stay revealed automatically. + if (list.length === 0) delete state.log[day]; // don't keep empty day arrays around + // Reveal is derived from the log, so unchecking recomputes the whole + // painting from scratch — the pixels that check was worth (including any + // streak it bridged) come back off. saveNow(); renderHabits(); renderArt(); renderStats(); renderTodayHeader(); } @@ -1529,6 +1596,52 @@ renderHabitEditList(); populateEmojiSelect(q('newHabitEmoji')); renderArtworkPanel(); + renderChangelog(); + } + + // Release history, newest first, distilled from the project's beans issues — + // one concise sentence per shipped change. Keep this in sync when a + // user-facing feature or fix lands. + var CHANGELOG = [ + { date: '2026-07-18', items: [ + 'Added a day navigator so you can step back to an earlier day and check off a habit you forgot — the pixels you missed paint in.' + ] }, + { date: '2026-07-16', items: [ + 'Reworked streaks to be per-habit: each habit now tracks and shows its own streak instead of one shared number.' + ] }, + { date: '2026-07-15', items: [ + 'Added a Data tab to export, import, and wipe your data, a page footer, and streak-boosted pixel rewards.' + ] }, + { date: '2026-07-14', items: [ + 'Made daily rewards escalate so each additional habit checked in a day reveals more pixels.', + 'Added a live browser-tab favicon that fills in to mirror your artwork’s progress.', + 'Renamed the app to “Paint by Habits” and refreshed its visual design.', + 'Fixed the celebration screen getting stuck on-screen after finishing a picture.', + 'Initial release: a single-file pixel-art habit tracker that reveals artwork as you build habits.' + ] } + ]; + + function renderChangelog(){ + var host = q('changelogList'); + host.innerHTML = ''; + CHANGELOG.forEach(function(entry){ + var wrap = document.createElement('div'); + wrap.className = 'changelog-entry'; + var dateEl = document.createElement('div'); + dateEl.className = 'changelog-date'; + dateEl.textContent = parseDateLocal(entry.date) + .toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }); + var ul = document.createElement('ul'); + ul.className = 'changelog-list'; + entry.items.forEach(function(text){ + var li = document.createElement('li'); + li.textContent = text; + ul.appendChild(li); + }); + wrap.appendChild(dateEl); + wrap.appendChild(ul); + host.appendChild(wrap); + }); } function renderHabitEditList(){ @@ -1761,6 +1874,9 @@ function checkRollover(){ var t = todayStr(); if (t !== currentDay) { + // If the user was viewing "today", carry them to the new today; if they + // were editing a past day, leave them there. + if (selectedDay === currentDay) selectedDay = t; currentDay = t; render(); } @@ -1768,6 +1884,17 @@ q('themeToggle').addEventListener('click', toggleTheme); + /* ---- day navigation (backfill missed days) ---- */ + function gotoDay(day){ + selectedDay = day; + clampSelectedDay(); + renderHabits(); + renderTodayHeader(); + } + q('dayPrev').addEventListener('click', function(){ gotoDay(addDaysStr(selectedDay, -1)); }); + q('dayNext').addEventListener('click', function(){ gotoDay(addDaysStr(selectedDay, 1)); }); + q('dayBackToday').addEventListener('click', function(){ gotoDay(todayStr()); }); + /* ========================================================================== INIT ========================================================================== */