Implements docs/superpowers/specs/2026-09-02-fast-entry-refinements-design.md.
Three changes shipped together because they share one tab order.
Bulk rows are description -> amount -> note, and nothing else is a tab stop.
The list always ends in exactly one blank row, appended as soon as the last
row is filled, so Tab flows into a new line with no special case and new rows
appear on touch too. The Add-line button is gone; the per-row remove button is
now a hover/focus x with tabIndex={-1}, and Backspace in an empty description
removes the row and lands on the previous row's note.
Purchases no longer demand a day. `date` accepts "2026-09" as well as
"2026-09-14"; entry defaults to the month, with an opt-in "set exact date"
link. No migration: monthOf() already parsed both shapes, verified before
choosing this over splitting the field into month + optional day, which would
have meant a schema migration in the browser against the user's only copy of
their data for an identical outcome.
Notes are an optional field on Purchase, in both entry forms and shown beside
the description in the month lists. Empty notes are omitted rather than stored
as "", so absent stays absent.
Also closes a pre-existing gap the spec surfaced: the JSON importer never
validated purchase.date, so a malformed date passed import and threw later
inside the balance fold. It is now rejected at the boundary, with out-of-range
months and days covered.
A row holding only a note is deliberately NOT blank: it asks for the missing
description and amount rather than being silently discarded.
Keyboard behaviour - tab traversal, Backspace-to-remove, focus after append -
cannot be verified in this environment and needs confirming in a browser.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A grocery trip is twenty lines, and opening the full dialog twenty times
is the thing this exists to avoid.
PurchaseDialog gains a One / Many toggle, shown only when adding — editing
an existing purchase edits exactly one, so the toggle is hidden there. In
Many mode the date, post and currency are set once at the top and each line
is just a description and an amount; Enter on an amount starts the next
line, so a receipt can be typed without leaving the keyboard. Blank trailing
rows are ignored rather than flagged.
The whole batch commits in a single mutate: one write, one queue entry, and
no way to half-commit a shopping trip.
Lines are deliberately simple. A line needing a split or a payment plan is
saved plain and then edited from the month view — building per-line split
and plan editors would have been most of the work for a case the grocery
run never hits, and would have slowed the common path.
Line logic lives in a pure module (src/ui/bulkEntry.ts) following the
purchaseForm.ts pattern, so it is testable with no DOM: 20 tests covering
blank-row handling, per-row validation messages, the running total, and
that N lines become N purchases landing wholly in the shared post. Each
line is built through the same toPurchase the single form uses, so there
is one definition of what a purchase is.
Validation mirrors validatePurchase in what it deliberately does NOT
reject: a negative amount is a refund, not an error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>