diff --git a/src/uix/eidos/TYPOGRAPHY_ENGINE_RFC.md b/src/uix/eidos/TYPOGRAPHY_ENGINE_RFC.md index 1839410b1..15e63d4e0 100644 --- a/src/uix/eidos/TYPOGRAPHY_ENGINE_RFC.md +++ b/src/uix/eidos/TYPOGRAPHY_ENGINE_RFC.md @@ -96,8 +96,18 @@ size = clamp(minRem, interceptRem + slope·100vw, maxRem) - **`unicode-range`** subsetting (latin / latin-ext separados) → cargar solo lo usado. - **`preload`** de la familia crítica (primary regular). - **Tipo**: `FontFamily` += `axes?: { wght?: [min,max]; opsz?: [min,max] }` + - `sizeAdjust?` / overrides de métricas + `unicodeRanges?`. `source` ya existe. -- **Optical sizing**: `font-optical-sizing: auto` cuando la familia declara `opsz`. + `sizeAdjust?` / overrides de métricas + `unicodeRanges?`. `source` ya existe. ✅ +- **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) diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index d4181c282..af12d50f2 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -441,6 +441,26 @@ describe('ActiveEidos config', () => { 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).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', () => { const css = createThemeBaseEidos().renderStaticCss() expect(css).toContain('--tracking-tight: -0.02em;') diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts index e1d079699..7165c5aea 100644 --- a/src/uix/eidos/lib/render-css.ts +++ b/src/uix/eidos/lib/render-css.ts @@ -199,6 +199,11 @@ export function renderStaticCss(options: EidosConfig): string { if (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) { @@ -1923,22 +1928,31 @@ function renderFontFaceBlocks(typography: TypographyPrimitiveSet): string { if (seen.has(family.family)) continue seen.add(family.family) 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)) } 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 .map((s) => `url('${s.url}')${s.format ? ` format('${s.format}')` : ''}`) .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 = [ '@font-face {', `\tfont-family: '${family}';`, `\tsrc: ${src};`, - `\tfont-weight: ${face.weight ?? 400};`, + `\tfont-weight: ${weight};`, `\tfont-style: ${face.style ?? 'normal'};`, `\tfont-display: ${face.display ?? defaultDisplay ?? 'swap'};` ]