fix(ui): restore boot-intro continue and fix story tree a11y

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.
This commit is contained in:
ginnoir
2026-06-11 22:25:51 -05:00
parent 91348ed42f
commit 880abe5888
6 changed files with 68 additions and 9 deletions
+49
View File
@@ -256,6 +256,55 @@ describe('action columns projection', () => {
});
});
describe('story.atBootIntro', () => {
it('is true at the boot-entry node and false after entering a different node', () => {
const base = buildContent({
resources: [{ id: 'coin', name: 'Coin', startAmount: 0 }],
actions: [
{
id: 'forage',
name: 'Forage',
group: DEFAULT_GROUP,
durationMs: 1000,
yields: [{ resourceId: 'coin', amount: 1 }],
},
],
});
const story = buildStoryContent(
[
{
id: 'boot_intro',
prose: 'Boot.',
triggers: [{ type: 'boot', targetNodeId: 'boot_intro' }],
},
{
id: 'fork_choice',
prose: 'Which way?',
choices: [
{
id: 'pick_a',
label: 'High road',
outcomes: [{ type: 'setFlag', flag: 'route_a' }],
targetNodeId: 'route_a_beat',
},
],
},
{ id: 'route_a_beat', prose: 'The high road.' },
],
base.actionsById,
base.resourcesById,
);
const c = { ...base, ...story };
const state = createGameState(c);
// Simulate boot: enter the boot-entry node
enterStoryNode(state, c, 'boot_intro');
expect(toView(state, c).story.atBootIntro).toBe(true);
// After moving past boot into fork_choice, atBootIntro must be false
enterStoryNode(state, c, 'fork_choice');
expect(toView(state, c).story.atBootIntro).toBe(false);
});
});
describe('story tree projection', () => {
it('builds a tree marking seen and active nodes', () => {
const state = createGameState(content);