Skip to content

Latest commit

 

History

History
428 lines (302 loc) · 51.7 KB

File metadata and controls

428 lines (302 loc) · 51.7 KB

QuestLoom Implementation Plan

Goals

  1. Fully operational locally — App runs and persists data with no backend; usable as a personal tool during development.
  2. Local functionality as soon as possible — Bootstrap the app and core flows first; iterate on polish and advanced features.
  3. Redirectable to hosted — Data access is behind an abstraction so the same app can later use a hosted API without rewriting feature code.

Redirectability: Data Access Layer

To keep the early local solution redirectable:

  • Repositories / services — Feature code (components, stores) calls repository functions (e.g. questRepository.getAllByGame(gameId)), not Dexie directly.
  • Single implementation for now — Repositories use Dexie (IndexedDB) as the only backend. Same interfaces can later be implemented by API clients (e.g. questRepository = createQuestApiClient(baseUrl)).
  • Shared types — Entity and DTO types live in src/types/ and are used by both local and (future) remote implementations.

No backend, auth, or sync in the initial implementation; add them when moving toward commercialization.


Phase 0: Project Bootstrap ✅ Complete

Goal: Run the app locally with a minimal shell; establish tooling and structure.

Status: Validation steps succeeded; node/npm are available for subsequent phases.

0.1 Create Vite + React + TypeScript app ✅ Complete

  • In repo root: npm create vite@latest . -- --template react-ts (use . to create in current directory; accept overwrite for existing files if prompted, or create in a temp dir and merge).
  • Install dependencies: npm install.
  • Verify: npm run dev — default Vite React page loads.

0.2 Add tooling and styling ✅ Complete

  • Tailwind CSS: npm install -D tailwindcss postcss autoprefixer then npx tailwindcss init -p; configure tailwind.config.js content for ./index.html and ./src/**/*.{js,ts,jsx,tsx}; add Tailwind directives to src/index.css.
  • ESLint + Prettier: Extend ESLint for TypeScript and React (e.g. @typescript-eslint, eslint-plugin-react); add Prettier and avoid conflicts (e.g. eslint-config-prettier).
  • Zustand: npm install zustand.
  • Dexie: npm install dexie (and dexie-react-hooks if you want reactive queries in React).

0.3 Project structure ✅ Complete

  • Create folders under src/: components/, features/, hooks/, stores/, types/, utils/, and optionally lib/ (for shared data layer).
  • Replace the default Vite page with a minimal app shell: one layout with a simple header/title (e.g. "QuestLoom") and a placeholder main area. No routing yet if you prefer a single view; add a simple router (e.g. React Router) when you add multiple views.

0.4 Definition of done (Phase 0) ✅ Complete

  • npm run dev runs and shows a QuestLoom shell (header + main area).
  • npm run build succeeds.
  • Tailwind is applied; one styled element confirms it.
  • ESLint and Prettier run (e.g. via npm run lint / format script).

Validate Phase 0 (run in project root when node/npm are available):

npm install
npm run dev    # Open http://localhost:5173 — should see QuestLoom header and main area
npm run build  # Should complete without errors
npm run lint   # Should pass
npm run format # Optional: format code

Immediate next steps (in order):

  1. Scaffold Vite + React + TS in the repo (or merge from a temp vite run).
  2. Install Tailwind, configure it, add a minimal global layout in App.tsx.
  3. Install Zustand and Dexie; add src/types/ and src/lib/ (or src/data/) for future repositories.
  4. Add ESLint + Prettier and a single script to run lint.

Phase 1: Local Data Foundation ✅ Complete

Goal: Persist one entity type in IndexedDB with a clear game/playthrough boundary; app reads and writes via a repository, not Dexie directly.

1.1 Types and Dexie schema ✅ Complete

  • Add TypeScript types for entities from docs/data-models.md: Game, Playthrough, Quest, Insight, Item, Person, Place, Map, Thread. Use a one file per entity and shared types for IDs and enums.
  • Define Dexie database: one Dexie instance with tables that mirror entities. Every table that is playthrough-scoped has playthroughId (and usually gameId); game-scoped tables have gameId only. Index gameId and playthroughId for queries.
  • Game vs playthrough tables (conceptual):
    • Game-scoped (intrinsic): games, quests, insights, items, persons, places, maps, threads — each row has gameId; survives "clear progress."
    • Playthrough-scoped (user): e.g. playthroughs, questProgress, itemState, notes, or similar — each row has playthroughId; cleared or replaced on new playthrough. (Exact table split can follow a first cut: e.g. store "progress" and "notes" in playthrough tables; entity definitions in game tables.)

1.2 Repository layer (redirectable) ✅ Complete

  • Implement one repository first (e.g. GameRepository): getAll(), getById(id), create(game), update(game), delete(id). Implementation uses Dexie; interface lives in src/lib/repositories/ or src/data/ so a future GameApiClient can implement the same interface.
  • Current game / current playthrough — Use Zustand (e.g. useAppStore) to hold currentGameId and currentPlaythroughId; optional persistence of "last selected" in localStorage for convenience. No auth; single user on this device.

