Files
astrolabe/src/app/stores/AppStore.ts
T
oleh efb5a9bbe0 Data inspector: input/resolved rows below the chart, with a resizable divider
A collapsible Data panel under the Live Preview and Chart Builder charts shows the rows the chart actually uses, switching between Input (parsed source) and Resolved (post-transform) views read from the live Vega view. Collapsed by default; the open-state and a draggable height divider persist.

Rows come through a new RenderHandle.inspectData() accessor, so no component touches the view: core/result-data picks the most-upstream source and most-downstream result from the compiled dataflow, read lazily. The divider reuses the window-splitter pattern (horizontal variant).

Consolidations: a shared DataTable primitive replaces the inspector's and the builder's duplicate read-only tables; useResizeDrag merges the col/row drag-gesture twins.

Docs: spec 04/06 and arch 05/10 updated; the now-shipped exploration memo removed.
2026-06-18 02:22:03 +03:00

105 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 0100 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 <html data-theme> 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<AppState>((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 }),
}));