From 9b2793b264dcedac278644371d87ed98848e1fa8 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 11 Jun 2026 21:10:07 -0500 Subject: [PATCH] feat(state): nav panel store and expanded prefs --- src/state/__tests__/prefs.test.ts | 15 ++++++++- src/state/__tests__/store.test.ts | 51 +++++++++++++++++++++++++++++++ src/state/prefs.ts | 6 +++- src/state/store.ts | 20 ++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/state/__tests__/store.test.ts diff --git a/src/state/__tests__/prefs.test.ts b/src/state/__tests__/prefs.test.ts index 56d2a17..8d5727a 100644 --- a/src/state/__tests__/prefs.test.ts +++ b/src/state/__tests__/prefs.test.ts @@ -15,7 +15,12 @@ describe('prefs', () => { }); it('returns defaults when localStorage empty', () => { - expect(getPrefs()).toEqual({ storyOpenMode: 'auto', actionDetailMode: 'inline' }); + expect(getPrefs()).toEqual({ + storyOpenMode: 'auto', + actionDetailMode: 'inline', + collapsedActionGroups: {}, + showEventLog: true, + }); }); it('round-trips updated prefs', () => { @@ -23,3 +28,11 @@ describe('prefs', () => { expect(getPrefs().storyOpenMode).toBe('manual'); }); }); + +describe('expanded prefs', () => { + it('defaults collapsedActionGroups and showEventLog', () => { + const prefs = getPrefs(); + expect(prefs.collapsedActionGroups).toEqual({}); + expect(prefs.showEventLog).toBe(true); + }); +}); diff --git a/src/state/__tests__/store.test.ts b/src/state/__tests__/store.test.ts new file mode 100644 index 0000000..026e521 --- /dev/null +++ b/src/state/__tests__/store.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useGameStore } from '../store'; + +describe('game store nav and prefs', () => { + beforeEach(() => { + const store: Record = {}; + vi.stubGlobal('localStorage', { + getItem(key: string) { + return store[key] ?? null; + }, + setItem(key: string, value: string) { + store[key] = value; + }, + }); + }); + + it('has default activePanel and selectedStoryNodeId', () => { + const state = useGameStore.getState(); + expect(state.activePanel).toBe('play'); + expect(state.selectedStoryNodeId).toBeNull(); + }); + + it('updates activePanel via setActivePanel', () => { + const state = useGameStore.getState(); + state.setActivePanel('story'); + expect(useGameStore.getState().activePanel).toBe('story'); + + // Reset back + useGameStore.getState().setActivePanel('play'); + }); + + it('updates selectedStoryNodeId via setSelectedStoryNodeId', () => { + const state = useGameStore.getState(); + state.setSelectedStoryNodeId('node-1'); + expect(useGameStore.getState().selectedStoryNodeId).toBe('node-1'); + + state.setSelectedStoryNodeId(null); + expect(useGameStore.getState().selectedStoryNodeId).toBeNull(); + }); + + it('toggles collapsed action groups in prefs', () => { + const state = useGameStore.getState(); + expect(state.prefs.collapsedActionGroups['skills:gather']).toBeUndefined(); + + state.toggleActionGroupCollapsed('skills:gather'); + expect(useGameStore.getState().prefs.collapsedActionGroups['skills:gather']).toBe(true); + + useGameStore.getState().toggleActionGroupCollapsed('skills:gather'); + expect(useGameStore.getState().prefs.collapsedActionGroups['skills:gather']).toBe(false); + }); +}); diff --git a/src/state/prefs.ts b/src/state/prefs.ts index 1055b71..feca74e 100644 --- a/src/state/prefs.ts +++ b/src/state/prefs.ts @@ -1,4 +1,4 @@ -const PREFS_KEY = 'idlegame:prefs:v1'; +const PREFS_KEY = 'idlegame:prefs:v2'; export type StoryOpenMode = 'auto' | 'choices-only' | 'manual'; export type ActionDetailMode = 'inline' | 'hover' | 'info-button'; @@ -6,11 +6,15 @@ export type ActionDetailMode = 'inline' | 'hover' | 'info-button'; export interface GamePrefs { storyOpenMode: StoryOpenMode; actionDetailMode: ActionDetailMode; + collapsedActionGroups: Record; + showEventLog: boolean; } const DEFAULTS: GamePrefs = { storyOpenMode: 'auto', actionDetailMode: 'inline', + collapsedActionGroups: {}, + showEventLog: true, }; export function getPrefs(): GamePrefs { diff --git a/src/state/store.ts b/src/state/store.ts index 4a40a14..5e161b9 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -16,6 +16,8 @@ export interface StoryLogEntry { choiceLabel?: string; } +export type ActivePanel = 'play' | 'story' | 'settings' | 'about'; + export interface GameStoreState extends GameView { log: string[]; storyPanelOpen: boolean; @@ -23,6 +25,8 @@ export interface GameStoreState extends GameView { storyLog: StoryLogEntry[]; prefs: GamePrefs; settingsOpen: boolean; + activePanel: ActivePanel; + selectedStoryNodeId: string | null; setView: (view: GameView) => void; appendLog: (line: string) => void; appendStoryLog: (entry: StoryLogEntry) => void; @@ -30,6 +34,9 @@ export interface GameStoreState extends GameView { setStoryHasUnread: (unread: boolean) => void; setPrefs: (partial: Partial) => void; setSettingsOpen: (open: boolean) => void; + setActivePanel: (panel: ActivePanel) => void; + setSelectedStoryNodeId: (id: string | null) => void; + toggleActionGroupCollapsed: (groupKey: string) => void; } export const useGameStore = create((set) => ({ @@ -48,6 +55,8 @@ export const useGameStore = create((set) => ({ storyLog: [], prefs: getPrefs(), settingsOpen: false, + activePanel: 'play', + selectedStoryNodeId: null, setView: (view) => set((state) => ({ ...state, ...view })), appendLog: (line) => set((state) => ({ log: [...state.log, line].slice(-MAX_LOG_LINES) })), appendStoryLog: (entry) => set((state) => ({ storyLog: [...state.storyLog, entry] })), @@ -58,4 +67,15 @@ export const useGameStore = create((set) => ({ set({ prefs }); }, setSettingsOpen: (open) => set({ settingsOpen: open }), + setActivePanel: (panel) => set({ activePanel: panel }), + setSelectedStoryNodeId: (id) => set({ selectedStoryNodeId: id }), + toggleActionGroupCollapsed: (groupKey) => + set((state) => { + const nextCollapsed = { + ...state.prefs.collapsedActionGroups, + [groupKey]: !state.prefs.collapsedActionGroups[groupKey], + }; + const prefs = persistPrefs({ collapsedActionGroups: nextCollapsed }); + return { prefs }; + }), }));