Two faults behind one report. The card clipped long descriptions, and it
never rendered the how field at all - so for SHIFT + REMAIN, where the
instruction IS a three-second hold, the most important part was missing
rather than merely shortened.
Measured rather than assumed: that entry is Live mode, how is "Hold
SHIFT, hold REMAIN 3 s", desc is 112 characters, and the pair renders
43px tall into a 29px two-line clamp. A full line was being lost.
The gesture now leads the card text in the accent colour, and a MORE
button opens a sheet with the combo, the gesture, the full description,
the modes it applies in and the manual page. Whether to offer MORE is
decided by measuring scrollHeight against clientHeight, not by guessing
a character count, so it appears exactly when text is really cut. 39 of
159 entries qualify: 24 carry a gesture, 17 have a description over 110
characters.
The card stays 84px because the panel needs that space, so a sheet is
the right place for the overflow, as in the reference.
141 tests, typecheck clean, installed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Vite builds to dist/, deliberately not www/. www/ is still produced by
sync-www.sh for the Phase 1 APK, which stays installable throughout the
port; cutover is a one-line webDir change in capacitor.config.json.
base is relative because Capacitor serves the bundle from the APK, where
absolute asset paths would 404.
index.html omits viewport-fit=cover on purpose. That string is what
makes Capacitor's SystemBars plugin pass insets through to the page;
without it the plugin pads the decor view and the layout is always
between the status and gesture bars. Phase 1 learned this the hard way.
Fonts are self-hosted for offline use, latin subsets only: three files
at 68K, against the 133K Barlow costs today. Space Grotesk is variable,
so one file covers 400-700. Dropped latin-ext and vietnamese.
Carets are drawn as SVG, not typed. U+25BE and U+23EE sit outside every
Latin subset and would fall back to a system font mid-label.
base.css carries the settled palette and the measured vertical budget as
tokens, so a component cannot quietly exceed it:
8 + 44 + 8 + 525 + 8 + 84 + 8 + 50 = 735
Verified: typecheck clean, 103 tests, vite build emits with the fonts
copied, and www/ is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>