Definition of done (Phase 1.2):

  • IGameRepository is defined and implemented by a Dexie-based implementation; getAll, getById, create, update, delete work against db.games.
  • Singleton gameRepository is exported from src/lib/repositories and is the only way feature code should access game data.
  • Zustand app store (useAppStore) holds currentGameId and currentPlaythroughId with setters; last selected is persisted in localStorage (keys: questloom-current-game-id, questloom-current-playthrough-id).
  • Reusable ID util (src/utils/generateId.ts) exists and is used by the game repository for create.
  • No direct Dexie usage outside src/lib/; npm run lint and npm run build pass.

Remaining for Phase 1: Proceed to 1.3 Minimal UI (game list / create game screen using gameRepository and useAppStore). Then validate 1.4 (repository used by UI, user can create and see games, selection persists).

1.3 Minimal UI ✅ Complete

  • Game list / create game — Single screen: list existing games (from Dexie via repository); button "New game" that creates a game and optionally a default playthrough, then sets it as current. Data flows: UI → repository → Dexie; UI reads from repository (or from a Zustand store that the repository updates).
  • Implemented: GameListScreen, CreateGameForm, minimal PlaythroughRepository; selecting a game sets it (and first playthrough) as current; selection persists in localStorage.

1.4 Definition of done (Phase 1) ✅ Complete

  • Types and Dexie schema in place; game and playthrough separation is clear in the schema.
  • At least one repository (games) implemented and used by the UI; no direct Dexie calls in components/stores. (Repository and store exist; UI in 1.3 consumes them.)
  • User can create a game and see it in a list; selection persists in memory and in localStorage. (1.3 Minimal UI complete.)
  • User can delete a game. App prompts for confirmation before delete, then deletes the game and all associated playthroughs.
  • Debug utility exists to purge the local database.
  • Debug utility exists to purge this app's local storage values.
  • App runs fully locally; no network required.
  • All files in docs are updated with the changes made in this phase.

Phase 2: Core Entities and CRUD ✅ Complete

Goal: All core entities (Quest, Insight, Item, Person, Place, Map, Thread) can be created, read, updated, and deleted in the app; data is scoped by game or playthrough as per data-models.

2.1 Game View ✅ Complete

  • When a game is set to current, rather than remaining on the game list with a "Current" badge shown, instead swap to the view for that game. In 2.1 it will only show the name of the current game and the current playthrough.
  • Clicking the app logo will navigate back to the game list, unsetting the current game and playthrough.
  • Update docs as needed to reflect this functionality.

View switching is state-based: when currentGameId is set, the app renders the game view (game name + playthrough name); when null, it renders the game list. No URL routing yet.

2.2 Playthrough Management ✅ Complete

  • While in the game view, the user is able to open a panel to manage their playthroughs.
  • The user can select a different available playthrough to swap to it as current.
  • The user can change the name of each playthrough.
  • The user can create a new playthrough, giving it a name and automatically swapping to it.
  • The user can delete a playthrough, with a confirmation dialog for safety. If this was the current playthrough, the user will automatically be swapped to the next available playthrough. If this was the last playthrough, a new playthrough will be automatically created and set as current.
  • Update docs as needed to reflect this functionality.

Implemented: Game view shows a button (current playthrough name) that opens a slide-out PlaythroughPanel. The panel lists playthroughs (with "Current" indicator), supports select (and closes panel), inline rename, create (form with name), and delete (ConfirmDialog). Delete of current swaps to first remaining or creates a "Default" playthrough if none remain; delete of non-current leaves selection unchanged. Playthrough repository extended with getById, update, and delete(id).

2.3 Repositories and Scoping ✅ Complete

  • Add repositories for: Quest, Insight, Item, Person, Place, Map, Thread. Each method is scoped by gameId (and playthroughId where the entity is playthrough-scoped). Follow the same interface pattern as Phase 1 so swapping to an API later only replaces the implementation.
  • Playthrough-scoped data — Decide which fields are "progress" (e.g. quest status, item status, notes) and store them in playthrough tables or in columns keyed by playthroughId; game tables hold only intrinsic definitions.
  • Update docs as needed to reflect this functionality.

Implemented: All seven entity repositories in src/lib/repositories/ with getByGameId, getById, create, update, delete, deleteByGameId. Quest/Insight/Item also expose progress/state get/upsert/deleteByPlaythroughId; EntityDiscoveryRepository for discovery. Thread supports optional playthroughId (game- vs playthrough-scoped). Game delete cascades to all game-scoped entities; playthrough delete cascades to questProgress, insightProgress, itemState, entityDiscovery, and playthrough-scoped threads.

2.4 Feature Modules and UI ✅ Complete

  • One feature per entity: features/quests/, features/insights/, features/items/, features/people/, features/places/, features/maps/, features/threads/. Each feature uses repositories and shared components.
  • Simple CRUD UI — Within the game view, list + create/edit/delete forms for each entity, scoped to the current game (and playthrough where relevant). Navigation: sidebar to switch between Quests, Insights, Items, People, Places, Maps, Threads (seven sections). No loom yet; focus on data entry and list/detail views.
  • Update docs as needed to reflect this functionality.

