feat(eidos): wire variable-font axes (weight range + optical sizing)

`FontFamily.axes` ({ wght, opsz }) was declared but never consumed — the
TYPOGRAPHY_ENGINE_RFC §5 promise was unmaterialized. Wire it:

  - axes.wght -> when a face omits `weight`, @font-face emits the range
    (`font-weight: 100 900`) so one variable face spans the whole axis
  - axes.opsz -> `:root { font-optical-sizing: auto }` so the optical-size
    axis tracks the rendered font-size

Engine-only, inert until a theme declares axes (shipped fonts are static
TTFs — same posture as wide-gamut color: ready, not yet exercised by
assets). Zero change to generated/base.css (no family declares axes).

`npm run check` 0 errors; new test asserts the emission; the generated-css
test confirms base output unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 76dd3e9d66
commit 6a848b50e3

@ -96,8 +96,18 @@ size = clamp(minRem, interceptRem + slope·100vw, maxRem)
- **`unicode-range`** subsetting (latin / latin-ext separados) → cargar solo lo usado. - **`unicode-range`** subsetting (latin / latin-ext separados) → cargar solo lo usado.
- **`preload`** de la familia crítica (primary regular). - **`preload`** de la familia crítica (primary regular).
- **Tipo**: `FontFamily` += `axes?: { wght?: [min,max]; opsz?: [min,max] }` + - **Tipo**: `FontFamily` += `axes?: { wght?: [min,max]; opsz?: [min,max] }` +
`sizeAdjust?` / overrides de métricas + `unicodeRanges?`. `source` ya existe. `sizeAdjust?` / overrides de métricas + `unicodeRanges?`. `source` ya existe. ✅
- **Optical sizing**: `font-optical-sizing: auto` cuando la familia declara `opsz`. - **Optical sizing** ✅: `font-optical-sizing: auto` en `:root` cuando alguna familia
declara `opsz`.
- **Eje de peso variable** ✅: cuando un `face` omite `weight` y la familia declara
`axes.wght`, el `@font-face` emite el rango (`font-weight: 100 900`) → un solo face
cubre todo el eje. (`render-css > renderFontFace`; test en `active-eidos-config.test.ts`.)
> **Estado §5**: el **motor** está cableado — `@font-face` config-driven, fallback
> anti-CLS, `unicode-range`, y ahora el consumo de `axes` (rango de peso + optical
> sizing). Inerte hasta que un tema declare `axes` — como el wide-gamut de color: listo,
> pero no ejercitado por los assets actuales (TTF estáticas). **Pendiente de assets**:
> migrar a woff2 variable reales; `preload` (cierre aparte).
## 6. Tracking · leading · features · measure · wrap (Fase 3) ## 6. Tracking · leading · features · measure · wrap (Fase 3)

@ -441,6 +441,26 @@ describe('ActiveEidos config', () => {
expect((css.match(/font-family: 'Lora';/g) ?? []).length).toBe(8) expect((css.match(/font-family: 'Lora';/g) ?? []).length).toBe(8)
}) })
it('emits variable-font output from family axes (weight range + optical sizing)', () => {
const cfg = structuredClone(THEME_BASE_OPTIONS)
const t = cfg.primitives.typography!
// Declare `primary` as a variable font: one face spanning the wght axis + an opsz
// axis (enables optical sizing). The face omits an explicit weight on purpose so the
// range is derived from the axis.
;(t.families as Record<string, unknown>).primary = {
...t.families.primary,
axes: { wght: [100, 900], opsz: [8, 144] },
faces: [
{ sources: [{ url: '/fonts/variable/InstrumentSans.woff2', format: 'woff2-variations' }] }
]
}
const css = createEidos(cfg).renderStaticCss()
// wght axis → @font-face weight range (a single face spans 100–900)
expect(css).toContain('font-weight: 100 900;')
// opsz axis → optical sizing enabled in :root
expect(css).toContain('font-optical-sizing: auto')
})
it('emits the Phase 3 typography scales + optical tracking', () => { it('emits the Phase 3 typography scales + optical tracking', () => {
const css = createThemeBaseEidos().renderStaticCss() const css = createThemeBaseEidos().renderStaticCss()
expect(css).toContain('--tracking-tight: -0.02em;') expect(css).toContain('--tracking-tight: -0.02em;')

@ -199,6 +199,11 @@ export function renderStaticCss(options: EidosConfig): string {
if (primitives.typography) { if (primitives.typography) {
appendTypographyDeclarations(declarations, primitives.typography) appendTypographyDeclarations(declarations, primitives.typography)
// Variable fonts: enable optical sizing globally when any family declares an `opsz`
// axis, so the font's optical-size axis tracks the rendered font-size. (RFC §5)
if (Object.values(primitives.typography.families).some((f) => f.axes?.opsz)) {
declarations.push('font-optical-sizing: auto')
}
} }
if (primitives.size) { if (primitives.size) {
@ -1923,22 +1928,31 @@ function renderFontFaceBlocks(typography: TypographyPrimitiveSet): string {
if (seen.has(family.family)) continue if (seen.has(family.family)) continue
seen.add(family.family) seen.add(family.family)
for (const face of family.faces) { for (const face of family.faces) {
blocks.push(renderFontFace(family.family, face, family.display)) blocks.push(renderFontFace(family.family, face, family.display, family.axes))
} }
if (family.fallback) blocks.push(renderFallbackFontFace(family.family, family.fallback)) if (family.fallback) blocks.push(renderFallbackFontFace(family.family, family.fallback))
} }
return blocks.join('\n\n') return blocks.join('\n\n')
} }
function renderFontFace(family: string, face: FontFace, defaultDisplay: string | undefined): string { function renderFontFace(
family: string,
face: FontFace,
defaultDisplay: string | undefined,
axes?: FontFamily['axes']
): string {
const src = face.sources const src = face.sources
.map((s) => `url('${s.url}')${s.format ? ` format('${s.format}')` : ''}`) .map((s) => `url('${s.url}')${s.format ? ` format('${s.format}')` : ''}`)
.join(', ') .join(', ')
// Variable fonts: when the face omits an explicit weight and the family declares a
// `wght` axis range, emit the range (`font-weight: 100 900`) so a single face spans the
// whole weight axis. (TYPOGRAPHY_ENGINE_RFC §5 — variable fonts)
const weight = face.weight ?? (axes?.wght ? `${axes.wght[0]} ${axes.wght[1]}` : 400)
const lines = [ const lines = [
'@font-face {', '@font-face {',
`\tfont-family: '${family}';`, `\tfont-family: '${family}';`,
`\tsrc: ${src};`, `\tsrc: ${src};`,
`\tfont-weight: ${face.weight ?? 400};`, `\tfont-weight: ${weight};`,
`\tfont-style: ${face.style ?? 'normal'};`, `\tfont-style: ${face.style ?? 'normal'};`,
`\tfont-display: ${face.display ?? defaultDisplay ?? 'swap'};` `\tfont-display: ${face.display ?? defaultDisplay ?? 'swap'};`
] ]

Loading…
Cancel
Save

Powered by TurnKey Linux.