Task 2 of Plan 02-03: ship the Memory Journal UI surfaces (D-23/D-24/D-25)
and wire harvest + compost pointer events through the Garden scene to the
sim → store → React reveal flow.
src/ui/journal/ (new module):
- Journal.tsx — full-screen modal (D-24); fragments grouped by Season;
DOM-rendered text with userSelect: text per MEMR-05; reads
harvestedFragmentIds from the store; resolves ids against the eager
`fragments` corpus (defensive — unresolvable ids skip silently).
- FragmentRevealModal.tsx — D-25 active-play reveal modal; backdrop click
+ inner Close button dismiss; event.stopPropagation on the article
body so clicking inside the text doesn't dismiss; defensive silent
dismiss on unresolvable id.
- journal-icon.tsx — D-23 + D-29 corner affordance; gated by
selectJournalRevealed (`harvestedFragmentIds.length > 0`); local open
state (no store pollution); 'j' hotkey deferred to Plan 02-05.
- index.ts — barrel.
- 16 new Vitest cases across 3 test files (Journal: 7 / FragmentRevealModal:
6 / journal-icon: 3); all green.
src/App.tsx — mount FragmentRevealModal + JournalIcon as DOM siblings of
PhaserGame.
src/ui/index.ts — re-export ./journal.
src/game/scenes/Garden.ts — harvest/compost pointer flow:
- create() builds a SimContext from the eager `fragments` corpus filtered
to Season 1; passed to every simulateOneTick call (Phase 4+ should swap
to await loadSeasonFragments(currentSeason) when Season transitions land).
- handleTilePointerDown branches on tile state: empty → seed picker
event; ready plant → enqueue 'harvest' command; immature plant → enqueue
'compost' command (TODO Plan 02-04: render the Ink-authored compost
acknowledgement beat from compost-acknowledgements.ink).
- update() detects newly-appended harvestedFragmentIds and sets
fragmentRevealId so the reveal modal pops with the new fragment text.
- BLOCKER 3 invariant preserved — sim writes tickCount, never lastTickAt.
content/dialogue/season1/compost-acknowledgements.ink — authored content
for the GARD-04 + D-07 compost beat. 6 short lines in the gardener-keeper
voice (NOT Lura — she's the warmth anchor; the garden's voice is the
contrast). Plan 02-04 wires the inkjs runtime; Plan 02-03 ships the
content so the writer can iterate independently.
214/214 tests green (was 163; +51 new this plan); npm run lint exits 0;
npm run ci exits 0; npm run build exits 0 with the expected
INEFFECTIVE_DYNAMIC_IMPORT warnings (eager `fragments` export still
imports the Season-1 yaml/md statically alongside the lazy
loadSeasonFragments path — documented in 02-02-SUMMARY.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>