Implemented: GameView includes a sidebar (responsive: horizontal scroll on small screens, vertical on md+) and content area. Each section renders a list screen (QuestListScreen, InsightListScreen, ItemListScreen, PersonListScreen, PlaceListScreen, MapListScreen, ThreadListScreen) with create/edit forms and delete (ConfirmDialog). Shared components: PlacePicker, MapPicker, EntityPicker; getEntityDisplayName for thread labels. Quest/Insight/Item lists show and edit playthrough progress/state (status dropdown) when a playthrough is selected.

2.5 Definition of done (Phase 2) ✅ Complete

  • All entity types have repository APIs and Dexie persistence; game vs playthrough scoping is enforced.
  • User can select, create, edit, and delete playthroughs.
  • User can view, create, edit, and delete quests, insights, items, people, places, maps, and threads for the current game.
  • "New playthrough" clears only playthrough data; game data remains.
  • App remains fully local and redirectable (repositories are the only data access).
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Phase 2.5 validation: Repositories (Game, Playthrough, Quest, Insight, Item, Person, Place, Map, Thread, EntityDiscovery) live in src/lib/repositories/; Dexie is used only inside src/lib/. Playthrough delete cascades to quest progress, insight progress, item state, entity discovery, and playthrough-scoped threads; game data is unchanged. Creating a new playthrough adds a playthrough row only; game-scoped entities remain. Lint and format pass.

Items left for future action (see docs/issues/):


Phase 3: Threads and Loom View ✅ Complete

Goal: Users can create and view threads between entities; a loom (graph) view shows the network and supports "follow a thread" exploration.

3.1 Thread creation and listing ✅ Complete

  • Generalized thread representation — Threads are the primary representation of entity connections. The UI creates a representative thread when you link entities (quest giver, item location, place map) and keeps the existing entity field in sync (dual-write). Reserved thread labels: giver, location, map.
  • Thread repositorygetThreadsFromEntity(gameId, entityId, playthroughId?) and deleteThreadsInvolvingEntity(gameId, entityId) added. Deleting any entity (quest, insight, item, person, place, map) cascades to remove threads involving that entity.
  • UIEntityConnections component shows threads from an entity; each list screen (Quests, Insights, Items, People, Places) has an expandable row with a "Connections" button that reveals threads for that entity.

3.2 Loom (graph) view ✅ Complete

  • Loom View — Threads tab in game view is replaced with Loom tab, opening the Loom view. This is the primary view in which threads will be visualized. Connections section remains as part of the detail view for entities.
  • React Flow — Integrate to fulfill Loom view; nodes = entities (quest, insight, item, person, place, map), edges = threads. Load current game’s entities and threads via repositories; map to nodes/edges. Custom node component (EntityNode) shows entity type and key info.
  • Layout — Force-directed layout via d3-force (threads are relationship-focused, not hierarchical). Auto-layout on load.
  • Interactions — Select a node to highlight its edges; click an edge to focus/select source and target. Fit view control in the Loom. (Path highlight between two nodes can be a follow-up.)

3.3 Definition of done (Phase 3) ✅ Complete

  • Threads are created and stored; thread list and per-entity thread views work (Phase 3.1).
  • Loom view renders the graph for the current game; user can explore by following threads (Phase 3.2).
  • Still local-only; repositories unchanged for future redirect.
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Items left for future action (see docs/issues/):


Phase 4: Map View ✅ Complete

Goal: Give maps a dedicated experience: browse maps as a grid of previews, set map images via URL or upload, and view a selected map in a zoomable, pannable map view with intuitive sidebar tab behavior.

4.1 Map tab and map selection grid ✅ Complete

  • Map tab behavior — Ensure the existing Maps sidebar tab (from Phase 2) can represent two modes: selection (grid of maps) and map view (single map). Track map UI state in a store (e.g. useGameViewStore) with fields like mapUiMode: 'selection' | 'view' and lastViewedMapId.
  • Tab interaction rules — Implement logic so that:
    • When the user is on a different sidebar tab, clicking Maps opens the last viewed map view if lastViewedMapId is set; otherwise it opens map selection.
    • When the user is already in the map view, clicking the Maps tab switches back to map selection.
    • When the user is in map selection and chooses a map, the UI switches to map view and updates lastViewedMapId.
  • Selection grid — Replace the existing list-style maps screen with a responsive grid of tiles. Each tile shows the map name, a small preview of the map image (or a placeholder if none is set), and a subtle hover/focus state. Use Tailwind utilities and existing card components for visual consistency.
  • Selection actions — Clicking a tile opens the map view for that map. Keep create/edit/delete controls available from this grid (e.g. a toolbar button for "New map" and contextual actions per tile).

Implemented: Added a useGameViewStore to track mapUiMode and lastViewedMapId, updated GameView so the Maps sidebar tab toggles between the selection grid and the last viewed map according to the rules above, and refactored the maps feature into a responsive grid of map tiles with image previews, a toolbar “New map” button, and per-tile Edit/Delete actions; clicking a tile opens the corresponding map view and records it as last viewed.

