Rows stopped being vertically centred. Aligning them to flex-start so the
controls stay on the first line of a wrapped task also let the 32px row
buttons set the row height, which left the 24px checkbox and text sitting
4px above centre on every single-line row. Everything on the first line
now occupies the same 32px band, so a single-line row centres and a
wrapped one still puts its controls on the first line. A padding shorthand
later in the same rule was quietly resetting the longhand that does it.
The phone layout was spending 372px of an 812px screen, 46% of it, before
the first task appeared. The header stacked into two rows because the
wordmark and the controls could not share one: they need 422px and there
are 343. The wordmark goes below 480px, since the logo carries the
identity and an installed app already has its name under the icon, and it
stays in the accessibility tree so the page keeps its h1. The heading and
date share a line again rather than stacking unconditionally. That is 372
down to 261, 46% down to 32%.
That left the rows themselves too tight, because both actions stay visible
where there is no hover and were taking 44% of the row width, cutting the
text to 21 characters a line. Tighter gaps and padding, and a smaller
drawn button whose 44px target comes from the overlay underneath rather
than its own box, bring that back to 24.
A pass over what the last four changes added.
The focus-visible rule named a handful of controls by hand and had gone
stale. It still pointed at .today-btn, which no longer exists, and none of
the five newer controls were listed, so tabbing to the defer arrow, either
day option, the task text, the Undo button or the dismiss cross showed
nothing at all. They are all covered now, and the row buttons reveal
themselves when focused, since a ring on a transparent button is not much
use.
Tap targets on touch: the day options came out 36px tall because the
switch's own padding and border ate into its height, and the task text was
24. The switch is 46 with the padding removed so its buttons get the full
44, and the theme toggle matches so the header stays level. The text grows
by padding with the same amount taken back as negative margin, so it is
44px to a finger without moving off the first line.
Also drops the fade on the carry-over notice. It was hiding whether the
thing was actually unmounting, and a fade earns very little on something
that appears once a day.
Checked at both widths and in both themes: contrast passes on every new
element, nothing scrolls sideways, and a fresh load runs the whole feature
set with no console errors.
Two pieces of copy that were making the app harder to read than it needs
to be.
Carry-over happened in total silence. Open the app after a weekend and
unfamiliar tasks were sitting in Today with nothing explaining where they
came from, which reads as a bug rather than the feature it is. A line now
says how many were brought forward, on the day it happened, and it can be
dismissed.
The day control was a single button labelled with the day you were already
on, which then took you somewhere else. Clicking it was the only way to
find out what it did, and the date underneath already said where you were.
It is two options now with the current one marked, so the control states
both where you are and what the alternative is without being touched.
Also drops labelFor from dates.js along with its tests. It existed to
label that one button and nothing uses it now.
Undo existed but nothing said so. The only route was Cmd/Ctrl+Z, which was
documented in a README nobody reads mid-task, and which does not exist on
a phone at all. On touch there was simply no way to get a deleted task
back.
Deleting a task or clearing completed ones now brings up a short-lived bar
with a real Undo button, so touch has a route for the first time. It also
shows the keyboard shortcut, which is the point: someone learns it once,
here, at the only moment they actually care.
It uses the existing tokens, sits out of the way at the bottom, clears
itself after seven seconds and goes as soon as it is used. The shortcut
badge is hidden where no keyboard exists rather than advertising a chord
nobody can press.
Fixing a typo meant deleting the task and typing it again, which also cost
you its position in the list. Click the text to edit it. Enter saves,
Escape cancels, clicking away saves.
The rename rule lives in tasks.js and is tested. It refuses blank input
rather than treating it as a delete, since selecting all and hitting enter
by accident should give you your task back, and it returns the original
array untouched when nothing changed so an idle edit does not write to
storage.
The interesting part was what editing collides with. Keystrokes are
stopped from reaching the row, or Delete would remove the task you are
typing into and Alt+Arrow would reorder it mid-edit. A drag cannot start
from inside the field, and the click a drag leaves behind is swallowed, or
dropping a row would open it for editing. Switching day, deferring a task
and the midnight rollover all close an open editor, so it cannot be left
hanging over a row that is no longer there.
The app is built around two days and had no way to move anything between
them. "I won't get to this today" is the most obvious thing someone wants
from a two-day list, and the only route was deleting the task and typing
it again somewhere else.
Each row gets an arrow next to delete. On Today it sends the task forward,
on Tomorrow it points back, so one control covers both directions and the
label says which one you are getting. Alt+Right and Alt+Left do the same
from the keyboard, and each only acts in the direction that makes sense
from the day you are on.
The move itself is in src/lib/defer.js with tests, since it touches two
date keys at once and the failure modes are worth pinning down: a missing
id, the same day twice, an empty destination, and a task that somehow
already exists on the other side.
A pass over desktop, tablet and phone widths, measuring rather than
eyeballing.
The one that matters: a task could not be deleted on a phone. The delete
button is revealed on hover, touch devices report hover: none, so it sat
at zero opacity with nothing to reveal it. The README told people to hover
over a task and click it. It is now always visible where hover does not
exist.
Touch targets were under the 44px minimum: the checkbox at 24, delete at
32, the footer links at 23 tall. They now get 44px hit areas from overlays
that leave the drawn size alone, behind a pointer: coarse query so a mouse
keeps small precise targets and the full row stays draggable.
Two contrast failures in light mode. The date was the brand blue on the
near-white background at 3.54:1, so text now uses a darker --accent-text
and reads 5.14:1. Completed rows carried opacity 0.8 over an already
mid-contrast blue, which came to 3.34:1; the tint, the strikethrough and
the colour say "done" well enough without it, and dropping it gives 4.78:1.
Both themes now pass AA everywhere measured.
The checkbox sat vertically centred, so on a task wrapping to five lines
it floated in the middle of the block. Rows align to the top and task text
gets a 24px line box, matching the checkbox exactly, which also spaces
wrapped lines better.
Reduced motion was only honoured by the empty-state drawing. Svelte's
transitions run in JavaScript and never saw the media query, so tasks
still flew in on a stagger. The component reads the preference directly
and collapses its durations, and CSS covers the rest. The lift on a
dragged card stays, since it tracks the pointer rather than playing at you.
Long unbroken words already wrapped and no breakpoint scrolled sideways.
Two things that were deliberately left out of 2.0.0.
Export writes every stored day to a JSON file. Import reads one back, and
only ever adds: a task whose id is already there is left alone, so
importing the same file twice does nothing and importing into a list
you're using can't lose work. That is also why it needs no confirmation
dialog. The trade is that import restores rather than reverts.
Both sit in a quiet line under the task list rather than the header, since
they get used about twice a year and the header is what you look at all
day.
For the PWA half, the service worker is about fifty lines with no
dependency, because the strategy falls out of how Vite builds. Documents
go network first and fall back to cache, so a deploy is picked up as soon
as you're online and nobody ends up stuck on an old build. Fingerprinted
assets go cache first and are kept, since their names change when their
contents do. No build-time asset manifest needed.
Icons are SVG in the manifest, which stays sharp at any size and costs
about a kilobyte, plus one 180px PNG because iOS wants a raster
apple-touch-icon. Two theme-color metas so the phone status bar follows
the theme, and safe-area padding on the header, without which the header
sits under the clock once installed on an iPhone.
Verified against the production build: worker registers and claims the
page, shell and assets land in cache, and with the server stopped the app
still loads, adds a task and persists it.
Removes docs/superpowers, which held a design note and an implementation
plan. Those were working notes rather than anything the project needs, and
they don't belong in the repo.
Everything else here is wording: em dashes swapped for ordinary
punctuation across the README and the code comments, and a few sentences
straightened out.
Letting go of a card was abrupt and pulled its neighbours with it. Frame
capture showed non-dragged rows moving about 68px after release, still
going 13 frames later.
Two things owned transform on the same elements: manual shift transforms
for the gap, and animate:flip for the reorder. On drop, flip measured a
"before" rect that already had a manual offset in it, then the order
changed and the offsets cleared in the same update, so it animated toward
a position that never existed.
One owner each now. The list reorders as the pointer crosses each
boundary rather than on release, so flip moves the displaced cards on its
own, one swap at a time, and the swaps look right while you're dragging.
Only the lifted card is positioned by hand, with flip off for it so it
stays under the pointer. By release the order is already final, and
nothing moves but that card settling.
After the change, every non-dragged row moves 0px on release and the
dropped card eases 4px into place.
Slot geometry is replaced with offsetTop and offsetHeight, layout values
that ignore transforms, so the target can't feed back into itself and
can't go stale.
Removes src/lib/drag.js and its tests. The rule they covered doesn't exist
in this design any more.
The old feedback was a card faded to 0.4 opacity and a 3px line above the
target. The fade read as "disabled" rather than "held", and a thin line is
a weak signal for where something lands.
The dragged card lifts now: full opacity, accent border, raised shadow, a
small scale and tilt. Cards it passes slide by the space it vacated, which
opens a real gap at the destination. The gap can't disagree with where the
card lands, because it is where the card lands.
Also fixes something found while building it. targetIndexFor measured live
rects, but those rows carry the shift transforms the drag applies, so the
midpoints used to pick the target moved as a result of picking it and the
choice oscillated. Slot geometry is captured once at drag start, relative
to the list so scrolling mid-drag is fine.
The displacement rule moves to src/lib/drag.js with tests. Its up/down
boundaries are where off-by-ones live.
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.