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/.
16 KiB
M1 PR3 — Shell UI & Action Column Design Spec
Status: Approved by ginnoir (brainstorm 2026-06-11).
Branch:feat/m1-progressionoffmain(first chunk before automation/prestige/content).
Supersedes (partially): PR2 story overlay UX indocs/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)
- Readability — full-screen story overlay (
bg-black/80) ghosts underlying UI; hard to read. - Redundant choice paths — story panel choice buttons and Play actions both visible; unclear authority.
- Structural mismatch — single centered column does not match intended Chronicle-style shell (nav / main / sidebar).
- 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).
storyOpenModepref remapped:autoswitches 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;
showEventLogpref (defaulttrue).
Removed
StoryPanelfull-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)
storyHintinline subtitle (default) / hover / info-button per pref.storyTooltipfor 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, manualactionQueue, progress bar. - Primary manual play column.
loop
- Runs only when
activeActionIdis null and manualactionQueueis empty. - Player enables/disables per loop action (toggle); priority order when multiple enabled (content
loopPrioritynumber, 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
storyChoiceIdlinks toStoryChoice.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:
- Parse version; reject unknown versions explicitly.
- Every id must exist in
content.actionsById. - Every id must be automation-unlocked for the current player state (
manualCompletionCounts[id] >= threshold). - 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
storyNodesByIdedges 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:
timedandlooprequiredurationMs.instant,story,contextmust not require duration for execution (engine enforces).storyrequires validstoryChoiceIdreferencing 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. manualCompletionCountsgates 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.