4.2 Map create/edit: image sources ✅ Complete

  • Map image fields — Extend the Map entity and repository to support an image reference that can come from either a URL or an uploaded asset (e.g. imageSourceType: 'url' | 'upload', imageUrl?: string, imageBlobId?: string). Keep storage details encapsulated in the repository layer.
  • URL input — In the map create/edit form, add an "Image URL" option with validation (basic URL format, test fetch for preview). Selecting this option stores imageSourceType = 'url' and the provided URL; show a live preview thumbnail in the form.
  • Upload from disk — Add a file input control for image uploads (PNG/JPEG/WebP). When a file is chosen, read and store it via the repository (e.g. as a blob or file reference managed by Dexie), setting imageSourceType = 'upload'. Show upload progress where appropriate and render a preview once stored or buffered.
  • Drag and drop — Add a drag-and-drop zone on the create/edit form that accepts image files and routes them through the same upload pipeline as the file input. Highlight the drop zone on drag-over; reject non-image files with a user-friendly message.
  • Editing behavior — When editing an existing map, pre-populate the current image source, allow switching between URL and upload, and ensure the repository cleans up any orphaned uploaded blobs when the source changes or a map is deleted.

Implemented: Extended Map and CreateMapInput with optional imageSourceType, imageUrl, and imageBlobId; added Dexie mapImages table for uploaded blobs (schema v3). Map repository now exposes setImageFromUrl, setImageFromUpload, and clearImage, and deletes blob rows on map delete or source change. MapForm offers image source radios (None, URL, Upload), URL validation (http/https) with live preview, file input (PNG/JPEG/WebP, max 10 MB) with object-URL preview and "Remove image," and a drag-and-drop zone that routes the first valid image through the same pipeline. Create/edit flows persist and switch between sources; edit mode pre-populates current source and shows "Using uploaded image" when an upload is present. MapListScreen shows "Uploaded image" in the grid for maps with uploads; full blob display in MapView is deferred to 4.3.

4.3 Map view: render, zoom, and pan ✅ Complete

  • Map view screen — Introduce a dedicated MapView screen/component that takes a mapId, loads the map via the repository, and renders the map image in the main content area of the Maps tab.
  • Image rendering — Resolve the correct image source (URL vs uploaded blob) and display it at a suitable base zoom level. Handle loading and error states (e.g. failed URL fetch, missing image).
  • Zoom and pan — Implement client-side zoom and pan (e.g. via CSS transforms and pointer/mouse wheel handlers, or a lightweight pan/zoom helper library). Support mouse wheel and pinch zoom, click-and-drag (or touch drag) pan, and a "reset view" control to fit the map to the viewport.
  • Integration with tab behavior — Ensure that entering map view from the grid sets mapUiMode = 'view' and lastViewedMapId, and that the sidebar tab interactions defined in 4.1 correctly switch between selection and view modes without losing the current zoom/pan state when returning to the same map.
  • Accessibility and responsiveness — Make zoom/pan controls keyboard-accessible where practical, keep the map view usable on smaller screens, and ensure the map grid and map view share consistent styling with other feature tabs.

Implemented: Added getMapImageDisplayUrl(mapId) to IMapRepository and MapRepository to return a displayable URL for URL or uploaded blob (with optional revoke for object URLs). MapView loads the map and image via the repository, revokes object URLs on unmount or when switching maps, and shows loading / "No image set" / "Failed to load image" states. Zoom and pan use React state and CSS transform (scale, translate); wheel zooms toward cursor; pointer down/move/up with setPointerCapture for pan. Toolbar has Reset view, Zoom in, and Zoom out (keyboard-focusable, aria-labels). gameViewStore holds mapViewTransform per map and setMapViewTransform; MapView restores stored transform when re-entering a map and persists on pan end and zoom. Styling aligned with other feature tabs; pan/zoom area uses min-h-0 and flex-1 for responsiveness.

4.4 Map entity and loom integration ✅ Complete

  • Map as view-only entity — Clarified in docs/data-models.md and types/repositories that the Map entity exists primarily to back the map view experience and markers, and does not appear as a node in the Loom or as a thread endpoint. The loom graph hook now omits EntityType.MAP nodes entirely.
  • Place as map representative — Established Place as the entity type used in threads and the Loom to represent locations and maps. UI copy and behavior now reinforce that threads connect places (and other entities), not maps; maps are represented via their associated top-level place.
  • Top-level place per map — Extended map creation logic so that creating a map automatically creates a corresponding top-level Place (e.g. "Map: Tavern District") associated with that map. The association is stored bidirectionally (Map.topLevelPlaceIdPlace.map), with Dexie schema v4 adding an index for topLevelPlaceId on the maps table.
  • Loom node adjustments — Updated the Loom view configuration so that nodes of type MAP are no longer rendered; the existing place nodes represent all locations, including each map’s top-level place. Legacy map-related threads are ignored by the Loom since there are no map nodes.
  • Cascade on map delete — When a map is deleted, the map repository now deletes any uploaded image blobs and all places scoped specifically to that map (including the associated top-level place), reusing existing cascading delete patterns so threads involving those places are cleaned up consistently.
  • Name synchronization — Implemented two-way name syncing such that renaming a map also renames its associated top-level place (with a stable "Map: " prefix), and renaming that top-level place updates the underlying map name while keeping the prefix on the place only. Shared helpers in src/utils/mapNames.ts enforce consistent prefix handling and avoid duplicated prefixes.

