Files
idlegame/docs/superpowers/specs/2026-06-11-m1-pr3-shell-ui-design.md
T
ginnoir 203478268c
CI / verify (push) Successful in 1m31s
CI / verify (pull_request) Successful in 1m2s
docs(m1): add PR3 shell UI spec and implementation plans
Capture the PR3 UX brainstorm: three-region shell, action columns by kind, story-via-actions, Story tab tree+log, and universal automation with shareable recipes. Includes T3.0/T3.1 implementation plans and M1 vertical slice plan. Also ignore local .worktrees/.
2026-06-11 20:01:30 -05:00

16 KiB

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):

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<string, boolean>.

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):

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):

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

manualCompletionCounts: Record<string, number>  // default {}; gates automation
automationQueue: string[]                       // action ids
enabledLoopActionIds: Record<string, boolean>   // 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

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.