Phase 2: persistence, and real pickers instead of cycling

The stored blob is now read once at startup through useReducer's lazy
initialiser and written whenever it changes, so deck, direction,
answering mode, context and the Leitner boxes all survive a restart.

persistedState.ts is the only part that touches the browser; the parsing
stays in domain/storage.ts. Every access is guarded, because localStorage
throws rather than returning null in a private window, with site data
blocked, or in a WebView that has not finished starting.

That guard is now tested, along with the claim this whole migration rests
on: a payload copied verbatim out of CapacitorStorage.xml on the phone
loads back with its boxes intact. If that test ever fails, the React app
has orphaned real drill progress. Also covered: damaged JSON, storage
that throws, localStorage absent entirely, and a refused quota on write.

Explore's pickers open a bottom sheet rather than cycling blindly through
options, and the drill settings button is no longer dead - deck,
direction and answering mode, each showing how many cards it selects.
Sheet is shared by all three.

Also fixed a silent one: .sheetLabel was used three times and did not
exist. CSS modules are index-typed, so that compiles clean and renders
with no class at all.

141 tests, typecheck clean. Sheets verified by rendering them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 10:46:42 +03:00
parent 9e1ffa0c85
commit b875342864
6 changed files with 427 additions and 28 deletions

View File

@@ -99,3 +99,16 @@
color: var(--on-accent);
font-weight: 700;
}
/* Group headings inside a sheet: Deck, Direction, Answering. */
.sheetLabel {
margin: 8px 0 2px;
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.12em;
color: var(--muted);
}
.sheetLabel:first-child {
margin-top: 0;
}