Implemented: The loom graph hook (useLoomGraph) now loads only quests, insights, items, people, and places as entities, omitting maps so they never appear as Loom nodes. The Map type gained a topLevelPlaceId field and Dexie schema v4 added an index on this field. MapForm orchestrates creation and editing of maps so that each map has exactly one top-level place with a normalized "Map: " prefix, and a shared utility ensures map names themselves never store the prefix. PlaceForm treats top-level places specially: when editing, it shows the underlying map name (without prefix), keeps the map link fixed, and maintains bidirectional name sync; for non–top-level places, assigning a map also creates a representative thread from that place to the map’s top-level place using the reserved map label. Tooltips on the disabled map picker clarify when a place is acting as the top-level representation of its map.

4.5 Map markers: data and display ✅ Complete

  • Marker model — Introduce a MapMarker data model and repository keyed by game (and optionally playthrough) that links a mapId and an entity endpoint (entityId) with a persistent position stored in a map-local logical coordinate space. Coordinates are finite numbers but are not clamped to the image bounds so markers can exist at or beyond the map image’s periphery. Markers also support an optional short label/description so entities can have multiple differentiated markers (for example, multiple locations or narrative states). Existing markers are not automatically adjusted when map images change size or aspect ratio; robust re-alignment is deferred to a later phase.
  • Eligible entities — Ensure that all entities except maps and threads can have markers (same set as THREAD_ENDPOINT_ENTITY_TYPES from EntityType.ts). Guardrails in the marker creation UI will be added in Phase 4.6; the repository already enforces eligibility.
  • Initial marker rendering — In MapView, load markers for the current map and render them on top of the image at their logical positions, transforming them alongside zoom and pan so they remain correctly aligned within the map’s coordinate space.
  • Basic marker styling — Implement simple, readable marker visuals for this phase: small circular badges where the marker color is tied to the entity type and the first letter of the entity’s name is displayed inside.
  • Tooltips — On hover or focus, show a minimal tooltip via the native title attribute on the marker with the full entity name, resolved through getEntityDisplayName.

Implemented: Standalone MapMarker type and mapMarkers Dexie table (schema v5); IMapMarkerRepository and mapMarkerRepository with getByMapId, create, update, delete, deleteByMapId, deleteByEntity. Map delete and entity deletes cascade to markers. Positions are stored as logical coordinates (0–1 or unbounded); MapView renders by scaling to the image’s intrinsic size so "Reset view" fits the map and markers stay aligned. MapView uses a transform wrapper sized to the image (not the viewport) so zoom/pan and fit-to-view behave correctly. Markers render as MapMarkerBadge (entity-type color, first letter of entity name, native tooltip; optional label in tooltip). Map and marker content use select-none to prevent text selection. A temporary debug "Add test marker" button creates a marker for a random entity at a random position for validation; remove in Phase 4.6.

4.6 Map markers: interaction and context menu ✅ Complete

  • Remove temporary debug — Remove the Phase 4.5 temporary debug control from MapView (the "Add test marker" button and related state/handler). It is flagged in code with "REMOVE in Phase 4.6" and exists only to validate marker behavior before the context menu is implemented.
  • Pan vs move safeguards — Default interactions prioritize safe panning: click-and-drag on the map pans, clicking a marker selects it. Moving a marker requires an explicit action (e.g. "Move marker" from a context menu) so markers are not accidentally dragged while panning. Middle mouse button always pans, with no selection behavior.
  • Map context menu — Implement a right-click (or long-press on touch) context menu on the map background that anchors at the clicked location and lists actions relevant to that point.
  • Add marker here (existing entity) — From the context menu, allow the user to "Add marker here" for an existing entity by opening a lightweight picker limited to eligible entity types. On selection, create a MapMarker at that location for the chosen entity.
  • Add marker here (new entity) — Support creating a new entity (e.g. place, item, person, quest, insight) and placing its marker in a single flow (modal or side panel). After creation via the appropriate repository, automatically create a MapMarker at the context menu location.
  • Marker context menu — When right-clicking on an existing marker (or long-press on touch), show a marker-specific context menu with actions including Move marker and Delete marker (with variations below).
  • Move marker flow — Choosing "Move marker" enters a move mode where the marker visually attaches to the cursor; panning is restricted to the middle mouse button. On the next click (mouse up or tap), the marker’s position is updated to the new location and move mode ends; ESC cancels and restores the original position.
  • Delete marker only — Provide an option to delete only the marker while leaving the underlying entity intact. This removes the MapMarker record but does not touch the entity or its threads.
  • Delete marker and entity — Provide an option to delete both the marker and the associated entity, with clear confirmation text describing cascading consequences. Reuse existing entity delete flows so all associated threads and discovery data are removed consistently.

Implemented: Debug control removed; clientToLogical helper added (no clamping, periphery supported). Pan only on left/middle on map background; middle always pans; left on marker does not pan. Reusable ContextMenu component; map context menu (right-click or 500 ms long-press) with "Add marker here (existing entity)" (modal: type + EntityPicker, then create marker) and "Add marker here (new entity)" (modal: type + name/title, optional location for Item, then create entity + marker). Marker context menu: Move marker (enters move mode), Delete marker only (ConfirmDialog, then mapMarkerRepository.delete), Delete marker and entity (ConfirmDialog danger, then entity repo delete). Move mode: marker follows cursor via moveModePendingPosition, commit on pointer up, ESC cancels. Long-press opens the same context menus on touch.

