5d58d6cc7b
- src/save/payload.ts (W2): shared buildPayloadFromStore (state, nowMs)
+ hydrateStoreFromPayload (state, payload). Two-arg signature unifies
Settings.tsx (passes Date.now()) and PhaserGame.tsx saveSync (passes
clock.now()). BLOCKER 3 — lastTickAt is wall-clock ms, owned by the
application layer; the sim never writes it.
- src/ui/letter/Letter.tsx + test: D-20 full-screen overlay (UX-02);
loads compiled letter Ink, binds plants_bloomed/fragment_titles/
lura_was_here slots from session.pendingLetterEventBlock, dismisses
via Tend the garden button or backdrop click. Pitfall 9 — dismiss
calls bootstrapAudioContext synchronously inside the click handler.
- src/ui/settings/Settings.tsx + test: D-28 save-management UI
(Export/Import/Restore). BLOCKER 2 — Import pipeline is
importFromBase64 -> unwrap (CRC verify) -> migrate -> hydrate. No
audio sliders, no a11y polish (Phase 8 owns those).
- src/ui/settings/persistence-toast.tsx: D-30 one-time soft toast in
voice when navigator.storage.persist() denies. Reads
showPersistenceToast from session slice; sets persistenceToastShown
after the timeout fires.
- src/PhaserGame.tsx: full boot path rewrite. Clock selection
(?devtime=fake URL flag, production-guarded by import.meta.env.PROD),
save load (BLOCKER 1 — unwrap then migrate), silent offline catchup
via drainTicks(silent=true), letter overlay open at >=5min absence,
requestPersistence + showPersistenceToast wiring, Phaser start AFTER
hydration, registerSaveLifecycleHooks with synchronous LocalStorage
saveSync (Pitfall 7) + best-effort IDB write. W5 — lifecycle handle
held in ref so outer cleanup detaches.
- src/store/session-slice.ts: showPersistenceToast transient flag +
setShowPersistenceToast action.
- src/ui/journal/journal-icon.tsx: 'j' hotkey listener via
tlg:toggle-journal CustomEvent (D-29).
- src/game/scenes/Garden.ts: formalized clock read via readClockSlot()
helper; falls back to wallClock if window.__tlgClock missing.
- src/App.tsx: mount Letter, Settings, PersistenceToast, SettingsIcon
(corner button); D-29 keyboard shortcuts (',' toggles Settings, 'j'
toggles Journal via window event).
- 308/308 tests green (was 295; +13 new — 7 Letter + 6 Settings).
npm run ci exits 0; Vite emits letter Ink as a separate lazy chunk.
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { PhaserGame, type IRefPhaserGame } from './PhaserGame.tsx';
|
|
import { BeginScreen } from './ui/begin';
|
|
import { SeedPicker } from './ui/garden';
|
|
import { FragmentRevealModal, JournalIcon } from './ui/journal';
|
|
import { LuraDialogue } from './ui/dialogue';
|
|
import { Letter } from './ui/letter';
|
|
import { Settings, PersistenceToast } from './ui/settings';
|
|
import { useAppStore } from './store';
|
|
|
|
function App() {
|
|
// PhaserGame ref — Phase 2+ will use this to access the active scene from React.
|
|
const phaserRef = useRef<IRefPhaserGame | null>(null);
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
// D-30 — toast surfaces for one cycle when the boot path's
|
|
// requestPersistence resolves with denied. PhaserGame writes
|
|
// showPersistenceToast=true; the toast component reads it.
|
|
const showPersistenceToast = useAppStore((s) => s.showPersistenceToast);
|
|
|
|
// D-29 — keyboard shortcuts for Settings and the Memory Journal.
|
|
// Comma toggles Settings (a tasteful nod — settings is a subordinate
|
|
// concern, easy to reach, no browser conflict).
|
|
// 'j' toggles the Memory Journal via a window CustomEvent that
|
|
// JournalIcon listens for — keeps the icon's open/close state local
|
|
// (V1Payload has no journal-open flag, by design — see Plan 02-03
|
|
// SUMMARY).
|
|
useEffect(() => {
|
|
const onKeyDown = (e: KeyboardEvent): void => {
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
const target = e.target as HTMLElement | null;
|
|
if (
|
|
target &&
|
|
(target.tagName === 'INPUT' ||
|
|
target.tagName === 'TEXTAREA' ||
|
|
target.isContentEditable)
|
|
) {
|
|
return;
|
|
}
|
|
if (e.key === ',') {
|
|
e.preventDefault();
|
|
setSettingsOpen((o) => !o);
|
|
} else if (e.key === 'j' || e.key === 'J') {
|
|
e.preventDefault();
|
|
window.dispatchEvent(new CustomEvent('tlg:toggle-journal'));
|
|
}
|
|
};
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
}, []);
|
|
|
|
return (
|
|
<div id="app">
|
|
<PhaserGame ref={phaserRef} />
|
|
<BeginScreen />
|
|
<SeedPicker />
|
|
<FragmentRevealModal />
|
|
<JournalIcon />
|
|
<LuraDialogue />
|
|
<Letter />
|
|
<Settings open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
|
<PersistenceToast show={showPersistenceToast} />
|
|
<button
|
|
data-testid="settings-icon"
|
|
aria-label="Open settings"
|
|
onClick={() => setSettingsOpen(true)}
|
|
style={{
|
|
position: 'fixed',
|
|
bottom: 20,
|
|
right: 76,
|
|
zIndex: 40,
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
background: '#2a2a2e',
|
|
color: '#e8e0d0',
|
|
border: '1px solid #4d4d52',
|
|
cursor: 'pointer',
|
|
fontFamily: 'serif',
|
|
fontSize: '1.2rem',
|
|
}}
|
|
>
|
|
⚙
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|