Export and import encounter, undo/redo history, and player characters as a downloadable .json file. Export/import actions are in the action bar overflow menu. Import validates using existing rehydration functions and shows a confirmation dialog when replacing a non-empty encounter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.0 KiB
Implementation Plan: JSON Import/Export
Branch: 007-json-import-export | Date: 2026-03-27 | Spec: spec.md
Input: Feature specification from /specs/007-json-import-export/spec.md
Summary
Add JSON import/export for the full application state (encounter, undo/redo history, player characters). Export creates a downloadable .json file; import reads a file, validates it using existing rehydration functions, and replaces the current state after user confirmation. UI is integrated via the action bar overflow menu.
Technical Context
Language/Version: TypeScript 5.8 (strict mode, verbatimModuleSyntax)
Primary Dependencies: React 19, Vite 6, Tailwind CSS v4, Lucide React
Storage: localStorage (encounter, undo/redo, player characters)
Testing: Vitest (v8 coverage)
Target Platform: Browser (desktop + mobile)
Project Type: Web application (SPA)
Performance Goals: Export/import completes in under 1 second for typical encounters
Constraints: No server-side component; browser-only file operations
Scale/Scope: Encounters with up to ~50 combatants, undo stacks of up to 50 snapshots
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Principle | Status | Notes |
|---|---|---|
| I. Deterministic Domain Core | PASS | No new domain logic needed. ExportBundle type is pure data. Validation reuses existing pure functions. |
| II. Layered Architecture | PASS | Type in domain, validation in adapter layer (co-located with existing rehydration functions), file I/O and UI in adapter layer. No reverse dependencies. |
| II-A. Context-Based State Flow | PASS | Import reads/writes state via existing contexts. No new props beyond per-instance config. |
| III. Clarification-First | PASS | All decisions documented in research.md. No ambiguities remain. |
| IV. Escalation Gates | PASS | Feature scope matches spec exactly. No out-of-scope additions. |
| V. MVP Baseline Language | PASS | Format versioning and selective import noted as "not included in MVP baseline." |
| VI. No Gameplay Rules | PASS | No gameplay mechanics involved. |
Post-Phase 1 re-check: All gates still pass. The ExportBundle type is a pure data structure in the domain layer. The validation logic lives in the adapter layer alongside existing rehydration functions (it depends on adapter-layer rehydrateEncounter and rehydrateCharacter). File I/O and UI live in the adapter layer.
Project Structure
Documentation (this feature)
specs/007-json-import-export/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
└── tasks.md # Phase 2 output (/speckit.tasks)
Source Code (repository root)
packages/domain/src/
├── export-bundle.ts # ExportBundle type definition
apps/web/src/
├── persistence/
│ └── export-import.ts # Export assembly + import validation + file handling
├── hooks/
│ └── use-encounter.ts # Existing — needs import/export state setters exposed
├── components/
│ ├── action-bar.tsx # Existing — add overflow menu items
│ └── import-confirm-prompt.tsx # Confirmation dialog for import
Structure Decision: Follows existing project structure. New files are minimal — one domain type, one persistence module (validation + export assembly + file handling), one UI component. Most work integrates into existing files.
Implementation Phases
Phase 1: ExportBundle Type + Validation Use Case
Goal: Define the export format and validation logic with full test coverage.
- Create
ExportBundletype inpackages/domain/src/export-bundle.ts - Export from domain package index
- Create
validateImportBundle()inapps/web/src/persistence/export-import.ts— acceptsunknown, returns validatedExportBundle | DomainError - Import
rehydrateEncounter()from./encounter-storage.tsfor encounter and undo/redo stack validation - Export
rehydrateCharacter()fromplayer-character-storage.tsand import it for player character validation - Write tests covering: valid bundles, missing fields, invalid encounter, invalid player characters, empty stacks, unknown version
Phase 2: Export Functionality
Goal: Users can download the current state as a .json file.
- Create
export-import.tsinapps/web/src/persistence/ - Implement
assembleExportBundle(encounter, undoRedoState, playerCharacters)— pure function returningExportBundle - Implement
triggerDownload(bundle: ExportBundle)— creates JSON blob, generates filename with date, triggers browser download - Add "Export Encounter" item to action bar overflow menu
- Wire button to read from contexts and call export functions
Phase 3: Import Functionality + Confirmation
Goal: Users can import a .json file with confirmation and error handling.
- Add "Import Encounter" item to action bar overflow menu
- Implement file picker trigger (hidden
<input type="file">) - On file selected: read text, parse JSON, validate via use case
- If validation fails: show error toast
- If encounter is non-empty: show confirmation dialog
- On confirm (or if encounter is empty): replace encounter, undo/redo, and player characters via context setters
- Write integration test: export → import round-trip produces identical state
Notes
- Import
rehydrateEncounter()fromapps/web/src/persistence/encounter-storage.tsfor encounter validation — do not duplicate - Export
rehydrateCharacter()fromapps/web/src/persistence/player-character-storage.tsso it can be imported for player character validation - Follow existing file picker pattern from
apps/web/src/components/source-fetch-prompt.tsx - Follow existing overflow menu pattern in
apps/web/src/components/action-bar.tsx - Follow existing
<dialog>pattern fromapps/web/src/components/settings-modal.tsx - Commit after each phase checkpoint