Theme Builder: Layout/Axes/Legend/Type panels, scheme-render fix, fail-loud gallery

This commit is contained in:
2026-06-14 13:08:29 +03:00
parent b03c3b08ab
commit 91b8b1e7fe
19 changed files with 1381 additions and 135 deletions
+114
View File
@@ -0,0 +1,114 @@
/**
* Theme Builder — Legend panel (docs/chart-theming-scope.md §5).
*
* Structured controls over the base `legend` config: placement (`orient`), the
* title's and labels' color and size, and the symbol size. Legend type (size)
* lives here rather than the Type panel so every legend property a brand tunes
* sits together — the scope's "label/title color+size" grouping.
*/
import type { JsonObject } from '@core/spec-config';
import { asNumber, asString, type ConfigPath, getConfigValue } from '@core/theme-controls';
import { useConfigSetter } from '../stores/CustomThemeStore';
import { ColorRow, ControlSection, NumberRow, SelectRow } from './ThemeFields';
import type { SelectControlOption } from './SelectControl';
import styles from './ThemeFields.module.css';
const ORIENT: ConfigPath = ['legend', 'orient'];
const TITLE_COLOR: ConfigPath = ['legend', 'titleColor'];
const TITLE_SIZE: ConfigPath = ['legend', 'titleFontSize'];
const LABEL_COLOR: ConfigPath = ['legend', 'labelColor'];
const LABEL_SIZE: ConfigPath = ['legend', 'labelFontSize'];
const SYMBOL_SIZE: ConfigPath = ['legend', 'symbolSize'];
const TEXT_GREY = '#888888';
// Vega-Lite legend `orient` values; '' is the unset (theme default) sentinel.
type Orient = '' | 'right' | 'left' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'none';
const orientOptions: SelectControlOption<Orient>[] = [
{ value: '', label: 'Theme default' },
{ value: 'right', label: 'Right' },
{ value: 'left', label: 'Left' },
{ value: 'top', label: 'Top' },
{ value: 'bottom', label: 'Bottom' },
{ value: 'top-left', label: 'Top-left' },
{ value: 'top-right', label: 'Top-right' },
{ value: 'none', label: 'Hidden' },
];
const orientValue = (v: unknown): Orient => {
const s = asString(v);
return s !== undefined && orientOptions.some((o) => o.value === s) ? (s as Orient) : '';
};
export function LegendControls({ config }: { config: JsonObject }) {
const set = useConfigSetter();
const orient = orientValue(getConfigValue(config, ORIENT));
const titleColor = asString(getConfigValue(config, TITLE_COLOR));
const titleSize = asNumber(getConfigValue(config, TITLE_SIZE));
const labelColor = asString(getConfigValue(config, LABEL_COLOR));
const labelSize = asNumber(getConfigValue(config, LABEL_SIZE));
const symbolSize = asNumber(getConfigValue(config, SYMBOL_SIZE));
return (
<div className={styles.panel}>
<ControlSection title="Placement" hint="Where the legend sits relative to the plot.">
<SelectRow
id="legend-orient"
label="Position"
options={orientOptions}
value={orient}
onSelect={(o) => set(ORIENT, o === '' ? undefined : o)}
/>
</ControlSection>
<ControlSection title="Title">
<ColorRow
label="Color"
name="Legend title color"
value={titleColor}
fallback={TEXT_GREY}
onChange={(hex) => set(TITLE_COLOR, hex)}
onClear={() => set(TITLE_COLOR, undefined)}
/>
<NumberRow
label="Size"
name="Legend title size"
value={titleSize}
min={0}
unit="px"
onChange={(n) => set(TITLE_SIZE, n)}
/>
</ControlSection>
<ControlSection title="Labels">
<ColorRow
label="Color"
name="Legend label color"
value={labelColor}
fallback={TEXT_GREY}
onChange={(hex) => set(LABEL_COLOR, hex)}
onClear={() => set(LABEL_COLOR, undefined)}
/>
<NumberRow
label="Size"
name="Legend label size"
value={labelSize}
min={0}
unit="px"
onChange={(n) => set(LABEL_SIZE, n)}
/>
</ControlSection>
<ControlSection title="Symbols" hint="The colored keys beside each label.">
<NumberRow
label="Symbol size"
value={symbolSize}
min={0}
unit="px²"
onChange={(n) => set(SYMBOL_SIZE, n)}
/>
</ControlSection>
</div>
);
}