- Fully operational locally — App runs and persists data with no backend; usable as a personal tool during development.
- Local functionality as soon as possible — Bootstrap the app and core flows first; iterate on polish and advanced features.
- Redirectable to hosted — Data access is behind an abstraction so the same app can later use a hosted API without rewriting feature code.
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.
Goal: Run the app locally with a minimal shell; establish tooling and structure.
Status: Validation steps succeeded; node/npm are available for subsequent phases.
- 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.
- Tailwind CSS:
npm install -D tailwindcss postcss autoprefixerthennpx tailwindcss init -p; configuretailwind.config.jscontent for./index.htmland./src/**/*.{js,ts,jsx,tsx}; add Tailwind directives tosrc/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(anddexie-react-hooksif you want reactive queries in React).
- Create folders under
src/:components/,features/,hooks/,stores/,types/,utils/, and optionallylib/(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.
-
npm run devruns and shows a QuestLoom shell (header + main area). -
npm run buildsucceeds. - 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 codeImmediate next steps (in order):
- Scaffold Vite + React + TS in the repo (or merge from a temp
viterun). - Install Tailwind, configure it, add a minimal global layout in
App.tsx. - Install Zustand and Dexie; add
src/types/andsrc/lib/(orsrc/data/) for future repositories. - Add ESLint + Prettier and a single script to run lint.
Goal: Persist one entity type in IndexedDB with a clear game/playthrough boundary; app reads and writes via a repository, not Dexie directly.
- 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
Dexieinstance with tables that mirror entities. Every table that is playthrough-scoped hasplaythroughId(and usuallygameId); game-scoped tables havegameIdonly. IndexgameIdandplaythroughIdfor queries. - Game vs playthrough tables (conceptual):
- Game-scoped (intrinsic):
games,quests,insights,items,persons,places,maps,threads— each row hasgameId; survives "clear progress." - Playthrough-scoped (user): e.g.
playthroughs,questProgress,itemState,notes, or similar — each row hasplaythroughId; 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.)
- Game-scoped (intrinsic):
- Implement one repository first (e.g.
GameRepository):getAll(),getById(id),create(game),update(game),delete(id). Implementation uses Dexie; interface lives insrc/lib/repositories/orsrc/data/so a futureGameApiClientcan implement the same interface. - Current game / current playthrough — Use Zustand (e.g.
useAppStore) to holdcurrentGameIdandcurrentPlaythroughId; optional persistence of "last selected" inlocalStoragefor convenience. No auth; single user on this device.
Definition of done (Phase 1.2):
-
IGameRepositoryis defined and implemented by a Dexie-based implementation;getAll,getById,create,update,deletework againstdb.games. - Singleton
gameRepositoryis exported fromsrc/lib/repositoriesand is the only way feature code should access game data. - Zustand app store (
useAppStore) holdscurrentGameIdandcurrentPlaythroughIdwith setters; last selected is persisted inlocalStorage(keys:questloom-current-game-id,questloom-current-playthrough-id). - Reusable ID util (
src/utils/generateId.ts) exists and is used by the game repository forcreate. - No direct Dexie usage outside
src/lib/;npm run lintandnpm run buildpass.
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).
- 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, minimalPlaythroughRepository; selecting a game sets it (and first playthrough) as current; selection persists in localStorage.
- 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.
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.
- 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.
- 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).
- Add repositories for: Quest, Insight, Item, Person, Place, Map, Thread. Each method is scoped by
gameId(andplaythroughIdwhere 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.
- 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.
- 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/):
- item-unpossessed-status.md — Default "unpossessed" item status.
- status-labels-reusability.md — Elevate status labels for reuse across list/detail views.
- repetitive-prop-definitions.md — Generic Create/Edit props for entity forms.
Goal: Users can create and view threads between entities; a loom (graph) view shows the network and supports "follow a thread" exploration.
- 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 repository —
getThreadsFromEntity(gameId, entityId, playthroughId?)anddeleteThreadsInvolvingEntity(gameId, entityId)added. Deleting any entity (quest, insight, item, person, place, map) cascades to remove threads involving that entity. - UI —
EntityConnectionscomponent 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.
- 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.)
- 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/):
- loom-path-highlighting.md — Highlight paths between two entities in the Loom view.
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.
- 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 likemapUiMode: 'selection' | 'view'andlastViewedMapId. - 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
lastViewedMapIdis 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.
- When the user is on a different sidebar tab, clicking Maps opens the last viewed map view if
- 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.
- Map image fields — Extend the
Mapentity 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.
- Map view screen — Introduce a dedicated
MapViewscreen/component that takes amapId, 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'andlastViewedMapId, 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.
- Map as view-only entity — Clarified in
docs/data-models.mdand types/repositories that theMapentity 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 omitsEntityType.MAPnodes entirely. - Place as map representative — Established
Placeas 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.topLevelPlaceId↔Place.map), with Dexie schema v4 adding an index fortopLevelPlaceIdon the maps table. - Loom node adjustments — Updated the Loom view configuration so that nodes of type
MAPare 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 insrc/utils/mapNames.tsenforce 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.
- Marker model — Introduce a
MapMarkerdata model and repository keyed by game (and optionally playthrough) that links amapIdand 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_TYPESfromEntityType.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
titleattribute on the marker with the full entity name, resolved throughgetEntityDisplayName.
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.
- 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
MapMarkerat 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
MapMarkerat 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
MapMarkerrecord 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.
- 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).
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).
Assume no legacy content; status changes are breaking and can be applied directly.
- Item status — Not 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 status — Unknown (default; renamed from "active"), known (renamed from "resolved"; only this status qualifies for requirements targeting the insight), irrelevant. Only "known" satisfies requirement checks.
- Quest status — Available (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 status — Alive (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,QuestStatusand 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.
- Requirements tracked with threads — Requirements are tracked with threads using subtype (
ThreadSubtype.REQUIRESfor entity-level,ThreadSubtype.OBJECTIVE_REQUIRESfor quest objective dependency). Display labels are "Requires" and "Objective" fromgetThreadSubtypeDisplayLabel. 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
entityIdandallowedStatuses; completability is derived, completion is manual (checkbox). Objective dependencies are dual-written to threads withsubtype: ThreadSubtype.OBJECTIVE_REQUIRESand 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.
- 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 connectivity — Places 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.
- 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.
- 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).
- Eligibility — Any entity that can have a map marker can be located at one or more Places via LOCATION threads (thread-only; no
locationfield 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
locationfield; 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.
- 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.
- 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.
- 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 mode — Escape 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.
- 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.
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 futurecreateRemoteDataSource(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.
- 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.