|
|
|
|
@ -1,191 +1,14 @@
|
|
|
|
|
# RFC — Motor de tipografía reference-grade (eidos)
|
|
|
|
|
# RFC — Reference-grade typography engine (moved)
|
|
|
|
|
|
|
|
|
|
> **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.
|
|
|
|
|
✅ Implemented (phases 1–5 closed). Fluid rem `clamp()` behind the frozen
|
|
|
|
|
`--font-size-*` names (scaling composes on top), config-driven `@font-face`
|
|
|
|
|
with anti-CLS metric fallback + `unicode-range` + variable `axes` (engine
|
|
|
|
|
ready, assets still static TTF), tracking/leading/features/measure/wrap
|
|
|
|
|
scales, the intentional two-zone authored scale, and the runtime builder
|
|
|
|
|
`applyTypeScale(seed)` (`buildTypeScale`).
|
|
|
|
|
|
|
|
|
|
## 1. Estado actual (auditoría)
|
|
|
|
|
**The RFC moved to the docs corpus:**
|
|
|
|
|
[`docs/rfcs/rfc-typography.md`](../../../docs/rfcs/rfc-typography.md)
|
|
|
|
|
— audit, reference comparison, the additive inclusion model, phases.
|
|
|
|
|
|
|
|
|
|
`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 uno `size` / `lineHeight` / `letterSpacing`
|
|
|
|
|
**fijos 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 — ver `lib/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-X` mantiene su nombre; su valor pasa de `calc(px*scaling)` a
|
|
|
|
|
`calc(clamp(...rem...) * var(--scaling))` — **transparente para los consumidores**.
|
|
|
|
|
- **El eje `--scaling`** (zoom discreto del sistema) compone **encima** del clamp vía
|
|
|
|
|
`calc(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 — el `clamp` cubre el viewport de forma continua. El emisor de
|
|
|
|
|
`@media` por breakpoint para size se puede retirar (menos CSS).
|
|
|
|
|
- **Tipo**: `TextMetric.size` acepta hoy `string`; se extiende a
|
|
|
|
|
`string | FluidSize` donde `FluidSize = { 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.css` de TTF estáticas por peso a **woff2
|
|
|
|
|
variable** (Instrument Sans / Lora / Azeret Mono tienen versión variable con eje
|
|
|
|
|
`wght`; Lora además `ital`). Un archivo por familia en vez de 3–10.
|
|
|
|
|
- **Anti-CLS**: por cada familia, un `@font-face` de **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-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` 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)
|
|
|
|
|
|
|
|
|
|
- **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}`
|
|
|
|
|
(presets `tabular`, `oldstyle`, `ligatures`, `smallcaps`). Prop `numeric` en componentes.
|
|
|
|
|
- **Measure**: token `--measure-{key}` (`60ch`/`66ch`/`72ch`) para prosa legible.
|
|
|
|
|
- **`text-wrap`**: props `wrap: 'balance' | 'pretty' | 'nowrap'` en `<Text>`/`<Heading>`
|
|
|
|
|
(keyword CSS, sin token); default `balance` en headings, `pretty` en 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É significa `h1`.
|
|
|
|
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
|
|
|
|
1. ✅ **Motor fluido** — `type-scale.ts` (puro `fluidClamp`) + `FluidSize` + `clamp()` rem
|
|
|
|
|
en `appendTypographyDeclarations` (scaling compone); `@media` de size retirado.
|
|
|
|
|
2. ✅ **Fuentes** — `@font-face` config-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).
|
|
|
|
|
3. ✅ **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`).
|
|
|
|
|
4. ✅ **Escala — diseño de dos zonas (intencional)**, ver abajo. NO se cambiaron los
|
|
|
|
|
valores (sería regresión perceptual): la "irregularidad" es deliberada.
|
|
|
|
|
5. ✅ **Docs + demo** — este RFC + demo `/temas/tipografia` (escala/familias/ejes en vivo)
|
|
|
|
|
+ **`eidos.applyTypeScale(seed)`** runtime (`buildTypeScale`, hermano de `applyColorScheme`).
|
|
|
|
|
|
|
|
|
|
### 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 check` 0 errores; `render-css` snapshot 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-wrap` lo
|
|
|
|
|
hacen; `<SText>` solo mide).
|
|
|
|
|
- Subsetting dinámico en runtime (los `unicode-range` estáticos bastan).
|
|
|
|
|
The measurement arm stays here: `lib/canvas-text/` (`<SText>`).
|
|
|
|
|
|