4.7 Definition of done (Phase 4) ✅ Complete

  • The Maps sidebar tab supports both a selection grid and a map view, with tab clicks behaving as specified (toggle selection/view; return to last viewed map when coming from other tabs).
  • The map selection experience is a grid of tiles showing map names and image previews, with create/edit/delete actions available.
  • Creating or editing a map allows setting the image via URL, file upload, or drag-and-drop, and the image is persisted via the map repository.
  • The map view renders the selected map image and supports smooth zoom and pan interactions.
  • Maps are represented in the Loom via associated top-level places; maps themselves do not appear as Loom nodes or thread endpoints.
  • Each map has a top-level place that is created, renamed, and deleted in lockstep with the map, with Loom and thread data (including representative map threads from non–top-level places) updating accordingly.
  • Map markers are stored as persistent data linked to maps and non-map/thread entities, rendered on the map with simple type-colored visuals and tooltips (Phase 4.5).
  • Users can add, move, and delete markers via deliberate interactions and a context menu, including flows that create new entities at a location or delete entities with full cascading behavior (Phase 4.6).
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Items left for future action (see docs/issues/): map-marker-realignment.md — Re-align or prompt when map image is changed (size/aspect ratio).


Phase 5: Contextual Progression and Spoiler Safety

Goal: Surface "what you can do next" from current items/insights and position; support progression-gated availability and place connectivity (Paths and direct Place–Place); unified status and requirement model; hide information until the user has the right progression (spoiler-friendly).

5.1 Playthrough-scoped status enums (Item, Insight, Quest, Person) ✅ Complete

Assume no legacy content; status changes are breaking and can be applied directly.

  • Item statusNot acquired (default for new items and new playthroughs), acquired (renamed from "possessed"; only this status counts as owned for fulfilling requirements), used, lost. Remove the "other" status entirely.
  • Insight statusUnknown (default; renamed from "active"), known (renamed from "resolved"; only this status qualifies for requirements targeting the insight), irrelevant. Only "known" satisfies requirement checks.
  • Quest statusAvailable (new; quest is available but not yet active; default), active (in progress), completed, abandoned (new; failed/forfeit/uncompletable). Remove "blocked"; use the generalized requirement logic (5.2) instead so unavailability is derived from requirements.
  • Person statusAlive (default), dead, unknown. Person status is playthrough-scoped (e.g. PersonProgress or similar) so it can change during a playthrough.
  • Data and UI — Update ItemStatus, InsightStatus, QuestStatus and add Person status type and playthrough-scoped storage; ensure repositories and UI use the new values and defaults. New playthroughs and new entities get the specified defaults.

5.2 Entity requirements (thread-based) and availability ✅ Complete

  • Requirements tracked with threads — Requirements are tracked with threads using subtype (ThreadSubtype.REQUIRES for entity-level, ThreadSubtype.OBJECTIVE_REQUIRES for quest objective dependency). Display labels are "Requires" and "Objective" from getThreadSubtypeDisplayLabel. Threads are visible on the Loom. Satisfaction uses a configurable allowed status set per target type (defaults: Item → [acquired], Insight → [known], Quest → [completed], Person → [alive]).
  • Unavailable (derived) — If the player does not meet an entity’s requirements, that entity is unavailable. Unavailability is a derived boolean (computed from playthrough state and requirement threads), not a stored status; it updates automatically when playthrough state or requirements change.
  • Scope — Apply to quests (e.g. insight/item required to start), items (item required to acquire), Path unlock/traversal, and optionally visibility. Replacing "blocked" quest status with derived unavailability from requirements (5.1).
  • Quest objectives as requirement-like — Objectives can have entityId and allowedStatuses; completability is derived, completion is manual (checkbox). Objective dependencies are dual-written to threads with subtype: ThreadSubtype.OBJECTIVE_REQUIRES and appear on the Loom. Objective links and allowed statuses are configured in QuestForm (objectives section); threads are synced on quest save.

Implemented: Thread stores subtype (ThreadSubtype), requirementAllowedStatuses, and objectiveIndex; reserved subtypes REQUIRES and OBJECTIVE_REQUIRES; default allowed sets in defaultAllowedStatuses; getRequirementThreadsFromEntity; requirement evaluation and checkEntityAvailability; quest/item list Unavailable and unmet-requirement names; Loom distinct edge labels/styling via getThreadSubtypeDisplayLabel. Entity-level requirements: created and edited from each entity's detail view via RequirementForm inside RequirementList (expand a row in Quest/Insight/Item/Person/Place list → Requirements block: Add/Edit/Delete). Objective requirements: configured in QuestForm (objective entity link + allowed statuses), synced to threads on save. The Thread list screen is not in the UI (Loom tab shows the graph only); ThreadForm exists but is unreachable for creating requirements.

