import { create } from 'zustand'; import type { FitMode } from '@core/rendering'; import type { UiTheme } from '@core/theme'; import type { ChartThemeSelection } from '@core/vega-themes'; import type { ModalName } from '../modals/types'; /** Default data-inspector height (px) when first expanded, before any drag. */ const DATA_INSPECTOR_DEFAULT_HEIGHT = 220; /** Floor (px) for the inspector and for the chart above it while the divider drags. */ const DATA_INSPECTOR_MIN_HEIGHT = 96; const CHART_MIN_HEIGHT = 120; /** * Clamp a desired inspector height so it keeps its own minimum and leaves the * chart above it at least its minimum. `availForBoth` is the height the chart and * inspector share (the preview body, minus the divider). Pure — the single place * the divider constraint lives, unit-tested without a DOM. Mirrors PanesStore's * `clampSideWidth`. */ export function clampInspectorHeight(desired: number, availForBoth: number): number { const max = Math.max(DATA_INSPECTOR_MIN_HEIGHT, availForBoth - CHART_MIN_HEIGHT); return Math.max(DATA_INSPECTOR_MIN_HEIGHT, Math.min(desired, max)); } /** * The inspector's 0–100 position for the divider's `aria-valuenow` (WAI-ARIA APG → * Window Splitter): 0 = inspector at its minimum, 100 = at its maximum (chart at * its minimum). Null when the span has no range, so the caller omits the attribute. */ export function inspectorHeightValue(height: number, availForBoth: number): number | null { const min = DATA_INSPECTOR_MIN_HEIGHT; const max = Math.max(min, availForBoth - CHART_MIN_HEIGHT); if (max <= min) return null; const pct = ((height - min) / (max - min)) * 100; return Math.round(Math.min(100, Math.max(0, pct))); } /** * Centralized cross-cutting application state, as a Zustand store. Keep this * lean — durable, feature-specific state (snippets, datasets, settings) lands * in its own store module (e.g. stores/SnippetStore) as the app grows. * * Usable inside React via the `useAppStore` hook (with a selector) and outside * React via `useAppStore.getState()` / `.setState()` / `.subscribe()` — see * docs/architecture/01-state-and-stores.md. */ export interface AppState { /** Active UI theme; mirrored onto by a subscriber. */ uiTheme: UiTheme; /** Preview sizing mode (spec §04); persisted to Settings as `previewFitMode`. */ previewFitMode: FitMode; /** Chart theme selection (spec §04); persisted to Settings as `chartTheme`. */ chartTheme: ChartThemeSelection; /** * Whether the live-preview data inspector is expanded (spec §04). A preview-pane * preference like `previewFitMode`; persisted to Settings as `dataInspectorOpen`. */ dataInspectorOpen: boolean; /** * Height (px) of the expanded data inspector — set by its resizable divider, so * the chart above keeps the rest of the pane. Persisted as `dataInspectorHeight`. */ dataInspectorHeight: number; /** The currently open modal, or null. */ activeModal: ModalName | null; setTheme: (theme: UiTheme) => void; /** Flip between light and dark — the header ThemeToggle's action. */ toggleTheme: () => void; /** Set the preview fit mode — the Live Preview Fit control's action. */ setPreviewFitMode: (mode: FitMode) => void; /** Set the chart theme — the Live Preview settings cluster's action. */ setChartTheme: (theme: ChartThemeSelection) => void; /** Show/hide the data inspector — its disclosure toggle. */ setDataInspectorOpen: (open: boolean) => void; /** Set the data inspector's height (caller clamps via `clampInspectorHeight`). */ setDataInspectorHeight: (height: number) => void; /** * Low-level modal setter — the single primitive that mutates `activeModal`. * High-level open/close (snapshot for unsaved-change detection, URL sync, * discard confirmation) lives in the modal coordinator (docs/architecture/03), * which calls this; arrives with the modal system in M3. */ setActiveModal: (modal: ModalName | null) => void; } export const useAppStore = create((set) => ({ uiTheme: 'light', previewFitMode: 'default', chartTheme: 'astrolabe', dataInspectorOpen: false, dataInspectorHeight: DATA_INSPECTOR_DEFAULT_HEIGHT, activeModal: null, setTheme: (uiTheme) => set({ uiTheme }), toggleTheme: () => set((s) => ({ uiTheme: s.uiTheme === 'dark' ? 'light' : 'dark' })), setPreviewFitMode: (previewFitMode) => set({ previewFitMode }), setChartTheme: (chartTheme) => set({ chartTheme }), setDataInspectorOpen: (dataInspectorOpen) => set({ dataInspectorOpen }), setDataInspectorHeight: (dataInspectorHeight) => set({ dataInspectorHeight }), setActiveModal: (activeModal) => set({ activeModal }), }));