11 KiB
RFC — Motor de tipografía reference-grade (eidos)
Estado: PROPUESTA (2026-06-05). Plan para elevar la tipografía de eidos a la par o por encima de los frameworks de referencia, replicando el enfoque del color (
COLOR_ENGINE_RFC.md): auditar → comparar → extender de forma ADITIVA detrás del contrato de tokens congelado, por fases.
1. Estado actual (auditoría)
TypographyPrimitiveSet (lib/primitives/typography.ts + config-types.ts) →
emisión en render-css.ts → tokens → componentes <Text> / <Heading> / <SText>.
- 4 familias (primary Instrument Sans · secondary/display Lora · mono Azeret Mono).
- 8 tamaños
xxs 10px … xxxl 80px, cada unosize/lineHeight/letterSpacingfijos en px. - 4 pesos (400/500/600/700).
- 13 estilos nombrados (hero, h1–h6, body, prose, label, caption, code) con size
responsive por breakpoint en algunos (
{ base, md }). - Eje
--scaling(zoom global 90–110%) que multiplica los px. - Tokens:
--font-family-*,--font-size-*(calc(px * --scaling)),--font-line-height-*,--font-letter-spacing-*,--font-weight-*,--style-{name}-*. - Brazo de medición: el motor
canvas-text(<SText>) cuenta líneas exactas replicando el navegador (productionizado — verlib/canvas-text/fix-stext.md).
Bueno hoy: estilos semánticos nombrados, sizes responsive, eje de scaling (poco común), contrato de tokens + theming, medición exacta de líneas.
2. Comparación con referencia (el gap)
| Capacidad | Referencia | Eidos hoy |
|---|---|---|
Tipografía fluida (clamp() por viewport) |
Utopia, Carbon, Tailwind(opt) | ❌ px fijos + breakpoints manuales |
| Variable fonts (1 archivo, wght + opsz) | Material 3, Apple | ❌ TTF estáticas por peso |
| WOFF2 + subsetting + fallback con métricas (anti-CLS) | Next/Fontaine | ❌ TTF, sin subset, sin size-adjust |
Optical sizing (opsz) |
Apple, Material | ❌ |
| Tracking por tamaño (óptico) | Material, Apple, Radix | ❌ todo 0 |
text-wrap: balance/pretty |
Tailwind, Chakra | ❌ |
| Measure (ancho de línea ~65ch) | clásico, Tailwind prose | ❌ |
font-feature-settings (tabular-nums…) |
Tailwind, Radix | ❌ |
| Estilos nombrados · eje de scaling | Chakra/Material · Apple(parcial) | ✅ |
3. Modelo de inclusión: extender, no reemplazar
Principio rector (idéntico a color): todo lo nuevo es ADITIVO sobre
TypographyPrimitiveSet, detrás del contrato de tokens congelado. Lo viejo (px
fijos) sigue funcionando; lo nuevo se activa por config. Los nombres de token
(--font-size-X, --style-Y-*) no cambian — solo cambia la fórmula del valor
(igual que en color el token --scale-* pasó de hex a oklch() sin renombrar).
Capas de la implementación, paralelas a color:
| Pieza | Dónde | Análogo en color |
|---|---|---|
| Matemática pura (fluid scale Utopia) | eidos/lib/type-scale.ts (puro) |
arts/color / build-scheme.ts |
| Tipo + config | config-types.ts (TypographyPrimitiveSet) |
EidosConfig |
| Emisión CSS | render-css.ts (appendTypographyDeclarations) |
appendColorScaleDeclarations |
| Fuentes | themes/fonts.css (woff2/variable) |
themes/base (paleta) |
| Componentes | <Text> / <Heading> (props nuevas) |
<Button> etc. |
| Runtime opcional | eidos.applyTypeScale(seed) |
eidos.applyColorScheme(seed) |
4. Tipografía fluida (Fase 1, el corazón)
Fórmula (estilo Utopia) — un tamaño se vuelve un clamp(min, fluido, max) en
rem (a11y: respeta el zoom del navegador), donde el tramo fluido interpola entre
dos viewports:
slope = (maxRem − minRem) / (maxVw − minVw)
interceptRem = minRem − slope · minVw
size = clamp(minRem, interceptRem + slope·100vw, maxRem)
- rem, no px: mejora la accesibilidad (el zoom de fuente del SO/navegador escala).
El token
--font-size-Xmantiene su nombre; su valor pasa decalc(px*scaling)acalc(clamp(...rem...) * var(--scaling))— transparente para los consumidores. - El eje
--scaling(zoom discreto del sistema) compone encima del clamp víacalc(clamp(...) * var(--scaling)). Dos ejes ortogonales: fluid (continuo, por viewport) + scaling (discreto, preferencia). - Simplifica los estilos nombrados: hero/h1/h2 ya no necesitan size responsive
{ base, md }manual — elclampcubre el viewport de forma continua. El emisor de@mediapor breakpoint para size se puede retirar (menos CSS). - Tipo:
TextMetric.sizeacepta hoystring; se extiende astring | FluidSizedondeFluidSize = { min: string; max: string; minVw?: string; maxVw?: string }.string(px fijo) sigue válido → backward-compatible.
5. Fuentes modernas (Fase 2, mayor golpe perf+calidad)
- WOFF2 + variable: migrar
fonts.cssde TTF estáticas por peso a woff2 variable (Instrument Sans / Lora / Azeret Mono tienen versión variable con ejewght; Lora ademásital). Un archivo por familia en vez de 3–10. - Anti-CLS: por cada familia, un
@font-facede fallback con métricas sobreescritas (size-adjust,ascent-override,descent-override,line-gap-override) que iguala la geometría de la webfont al fallback de sistema → cero salto de layout al cargar (enfoque Fontaine /next/font). Las métricas se precalculan por fuente. unicode-rangesubsetting (latin / latin-ext separados) → cargar solo lo usado.preloadde la familia crítica (primary regular).- Tipo:
FontFamily+=axes?: { wght?: [min,max]; opsz?: [min,max] }+sizeAdjust?/ overrides de métricas +unicodeRanges?.sourceya existe. ✅ - Optical sizing ✅:
font-optical-sizing: autoen:rootcuando alguna familia declaraopsz. - Eje de peso variable ✅: cuando un
faceomiteweighty la familia declaraaxes.wght, el@font-faceemite el rango (font-weight: 100 900) → un solo face cubre todo el eje. (render-css > renderFontFace; test enactive-eidos-config.test.ts.)
Estado §5: el motor está cableado —
@font-faceconfig-driven, fallback anti-CLS,unicode-range, y ahora el consumo deaxes(rango de peso + optical sizing). Inerte hasta que un tema declareaxes— 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)
- Tracking/leading como escalas (estilo Tailwind):
TypographyPrimitiveSet+=tracking+leading→--tracking-{key}/--leading-{key}. Tracking óptico por defecto (negativo en display, ~0 en body, positivo en xs/caption). font-feature-settings:features?: Record<string,string>→--font-feature-{key}(presetstabular,oldstyle,ligatures,smallcaps). Propnumericen componentes.- Measure: token
--measure-{key}(60ch/66ch/72ch) para prosa legible. text-wrap: propswrap: 'balance' | 'pretty' | 'nowrap'en<Text>/<Heading>(keyword CSS, sin token); defaultbalanceen headings,prettyen prose.
7. Vínculo con <SText> (brazo de medición)
El motor canvas-text cuenta líneas exactas replicando el navegador (ya
productionizado). Es la otra mitad del sistema: la tipografía visual (este RFC) y
la medición (SText). Al emitir fluid/tracking, <SText> los mide igual (lee
getComputedStyle real), así que el conteo sigue siendo fiel sin cambios — el epoch de
useFontReady (T1) ya cubre el re-cálculo tras cargar variable fonts.
8. Doctrina (paralela a color)
- Escala + estilos nombrados = canon del eidos (como roles/variants de color): valores themeables, pero el set de estilos es canon.
- Fuentes = dato del tema (como la paleta): el tema trae las suyas.
- Fluid / tracking / features = capacidades del motor, default-on con fallback (como el wide-gamut OKLCH): sRGB-idéntico donde no hay soporte.
- Theme = retintar/retipar lo perceptualmente fijo: cambia QUÉ fuente es
primary, no QUÉ significah1.
9. Contrato de tokens (añadidos)
Congelados, bare-prefixed (§6 THEMING). Nuevos:
--tracking-{key}, --leading-{key}, --font-feature-{key}, --measure-{key}.
Reformulados (mismo nombre, valor fluido): --font-size-{key}.
Sin cambios de nombre → cero rotura de componentes.
10. Fases — estado (cerrado)
- ✅ Motor fluido —
type-scale.ts(purofluidClamp) +FluidSize+clamp()rem enappendTypographyDeclarations(scaling compone);@mediade size retirado. - ✅ Fuentes —
@font-faceconfig-driven + fallback anti-CLS (métricas) +unicode-range+FontFamily.axes(rango de peso variable +font-optical-sizing) + font preloads (collectFontPreloads/eidos.fontPreloads()). Assets aún TTF estáticas → la variabilidad es capacidad de motor, no visible todavía (como wide-gamut). - ✅ Tracking/leading/features/measure/wrap — escalas config-driven + props de
componente (
<Text>/<Heading>) + tokens semánticos (--leading-{role}/--tracking-{role}) ahora config-driven (typography.semanticLeading/semanticTracking). - ✅ Escala — diseño de dos zonas (intencional), ver abajo. NO se cambiaron los valores (sería regresión perceptual): la "irregularidad" es deliberada.
- ✅ Docs + demo — este RFC + demo
/temas/tipografia(escala/familias/ejes en vivo)eidos.applyTypeScale(seed)runtime (buildTypeScale, hermano deapplyColorScheme).
Fase 4 — la escala es un diseño de dos zonas (intencional)
La escala (xxs 10 · xs 12 · sm 14 · md 16 · lg 18→20 · xl 24→28 · xxl 40→48 · xxxl 64→80) NO es un único ratio modular, y eso es deliberado:
- Zona UI (xxs–md): pasos finos (~1.14–1.2) para controles, etiquetas y cuerpo, donde la granularidad importa y los saltos grandes molestan.
- Zona display (lg–xxxl): saltos grandes (~1.25–1.7) para jerarquía y titulares, donde el contraste manda.
Un ratio único uniforme (lo que produce applyTypeScale) es la alternativa opt-in en
runtime; la escala autorada conserva su afinado de dos zonas (como Radix / Material, que
también afinan en vez de imponer un ratio puro). Cambiarla sería una regresión, no una
mejora — por eso Fase 4 es documentación, no recableado.
Verificación por fase
npm run check0 errores;render-csssnapshot regenerado (generated/base.css).- Backward-compat: un config con sizes en px (string) sigue emitiendo igual.
- Navegador: el fluid escala con el viewport;
<SText>cuenta líneas correctas; sin CLS al cargar las variable fonts.
Fuera de alcance
- Justificación / guionado / balanceo de líneas propio (el navegador +
text-wraplo hacen;<SText>solo mide). - Subsetting dinámico en runtime (los
unicode-rangeestáticos bastan).