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
4 changed files with 75 additions and 12 deletions
Showing only changes of commit 09e2d06b87 - Show all commits
+41 -1
View File
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getPrefs, setPrefs } from '../prefs';
describe('prefs', () => {
@@ -14,6 +14,10 @@ describe('prefs', () => {
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('returns defaults when localStorage empty', () => {
expect(getPrefs()).toEqual({
storyOpenMode: 'auto',
@@ -27,6 +31,42 @@ describe('prefs', () => {
setPrefs({ storyOpenMode: 'manual', actionDetailMode: 'hover' });
expect(getPrefs().storyOpenMode).toBe('manual');
});
it('migrates v1 prefs to v2 if v2 does not exist', () => {
const v1Prefs = {
storyOpenMode: 'manual',
actionDetailMode: 'hover',
collapsedActionGroups: { 'some-group': true },
};
localStorage.setItem('idlegame:prefs:v1', JSON.stringify(v1Prefs));
const migratedPrefs = getPrefs();
expect(migratedPrefs).toEqual({
storyOpenMode: 'manual',
actionDetailMode: 'hover',
collapsedActionGroups: { 'some-group': true },
showEventLog: true,
});
const rawV2 = localStorage.getItem('idlegame:prefs:v2');
expect(rawV2).not.toBeNull();
expect(JSON.parse(rawV2 ?? 'null')).toEqual(migratedPrefs);
});
it('returns defaults and does not migrate if v1 prefs is invalid JSON', () => {
localStorage.setItem('idlegame:prefs:v1', '{invalid-json}');
const prefs = getPrefs();
expect(prefs).toEqual({
storyOpenMode: 'auto',
actionDetailMode: 'inline',
collapsedActionGroups: {},
showEventLog: true,
});
expect(localStorage.getItem('idlegame:prefs:v2')).toBeNull();
});
});
describe('expanded prefs', () => {
+11 -1
View File
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getPrefs } from '../prefs';
import { useGameStore } from '../store';
describe('game store nav and prefs', () => {
@@ -12,6 +13,15 @@ describe('game store nav and prefs', () => {
store[key] = value;
},
});
useGameStore.setState({
activePanel: 'play',
selectedStoryNodeId: null,
prefs: getPrefs(),
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('has default activePanel and selectedStoryNodeId', () => {
+14 -1
View File
@@ -21,7 +21,20 @@ export function getPrefs(): GamePrefs {
if (typeof localStorage === 'undefined') return { ...DEFAULTS };
try {
const raw = localStorage.getItem(PREFS_KEY);
if (!raw) return { ...DEFAULTS };
if (!raw) {
const rawV1 = localStorage.getItem('idlegame:prefs:v1');
if (rawV1) {
try {
const parsedV1 = JSON.parse(rawV1);
const migrated = { ...DEFAULTS, ...parsedV1 };
localStorage.setItem(PREFS_KEY, JSON.stringify(migrated));
return migrated;
} catch {
return { ...DEFAULTS };
}
}
return { ...DEFAULTS };
}
return { ...DEFAULTS, ...JSON.parse(raw) };
} catch {
return { ...DEFAULTS };
+9 -9
View File
@@ -69,13 +69,13 @@ export const useGameStore = create<GameStoreState>((set) => ({
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 };
}),
toggleActionGroupCollapsed: (groupKey) => {
const currentPrefs = useGameStore.getState().prefs;
const nextCollapsed = {
...currentPrefs.collapsedActionGroups,
[groupKey]: !currentPrefs.collapsedActionGroups[groupKey],
};
const prefs = persistPrefs({ collapsedActionGroups: nextCollapsed });
set({ prefs });
},
}));