5.3 Path entity: data model, status, and connectivity ✅ Complete

  • New entity: Path — Introduce a Path entity that connects only to Places. Connectivity is expressed as: Place ↔ Thread ↔ Path ↔ Thread ↔ Place. Paths are game-scoped (intrinsic world structure).
  • Path status — Paths have a status (modeled as playthrough-scoped PathProgress.status) with three values:
    • Restricted — Untraversable unless requirements are met (e.g. locked door; key required to pass).
    • Opened — Traversable regardless of requirements (e.g. door unlocked; can pass freely).
    • Blocked — Untraversable regardless of requirements (e.g. bridge collapsed; no longer crossable).
  • Cardinality — A Path can be connected to two or more Places (multiple Threads from the same Path to different Places).
  • Per-connection traversal — Paths may have different traversal requirements per connection/thread (e.g. ledge requiring grappling hook to go up, trivial to jump down). Each Place–Path thread can carry optional traversal conditions via requirementAllowedStatuses; evaluation is directional and will be applied in Phase 5.5 reachability logic.
  • Map markers — Paths can have map markers (same mechanism as other marker-eligible entities).
  • Map-to-map transitions — Paths can connect top-level map places to other top-level map places, acting as transitions between maps.
  • Location rule — Other entities can only be located at a Place, not at a Path.
  • Place–Place direct connectivityPlaces can be connected directly to other Places (Place ↔ Thread ↔ Place) without an intermediate Path. Direct Place–Place links imply unimpeded movement with no requirements; the player can move between them freely. To introduce requirements between places, use a Path (with status and requirement semantics) instead.

Implemented: Added a game-scoped Path entity (PathId, Path type, Dexie paths table) and playthrough-scoped PathProgress with PathStatus (RESTRICTED, OPENED, BLOCKED) and a Dexie pathProgress table (schema v7). Extended EntityType with PATH, updated THREAD_ENDPOINT_ENTITY_TYPES, parseEntityId, entity type labels, default allowed status maps, and color helper so Paths participate in threads and markers. Introduced ThreadSubtype.CONNECTS_PATH and ThreadSubtype.DIRECT_PLACE_LINK with display labels and documented semantics; Place–Path connections use CONNECTS_PATH and can carry optional per-connection traversal requirements via requirementAllowedStatuses, while direct Place–Place links use DIRECT_PLACE_LINK for unimpeded movement. Updated MapMarker and its repository contracts so Paths are marker-eligible alongside other endpoint entities, and refreshed docs/data-models.md to describe Path, PathProgress, status semantics, connectivity (Place–Path and direct Place–Place), the location rule (entities located only at Places), and map-to-map transitions.

5.4 Path: repository, Loom, and UI ✅ Complete

  • Repository and types — Add Path type, PathId, Path status enum, and PathRepository (CRUD, scoped by gameId). Threads can reference Path as source or target (extend EntityType to include Path). Cascade: deleting a Path removes its threads; deleting a Place removes threads that involved that Place (existing behavior).
  • Loom behavior — Paths appear as nodes; edges are Threads (Place–Path, Path–Place) and direct Place–Place threads (unimpeded). Traversability: direct Place–Place links are always traversable; for Paths, only opened Paths, or restricted Paths whose requirements are met, are traversable (per-path conditions apply). Blocked Paths are never traversable. Non-traversable Path connections are greyed out so they visually recede.
  • UI — Feature module for Paths: list, create, edit, delete. When editing a Path: name/description, status (restricted / opened / blocked), and requirement(s) for restricted Paths. UI shows connections for each Path via threads and supports configuring entity-level requirements; map marker creation supports Path as an eligible entity type.
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Implemented: Introduced a Dexie-backed PathRepository with full CRUD and playthrough-scoped PathProgress (status) APIs, including cascades to threads and map markers on delete. The Loom graph (useLoomGraph) now loads Path entities and Path progress, renders Paths as nodes, and styles Place–Place (DIRECT_PLACE_LINK) and Place–Path (CONNECTS_PATH) edges based on traversability (opened vs blocked/restricted with unmet requirements) using Path status plus entity-level requirement evaluation. A new Paths feature module (PathListScreen, PathForm) appears as a Paths tab in the game view sidebar, providing list/create/edit/delete for paths, playthrough-scoped status editing, and an inline connections editor in PathForm that creates and removes Place–Path connectivity threads. Each path row in the list also surfaces connections (EntityConnections) and entity-level requirements (RequirementList) so requirements for traversing a path can be configured alongside its connections. Map marker flows now fully support Paths as marker endpoints (selection, tooltip naming, and “delete marker and entity”), and lint/build continue to pass.

5.5 Current position and reachability ✅ Complete

  • Current position — Playthrough has a current position (a Place). The user can freely update it to any Place. Current position is the start location for Loom traversal.
  • Reachability — From current position, follow direct Place–Place links (always traversable) and traversable Paths (opened, or restricted with requirements met; per-connection conditions applied). The set of reachable Places is derived from this graph.
  • Unreachable Place → unavailable — If a Place cannot be reached, any entity located at that Place is unavailable (in addition to requirement-based unavailability from 5.2).
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Note: Temporary reachability debug logging was removed in Phase 5.6 when Loom and Map began consuming reachability for availability styling (see reachability-debug-logging-removal.md).

5.6 Location at Place and availability ✅ Complete

  • Eligibility — Any entity that can have a map marker can be located at one or more Places via LOCATION threads (thread-only; no location field on any entity). If all of an entity's location Places are unreachable (per 5.5), that entity is unavailable. An entity is unavailable if (1) its requirements are not met (5.2), or (2) it has at least one location Place and none of them are reachable.
  • Availability representation — Unavailable/unreachable entities keep their type-colored styling but are visually de-emphasized (desaturated) in the Loom and Map views so the type remains readable while signalling that they cannot currently be acted on. This updates as playthrough state changes.
  • Consistent location model — Item no longer has a location field; Quest, Insight, Item, Person all use LOCATION threads only; multiple places per entity supported.
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

