Files
initiative/specs/007-json-import-export/plan.md
Lukas fba83bebd6 Add JSON import/export for full encounter state
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>
2026-03-27 14:28:39 +01:00

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.

  1. Create ExportBundle type in packages/domain/src/export-bundle.ts
  2. Export from domain package index
  3. Create validateImportBundle() in apps/web/src/persistence/export-import.ts — accepts unknown, returns validated ExportBundle | DomainError
  4. Import rehydrateEncounter() from ./encounter-storage.ts for encounter and undo/redo stack validation
  5. Export rehydrateCharacter() from player-character-storage.ts and import it for player character validation
  6. 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.

  1. Create export-import.ts in apps/web/src/persistence/
  2. Implement assembleExportBundle(encounter, undoRedoState, playerCharacters) — pure function returning ExportBundle
  3. Implement triggerDownload(bundle: ExportBundle) — creates JSON blob, generates filename with date, triggers browser download
  4. Add "Export Encounter" item to action bar overflow menu
  5. 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.

  1. Add "Import Encounter" item to action bar overflow menu
  2. Implement file picker trigger (hidden <input type="file">)
  3. On file selected: read text, parse JSON, validate via use case
  4. If validation fails: show error toast
  5. If encounter is non-empty: show confirmation dialog
  6. On confirm (or if encounter is empty): replace encounter, undo/redo, and player characters via context setters
  7. Write integration test: export → import round-trip produces identical state

Notes

  • Import rehydrateEncounter() from apps/web/src/persistence/encounter-storage.ts for encounter validation — do not duplicate
  • Export rehydrateCharacter() from apps/web/src/persistence/player-character-storage.ts so 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 from apps/web/src/components/settings-modal.tsx
  • Commit after each phase checkpoint