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.
63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
import { useEffect, useState, type JSX } from 'react';
|
|
import { useAppStore } from '../../store';
|
|
import { uiStrings } from '../../content';
|
|
|
|
/**
|
|
* Persistence-result toast (CONTEXT D-30 + UX-13).
|
|
*
|
|
* One-time soft toast in voice on first save if `navigator.storage.persist()`
|
|
* was denied; nothing if granted. State remembered via
|
|
* `settings.persistenceToastShown` so the toast only fires once across
|
|
* sessions.
|
|
*
|
|
* Triggered by src/PhaserGame.tsx after the boot path's requestPersistence
|
|
* call resolves with `granted=false && apiAvailable=true && !persistenceToastShown`.
|
|
*
|
|
* Anti-FOMO compliant — the copy is in the gardener-keeper voice
|
|
* (uiStrings[1].settings.persistence_denied_toast: "The garden may forget,
|
|
* if your browser asks it to."). No nag, no streak, no daily-login pressure.
|
|
*/
|
|
const TOAST_DURATION_MS = 6500;
|
|
|
|
export function PersistenceToast({ show }: { show: boolean }): JSX.Element | null {
|
|
const [visible, setVisible] = useState(show);
|
|
const setShown = useAppStore((s) => s.setPersistenceToastShown);
|
|
const strings = uiStrings[1]?.settings;
|
|
|
|
useEffect(() => {
|
|
if (!show) {
|
|
setVisible(false);
|
|
return;
|
|
}
|
|
setVisible(true);
|
|
const t = setTimeout(() => {
|
|
setVisible(false);
|
|
setShown(true);
|
|
}, TOAST_DURATION_MS);
|
|
return () => clearTimeout(t);
|
|
}, [show, setShown]);
|
|
|
|
if (!visible || !strings) return null;
|
|
return (
|
|
<div
|
|
role="status"
|
|
data-testid="persistence-toast"
|
|
style={{
|
|
position: 'fixed',
|
|
bottom: 24,
|
|
left: 24,
|
|
zIndex: 30,
|
|
maxWidth: 420,
|
|
padding: '0.8rem 1.2rem',
|
|
background: '#1f1f23ee',
|
|
color: '#e8e0d0',
|
|
border: '1px solid #4d4d52',
|
|
fontFamily: 'serif',
|
|
fontStyle: 'italic',
|
|
}}
|
|
>
|
|
{strings.persistence_denied_toast}
|
|
</div>
|
|
);
|
|
}
|