Removing the splash exposed something it had been covering. Tasks loaded
in onMount, so every launch rendered one frame of the empty state first.
localStorage is synchronous, so there was never a reason to wait.
Migration and rollover run during initialisation and the first render has
the real list. onMount keeps only the rollover triggers.
The README described migration that never ran, and listed Backspace as a
delete key.
It now says what actually happens: unfinished work carries across gaps,
completed tasks are cleared with their day, storage prunes itself, and old
keys convert on first open. Adds Cmd/Ctrl+Z and Alt+Arrow, the touch long
press, the screenshot's new path, and how to run the tests.
HTML5 drag events never fire on touch, so reordering only ever worked with
a mouse.
Pointer events handle mouse, touch and pen in one path. The entry
condition has to differ: a mouse drag starts after 8px of movement, but a
touch drag can't use movement, because a vertical swipe on a list usually
means scroll. Touch starts on a 400ms long press instead, and moving
before that cancels it and lets the page scroll. touch-action applies only
while a drag is live, plus a non-passive touchmove blocker, since
touch-action can't stop a gesture already in flight.
Alt+Up and Alt+Down reorder from the keyboard. Focus is put back on the
moved row by hand, since list reconciliation drops it and the first
version only worked once.
Also removes dataTransfer.setData('text/html', event.target), which passed
a DOM node where a string was required.
The delete button was opacity:0 and only revealed on hover, so tabbing to
it showed nothing at all. The focus outline was being drawn on an
invisible element. It shows on :focus-within now too.
The row was role="button" with tabindex=0 while containing two real
buttons, which isn't valid. Rows are <li> in a <ul> now, so the list gets
announced with its count and position, and the checkbox carries the task
text plus aria-pressed.
Backspace no longer deletes. It's the key people hit meaning "go back",
and there was no way to get the task back afterwards. Delete still works,
and Cmd/Ctrl+Z reverses it.
Enter and Escape move from window to the input. On window, pressing Enter
with a task focused toggled that task and also added whatever was sitting
in the input.
Bundle goes from 337 kB to 52 kB, or 89 kB to 20 kB gzipped. Almost all of
it was lottie-web, imported at the top level to draw one decorative empty
state, so every visitor paid 305 kB whether they ever saw it or not. It's
an inline SVG now, and it respects prefers-reduced-motion.
The 500ms splash is gone. There was no async work behind it.
Dark mode is applied by a small script in the head before first paint, so
dark-mode users stop getting a white flash on load. The theme class moves
to <html>, and App.svelte reads its initial state from that class instead
of re-reading storage after mount.
publicDir moves from assets/ to public/, which stops shipping the 200 kB
README screenshot to everyone.
Svelte 4.2 to 5.56, Vite 5 to 8, vite-plugin-svelte 3 to 7. The old tree
wouldn't resolve incrementally, because a stale vite-plugin-svelte-inspector
pinned the v3 peer, so this is a clean reinstall.
Clears all 7 npm audit advisories, including a high-severity Rollup path
traversal.
Reactivity moves to runes: $state, $derived, $effect. Event directives
become properties, and main.js uses mount().
Also drops inputElement, a bind:this target nothing ever read.
App.svelte is a view layer now. Storage, dates, rollover and task
operations come from src/lib, and selectedDate (a toDateString value)
becomes selectedKey (an ISO key).
Rollover runs on four triggers, because no single one covers everything:
mount, a self-rescheduling midnight timer, visibilitychange, and window
focus. The timer handles a pinned tab crossing midnight on its own. The
other two handle machine sleep, where timers don't reliably fire.
Persistence goes through one setTasks path so the stored list can't drift
from the visible one.
Checked in the browser against seeded legacy data across three days: keys
migrate and collapse correctly, unfinished tasks carry while completed
ones drop, and a simulated midnight updates an open tab with no reload.
Task operations become pure functions over arrays.
Ids move from Date.now() to crypto.randomUUID(), with a fallback for
non-secure contexts, since people do self-host this over plain HTTP on a
LAN.
Undo gets its own module so tasks.js stays pure. It covers single deletes
and clear-completed batches, and clamps indices because the list can
change between recording an entry and undoing it.
Replaces checkAndMigrateTasks, which never ran. Its guard compared
currentDate, set from new Date() at init, against a fresh new Date() a few
milliseconds later in onMount, so it was never true. Unfinished tasks have
been sitting under old date keys ever since.
The rule now: unfinished tasks from every prior day move into today,
oldest first, ahead of whatever is already there. Completed ones are
dropped and the old key deleted. It spans gaps, which the original never
did even in theory, since it only looked one day back.
now is a parameter. That's the part that makes the boundary testable.
A factory over an injectable backend, so persistence can be tested without
a DOM.
Fixes the unguarded JSON.parse that would white-screen the app on one bad
value, and falls back to memory when localStorage itself throws (Safari
private mode, full quota).
migrateLegacyKeys converts old "Sat Aug 15 2026" keys in place. It's
idempotent, and it leaves anything it can't parse alone rather than
deleting it.
Switches task keys to ISO YYYY-MM-DD. They sort, so finding every day
before today is a string compare instead of re-parsing each key.
Two things worth knowing. fromKey builds from date parts rather than
parsing a bare ISO string, which gets read as UTC and lands on the wrong
day west of Greenwich. addDays goes through setDate, so it stays on the
same calendar day across a DST change instead of adding a flat 24 hours.
Groundwork before pulling logic out of App.svelte.
Vitest runs in a node environment. Nothing under test needs a DOM, because
every module takes its dependencies as arguments.
package-lock.json is tracked now and the Dockerfile installs with npm ci,
so the nightly rebuild can't quietly land on different versions.
.gitignore: .DS_Store was anchored to the root and missed nested copies,
and dist/ wasn't ignored at all.