Replaces the PR2 single-column overlay UX with the M1 three-region app shell and the behavior-kind action model (T3.0 of PR3).
App shell — left nav rail (Play / Story / Settings / About), center active panel, Play-only right rail (resources, inventory placeholder, optional collapsible event log). No modal overlay; store.activePanel drives the center; unread story beats raise a nav badge.
Action kinds — content actions gain kind (instant/loop/timed/story/context) + group, validated by Zod (kind-specific invariants). Pure-engine performAction dispatches by kind; instant applies immediately, timed enqueues, loop toggles enabledLoopActionIds (idle runner, never offline), story applies the linked choice.
Play columns — actions projected into columns ordered instant→loop→timed→story→context with collapsible, persisted theme groups. Story forks happen only via Story-kind actions.
Story tab — read-only 60/40 split: branching StoryTree (seen emphasized, unseen dimmed) + scrollable StoryProseLog with no choice buttons.
Save — enabledLoopActionIds round-trips; no save-version bump (fields default safely in v1).
Notable fix
Restored boot progression: the boot-intro Continue gate keyed off tree.length === 0, which is never true (the tree is built from content topology), so the player could never advance boot_intro → fork_choice and the fork actions never unlocked. Now gated by a real atBootIntro signal derived from the boot trigger, with a regression test.
## Summary
Replaces the PR2 single-column overlay UX with the M1 three-region app shell and the behavior-kind action model (T3.0 of PR3).
- **App shell** — left nav rail (Play / Story / Settings / About), center active panel, Play-only right rail (resources, inventory placeholder, optional collapsible event log). No modal overlay; `store.activePanel` drives the center; unread story beats raise a nav badge.
- **Action kinds** — content actions gain `kind` (`instant`/`loop`/`timed`/`story`/`context`) + `group`, validated by Zod (kind-specific invariants). Pure-engine `performAction` dispatches by kind; `instant` applies immediately, `timed` enqueues, `loop` toggles `enabledLoopActionIds` (idle runner, **never offline**), `story` applies the linked choice.
- **Play columns** — actions projected into columns ordered `instant→loop→timed→story→context` with collapsible, persisted theme groups. Story forks happen **only** via Story-kind actions.
- **Story tab** — read-only 60/40 split: branching `StoryTree` (seen emphasized, unseen dimmed) + scrollable `StoryProseLog` with no choice buttons.
- **Save** — `enabledLoopActionIds` round-trips; no save-version bump (fields default safely in v1).
## Notable fix
Restored boot progression: the boot-intro Continue gate keyed off `tree.length === 0`, which is never true (the tree is built from content topology), so the player could never advance `boot_intro → fork_choice` and the fork actions never unlocked. Now gated by a real `atBootIntro` signal derived from the boot trigger, with a regression test.
## Verification
```
pnpm typecheck # clean
pnpm lint # 0 warnings
pnpm test # 152 passed (18 files)
pnpm build # clean (PWA + SW)
```
Engine coverage well above the 80% floor (game.ts 90.8% / story.ts 95.8% / tickLoop.ts 95.2% / num.ts 94.4% stmts). Added guard tests for offline-loop safety and a hardened engine purity test.
## Test plan
- [ ] Boot → Story tab → Continue advances to the fork; fork actions appear in the Play **Story** column; picking one hides the sibling.
- [ ] Timed actions queue; loop `rest` toggles and runs only when idle (and only live, not offline).
- [ ] Right rail shows resources + inventory placeholder; event log collapsible and hideable via Settings.
- [ ] Reload preserves state, including enabled loops.
## Out of scope (later)
Automation queue + shareable recipes (T3.1), prestige (T3.2), mobile column stacking & save v2 migration (PR4), real item inventory.
Parent spec: `docs/superpowers/specs/2026-06-11-m1-pr3-shell-ui-design.md` · Plan: `docs/superpowers/plans/2026-06-11-m1-pr3-t30-shell-ui.md`
- Guard ActionCard click-outside listener behind open state
- Add aria-expanded to ActionGroup collapse toggle
- Use optional chaining and drop non-null assertions in column projection
Finding 1 (critical): atBootIntro was derived from tree.length === 0 in
StoryView.tsx, but buildStoryTree always returns all topology-root nodes,
so tree.length is always > 0 and the Continue button never rendered.
Fix: add atBootIntro: boolean to StoryView (viewModel), computed by
finding the boot-trigger node id without hardcoding the literal string,
then comparing to the current node. StoryView.tsx reads it from the store.
Also add a viewModel test covering both true and false cases.
Finding 2: onSelect in StoryView.tsx called useGameStore.getState()
directly, bypassing the runtime command layer. Fix: add
GameRuntime.selectStoryNode(id) and wire onSelect through gameRuntime.
Finding 3: aria-pressed on story tree nodes wrongly signals toggle-button
semantics. Fix: replace with aria-current={isSelected ? 'true' : undefined}.
Optional: hoist h-[calc(100dvh-6rem)] to SHELL_HEIGHT const in StoryView.tsx.
- save.test.ts: add regression test proving loop actions do NOT start
or yield during offline catch-up (applyOfflineProgress replays
tickGame directly; maybeStartLoopAction is never called, so an
enabled loop with no active action stays idle and yields 0).
- purity.test.ts: extend FORBIDDEN list to catch environment APIs
(Date.now(), localStorage., indexedDB., idb-keyval import, document.,
window., requestAnimationFrame()) in addition to the existing
React/react-dom/zustand import guards. Patterns are scoped to call
sites and member-access forms so prose comments (e.g. save.ts's
"Date scheduling" doc comment) do not false-positive. lz-string is
intentionally omitted — it is a pure compression library.
- NavRail.tsx: add sr-only "New story" span alongside the aria-hidden
pulse dot so screen readers can perceive the unread-story badge.
ginnoir
merged commit 4b7adf718d into main2026-06-11 22:52:29 -05:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Replaces the PR2 single-column overlay UX with the M1 three-region app shell and the behavior-kind action model (T3.0 of PR3).
store.activePaneldrives the center; unread story beats raise a nav badge.kind(instant/loop/timed/story/context) +group, validated by Zod (kind-specific invariants). Pure-engineperformActiondispatches by kind;instantapplies immediately,timedenqueues,looptogglesenabledLoopActionIds(idle runner, never offline),storyapplies the linked choice.instant→loop→timed→story→contextwith collapsible, persisted theme groups. Story forks happen only via Story-kind actions.StoryTree(seen emphasized, unseen dimmed) + scrollableStoryProseLogwith no choice buttons.enabledLoopActionIdsround-trips; no save-version bump (fields default safely in v1).Notable fix
Restored boot progression: the boot-intro Continue gate keyed off
tree.length === 0, which is never true (the tree is built from content topology), so the player could never advanceboot_intro → fork_choiceand the fork actions never unlocked. Now gated by a realatBootIntrosignal derived from the boot trigger, with a regression test.Verification
Engine coverage well above the 80% floor (game.ts 90.8% / story.ts 95.8% / tickLoop.ts 95.2% / num.ts 94.4% stmts). Added guard tests for offline-loop safety and a hardened engine purity test.
Test plan
resttoggles and runs only when idle (and only live, not offline).Out of scope (later)
Automation queue + shareable recipes (T3.1), prestige (T3.2), mobile column stacking & save v2 migration (PR4), real item inventory.
Parent spec:
docs/superpowers/specs/2026-06-11-m1-pr3-shell-ui-design.md· Plan:docs/superpowers/plans/2026-06-11-m1-pr3-t30-shell-ui.mdFinding 1 (critical): atBootIntro was derived from tree.length === 0 in StoryView.tsx, but buildStoryTree always returns all topology-root nodes, so tree.length is always > 0 and the Continue button never rendered. Fix: add atBootIntro: boolean to StoryView (viewModel), computed by finding the boot-trigger node id without hardcoding the literal string, then comparing to the current node. StoryView.tsx reads it from the store. Also add a viewModel test covering both true and false cases. Finding 2: onSelect in StoryView.tsx called useGameStore.getState() directly, bypassing the runtime command layer. Fix: add GameRuntime.selectStoryNode(id) and wire onSelect through gameRuntime. Finding 3: aria-pressed on story tree nodes wrongly signals toggle-button semantics. Fix: replace with aria-current={isSelected ? 'true' : undefined}. Optional: hoist h-[calc(100dvh-6rem)] to SHELL_HEIGHT const in StoryView.tsx.