mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Theme Builder: Layout/Axes/Legend/Type panels, scheme-render fix, fail-loud gallery
This commit is contained in:
@@ -139,6 +139,17 @@ async function rerender(node: HTMLElement, spec: TopLevelSpec, config: Config) {
|
||||
**render-size** limit (the chart is physically too big), distinct from the readability
|
||||
cardinality warnings — don't conflate them. Only an _unbounded_ axis overflows: a
|
||||
`width: 'container'` axis is bounded, so it's the deleted (natural-height) axis to watch.
|
||||
- **Load fonts before rendering.** Vega measures every text label via canvas
|
||||
`measureText` **regardless of renderer** (even the `'none'` probe runs layout),
|
||||
so a face that finishes loading after embed lays the whole chart out with
|
||||
fallback metrics. `renderSpec` therefore gates on `document.fonts.load` for the
|
||||
families a spec+config reference (`collectFontFamilies`, core) before any layout
|
||||
pass. This is a non-critical enhancement, so it waits on `allSettled` + a
|
||||
timeout: a face failing (offline, 404, a system family with no `@font-face`)
|
||||
degrades to fallback metrics rather than failing the chart (the sanctioned
|
||||
swallow under §7's fail-loud rule). Chart fonts are self-hosted in
|
||||
`styles/chart-fonts.css` (offered by the Theme Builder's font control); only
|
||||
their latin subsets are precached, the rest runtime-cached (vite.config Workbox).
|
||||
- **Do** call `view.finalize()` on every previous view before rendering a new
|
||||
one, and on component unmount.
|
||||
- **Do** keep exactly one live view per preview node.
|
||||
@@ -218,7 +229,9 @@ The gallery (`core/theme-preview-specs.ts`, fixed inline-data swatch specs)
|
||||
renders `draftConfig` per card through the shared `renderSpec` with the
|
||||
**canvas** renderer and a per-card debounce + chain-lock (the LivePreview
|
||||
serialization pattern, one lock per card) — so invalid JSON mid-edit never
|
||||
blanks the preview, and seven concurrent embeds never interleave on a node.
|
||||
blanks the preview, and seven concurrent embeds never interleave on a node. A
|
||||
card whose render throws shows the error message in place of the chart (the same
|
||||
fail-loud treatment as LivePreview, §7), never a silent blank.
|
||||
`applyFontToConfig(config, family)` is the font control's transform: it sets
|
||||
the top-level `font` and rewrites every `font`/`*Font` string slot at any
|
||||
depth — explicit slots would otherwise keep overriding the new default.
|
||||
@@ -238,25 +251,42 @@ that boundary deliberately: merge bakes the selected theme into `spec.config`
|
||||
|
||||
### Structured controls
|
||||
|
||||
The builder's panels (`ColorControls`; the Type tab's font control) are
|
||||
accelerators over the same `draftConfig`: each reads a value and writes one back
|
||||
through `CustomThemeStore.mutateDraftConfig(fn)` — the single transform path,
|
||||
which reparses, reformats, and updates `draftConfig` so the JSON editor and
|
||||
gallery follow (a parse error disables the controls). The pure transforms live
|
||||
in `core/theme-controls.ts`: immutable config path get/set, the named-scheme
|
||||
catalog (`THEME_SCHEMES`), and `schemeColors` (scheme name → hex swatches, from
|
||||
the `vega-scale` registry — a focused vega sub-package). A color family holds
|
||||
**either** a named scheme string **or** an explicit color array; the picker
|
||||
materializes one to the other. Family by scale: `range.category` (nominal),
|
||||
`range.ramp` (continuous; `range.heatmap` for `rect`), `range.diverging`
|
||||
(continuous color with a `domainMid`).
|
||||
The builder's panels — Color, Type, Layout, Axes & grid, Legend
|
||||
(`ColorControls` + `TypeControls`/`LayoutControls`/`AxesControls`/`LegendControls`
|
||||
on the shared `ThemeFields` field primitives) — are accelerators over the same
|
||||
`draftConfig`: each reads a value and writes one back through
|
||||
`CustomThemeStore.mutateDraftConfig(fn)` — the single transform path, which
|
||||
reparses, reformats, and updates `draftConfig` so the JSON editor and gallery
|
||||
follow (a parse error disables the controls). The pure transforms live in
|
||||
`core/theme-controls.ts`: immutable config path get/set, leaf coercion, the
|
||||
named-scheme catalog (`THEME_SCHEMES`), and `schemeColors` (scheme name → hex
|
||||
swatches, from the `vega-scale` registry — a focused vega sub-package). A color
|
||||
family holds **either** a named scheme as Vega's range-scheme **object**
|
||||
`{ scheme: name }` **or** an explicit color array; the picker materializes one to
|
||||
the other. Family by scale: `range.category` (nominal), `range.ramp` (continuous;
|
||||
`range.heatmap` for `rect`), `range.diverging` (continuous color with a
|
||||
`domainMid`).
|
||||
|
||||
- **Do** route structured edits through `mutateDraftConfig` + `setConfigValue`,
|
||||
which sets a value at a path **immutably, preserving sibling keys**, and
|
||||
deletes (pruning emptied ancestors) on `undefined` so a theme stays a diff.
|
||||
Repeated control labels across panels ("Size", "Color", "Weight") get a
|
||||
qualified accessible name while keeping the short visible label; the section is a
|
||||
`role="group"` labelled by its heading (APG group pattern), so the name a screen
|
||||
reader announces is unambiguous.
|
||||
|
||||
- **Do** bind a panel's writes to the shared `useConfigSetter()` hook (in
|
||||
`CustomThemeStore` — beside `mutateDraftConfig`, not the JSX field module, which
|
||||
stays component-only for fast refresh). It is `mutateDraftConfig` + `setConfigValue`:
|
||||
sets a value at a path **immutably, preserving sibling keys**, and deletes
|
||||
(pruning emptied ancestors) on `undefined` so a theme stays a diff. A new panel
|
||||
uses it rather than re-inlining the pair.
|
||||
- **Don't** rebuild the config from a fixed schema: vega-themes presets carry
|
||||
Vega-_layer_ keys (`symbol`/`shape`/`path`/`group`) absent from the Vega-Lite
|
||||
`Config` schema but forwarded to Vega — a rebuild drops them. Merge in place.
|
||||
- **Do** write a named scheme into `range.*` as the object `{ scheme: name }`. A
|
||||
bare scheme-name string passes vega-lite _compile_ but Vega rejects it at
|
||||
_render_ ("Unrecognized scale range value"), blanking the chart.
|
||||
`normalizeRangeSchemes` (core) heals the bare form at the render-resolution
|
||||
points (`chartConfigForSelection`; the builder gallery) for configs authored or
|
||||
saved before this was enforced.
|
||||
- **Do** add a `theme-preview-specs.ts` gallery card for any new color family,
|
||||
so no control ships without a visible mirror.
|
||||
|
||||
|
||||
@@ -154,9 +154,14 @@ ships, it is an explicit per-font user action, never automatic.
|
||||
active one falls back to Astrolabe. Custom themes travel in the §08 workspace
|
||||
export/import envelope (additive `themes` array, name auto-suffix on clash, ids
|
||||
reassigned by the store, rolled back with datasets on a failed import).
|
||||
5. **Shipped font roster** — fontsource packages, `@font-face` registration, selector
|
||||
metadata (which themes/fonts pair), `document.fonts.load` gate in the render path,
|
||||
precache strategy above. Roster finalized via visual specimen.
|
||||
5. **Shipped font roster** ✅ (2026-06-14) — 11 self-hosted families via @fontsource
|
||||
(`styles/chart-fonts.css`, full subsets bundled) extending `THEME_FONT_OPTIONS` to 17
|
||||
entries; `collectFontFamilies` (core) + a `document.fonts.load` gate at the top of
|
||||
`renderSpec` (before the layout/probe pass, which measures text regardless of
|
||||
renderer); Workbox precaches the `latin` subset of the roster (~520KB) plus every
|
||||
subset of the UI Plex Sans/Mono, and runtime-caches the rest (latin-ext + non-latin)
|
||||
CacheFirst so a script works offline after first use. Roster picked from a visual
|
||||
specimen. Not done here: theme↔font pairing metadata (a suggestion nicety, deferred).
|
||||
6. **User font upload** — FontFace-from-IndexedDB tier; theme entity's `fonts` field
|
||||
carries `{ family, source: 'file' }`.
|
||||
7. **Deferred** — Google Fonts opt-in tier; SVG export font embedding; built-in
|
||||
@@ -188,7 +193,10 @@ schema-typed model, or it silently drops those keys on a round-trip. Same shape
|
||||
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).
|
||||
either an explicit color array or a named scheme written as Vega's range-scheme object
|
||||
`{ scheme: name }`. (A bare scheme-name _string_ passes vega-lite compile but Vega
|
||||
rejects it at render — "Unrecognized scale range value" — so the controls write the
|
||||
object form, read either, and `normalizeRangeSchemes` heals the bare form at render.)
|
||||
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
|
||||
@@ -199,11 +207,11 @@ schema-typed model, or it silently drops those keys on a round-trip. Same shape
|
||||
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).
|
||||
`diverging`) · Type (base `font`, title + axis title/label size+weight) · Layout (`background`
|
||||
incl. transparent, `padding`, `view.stroke`/`fill`/cornerRadius) · Axes & grid (grid on/off +
|
||||
color + dash, domain, label color/angle, title color — base `axis` only; the 25 variants stay
|
||||
JSON) · Legend (orient, title/label color+size, symbol size). Legend _type_ (size) lives in
|
||||
the Legend panel rather than Type, so every legend property a brand tunes sits together.
|
||||
|
||||
**Build order:** (a) core foundation — scheme catalog + immutable config path get/set +
|
||||
`schemeColors` materialize, with tests; (b) Color panel (highest payoff); (c) Type, Layout,
|
||||
@@ -216,6 +224,60 @@ the app standardizes on v6.
|
||||
|
||||
## 6. Status log
|
||||
|
||||
- **2026-06-14 (Color panel bugfix)** — **scheme picks rendered blank.** A named scheme
|
||||
was written into `config.range.*` as a bare string, which vega-lite compiles but Vega
|
||||
rejects at render ("Unrecognized scale range value") — silently caught by the gallery's
|
||||
per-card try/catch, so the categorical/sequential/diverging charts blanked the moment a
|
||||
scheme was picked. Predates this session's panels/fonts (shipped with the Color panel).
|
||||
Fix: the controls write Vega's range-scheme object `{ scheme: name }` and read either
|
||||
form; `normalizeRangeSchemes` (core) heals a bare-form config at the render-resolution
|
||||
points (`chartConfigForSelection` for the live preview/export, and the builder gallery),
|
||||
so themes saved/imported with the old form self-heal. The gallery's catch now surfaces the
|
||||
error message in the card (fail-loud, arch 02) so a render failure on valid JSON isn't
|
||||
invisible again. Regression cover: a real
|
||||
vega-lite→vega compile/parse/run asserting `{ scheme }` renders and the bare string
|
||||
throws, plus `normalizeRangeSchemes` unit tests. Verified: typecheck, lint, tests (983).
|
||||
|
||||
- **2026-06-14 (slice 5)** — **shipped font roster.** 11 self-hosted families
|
||||
(`styles/chart-fonts.css`, imported in main.tsx, separate from the UI Plex in base.css):
|
||||
Inter · Libre Franklin · Roboto Condensed · IBM Plex Sans Condensed · IBM Plex Serif ·
|
||||
Source Serif 4 · Spectral · Space Grotesk · Playfair Display · Caveat · Space Mono, at
|
||||
400 + 600 (Space Mono 400 + 700). `THEME_FONT_OPTIONS` grew to 17 (roster grouped by
|
||||
role, then the system stacks); each roster stack carries a category fallback. The render
|
||||
path now gates on fonts: `collectFontFamilies` (core, the read-counterpart of
|
||||
`applyFontToConfig`; skips `data`/`datasets`) gathers the families a spec+config use and
|
||||
`renderSpec` awaits `document.fonts.load` for them before the first layout pass — Vega
|
||||
measures text via canvas `measureText` regardless of renderer, so a face loading after
|
||||
embed would lay out with fallback metrics. Best-effort + 3s-capped so a slow first fetch
|
||||
never freezes the preview. Precache strategy (vite.config Workbox): the `latin` subset
|
||||
of every family (~520KB for the roster) + all Plex Sans/Mono subsets (UI capability) are
|
||||
precached; latin-ext and non-latin scripts are runtime-cached CacheFirst (`*-latin-[0-9]*`
|
||||
excludes latin-ext; the Plex Sans brace-list avoids matching the condensed roster font).
|
||||
Verified: typecheck, lint, full tests (976), production build + precache-manifest
|
||||
inspection. Note: @fontsource ships legacy `.woff` beside `.woff2`; modern browsers use
|
||||
woff2, so the `.woff` sit unused in dist (pre-existing for Plex — neither precached nor
|
||||
runtime-cached).
|
||||
|
||||
- **2026-06-14 (slice 4b complete)** — **Layout / Axes & grid / Legend panels + Type
|
||||
size/weight.** The remaining structured-control panels, built on a small shared
|
||||
primitives module `ThemeFields.tsx` (`ControlSection`, `ColorRow`, `NumberRow`,
|
||||
`SelectRow`) so the panels read declaratively and match the Color panel's look. Each
|
||||
control writes one config path through the same inline `mutateDraftConfig` +
|
||||
`setConfigValue` the Color panel uses, with the minimal-diff delete (clearing a value
|
||||
removes the key, pruning emptied objects). Leaf coercion (`asString`/`asNumber`/
|
||||
`asBoolean`) moved into core `theme-controls.ts` beside the path get/set, tested there.
|
||||
Panels: **Layout** (background and `view` fill/border as tri-state default·transparent/
|
||||
none·custom, corner radius, scalar padding with a JSON hint when it's a per-side object);
|
||||
**Axes & grid** (grid visibility/color/dash-preset, domain/label/title color, label
|
||||
angle — base `axis` only); **Legend** (orient, title/label color+size, symbol size);
|
||||
**Type** rounded out with title and axis title/label size+weight (font family relocated
|
||||
into the extracted `TypeControls`). Resolved while building: each generic row label
|
||||
("Size", "Color", "Weight") repeats across sections, so `ControlSection` is a
|
||||
`role="group"` labelled by its heading and rows take an accessible-name override — the
|
||||
visible label stays short, the control's announced name is qualified ("Title size"). The
|
||||
font-roster decision (slice 5) was teed up with a throwaway visual specimen. Verified:
|
||||
typecheck, lint, full tests (969). Remaining in 4b: swatch reorder (Color panel).
|
||||
|
||||
- **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
|
||||
|
||||
Reference in New Issue
Block a user