mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart theming: custom named themes + Theme Builder
This commit is contained in:
@@ -15,21 +15,27 @@
|
||||
* (M3) plugs into prepareSpecForRender without changing this component.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import type { VisualizationSpec } from 'vega-embed';
|
||||
import type { FitMode } from '@core/rendering';
|
||||
import { DatasetNotFoundError, prepareSpecForRender } from '@core/rendering';
|
||||
import { CHART_THEME_OPTIONS, chartConfigForSelection } from '@core/vega-themes';
|
||||
import {
|
||||
chartConfigForSelection,
|
||||
chartThemeOptions,
|
||||
type ChartThemeSelection,
|
||||
} from '@core/vega-themes';
|
||||
import { openModal } from '../modals/ModalCoordinator';
|
||||
import { renderSpec, type RenderHandle } from '../services/chart-renderer';
|
||||
import { useAppStore } from '../stores/AppStore';
|
||||
import { useCustomThemeStore } from '../stores/CustomThemeStore';
|
||||
import { useDatasetStore } from '../stores/DatasetStore';
|
||||
import { usePreviewStore } from '../stores/PreviewStore';
|
||||
import { selectShownText, useSnippetStore } from '../stores/SnippetStore';
|
||||
import { useUserSettingsStore } from '../stores/UserSettingsStore';
|
||||
import { ChartExport } from './ChartExport';
|
||||
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
|
||||
import { SelectControl } from './SelectControl';
|
||||
import { SelectControl, type SelectControlOption } from './SelectControl';
|
||||
import { RangeControl, SettingRow, SettingsPopover } from './SettingsPopover';
|
||||
import styles from './LivePreview.module.css';
|
||||
|
||||
@@ -78,16 +84,42 @@ function FitControl() {
|
||||
* (and unmount) its own parent on open.
|
||||
*/
|
||||
// TODO: header placement/crowding parked for the batched council pass (docs/ux-second-pass.md).
|
||||
/** Sentinel option that opens the Theme Builder instead of selecting a theme. */
|
||||
const EDIT_THEMES = 'edit-themes';
|
||||
type ThemePickerValue = ChartThemeSelection | typeof EDIT_THEMES;
|
||||
|
||||
function ChartThemeControl() {
|
||||
const chartTheme = useAppStore((s) => s.chartTheme);
|
||||
const setChartTheme = useAppStore((s) => s.setChartTheme);
|
||||
const customThemes = useCustomThemeStore((s) => s.themes);
|
||||
// Fresh-array derivation — memoize so the picker doesn't re-render the world
|
||||
// (docs/architecture/01: derive with useMemo, never store).
|
||||
const options = useMemo<ReadonlyArray<SelectControlOption<ThemePickerValue>>>(() => {
|
||||
const list: SelectControlOption<ThemePickerValue>[] = [...chartThemeOptions(customThemes)];
|
||||
// The "manage" entry rides in the value list (the VS Code theme-picker
|
||||
// pattern); choosing it opens the builder and leaves the selection alone.
|
||||
// It closes the custom-themes block — right after the built-ins, BEFORE the
|
||||
// long preset roster — so it is visible without scrolling and sits next to
|
||||
// the entries it manages.
|
||||
// TODO: action row inside a value picker (visual separation? AT surprise?)
|
||||
// parked for the batched council pass (docs/ux-second-pass.md).
|
||||
list.splice(2 + customThemes.length, 0, {
|
||||
value: EDIT_THEMES,
|
||||
label: 'Edit themes…',
|
||||
detail: 'Create and manage custom themes',
|
||||
});
|
||||
return list;
|
||||
}, [customThemes]);
|
||||
return (
|
||||
<SelectControl
|
||||
id="preview-chart-theme"
|
||||
label="Chart theme"
|
||||
options={CHART_THEME_OPTIONS}
|
||||
options={options}
|
||||
value={chartTheme}
|
||||
onSelect={setChartTheme}
|
||||
onSelect={(value) => {
|
||||
if (value === EDIT_THEMES) openModal('themeBuilder');
|
||||
else setChartTheme(value);
|
||||
}}
|
||||
triggerTitle="Chart theme — how charts are styled when rendered and exported"
|
||||
/>
|
||||
);
|
||||
@@ -126,6 +158,9 @@ export function LivePreview() {
|
||||
const fitMode = useAppStore((s) => s.previewFitMode);
|
||||
const uiTheme = useAppStore((s) => s.uiTheme);
|
||||
const chartTheme = useAppStore((s) => s.chartTheme);
|
||||
// Custom themes feed `custom:<id>` selection resolution; re-rendering on a
|
||||
// change keeps the chart live while a selected theme is edited in the builder.
|
||||
const customThemes = useCustomThemeStore((s) => s.themes);
|
||||
// Datasets feed reference resolution (spec §04 step 1). Re-rendering on a
|
||||
// dataset change keeps a referencing chart live as its data is edited.
|
||||
const datasets = useDatasetStore(useShallow((s) => s.datasets));
|
||||
@@ -238,7 +273,7 @@ export function LivePreview() {
|
||||
|
||||
try {
|
||||
const prepared = prepareSpecForRender(parsed, { fitMode, datasets });
|
||||
const config = chartConfigForSelection(chartTheme, uiTheme);
|
||||
const config = chartConfigForSelection(chartTheme, uiTheme, customThemes);
|
||||
handleRef.current?.destroy();
|
||||
handleRef.current = null;
|
||||
const handle = await renderSpec(node, prepared as VisualizationSpec, config);
|
||||
@@ -286,6 +321,7 @@ export function LivePreview() {
|
||||
fitMode,
|
||||
uiTheme,
|
||||
chartTheme,
|
||||
customThemes,
|
||||
datasets,
|
||||
setError,
|
||||
setBusy,
|
||||
|
||||
Reference in New Issue
Block a user