The reworked artifact went mobile-first; this ports its shell, minus the
quirks it shipped with.
Routes. #lesson #today #settings #words #learn #sent #grammar #cj #hangul
#drill, each a history entry, so the phone's back gesture works. The word
sheet, a popover and the review screen are layers: each owns an entry while
open, and Back closes the topmost before it leaves a route — the artifact
read window.__onBack but never set it. shell/history.ts holds the rules,
free of React and tested against a history whose traversals land late, as a
browser's do. Unlike the artifact's router:
· re-tapping the current destination pushes nothing;
· leaving a route closes what is open on it, rewinding its entries first;
· a sub-page's ← goes back only when history leads to its parent, and
otherwise becomes the parent — history.length > 1 let ← leave the app.
Shell. The page never scrolls; each route owns one scroller, in svh. A
bottom bar under 600px, a 76px rail to 840px, a labelled rail beyond, with
the artifact's icons and a due badge on 복습. Routes mount on first visit and
stay mounted, so a draft or a drill survives a trip elsewhere. Settings,
the conjugation trainer and the reading drill are pages of their own; 학습
is a hub. The design tokens gain the layout set and a second register:
reference panels stay square, what a thumb works is rounded.
복습 is its own screen: tap anywhere to reveal, grades in the thumb zone,
and an empty queue says so instead of doing nothing. Its pool is "my
units" once there are twenty such words, as in the artifact — plus his own
words, which the artifact dropped from review at that point.
The lesson is a column that fits the screen: roadmap strip, conversation,
composer with the quick-reply chips, and a ··· menu for focus and clearing.
The word list is docked from 840px; below that it is a sheet with peek,
half and full detents and a drag handle, whose height comes off the shell
so the exercise above shrinks rather than being covered. Focusing an answer
drops it to peek; a new exercise takes it down from full.
Fixed on the way: the log now follows a reply by what the learner did, not
by distance — its own scroll event arrived after the stream had added more
than the threshold, which read as scrolling away and stopped the follow
mid-reply. Sending re-sticks. The keyboard's focus guard moves to
pointerdown; React 19 attaches touchstart passively, so preventDefault()
there was ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PWA manifest declared icon-192.png, icon-512.png and favicon.svg. None
of the three existed, so install-to-home-screen and the browser tab both
resolved to nothing. That was a defect in what the last pass claimed as
done.
Drawn procedurally with Pillow rather than authored as SVG: there is no
dependable SVG rasteriser here to convert from, and ImageMagick falls back
to its MSVG renderer, which cannot be trusted with text. Pillow's
FreeType + RAQM stack shapes Hangul correctly, which is the only hard
requirement. The mark uses the app's own register — the serif face already
used for 한칸 on the boot screen, in --jade on --bg.
Two glyphs read well at 180px and up; at favicon sizes 한칸 turns to mush,
so the small sizes carry 한 alone.
icon-192 · icon-512 · apple-touch-icon (180) 한칸
icon-512-maskable 한칸 at 56%, inside
Android's circular crop
favicon.ico (16/32/48) · favicon-32 한
Sizing is a binary search on the drawn ink extent, and centring uses the ink
bounds rather than the font's line box — CJK metrics leave asymmetric space
above and below, which would otherwise sit the mark visibly high.
Re-running the generator writes byte-identical files.
Android launcher icons are untouched; Android work is out of scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
React + Vite + TypeScript, PWA, offline-first. Six tabs: 수업 오늘 단어 문장
문법 한글, plus the full-screen SRS review overlay, the reading drill, the
conjugation trainer and the 두벌식 keyboard.
The visual language is carried over deliberately: two hand-tuned palettes,
three type stacks, about a dozen component classes, zero border-radius and
no icons anywhere — Korean glyphs do the work icons would.
THE GATE is the reason this app exists. buildGate() already took a
vocabQuery hook; filling it with a band query is what turns 371 hand-typed
words into something that scales. Three refinements sit inside that hook,
all of them narrowing:
1. words a not-yet-finished unit is the first to introduce are excluded,
so a frequency ceiling cannot smuggle 3.4's material into 2.1;
2. Phase 1 is filtered by the phonological ladder;
3. the list is capped at 800 by frequency, because renderGate() inlines
it into the prompt — strictly more restrictive than the band, so it
cannot leak.
prompt/tutor-system.md ships unchanged with {{GATE}} filled by renderGate().
Confidence is clamped per turn. The artifact wrote the model's ::progress
number straight into the sole gate on advancement, so one hallucinated 95
skipped a unit.
stub-tutor.ts stands in for the model on the artifact's exact contract —
onText receives cumulative text, an aborted turn keeps what it streamed —
so the real endpoint drops in without touching the UI. It rotates all four
task types and climbs progress gradually, which makes every render path
reachable with no server.
Two artifact bugs are not ported: task state lived in the full-page
re-render, so anything arriving mid-answer wiped typed text and placed
chips; and the day number was computed once at module load, so a session
left open overnight scheduled against yesterday.
Verified in a browser: all six tabs work, and after a hard reload with the
network cut every tab still works — including dictionary search out of OPFS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>