5.7 Contextual progression ✅ Complete

  • Logic — Given current playthrough state (item/insight/quest/person statuses, current position, reachable Places, Path status and traversability), compute "actionable" threads or next steps. Display in a dedicated section or in the Loom (e.g. highlight actionable edges, "what you can do next").
  • Integration — Use availability (5.2, 5.6) and reachability (5.5); only suggest entities and threads that are available and (where relevant) reachable. Use acquired items, known insights, and new status enums consistently.
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Implemented: MainViewType enum (Quests, Loom, Maps, Oracle, Places, Paths, Items, People, Insights, Threads) drives the game view sidebar and content. Oracle is a sidebar tab whose content shows "what you can do next" in the main panel (actionable entities: start quest, complete objective, acquire item, mark insight known, open path). Actionable route edges in the Loom are the shortest traversable paths from current position to actionable nodes (teal emphasis). Actionable map markers use the same teal ring emphasis. Logic in src/lib/contextualProgression/; hook useActionableNextSteps; Oracle component and GameView/Sidebar/Content refactored to MainViewType.

5.8 Spoiler visibility

  • Rules — Define which entities/insights/threads (and optionally Paths) are visible only after certain conditions. Store visibility rules with game data; evaluate against playthrough state. Use for filtering in lists and in the Loom (hide or soften not-yet-visible nodes/edges).
  • Hidden state styling — When spoiler protection hides an undiscovered entity, its Loom node and map markers use a neutral grey style and generic labelling so the underlying entity type color (and therefore type) is not revealed; discovery records drive this state.
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

5.9 Place regions on the map

  • Polygonal regions — In addition to map markers, Places can be represented as polygonal regions on the map. A region has any number of points (e.g. default 4). Regions render as faint outlines with the place name printed faintly at the center.
  • Hover — When mousing over the region, the outline and name become more visible.
  • Undiscovered — Places that are undiscovered (playthrough discovery state) can render as blacked out so the underlying map is obscured.
  • Editing region points — Region points are editable similar to map markers:
    • Enter edit mode — Right-click within the region; context menu option Edit region points. Points become visible and can be clicked and dragged to move.
    • Delete point — Right-click a point; context option to delete that point.
    • Add point — Right-click a region edge; context option to add a point at that position on the edge.
    • Exit edit modeEscape or a context menu option finish editing; shows confirmation dialog before saving or discarding the updated polygon and exits edit mode.
  • Data — Extend Place (or a related map-specific structure) to store one polygonal region per (Place, Map): ordered list of logical coordinates. Persist and load with the map view; support create/edit/delete of regions.
  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

5.10 Definition of done (Phase 5)

  • Item status: not acquired (default), acquired (renamed from possessed), used, lost; "other" removed. Only acquired items fulfill item requirements. Insight: unknown (default), known (only one qualifying for requirements), irrelevant. Quest: available (default), active, completed, abandoned; blocked removed. Person: alive (default), dead, unknown (5.1).
  • Requirements are tracked with threads (visible on Loom); unavailability is derived from requirement threads and playthrough state. Quest objectives can be tied to entity status for completability (5.2).
  • Path entity exists with status: restricted, opened, blocked. Place–Place direct links allowed (unimpeded); requirements only via Paths. Paths support per-connection traversal, map markers, map-to-map transitions (5.3, 5.4).
  • Playthrough has current position (Place); user can set it; it is the start for reachability; direct Place–Place and traversable Paths define reachable Places; unreachable Place ⇒ entities there unavailable (5.5, 5.6).
  • "What I can do next" is visible and driven by statuses, position, reachability, and Path traversability (5.7).
  • Spoiler gating hides or softens content until conditions are met (5.8).
  • Places can be represented as polygonal regions on the map; faint outline and name; hover more visible; undiscovered blacked out; region points editable (right-click in region/point/edge, add/delete/move, save/exit) (5.9).
  • Data and logic remain local; repository interfaces updated only for Path, playthrough position, status enums, and place regions as specified.
  • All documentation pages (including data-models.md, design-spec.md, features.md) are updated.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.

Phase 6: Polish and Redirectability Checklist

Goal: App is stable for daily use; codebase is ready to plug in a hosted backend when needed.

  • Maps — Optional full-screen map view and visual/interaction polish for the map markers and map view introduced in Phase 4 (e.g. refined marker design, animations, advanced filtering, or layering), plus any additional map-related polish not covered earlier.
  • Responsive and a11y — Touch-friendly controls, basic keyboard navigation, and semantic markup so the app works on tablet/phone during play.
  • Redirectability — Document repository interfaces; add a thin "data source" abstraction if helpful (e.g. createLocalDataSource() vs future createRemoteDataSource(baseUrl) that return the same repository interface). No backend code required yet; just a clear boundary so adding API clients later is a contained change.

6.1 Definition of done (Phase 6)

  • All documentation pages are updated reflecting the latest state of the app.
  • All items left to do are documented for future action.
  • All affected code passes code standards, style, and lint.