mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart theming: user font upload (FontFace-from-IndexedDB) + variable-font weight support
This commit is contained in:
@@ -514,9 +514,13 @@ is in the maintained plan, not the archive.
|
||||
|
||||
**Chart theming** (`exploration/chart-theming-scope.md`):
|
||||
|
||||
- **User font upload** — FontFace-from-IndexedDB tier; theme `fonts: { family, source: 'file' }`.
|
||||
- ~~**User font upload** — FontFace-from-IndexedDB tier~~ ✅ (incl. variable-font weight
|
||||
support; arch 05 → User-uploaded fonts). Font dependencies derive from the config, not a
|
||||
theme field.
|
||||
- **Google Fonts opt-in CDN tier** — keyless catalog, opt-in only.
|
||||
- **SVG export font embedding** — embed face data so exported SVGs render off-app.
|
||||
- **SVG export font embedding** — embed face data so exported SVGs render off-app; carries
|
||||
the **§08 export font-byte round-trip** too (shared base64 machinery — user fonts don't yet
|
||||
travel with an exported workspace).
|
||||
- **Theme↔font pairing metadata** — a suggestion nicety.
|
||||
- **Built-in expressive theme preset gallery** — e.g. "Editorial", "Terminal", "Sketch".
|
||||
|
||||
|
||||
@@ -149,6 +149,8 @@ async function rerender(node: HTMLElement, spec: TopLevelSpec, config: Config) {
|
||||
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).
|
||||
User-uploaded faces are registered on `document.fonts` too, so the same gate
|
||||
resolves them (§3 → User-uploaded fonts).
|
||||
- **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.
|
||||
@@ -248,6 +250,41 @@ key-by-key), so a snippet can always override or opt out locally. The
|
||||
that boundary deliberately: merge bakes the selected theme into `spec.config`
|
||||
(spec keys win — rendering unchanged), extract lifts `spec.config` out.
|
||||
|
||||
### User-uploaded fonts
|
||||
|
||||
Beyond the self-hosted roster, a user can upload font files (`.woff2`/`.woff`/
|
||||
`.ttf`/`.otf`) for chart themes. A `FontAsset` (`core/font-asset.ts`) holds the
|
||||
raw bytes and persists through the standard entity-store stack — a `fonts`
|
||||
IndexedDB store, `infrastructure/font-store.ts` (+ `font-migrations.ts`),
|
||||
`stores/FontStore.ts`, `orchestration/font-persistence.ts` — the same
|
||||
one-tier-each shape as datasets and custom themes. The browser seam that turns
|
||||
stored bytes into a live face, `infrastructure/font-faces.ts`, registers on
|
||||
`document.fonts` at startup (before the first render, so the §2 font gate
|
||||
resolves user faces like the roster) and on each upload; uploads/deletes are
|
||||
orchestrated by `services/fonts.ts`. The Type panel lists user fonts ahead of the
|
||||
roster and applies one via the same `applyFontToConfig` transform.
|
||||
|
||||
**Font dependencies are derived from the config, never stored on the theme.** A
|
||||
`CustomTheme` carries no font field: the family a theme (or a snippet) uses is
|
||||
already in its config's `font`/`*Font` slots, that JSON config is the source of
|
||||
truth, and snippets use uploaded fonts with no theme to carry such a field. So a
|
||||
used face is discovered by scanning configs/specs for the families they reference
|
||||
(`collectFontFamilies`, core) and matching against the font library — a stored
|
||||
field would only drift from the config it duplicates.
|
||||
(Embedding the matched faces' bytes into the §08 workspace export and per-chart
|
||||
SVG export is the remaining transfer work, on shared base64 machinery.)
|
||||
|
||||
**Variable fonts: weight is the only leverageable axis.** `parseFontAxes` reads
|
||||
the OpenType `fvar` table (uncompressed `ttf`/`otf` only — `woff`/`woff2` wrap
|
||||
their tables in compression we don't unpack, so those register as static), and a
|
||||
variable face is registered with its `wght`→`weight` and `wdth`→`stretch` ranges
|
||||
so one file serves the whole weight range, driven by the Type panel's weight
|
||||
controls. Only those two axes take effect because Vega's text rendering emits a
|
||||
CSS font shorthand (family/size/weight/style) with **no `font-variation-settings`
|
||||
hook** — optical size, grade, and custom axes pin at the registered default and
|
||||
can't be exposed. Declaring the `wdth` range also defaults the face to normal
|
||||
width rather than a variable font's possibly-condensed default instance.
|
||||
|
||||
### Structured controls
|
||||
|
||||
The builder's panels — Color, Type, Layout, Axes & grid, Legend
|
||||
|
||||
@@ -162,10 +162,20 @@ ships, it is an explicit per-font user action, never automatic.
|
||||
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
|
||||
expressive preset gallery ("Editorial", "Terminal", "Sketch") showcasing the roster.
|
||||
6. **User font upload** ✅ (2026-06-16) — `FontAsset` (`core/font-asset.ts`) + the `fonts`
|
||||
store @ DB v3, registered as a `FontFace` at startup so the render gate resolves user
|
||||
faces like the roster (full entity-store stack: adapter/migration, `FontStore`,
|
||||
`font-persistence`, `services/fonts`; arch 05 → User-uploaded fonts). The Type panel
|
||||
offers uploads ahead of the roster. **Variable fonts** are supported: `parseFontAxes`
|
||||
reads `fvar` (uncompressed ttf/otf) and the face registers with `wght`/`wdth` ranges, so
|
||||
one file drives the whole weight range — only weight/width survive Vega's text rendering
|
||||
(no `font-variation-settings` hook). No `CustomTheme.fonts` field: a used face is derived
|
||||
by scanning configs/specs (`collectFontFamilies`) — the config is the
|
||||
source of truth, and snippets use fonts with no theme to carry a field. Not done here:
|
||||
embedding font bytes into the §08 export + SVG export (shared base64 machinery, with item 7).
|
||||
7. **Deferred** — Google Fonts opt-in tier; SVG export font embedding (+ §08 font-byte
|
||||
round-trip, shared machinery); built-in expressive preset gallery ("Editorial",
|
||||
"Terminal", "Sketch") showcasing the roster.
|
||||
|
||||
**Rejected:** per-snippet theme field (2026-06-12 — `spec.config` + merge/extract covers
|
||||
it without a second mechanism).
|
||||
@@ -224,6 +234,14 @@ the app standardizes on v6.
|
||||
|
||||
## 6. Status log
|
||||
|
||||
- **2026-06-16 (slice 6)** — **user font upload + variable-font weight support.** New
|
||||
`FontAsset` entity (instance #4 of the entity-store kind, confirmed by an eng-council
|
||||
pre-build consult): `fonts` store @ DB v3, adapter/migration, `FontStore`,
|
||||
`font-persistence`, `services/fonts`, and the `font-faces` `FontFace`-registration seam
|
||||
wired into `startup`. Type panel gains an upload control + a managed list; user fonts lead
|
||||
the dropdown. Variable fonts parse `fvar` and register with `wght`/`wdth` ranges (weight
|
||||
is the only axis Vega's text rendering can drive). Font dependencies are derived from the
|
||||
config at export, not stored on the theme. Deferred: font bytes in the §08/SVG exports.
|
||||
- **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
|
||||
|
||||
Reference in New Issue
Block a user