Files
TheLastGarden/src/ui/begin/BeginScreen.tsx
T
josh 414a554549 feat(02-02): begin screen + seed picker + ui-strings + lazy content split
- content/seasons/01-soil/ui-strings.yaml: player-visible Phase-2 copy externalized per CLAUDE.md (Begin / seed picker / post-harvest beat / journal / settings / plant display names); voice reviewed against bible + anti-fomo-doctrine.md
- content/seasons/01-soil/fragments.yaml: placeholder Season-1 fragment file (Plan 02-03 expands to ≥10 authored)
- content/seasons/00-demo/: deleted (Phase-1 demo replaced)
- src/content/schemas/ui-strings.ts: UiStringsSchema (Zod) — validates structure of every season's ui-strings.yaml at load time
- src/content/schemas/index.ts + src/content/index.ts: re-export UiStringsSchema/UiStrings
- src/content/loader.ts: eager `uiStrings` glob + PIPE-02 lazy `loadSeasonFragments(seasonId)` (Plan 02-03+ exploit)
- src/ui/begin/use-audio-bootstrap.ts: bootstrapAudioContext() lazy-creates + resumes (RESEARCH Pattern 9; Pitfall 5 mitigation — context construction inside the gesture for iOS Safari) + installFirstInteractionGestureHandler() one-shot for D-22 returning players + __resetAudioBootstrapForTest()
- src/ui/begin/BeginScreen.tsx: D-21 typographic Begin screen — title + subtitle + CTA from uiStrings[1].begin; onClick calls bootstrapAudioContext synchronously inside the click event then dismisses the session gate (D-22)
- src/ui/begin/BeginScreen.test.tsx: 4 tests — render / D-22 skip / click bootstraps + dismisses / subtitle string
- src/ui/garden/SeedPicker.tsx: D-02 inline DOM popover; subscribes to 'tile-clicked-coords'; renders one button per unlocked plant type from uiStrings[1].plants; click enqueues plantSeed command via store.enqueueCommand
- src/ui/garden/SeedPicker.test.tsx: 6 tests — initial-null / coords-positioned / unlocked-only / enqueue / dismiss / multi-plant; mocks game/event-bus to avoid Phaser canvas init under happy-dom (deviation Rule 3)
- src/ui/{begin,garden,index}.ts: barrels
- src/App.tsx: mount BeginScreen + SeedPicker as overlay siblings to PhaserGame
- src/PhaserGame.tsx: bootstrap unlockedPlantTypes=['rosemary'] for first-run; install gesture handler + scene-ready listener
- npm run ci exits 0; 163/163 tests pass (10 new this commit + 25 from Task 1 + 128 baseline)
2026-05-09 09:43:47 -04:00

70 lines
2.1 KiB
TypeScript

import { type JSX } from 'react';
import { useAppStore } from '../../store';
import { uiStrings } from '../../content';
import { bootstrapAudioContext } from './use-audio-bootstrap';
/**
* D-21 + AEST-07: tasteful typographic Begin screen. Phase 3 swaps in
* the painted gesture-gate without changing this file's behavior.
*
* D-22: shown on first run only — gated by session.beginGateDismissed.
* Once dismissed, the gate stays down for the session; persistence
* across sessions is owned by the save lifecycle (Plan 02-05).
*
* CLAUDE.md banner concern #7 (Web Audio user-gesture): the click
* handler calls `bootstrapAudioContext()` SYNCHRONOUSLY (not inside a
* useEffect), so iOS Safari sees the AudioContext construction inside
* the gesture (Pitfall 5).
*/
export function BeginScreen(): JSX.Element | null {
const dismissed = useAppStore((s) => s.beginGateDismissed);
const dismissBeginGate = useAppStore((s) => s.dismissBeginGate);
if (dismissed) return null;
const strings = uiStrings[1]?.begin;
if (!strings) return null;
const onBegin = (): void => {
void bootstrapAudioContext(); // synchronous-inside-click; do NOT move into useEffect (Pitfall 5)
dismissBeginGate();
};
return (
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 100,
background: '#1a1a1a',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: '#e8e0d0',
fontFamily: 'serif',
}}
role="dialog"
aria-label={strings.title}
>
<h1 style={{ fontSize: '3rem', margin: 0, fontWeight: 300 }}>{strings.title}</h1>
<p style={{ marginTop: '1rem', opacity: 0.7, letterSpacing: '0.2em' }}>{strings.subtitle}</p>
<button
onClick={onBegin}
style={{
marginTop: '4rem',
padding: '0.6rem 2.4rem',
fontSize: '1.1rem',
background: 'transparent',
color: '#e8e0d0',
border: '1px solid #e8e0d0',
cursor: 'pointer',
fontFamily: 'serif',
}}
>
{strings.cta}
</button>
</div>
);
}