mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
efb5a9bbe0
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.
105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
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 <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 }),
|
||
}));
|