feat(renderer): useAppStore extended with modal discriminated union

This commit is contained in:
2026-06-05 18:28:02 +05:30
parent d0a26126cc
commit 9ad484744a
2 changed files with 77 additions and 3 deletions
+47 -2
View File
@@ -7,15 +7,44 @@ export interface PaneSizes {
preview: number; preview: number;
} }
export interface ConfirmProps {
title: string;
body: string;
confirmLabel?: string;
cancelLabel?: string;
destructive?: boolean;
onConfirm: () => void | Promise<void>;
onCancel?: () => void;
}
export type ModalState =
| { kind: null }
| { kind: 'export-pdf'; props: { sourcePath: string } }
| { kind: 'export-docx'; props: { sourcePath: string } }
| { kind: 'export-html'; props: { sourcePath: string } }
| { kind: 'export-batch'; props: { sourcePaths: string[] } }
| { kind: 'settings' }
| { kind: 'about' }
| { kind: 'welcome' }
| { kind: 'confirm'; props: ConfirmProps };
export type ModalKind = ModalState['kind'];
interface AppState { interface AppState {
sidebarVisible: boolean; sidebarVisible: boolean;
previewVisible: boolean; previewVisible: boolean;
zenMode: boolean; zenMode: boolean;
paneSizes: PaneSizes; paneSizes: PaneSizes;
modal: ModalState;
toggleSidebar: () => void; toggleSidebar: () => void;
togglePreview: () => void; togglePreview: () => void;
setZenMode: (value: boolean) => void; setZenMode: (value: boolean) => void;
setPaneSizes: (sizes: PaneSizes) => void; setPaneSizes: (sizes: PaneSizes) => void;
openModal: <K extends NonNullable<ModalKind>>(
kind: K,
...args: Extract<ModalState, { kind: K }> extends { props: infer P } ? [props?: P] : []
) => void;
closeModal: () => void;
} }
export const useAppStore = create<AppState>()( export const useAppStore = create<AppState>()(
@@ -25,11 +54,27 @@ export const useAppStore = create<AppState>()(
previewVisible: true, previewVisible: true,
zenMode: false, zenMode: false,
paneSizes: { sidebar: 20, editor: 50, preview: 30 }, paneSizes: { sidebar: 20, editor: 50, preview: 30 },
modal: { kind: null },
toggleSidebar: () => set((s) => ({ sidebarVisible: !s.sidebarVisible })), toggleSidebar: () => set((s) => ({ sidebarVisible: !s.sidebarVisible })),
togglePreview: () => set((s) => ({ previewVisible: !s.previewVisible })), togglePreview: () => set((s) => ({ previewVisible: !s.previewVisible })),
setZenMode: (value) => set({ zenMode: value }), setZenMode: (value) => set({ zenMode: value }),
setPaneSizes: (sizes) => set({ paneSizes: sizes }), setPaneSizes: (sizes) => set({ paneSizes: sizes }),
openModal: (kind, ...args) =>
set(() => {
const candidate = { kind, ...(args[0] ? { props: args[0] } : {}) } as ModalState;
return { modal: candidate };
}),
closeModal: () => set({ modal: { kind: null } }),
}), }),
{ name: 'mc-app-store' } {
name: 'mc-app-store',
partialize: (state) => ({
sidebarVisible: state.sidebarVisible,
previewVisible: state.previewVisible,
zenMode: state.zenMode,
paneSizes: state.paneSizes,
// modal is intentionally NOT persisted (runtime-only)
}),
}
) )
); );
+30 -1
View File
@@ -32,4 +32,33 @@ describe('useAppStore', () => {
useAppStore.getState().setZenMode(true); useAppStore.getState().setZenMode(true);
expect(useAppStore.getState().zenMode).toBe(true); expect(useAppStore.getState().zenMode).toBe(true);
}); });
}); });
describe('useAppStore (modal)', () => {
beforeEach(() => {
localStorage.clear();
useAppStore.setState({
sidebarVisible: true,
previewVisible: true,
zenMode: false,
paneSizes: { sidebar: 20, editor: 50, preview: 30 },
modal: { kind: null },
} as any);
});
it('openModal sets the modal state', () => {
useAppStore.getState().openModal('about');
expect(useAppStore.getState().modal).toEqual({ kind: 'about' });
});
it('openModal with kind requiring props passes them through', () => {
useAppStore.getState().openModal('export-pdf', { sourcePath: '/a.md' });
expect(useAppStore.getState().modal).toEqual({ kind: 'export-pdf', props: { sourcePath: '/a.md' } });
});
it('closeModal clears the modal state', () => {
useAppStore.getState().openModal('about');
useAppStore.getState().closeModal();
expect(useAppStore.getState().modal).toEqual({ kind: null });
});
});