Theme Builder: accordion panels across the full config surface, vertical-tab nav, reflective gallery

This commit is contained in:
2026-06-21 17:14:58 +03:00
parent 318a0919c6
commit d76a7a5014
24 changed files with 2420 additions and 845 deletions
+124
View File
@@ -0,0 +1,124 @@
/**
* Theme Builder — Formats panel (docs/chart-theming-scope.md §5).
*
* Default number and date formatting (`config.numberFormat`,
* `normalizedNumberFormat`, `timeFormat`) plus the count-field title — the
* brand-level "always show currency / thousands / short dates" knobs Vega-Lite
* applies to guide labels, text marks, and tooltips. The values are d3-format /
* d3-time-format strings; each field is free text with quick-fill presets for
* the common patterns, since the pattern space is open (the JSON stays the way
* to write any string). Each writes a minimal diff via the shared setter.
*/
import type { JsonObject } from '@core/spec-config';
import { asString, type ConfigPath, countSet, getConfigValue } from '@core/theme-controls';
import { useConfigSetter } from '../stores/CustomThemeStore';
import { Button } from './Button';
import { Accordion, type AccordionSection, TextRow } from './ThemeFields';
import styles from './ThemeFields.module.css';
const NUMBER: ConfigPath = ['numberFormat'];
const NORMALIZED: ConfigPath = ['normalizedNumberFormat'];
const TIME: ConfigPath = ['timeFormat'];
const COUNT_TITLE: ConfigPath = ['countTitle'];
interface Preset {
/** The d3 pattern written into the config. */
code: string;
/** A rendered example, shown as the chip label. */
sample: string;
}
const NUMBER_PRESETS: Preset[] = [
{ code: ',', sample: '1,234' },
{ code: '$,.2f', sample: '$1,234.00' },
{ code: '.0%', sample: '12%' },
{ code: '.2s', sample: '1.2k' },
{ code: '+,', sample: '+1,234' },
];
const TIME_PRESETS: Preset[] = [
{ code: '%b %Y', sample: 'Jan 2026' },
{ code: '%Y-%m-%d', sample: '2026-01-01' },
{ code: '%b %-d', sample: 'Jan 1' },
{ code: '%-d %b %Y', sample: '1 Jan 2026' },
];
/** A row of quick-fill chips that write a preset pattern into a config key. */
function Presets({ presets, onPick }: { presets: Preset[]; onPick: (code: string) => void }) {
return (
<div className={styles.presets}>
{presets.map((p) => (
<Button key={p.code} variant="ghost" onClick={() => onPick(p.code)} title={p.code}>
{p.sample}
</Button>
))}
</div>
);
}
export function FormatControls({ config }: { config: JsonObject }) {
const set = useConfigSetter();
const sections: AccordionSection[] = [
{
id: 'numbers',
title: 'Numbers',
hint: 'd3-format pattern for labels, text, and tooltips.',
badge: countSet(config, [NUMBER, NORMALIZED]),
children: (
<>
<TextRow
label="Format"
name="Number format"
value={asString(getConfigValue(config, NUMBER))}
placeholder="e.g. $,.2f"
onChange={(v) => set(NUMBER, v)}
/>
<Presets presets={NUMBER_PRESETS} onPick={(code) => set(NUMBER, code)} />
<TextRow
label="Stacked %"
name="Normalized number format"
value={asString(getConfigValue(config, NORMALIZED))}
placeholder="e.g. .0%"
onChange={(v) => set(NORMALIZED, v)}
/>
</>
),
},
{
id: 'dates',
title: 'Dates',
hint: 'd3-time-format pattern for raw time values.',
badge: countSet(config, [TIME]),
children: (
<>
<TextRow
label="Format"
name="Date format"
value={asString(getConfigValue(config, TIME))}
placeholder="e.g. %b %Y"
onChange={(v) => set(TIME, v)}
/>
<Presets presets={TIME_PRESETS} onPick={(code) => set(TIME, code)} />
</>
),
},
{
id: 'labels',
title: 'Labels',
badge: countSet(config, [COUNT_TITLE]),
children: (
<TextRow
label="Count title"
name="Count title"
value={asString(getConfigValue(config, COUNT_TITLE))}
placeholder="Count of Records"
onChange={(v) => set(COUNT_TITLE, v)}
/>
),
},
];
return <Accordion sections={sections} idPrefix="formats" />;
}