Chart theming: custom named themes + Theme Builder

This commit is contained in:
2026-06-12 18:15:54 +03:00
parent 44a601affd
commit b193464f55
32 changed files with 2220 additions and 70 deletions
+3 -3
View File
@@ -46,12 +46,12 @@ Notes:
## C. Modal System
The app shows at most one modal at a time. The modal set is: Datasets, About & Help, Donate, Chart Builder, and Extract-to-Dataset. (Settings are deliberately _not_ a modal — they are distributed to per-pane controls; see _Settings_.)
The app shows at most one modal at a time. The modal set is: Datasets, About & Help, Donate, Chart Builder, Extract-to-Dataset, and Theme Builder. (Settings are deliberately _not_ a modal — they are distributed to per-pane controls; see _Settings_.)
- Opening any modal closes whichever modal was previously open; the two never overlap.
- Every modal can be dismissed by: clicking its close button, pressing **Escape**, or clicking the backdrop outside the modal body.
- Every modal can be dismissed by: clicking its close button, pressing **Escape**, or clicking the backdrop outside the modal body. Exception: modals holding in-progress work (the Chart Builder and Theme Builder) ignore backdrop clicks; Escape and the close button still dismiss them.
- Clicking inside the modal body does not dismiss it.
- The Chart Builder and Extract-to-Dataset modals are opened from within the Datasets / snippet workflows (see _Chart Builder_ and _Datasets_), not from the header.
- The Chart Builder, Extract-to-Dataset, and Theme Builder modals are opened from within the Datasets / snippet / preview workflows (see _Chart Builder_, _Datasets_, and _Live Preview_), not from the header.
- Dismissing a modal returns the user to the underlying workspace unchanged.
## D. Keyboard Shortcuts
+17 -1
View File
@@ -42,14 +42,30 @@ The preview pane header carries a **Chart theme** picker — a value-select disc
- **Astrolabe** (default) — the house style; follows the app's light/dark theme.
- **Stock Vega-Lite** — injects nothing; charts render exactly as plain Vega-Lite defaults would anywhere else (white background, default palette and fonts).
- **Custom themes** — the user's saved themes (see _Theme Builder_ below), listed by name between the built-ins and the presets.
- **Edit themes…** — closes the custom-themes block (before the long preset roster, so it's visible without scrolling); opens the Theme Builder instead of changing the selection.
- **Presets** — the `vega-themes` preset configs (Excel, ggplot2, FiveThirtyEight, LA Times, Power BI, the Carbon family, …), rendered verbatim and independent of the app's light/dark theme.
Behavior:
- The choice is a **global preference**, not per-snippet; it persists across sessions, stored in _Settings_ as `ui.chartTheme`.
- The choice is a **global preference**, not per-snippet; it persists across sessions, stored in _Settings_ as `ui.chartTheme` (`custom:<id>` for a custom theme).
- The injected config applies at render time only — it is never written into the snippet's stored spec. A spec's own `config` block overrides the injected config property by property, so a snippet can opt out of any part of it locally (see also _Spec Editor → Spec ↔ config actions_).
- Image export reflects the selected theme: exports render from the same themed view.
- The Chart Builder preview and onboarding thumbnails are app surfaces and stay house-styled regardless of this choice.
- A selected custom theme whose record is missing (still loading, or deleted in another tab) renders as the house style; deleting the actively-selected theme resets the selection to Astrolabe.
## Theme Builder
The **Theme Builder** is a full-size modal for creating and editing custom chart themes — named, persistent Vega-Lite configs (see _Data Model → CustomTheme_). It opens from the Chart theme picker's "Edit themes…" entry.
Layout: a saved-theme list on the left; the open theme's editor on the right.
- **New theme** creates a theme seeded as a **copy of the chart theme currently selected** in the preview (house style, stock, a preset, or another custom theme), named after its source (e.g. "FiveThirtyEight copy") and auto-suffixed if taken. Duplicating a preset is the expected starting point. The other creation path is the editor's **Extract Config to New Theme** action (see _Spec Editor → Spec ↔ Config Actions_), which turns a pasted spec's `config` block into a theme directly.
- The editor shows the theme's **name** and its **config as editable JSON text**. Invalid JSON is reported inline and blocks saving; the text must parse to a JSON object.
- A **font control** applies a chosen font family across the whole config in one step: it sets the top-level `font` (Vega-Lite's default for every text mark, label, and title) and rewrites every explicit `font`/`labelFont`/`titleFont`/`subtitleFont` slot anywhere in the config — the slots that would otherwise keep overriding the new default. Offered fonts are limited to faces that render without loading (the app's own Plex faces and web-safe/system stacks) until the self-hosted font roster ships.
- A **gallery** of small fixed sample charts (bar with title, multi-series line with subtitle, stacked area, scatter with a gradient legend, heatmap, donut, facets with headers) re-renders live from the draft config — the same config-injection path the preview uses — so one edit is previewed across every chart surface a config styles. While the JSON is invalid, the gallery keeps showing the last valid state.
- **Save** commits the draft (disabled while unchanged or unparseable). Names are unique case-insensitively, like dataset names. **Delete** removes the theme after confirmation.
- Closing with unsaved edits prompts for discard, like other form modals. A backdrop click does not dismiss the builder (Escape and the close button do).
## Export control
+31 -15
View File
@@ -65,21 +65,21 @@ The current **Dataset** version is `2`. The v1→v2 migration reflects the URL-s
**UserSettings** holds persisted user preferences as a single structured record. The semantics and UX of each option are covered in _Settings_; the shape below is the storage contract.
| Field | Type | Meaning |
| ----------------------------- | ------- | ---------------------------------------------------------- |
| `version` | number | Schema version of the settings record, used for migration. |
| `editor.fontSize` | number | Editor font size. |
| `editor.theme` | string | Editor color theme identifier. |
| `editor.minimap` | boolean | Whether the editor minimap is shown. |
| `editor.wordWrap` | string | `on` or `off`. |
| `editor.lineNumbers` | string | `on` or `off`. |
| `editor.tabSize` | number | Spaces per indentation level. |
| `performance.renderDebounce` | number | Delay (ms) before re-rendering the preview after edits. |
| `ui.theme` | string | App theme: `light` or `dark`. |
| `ui.previewFitMode` | string | Preview sizing: `default`, `width`, `height`, or `full`. |
| `ui.chartTheme` | string | Chart theme: `astrolabe`, `stock`, or a preset id. |
| `formatting.dateFormat` | string | Date display mode: `smart`, `iso`, or `custom`. |
| `formatting.customDateFormat` | string | Pattern used when `dateFormat = custom`. |
| Field | Type | Meaning |
| ----------------------------- | ------- | -------------------------------------------------------------------------------------------- |
| `version` | number | Schema version of the settings record, used for migration. |
| `editor.fontSize` | number | Editor font size. |
| `editor.theme` | string | Editor color theme identifier. |
| `editor.minimap` | boolean | Whether the editor minimap is shown. |
| `editor.wordWrap` | string | `on` or `off`. |
| `editor.lineNumbers` | string | `on` or `off`. |
| `editor.tabSize` | number | Spaces per indentation level. |
| `performance.renderDebounce` | number | Delay (ms) before re-rendering the preview after edits. |
| `ui.theme` | string | App theme: `light` or `dark`. |
| `ui.previewFitMode` | string | Preview sizing: `default`, `width`, `height`, or `full`. |
| `ui.chartTheme` | string | Chart theme: `astrolabe`, `stock`, a preset id, or `custom:<id>` naming a _CustomTheme_ (G). |
| `formatting.dateFormat` | string | Date display mode: `smart`, `iso`, or `custom`. |
| `formatting.customDateFormat` | string | Pattern used when `dateFormat = custom`. |
A reference shape:
@@ -98,6 +98,7 @@ Some preferences persist independently of _UserSettings_ so they can update freq
| ---------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Snippet store | All _Snippet_ records | Local, with a practical budget of about 5 MB. A storage monitor tracks usage and surfaces warnings as the budget fills (see _Snippet Library_). |
| Dataset store | All _Dataset_ records | Local, in a separate, much higher-capacity store, suited to larger payloads. |
| Theme store | All _CustomTheme_ records (G) | Local, separate store; records are small (a config object plus metadata). |
| Settings & preferences | _UserSettings_ plus the app/UI preferences in (D) | Local, small. |
Everything stays in the browser — no server or account is involved. All tiers survive reload and function offline. Because capacity is finite and per-browser, _Import & Export_ is the supported path for backup and for moving data between browsers or devices.
@@ -110,3 +111,18 @@ Snippets and datasets are linked **bidirectionally by dataset name**: `snippet.d
- From a dataset, scanning snippets for its `name` in `datasetRefs` yields the snippets that reference it.
This name-based link is what the _Snippet Library_ and _Datasets_ surfaces use to show linkage in both directions. The actual resolution of a referenced dataset into spec data at render time is covered in _Live Preview_.
## G. CustomTheme
A **CustomTheme** is a user-named Vega-Lite config saved in the library and offered by the _Live Preview → Chart theme_ picker alongside the built-in themes and presets. It is created and edited in the _Theme Builder_ (see _Live Preview_).
| Field | Type | Meaning |
| ---------- | -------------------- | --------------------------------------------------------------------------------------------- |
| `id` | number | Unique numeric identifier. The picker/persistence selection id is the string `custom:<id>`. |
| `version` | number | Schema version of this record, used for read-time migration (see _Schema versioning_ above). |
| `name` | string | Unique, human-readable name shown in the picker (case-insensitive uniqueness, like datasets). |
| `config` | object | The Vega-Lite config injected at render time when this theme is selected. |
| `created` | ISO-timestamp string | When the theme was first created. |
| `modified` | ISO-timestamp string | When the theme was last changed. |
Selection is keyed by `id` (not name) so renaming a theme never invalidates the persisted `ui.chartTheme`. A persisted `custom:<id>` whose record no longer exists is not an error: charts render with the house style until the record appears (themes hydrate asynchronously), and deleting the actively-selected theme resets the selection to `astrolabe` explicitly. Custom themes are not yet included in the _Import & Export_ envelope (planned; see `docs/chart-theming-scope.md`).