/** * Theme Builder — Legend panel (docs/chart-theming-scope.md §5). * * Structured controls over the base `legend` config: placement, the title's and * labels' colour and size, the symbol keys, the continuous gradient bar, and an * optional box behind the legend. Legend type (size) lives here rather than the * Type panel so every legend property a brand tunes sits together. */ import type { JsonObject } from '@core/spec-config'; import { asNumber, asString, type ConfigPath, countSet, enumValue, getConfigValue, } from '@core/theme-controls'; import { useConfigSetter } from '../stores/CustomThemeStore'; import { Accordion, type AccordionSection, ColorRow, NumberRow, SelectRow } from './ThemeFields'; import type { SelectControlOption } from './SelectControl'; const ORIENT: ConfigPath = ['legend', 'orient']; const DIRECTION: ConfigPath = ['legend', 'direction']; const COLUMNS: ConfigPath = ['legend', 'columns']; 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 SYMBOL_TYPE: ConfigPath = ['legend', 'symbolType']; const GRADIENT_LENGTH: ConfigPath = ['legend', 'gradientLength']; const GRADIENT_THICKNESS: ConfigPath = ['legend', 'gradientThickness']; const FILL_COLOR: ConfigPath = ['legend', 'fillColor']; const STROKE_COLOR: ConfigPath = ['legend', 'strokeColor']; const CORNER_RADIUS: ConfigPath = ['legend', 'cornerRadius']; const PADDING: ConfigPath = ['legend', 'padding']; 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[] = [ { 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' }, ]; type Direction = '' | 'horizontal' | 'vertical'; const directionOptions: SelectControlOption[] = [ { value: '', label: 'Theme default' }, { value: 'vertical', label: 'Vertical' }, { value: 'horizontal', label: 'Horizontal' }, ]; // Discrete-legend key shapes (continuous legends render a gradient bar instead). type SymbolType = '' | 'circle' | 'square' | 'cross' | 'diamond' | 'triangle-up' | 'stroke'; const symbolTypeOptions: SelectControlOption[] = [ { value: '', label: 'Theme default' }, { value: 'circle', label: 'Circle' }, { value: 'square', label: 'Square' }, { value: 'cross', label: 'Cross' }, { value: 'diamond', label: 'Diamond' }, { value: 'triangle-up', label: 'Triangle' }, { value: 'stroke', label: 'Line' }, ]; export function LegendControls({ config }: { config: JsonObject }) { const set = useConfigSetter(); const sections: AccordionSection[] = [ { id: 'placement', title: 'Placement', hint: 'Where the legend sits relative to the plot.', badge: countSet(config, [ORIENT, DIRECTION, COLUMNS]), children: ( <> set(ORIENT, o === '' ? undefined : o)} /> set(DIRECTION, d === '' ? undefined : d)} /> set(COLUMNS, n)} /> ), }, { id: 'title', title: 'Title', badge: countSet(config, [TITLE_COLOR, TITLE_SIZE]), children: ( <> set(TITLE_COLOR, hex)} onClear={() => set(TITLE_COLOR, undefined)} /> set(TITLE_SIZE, n)} /> ), }, { id: 'labels', title: 'Labels', badge: countSet(config, [LABEL_COLOR, LABEL_SIZE]), children: ( <> set(LABEL_COLOR, hex)} onClear={() => set(LABEL_COLOR, undefined)} /> set(LABEL_SIZE, n)} /> ), }, { id: 'symbols', title: 'Symbols', hint: 'The colored keys beside each label.', badge: countSet(config, [SYMBOL_TYPE, SYMBOL_SIZE]), children: ( <> set(SYMBOL_TYPE, t === '' ? undefined : t)} /> set(SYMBOL_SIZE, n)} /> ), }, { id: 'gradient', title: 'Gradient', hint: 'The continuous color bar (quantitative legends).', badge: countSet(config, [GRADIENT_LENGTH, GRADIENT_THICKNESS]), children: ( <> set(GRADIENT_LENGTH, n)} /> set(GRADIENT_THICKNESS, n)} /> ), }, { id: 'box', title: 'Box', hint: 'An optional filled/bordered panel behind the legend.', badge: countSet(config, [FILL_COLOR, STROKE_COLOR, CORNER_RADIUS, PADDING]), children: ( <> set(FILL_COLOR, hex)} onClear={() => set(FILL_COLOR, undefined)} /> set(STROKE_COLOR, hex)} onClear={() => set(STROKE_COLOR, undefined)} /> set(CORNER_RADIUS, n)} /> set(PADDING, n)} /> ), }, ]; return ; }