# M1 PR3 — Shell UI & Action Column Design Spec > **Status:** Approved by ginnoir (brainstorm 2026-06-11). > **Branch:** `feat/m1-progression` off `main` (first chunk before automation/prestige/content). > **Supersedes (partially):** PR2 story overlay UX in `docs/superpowers/specs/2026-06-11-m1-pr2-playable-loop-design.md` §Story UI. > **Parent plan:** `docs/plans/2026-06-11-m1-vertical-slice.md` §PR3. ## Summary PR3 opens with a **shell refactor** that fixes PR2 UX debt before progression/content land: replace the full-screen story overlay with a three-region layout (nav rail / center panel / right rail), reorganize Play into **behavior-type action columns** with collapsible theme groups, move **all story forks to actions** (Story tab is read-only prose + branching tree), and establish **universal automation** with **shareable text recipes** referencing content action ids. ## What we decided (brainstorm) | Topic | Decision | |---|---| | Timing | **First chunk of PR3** — shell lands at start of `feat/m1-progression`, then T3.1 automation, T3.2 prestige, T3.3 content | | Story choices | **Actions only** — Story tab has no choice buttons | | Story tab | **Split pane:** branching tree (larger, ~60%) + long-form prose log (~40%); tree left, log right | | App shell | **Left nav** (Play, Story, Settings, About) → **center** active panel → **right rail** on Play (resources + inventory + optional event log) | | Action layout | **Columns by behavior kind**, collapsible **theme/purpose groups** inside each column | | Column order | **Instant → Loop → Timed → Story → Context** (Timed centered as primary column) | | Loop vs automation | **Loop** = idle filler while playing (queue empty, nothing timed active). **Automation** = hands-off repeat queue including offline | | Automation scope | **Universal** — instant, loop, timed, story, context all automatable **after first manual completion** on that action | | Story automation | Same rule — first time at a fork is conscious; after performing a branch once, that story action can be automated on repeat runs | | Automation sharing | Recipes **export/import as text**; steps reference stable **content action ids** | | Event log | **Optional** on Play right rail (collapsible + pref to hide; default visible) | | Mobile | Column stacking + responsive Story split deferred to PR4 (#10) | | Approach | **Shell first, typed columns second** within PR3 opener (T3.0) | ## Problem statement (PR2 debt) 1. **Readability** — full-screen story overlay (`bg-black/80`) ghosts underlying UI; hard to read. 2. **Redundant choice paths** — story panel choice buttons and Play actions both visible; unclear authority. 3. **Structural mismatch** — single centered column does not match intended Chronicle-style shell (nav / main / sidebar). 4. **Flat action list** — no distinction between instant, idle loop, timed queue, story decisions, or context switches. ## App shell ``` ┌──────────┬─────────────────────────────┬──────────────┐ │ Nav rail │ Center (active panel) │ Right rail │ │ │ │ (Play only) │ │ ● Play │ Play → action columns │ Resources │ │ Story │ Story → tree │ prose log │ Inventory * │ │ Settings │ Event log † │ │ About │ Settings / About → content │ │ └──────────┴─────────────────────────────┴──────────────┘ * Inventory placeholder = resource list until item system exists † Collapsible; pref to hide (default: visible) ``` ### Nav rail - Fixed left column; icons + labels on desktop, icons-only acceptable on narrow widths until PR4. - **Story unread badge** when new beats arrive (replaces overlay auto-open as primary signal). - `storyOpenMode` pref remapped: `auto` switches to Story nav or pulses badge instead of opening overlay. ### Center panel Swaps content by active nav item. No full-screen modal for story. ### Right rail (Play only) - **Resources** — always visible. - **Inventory** — resource list placeholder in M1; item grid later. - **Event log** — collapsible section; `showEventLog` pref (default `true`). ### Removed - `StoryPanel` full-screen overlay with choice buttons. - Header Story / Settings buttons replaced by nav rail (Settings may retain drawer or become full center panel — implementer picks cleaner fit). ## Play panel — action columns ### Column order (left → right) | # | Column | `kind` | Role | |---|---|---|---| | 1 | Instant | `instant` | One-shot interactions: buy, sell, trade | | 2 | Loop | `loop` | Idle upkeep between tasks (rest, passive recovery) | | 3 | **Timed** | `timed` | Primary gameplay; manual queue + progress bars | | 4 | Story | `story` | Narrative forks and hard decisions | | 5 | Context | `context` | Area changes, combat entry, context switches | Timed column is visually central. Narrow viewports: horizontal scroll with Timed near viewport center. ### Groups within columns Each action belongs to a **group** (content-defined theme/purpose): ```typescript group: { id: string // e.g. 'camp', 'tavern', 'buy' label: string // e.g. 'Camp activities', 'Buy' } ``` - Groups render as **collapsible sections** inside their kind column. - Examples: - **Instant:** Buy, Sell, Trade - **Loop:** Camp activities, Travel activities - **Timed:** Library, Tavern, Camp (location-themed) - **Story:** Arc-specific decision groups - **Context:** Region / combat entry groups - Collapse state stored in `GamePrefs.collapsedActionGroups: Record`. ### Action card UX (unchanged from PR2 where applicable) - `storyHint` inline subtitle (default) / hover / info-button per pref. - `storyTooltip` for mechanics and fork consequences. - **Story-kind actions** get distinct styling (amber fork badge, border) so diverging paths are obvious in Play. ## Action kinds — engine behavior ### `instant` - Click → validate afford/unlock → apply costs/yields immediately. - No queue slot, no duration. ### `timed` - Current PR1/PR2 behavior: `durationMs`, manual `actionQueue`, progress bar. - Primary manual play column. ### `loop` - Runs only when **`activeActionId` is null** and **manual `actionQueue` is empty**. - Player enables/disables per loop action (toggle); priority order when multiple enabled (content `loopPriority` number, lower first). - On completion while still idle, immediately repeats same loop action. - Respects costs; stops if unaffordable. - **Does not run offline** — idle-present behavior only. ### `story` - Represents one graph choice. Content field `storyChoiceId` links to `StoryChoice.id`. - On execute → `applyChoice(state, content, storyChoiceId)` → sibling story actions hide via flags/requirements. - First visit: only available choices shown; player picks consciously. - After first manual completion of that action id: eligible for automation queue. ### `context` - Switches active game context (location, combat scene, etc.). - M1: stub until opening arc requires it; schema + column chrome ship in T3.0. ## Automation (T3.1 — universal) ### Philosophy Once the player has **completed an action manually at least once**, they may add it to an **automation queue** that repeats hands-off, **including offline** (within existing offline cap). Applies to **all kinds** including `story` (after that specific branch was taken once). New/unseen branches remain manual until first completion. Prestige/knowledge catch-up (T3.2) treats previously cleared routes as already completed for automation unlock purposes — same knowledge model as story fast-forward. ### Queues | Queue | Purpose | |---|---| | `actionQueue` | Manual timed queue (player actively planning) | | `automationQueue` | Hands-off repeat pipeline | Automation runner executes when manual queue is idle (exact precedence documented in engine; automation fills gaps like a background worker, distinct from loop-kind idle actions — both may need priority rules: **manual > automation > loop**). ### Automation recipe export/import Recipes are **shareable text** referencing stable **content action ids** (not display names). **Canonical multi-line format (v1):** ```text idlegame-recipe/v1 # name: Early camp grind gather_supplies scout_path rest_briefly ``` - Line 1: magic header with version (`idlegame-recipe/v1`). - Optional `# name:` comment for human label (ignored by parser if malformed). - One action id per non-empty, non-comment line. - Lines must match `/^[a-z][a-z0-9_]*$/` (same id convention as content defs). **Single-line alias (optional parser support):** ```text idlegame-recipe/v1:gather_supplies,scout_path,rest_briefly ``` **Compressed variant (long recipes):** same payload JSON + `lz-string` URI encoding as saves (`toRecipeExportString` / `fromRecipeExportString`) — optional in T3.1 if recipes exceed ~20 steps; multi-line text remains the default share format. **Import validation:** 1. Parse version; reject unknown versions explicitly. 2. Every id must exist in `content.actionsById`. 3. Every id must be **automation-unlocked** for the current player state (`manualCompletionCounts[id] >= threshold`). 4. On failure: user-visible error listing unknown or locked ids — no partial apply unless ginnoir opts in later. **Export:** serializes current `automationQueue` (or named saved recipe slot if added later) to multi-line text; copy-to-clipboard in Settings or Play automation UI. ### State fields ```typescript manualCompletionCounts: Record // default {}; gates automation automationQueue: string[] // action ids enabledLoopActionIds: Record // loop toggles ``` Persist in save payload (PR3 adds fields; PR4 may bump `SAVE_VERSION` to 2 with migration). ## Story tab ### Layout Split pane, resizable on desktop: ``` ┌──────────────────────────┬─────────────────────┐ │ Branching tree (~60%) │ Prose log (~40%) │ │ │ │ │ ○ boot_intro │ [Full passage text │ │ ├─● route_a │ for selected or │ │ │ └─ merchant │ latest node] │ │ └─○ route_b (dimmed) │ │ │ │ Scrollable history │ └──────────────────────────┴─────────────────────┘ ``` - **Tree (primary surface):** built from story graph + `seenStoryNodeIds` / `storyFlags`. Taken paths emphasized; untaken branches dimmed. Click node → prose log scrolls to that beat. Larger pane because the tree needs manipulation room. - **Prose log (secondary):** full passage text, not truncated. Choice labels inline: `[You chose: Take the high road]`. - **No choice buttons.** ### Story map data - Tree nodes mirror `storyNodesById` edges via choices + triggers. - Visibility: seen nodes solid; future/locked dimmed or hidden per design. - M1 T3.0 may ship minimal tree (indented list) before polish; structure must support real graph. ### Unread / navigation - New story beats → Story nav badge. - `storyOpenMode: auto` → switch to Story tab or highlight badge (no overlay). ## Content schema changes ```typescript actionKindSchema = z.enum(['instant', 'loop', 'timed', 'story', 'context']) actionGroupSchema = z.object({ id: z.string().min(1), label: z.string().min(1), }) actionDefSchema = z.object({ id: z.string().min(1), name: z.string().min(1), kind: actionKindSchema.default('timed'), group: actionGroupSchema, durationMs: z.number().positive().optional(), // required for timed + loop loopPriority: z.number().int().nonnegative().optional(), costs: ..., yields: ..., unlock: ..., storyHint: ..., storyTooltip: ..., storyChoiceId: z.string().min(1).optional(), // required when kind === 'story' contextId: z.string().min(1).optional(), // required when kind === 'context' automation: z.object({ unlockAfterManualCompletions: z.number().int().positive().default(1), }).optional(), }) ``` **Validation rules:** - `timed` and `loop` require `durationMs`. - `instant`, `story`, `context` must not require duration for execution (engine enforces). - `story` requires valid `storyChoiceId` referencing a choice in the loaded story graph. - Column placement derived from `kind` (not a separate column field). ### Stub content migration (T3.0) | Current action | New kind | Group example | |---|---|---| | gather_supplies | timed | Camp | | scout_path | timed | Travel | | trade_at_camp | timed | Camp | | fortify_camp | timed | Camp (route A) | | push_onward | timed | Travel (route B) | | rest_briefly | loop | Camp activities | | *(new)* pick_high_road | story | Fork | | *(new)* follow_river | story | Fork | Remove parallel choice buttons from story graph UI; fork choices exist only as story-kind actions. ## Architecture ``` src/ui/AppShell.tsx nav rail + center + right rail layout src/ui/NavRail.tsx src/ui/PlayPanel.tsx action column grid src/ui/ActionColumn.tsx one kind column src/ui/ActionGroup.tsx collapsible group src/ui/StoryView.tsx split tree + prose log (replaces overlay StoryPanel) src/ui/StoryTree.tsx src/ui/StoryProseLog.tsx src/ui/RightRail.tsx resources, inventory placeholder, event log src/ui/AutomationBar.tsx automation queue UI + export/import (T3.1) src/content/schema.ts + kind, group, storyChoiceId, contextId, automation src/engine/game.ts instant execute, loop idle runner, completion counts src/engine/automation.ts automation queue runner, recipe parse/serialize (new) src/engine/recipe.ts recipe export/import validation (new) src/state/viewModel.ts column/group projection, automation eligibility src/state/runtime.ts nav commands, remove overlay; story action → applyChoice src/state/prefs.ts + collapsedActionGroups, showEventLog ``` **Boundaries unchanged:** engine pure TS; UI calls runtime only. ## PR3 task order (revised opener) | Task | Deliverable | Gitea | |---|---|---| | **T3.0** | App shell, action columns, story-via-actions, Story tab split, remove overlay | UI gate | | **T3.1** | Universal automation + automation queue + recipe export/import | #7 | | **T3.2** | Prestige + knowledge catch-up for automation/story | #8 | | **T3.3** | Opening arc content on new shell | #11 | ## Testing ### Engine - Instant action applies costs/yields without queue. - Loop runs when manual queue idle; stops when timed work queued. - Priority: manual timed > automation > loop (document exact rules in tests). - Story action executes `applyChoice`; siblings become unavailable. - `manualCompletionCounts` gates automation; story actions unlock after first perform. - Recipe round-trip: export → import → identical queue; reject unknown ids; reject locked ids. ### UI / view model - Actions projected into correct column and group. - Group collapse prefs persist. - Story tab: tree click selects prose; no choice buttons rendered. - Nav badge on unread story. ### Regression - Remove/update PR2 overlay tests and story choice button tests. - Existing queue/tick/story graph tests adapted for action-kind story forks. ## Out of scope (T3.0) - Polished graph visualization (minimal tree OK). - Item inventory (resource list placeholder). - Combat mechanics (context column stub). - Mobile responsive column stack (PR4 #10). - Save v2 migration (PR4 #9) — PR3 adds new state fields with defaults in v1 payload until bump. ## PR2 supersession note PR2 spec locked full-window overlay with in-panel choices. This spec **intentionally replaces** that UX. Engine story graph (`applyChoice`, triggers, flags) remains; only presentation and choice surface move to Play actions + Story read-only tab. ## Open questions (none blocking) All brainstorm decisions resolved 2026-06-11.