Theme Builder: structured color/type controls, scheme catalog, diverging preview

This commit is contained in:
2026-06-14 01:25:44 +03:00
parent 0470389b41
commit e4d1466ab0
17 changed files with 1607 additions and 52 deletions
+74 -1
View File
@@ -165,7 +165,80 @@ ships, it is an explicit per-font user action, never automatic.
**Rejected:** per-snippet theme field (2026-06-12 — `spec.config` + merge/extract covers
it without a second mechanism).
## 5. Status log
## 5. Structured controls (slice 4b)
The builder today is a raw JSON textarea + one font dropdown + the live gallery. Slice 4b
adds a strip of structured controls above the editor — accelerators that write into the
JSON, never replacing it. The JSON stays the source of truth and the full-power escape
hatch; controls cover the common ~80% (color, type, spacing, grid), not all 72 config
properties (that is the trap vega-editor deliberately avoids by staying JSON).
**Hard constraint — the builder must preserve unknown keys.** Verified by compiling: the
vega-themes presets carry Vega-_layer_ keys (`symbol`, `shape`, `path`, `group`) that are
not in the Vega-Lite `Config` schema, and Vega-Lite forwards the whole config to Vega
unchanged — they take effect. So a structured control must **merge into** the existing
config (immutable path-set that spreads siblings), never rebuild it from a closed
schema-typed model, or it silently drops those keys on a round-trip. Same shape as
`applyFontToConfig`, which walks and rewrites rather than reconstructing.
**Resolved design points:**
- **Surfacing — inline tab strip** (not popovers, not sub-modals). Tabs (Color / Type /
Layout / Axes & grid / Legend) sit between the toolbar and the JSON+gallery, all in the
one xlarge modal. No nested overlays/focus traps, no contention with the one-open-popover
registry, and panels + JSON + gallery stay visible together.
- **Color model — scheme picker that materializes to swatches.** A `range` family takes
either an explicit color array or a named Vega scheme string (both verified to compile).
Pick a named scheme for the quick path; "materialize" expands it to an editable swatch
array for brand tuning. Catalog ships 15 categorical + 24 sequential + 10 diverging
schemes; categorical schemes resolve to arrays, continuous ones to interpolators sampled
into stops for the gradient preview and the materialize action.
- Structured controls are gated on valid JSON (same as the font control): a parse error
disables them and the textarea is the fix.
- Controls write **minimal** config — clearing a value deletes the key rather than writing
a default, so a theme stays a diff against stock, not a full dump.
**Panels:** Color (`range.category` swatches/scheme, `mark.color`, `range.heatmap`/`ramp`/
`diverging`) · Type (base `font`, title/axis/legend size+weight) · Layout (`background` incl.
transparent, `padding`, `view.stroke`/`fill`/cornerRadius) · Axes & grid (grid on/off + color
- dash, domain, label color/angle — base `axis` only; the 25 variants stay JSON) · Legend
(orient, label/title color+size, symbol size).
**Build order:** (a) core foundation — scheme catalog + immutable config path get/set +
`schemeColors` materialize, with tests; (b) Color panel (highest payoff); (c) Type, Layout,
Axes, Legend panels; (d) wire the tab strip into the modal.
**Not in slice 4b:** the house style's own gaps — no `mark.color` (single-series charts stay
Vega-blue), unset legend/header/padding — are left for a separate house-style redo, not
papered over here. Minor cleanup noted: `theme-preview-specs.ts` declares `$schema` v5 while
the app standardizes on v6.
## 6. Status log
- **2026-06-14 (slice 4b, first increment)** — **structured-control foundation + Color
panel.** Core `theme-controls.ts`: immutable config path get/set (preserves siblings —
the Vega-layer-key guarantee — and prunes on delete) + the named-scheme catalog (15
categorical / 24 sequential / 10 diverging) + `schemeColors` resolution (categorical
arrays passthrough, continuous interpolators sampled to hex), all tested. `vega-scale`
added as a declared dep (focused sub-package, like `vega-expression`) with a typings
shim in `vite-env.d.ts` (its package.json `exports` omits `types`). Store gains the
generic `mutateDraftConfig(fn)` write path; `applyDraftFont` refactored onto it. Modal
gains an APG tab strip — **Color** (categorical scheme/swatches + materialize, default
`mark.color`, sequential/diverging gradient pickers) and **Type** (the relocated font
control). Tabpanel gated on valid JSON. From first-use feedback, same day: the modal
body is now **controls + JSON on the left, gallery as a full-height right rail** (the
previews were starved before); `SelectControl` gained an optional per-option `preview`
so the scheme dropdowns show swatch strips (categorical) / gradient bars (continuous);
every swatch is a reusable `SwatchRow` (color picker + copyable/editable hex field); and
sequential/diverging gained **Materialize → editable stops**, so custom gradient colors
are possible, not just named schemes. Second feedback pass: the raw JSON is now a
**collapsed disclosure** at the bottom of the controls column (it was eating half the
first screen), forced open only on a parse error; the structured controls fill the
column. `SelectControl` options gained a `labelStyle`, so the **font dropdown renders
each name in its own family** (the type analogue of the color swatches) and its trigger
shows the current font in-face. Verified: typecheck, lint, full tests (950).
Remaining: Layout / Axes & grid / Legend panels; swatch reorder.
- **2026-06-12 (slice 4 close-out)** — **custom themes in the §08 envelope.** The
workspace export now writes a `themes` array (additive — no format bump; importers