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>
112 lines
6.0 KiB
Markdown
112 lines
6.0 KiB
Markdown
# Implementation Plan: JSON Import/Export
|
|
|
|
**Branch**: `007-json-import-export` | **Date**: 2026-03-27 | **Spec**: [spec.md](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)
|
|
|
|
```text
|
|
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)
|
|
|
|
```text
|
|
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
|