feat(m1): PR3 T3.0 — shell UI, action kinds, story tab #16

Merged
ginnoir merged 26 commits from feat/m1-progression into main 2026-06-11 22:52:29 -05:00
Owner

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.
  • SaveenabledLoopActionIds 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

## 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`
ginnoir added 26 commits 2026-06-11 22:43:35 -05:00
Implements executeInstant, which applies an instant action's costs and
yields immediately with no queue slot and no duration requirement.
- 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.
- RightRail: add Inventory section (placeholder "Items coming soon") between
  Resources and Event Log, matching the existing heading style
- RightRail: gate Event Log section on prefs.showEventLog; add local-state
  collapse toggle with aria-expanded and chevron indicator
- SettingsPanel: add "Show event log" checkbox row bound to prefs.showEventLog
  via setPrefs, matching existing card styling
- PlayPanel: gate mobile EventLog behind prefs.showEventLog for consistency
  with desktop; mobile ResourceBar unchanged
Add aria-controls to the right-rail event-log collapse toggle pointing at
the log region, and title-case the settings label for consistency.
test(engine): guard offline-loop safety and harden purity test
CI / verify (push) Successful in 1m13s
CI / verify (pull_request) Successful in 58s
f7ad9e7ecd
- 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 main 2026-06-11 22:52:29 -05:00
Sign in to join this conversation.