Skip to content

feat(text): configureFonts() runtime font API (+ Hanken Grotesk sans-serif default) - #37

Merged
winlp4ever merged 5 commits into
mainfrom
feat/hanken-grotesk-sans
Aug 7, 2026
Merged

feat(text): configureFonts() runtime font API (+ Hanken Grotesk sans-serif default)#37
winlp4ever merged 5 commits into
mainfrom
feat/hanken-grotesk-sans

Conversation

@winlp4ever

@winlp4ever winlp4ever commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Exposes the font mapping as a runtime-customizable API so consumers can swap the font stacks / sizes / line-heights, with the built-in defaults preserved. Also sets the sans-serif default to Hanken Grotesk (folds in the original small change so the two don't conflict).

Public API

import { configureFonts } from '@canvas-harness/core'

configureFonts({
  family: { 'sans-serif': '"Hanken Grotesk", system-ui, sans-serif' }, // partial, merges over defaults
  size:   { L: 26 },        // optional — ripples through layout/auto-fit
  lineHeight: { L: 34 },    // optional
})

Also exported: getFontStack / getFontSizePx / getLineHeightPx (resolvers), resetFonts(), and DEFAULT_FONT_STACKS / DEFAULT_FONT_SIZES / DEFAULT_LINE_HEIGHTS + the FontConfig type.

How it works

  • A live registry in text/defaults.ts behind getters. Every resolution site — measure, layout, paint-canvas, renderer, textarea editor, edge labels, PNG/SVG export, height estimate — now reads through the getters, so measurement, paint, editing, and export can't drift apart.
  • configureFonts() bumps the font epoch → measure cache clears, bitmap cache invalidates (keyed on the epoch), mounted canvases repaint.
  • App-global by design: web fonts are loaded document-globally, so a per-store stack override wouldn't map to a real face. The consumer still loads the faces (@font-face / Google Fonts); the library only names them.
  • font-epoch.ts is now a dependency-free leaf emitter (exports bumpFontEpoch, no longer imports measure); measure subscribes and clears itself. This removes the import cycle the registry would otherwise introduce.
  • FONT_FAMILY_MAP / FONT_SIZE_MAP / LINE_HEIGHT_MAP kept as deprecated aliases of the new DEFAULT_* consts (back-compat; they reflect defaults, not overrides).

Default change

sans-serif now defaults to Hanken Grotesk; the playground loads it via Google Fonts.

Tests

packages/core/tests/font-config.test.ts — 7 unit tests: default = Hanken Grotesk, partial family/size/line-height merges leave others untouched, override flows into the resolved canvas-font string, epoch bumps on configure, resetFonts() restores.
Full suites: core 358 unit + 64 browser, react 24 browser+unit. Lint + typecheck clean.

No perf impact

Getter calls replace direct map indexing (both O(1)); render hot path unchanged.

Repoint FONT_FAMILY_MAP['sans-serif'] from Atkinson Hyperlegible Next to
Hanken Grotesk as the primary face (generic system fallbacks unchanged),
and load Hanken Grotesk instead of Atkinson in the playground's Google
Fonts link so canvas measurement matches the loaded face. The default
family for unstyled nodes is unchanged (still handwriting); this only
changes what the sans-serif token renders. The font-epoch cache
invalidation already repaints once the new face finishes loading.
Make the font stacks / sizes / line-heights customizable at runtime
instead of being fixed module constants.

- Add a live font registry in text/defaults.ts behind
  getFontStack/getFontSizePx/getLineHeightPx getters, plus
  configureFonts({ family?, size?, lineHeight? }) (partial deep-merge
  over defaults) and resetFonts(). App-global by design — web fonts are
  loaded document-globally, so a per-store stack override wouldn't map to
  a real face. The consumer still loads the faces; the library names them.
- Route every resolution site (measure, layout, paint-canvas, renderer,
  textarea editor, edge labels, PNG/SVG export, height estimate) through
  the getters so measurement, paint, editing and export stay consistent.
- configureFonts() bumps the font epoch → measure cache clears + bitmap
  cache invalidates (keyed on epoch) + mounted canvases repaint.
- Make font-epoch.ts a dependency-free leaf emitter (export bumpFontEpoch,
  drop its measure import); measure now subscribes and clears itself.
  Removes the import cycle the registry would otherwise create.
- Keep FONT_FAMILY_MAP / FONT_SIZE_MAP / LINE_HEIGHT_MAP as deprecated
  aliases of the new DEFAULT_* consts for back-compat.

7 unit tests (override merge, canvas-font resolution, epoch bump, reset).
Full suites (core 358 unit + 64 browser, react 24), lint, typecheck pass.
@winlp4ever winlp4ever changed the title feat(text): sans-serif token → Hanken Grotesk feat(text): configureFonts() runtime font API (+ Hanken Grotesk sans-serif default) Aug 6, 2026
- Ignore explicit-undefined override values in configureFonts (merge only
  defined keys). A caller passing { M: cfg.customM } with an undefined
  value would otherwise poison the registry → undefined size → NaN
  geometry across measure/paint/editor/export.
- Don't latch fontTrackingInitialized when there's no document, so a later
  call once the DOM exists can still attach the document.fonts listeners.
  Matters now that measure.ts subscribes at import time rather than the
  renderer at browser runtime.
- Correct the configureFonts / FontConfig.size docs: overrides apply to
  text laid out after the call; already-persisted node w/h are not
  reflowed (configure at startup, before nodes are created/loaded).

Adds a test for the undefined-override case. Full suites (core 359 unit +
64 browser, react 24), lint, typecheck pass.
Document the five font-family tokens, how to override a stack (or the
sizes/line-heights) via configureFonts, the load-the-face-yourself
caveat, and list the API in the core surface section.
@winlp4ever
winlp4ever merged commit 847706b into main Aug 7, 2026
4 checks passed
winlp4ever added a commit that referenced this pull request Aug 7, 2026
#38)

* revert(text): restore Atkinson Hyperlegible Next as the sans-serif default

Hanken Grotesk (#37) read as flatter in practice — Atkinson Hyperlegible
Next has more character, so restore it as the built-in sans-serif stack.
The configureFonts() API from #37 stays; only the default face changes,
so anyone who wants Hanken Grotesk can still opt in via configureFonts.
Playground font link + the default-stack test updated to match.

* docs(text): use Inter (not Hanken Grotesk) in the configureFonts examples

Revert #38 removed Hanken Grotesk from the codebase; the override examples
in the README + configureFonts/FontConfig doc-comments still referenced it,
which read as a stray leftover. Use Inter as a neutral example font.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant