|
|
|
|
@ -1,619 +1,17 @@
|
|
|
|
|
# RFC — Motor de color de nueva generación (OKLCH · P3 · APCA · generador 1-seed)
|
|
|
|
|
|
|
|
|
|
> **Estado: PROPUESTA (2026-06-04).**
|
|
|
|
|
>
|
|
|
|
|
> Este RFC es la **capa física** del color. El **modelo conceptual**
|
|
|
|
|
> (paleta → roles de jerarquía → intents auto-derivados) está **cerrado** en
|
|
|
|
|
> [`THEMING.md §25`](./THEMING.md) y NO se toca. Aquí cambiamos **cómo se
|
|
|
|
|
> producen** las variables de color, no qué variables existen ni qué significan.
|
|
|
|
|
>
|
|
|
|
|
> Supersedes las fases diferidas de [`COLOR_MODEL_RFC.md §5`](./COLOR_MODEL_RFC.md)
|
|
|
|
|
> (fase 2 «ampliar la librería» ya hecha — 33 escalas; fase 3 «generador 1-hex»
|
|
|
|
|
> y el wide-gamut/APCA es lo que este RFC concreta y eleva).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 0. TL;DR — qué cambia y qué NO
|
|
|
|
|
|
|
|
|
|
**Cambia (la capa física / autoría):**
|
|
|
|
|
|
|
|
|
|
1. **Autoría en OKLCH.** Una escala puede declararse como **una semilla** (`seed`)
|
|
|
|
|
en vez de 12 hex a mano. El motor genera el ramp funcional de 12 pasos.
|
|
|
|
|
2. **Generador 1-seed → 12 pasos × light/dark** por *morph de plantilla* (método
|
|
|
|
|
Radix): reusa las 33 escalas afinadas como **donantes de curva**, re-tinta a la
|
|
|
|
|
semilla. Mata el «autorar 12 pasos × 2 modos a mano» y el bug clase-`loss`.
|
|
|
|
|
3. **Salida wide-gamut.** Cada token de color se emite como **OKLCH con fallback
|
|
|
|
|
hex sRGB** (estilo Tailwind v4) — wide-gamut automático donde el navegador lo
|
|
|
|
|
soporta, universal donde no. Opción `@media (color-gamut: p3)` para fidelidad-máx.
|
|
|
|
|
4. **Contraste APCA.** La decisión texto-on-solid pasa de WCAG2 (gamma-linealizado)
|
|
|
|
|
a **APCA (Lc)**, con un suelo WCAG2 como red de seguridad (APCA es borrador WCAG3).
|
|
|
|
|
5. **Saneo**: `primary≡loss=purple` en el base se arregla; afinado conservador del
|
|
|
|
|
mapa slot→step (P3-3 del audit).
|
|
|
|
|
|
|
|
|
|
**NO cambia (invariantes — ver §10):**
|
|
|
|
|
|
|
|
|
|
- Los **nombres** de todas las vars: `--scale-{name}-{step}`, `--scale-{name}-a{step}`,
|
|
|
|
|
`--primitive-{role}-{step}`, `--color-{role}-{slot}`, `--color-{role}-surface`.
|
|
|
|
|
- El **modelo de 3 capas** (§25), los **9 roles**, los **13 slots**, las **8 sema
|
|
|
|
|
families**, los **variants canon** (§19).
|
|
|
|
|
- El **TSC** (scope-as-data, `scopeCovers`, cross-axis), el **purge**, la
|
|
|
|
|
**introspección de contraste**, el **alpha compositing-inverse**.
|
|
|
|
|
- **Aguas abajo (recipes, eidos CSS, componentes, demos): cero cambios.**
|
|
|
|
|
- Todo se calcula **en build-time**; la salida sigue siendo CSS estático.
|
|
|
|
|
|
|
|
|
|
**El generador es ISOMÓRFICO, no build-only** (descartado el «Santo Grial» de
|
|
|
|
|
relative-colors *en CSS* como mecanismo). El eje que sacrifica la introspección
|
|
|
|
|
(pick APCA) y el alpha compositing-inverse **no es build-vs-runtime** — es
|
|
|
|
|
**CSS-puro vs JS**. Esas dos propiedades solo necesitan el valor numérico del color
|
|
|
|
|
en el momento de decidir; la matemática (OKLCH↔sRGB↔P3 + gamut-map + APCA +
|
|
|
|
|
inverse-alpha) es **pura, sin DOM**, así que corre idéntica en build **y** en runtime:
|
|
|
|
|
|
|
|
|
|
- **build** → temas estáticos (camino `render-css.ts`).
|
|
|
|
|
- **runtime JS** → white-label / live theming: el usuario elige un hex, el motor
|
|
|
|
|
genera la escala + dark (hex fallback + `oklch()` wide-gamut) y la escribe vía
|
|
|
|
|
`ActiveEidos.applyColorScheme(seed)`.
|
|
|
|
|
|
|
|
|
|
En **ambos** modos la salida son **valores estáticos ya resueltos** (contraste
|
|
|
|
|
elegido por APCA, alpha invertido) → **cero sacrificio en cualquier modo**. Lo único
|
|
|
|
|
que pierde ambas cosas es resolver el color *dentro* del CSS (`oklch(from …)`), que se
|
|
|
|
|
degrada a **azúcar opcional** para derivaciones triviales (§11.6), nunca el motor.
|
|
|
|
|
Ver §6.1 (isomorfismo) y §12 (coste). El generador se promueve a **`uix.color`**, un
|
|
|
|
|
*art* isomórfico paralelo a `uix.motion`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 1. Motivación
|
|
|
|
|
|
|
|
|
|
La auditoría ([`THEMING_AUDIT_2026-06-01.md`](./THEMING_AUDIT_2026-06-01.md))
|
|
|
|
|
confirmó que el agujero de eidos **no es el bloat** (el `eidos:purge` ya da piso
|
|
|
|
|
~11 KB gzip; el bloat sobre el cable es competitivo) sino la **fidelidad y
|
|
|
|
|
ergonomía del color**:
|
|
|
|
|
|
|
|
|
|
- **P3-1** — Cero wide-gamut. Todo es sRGB hex. Radix ships P3 para toda su paleta.
|
|
|
|
|
**Ni siquiera está en backlog.**
|
|
|
|
|
- **Sin OKLCH ni generador.** Una marca con un hue fuera de las 33 escalas tiene que
|
|
|
|
|
**autorar 12 pasos × 2 modos a mano** (`config-types.ts` `ColorScale` = 12 hex).
|
|
|
|
|
Fue lo que produjo el bug `loss: indigo` en untitled-ui.
|
|
|
|
|
- **Contraste WCAG2** (`render-css.ts:1629` `wcagContrastRatio`), impreciso en
|
|
|
|
|
mid-tones. Radix decide con APCA.
|
|
|
|
|
- **Residuo visible**: en el tema base `primary` y `loss` mapean **ambos a `purple`**
|
|
|
|
|
(`themes/base.ts:18,31`) → indistinguibles, pese a que `plum`/`indigo` ya existen.
|
|
|
|
|
|
|
|
|
|
Un framework que aspira a ser referencia perceptual (la promesa de la capa sema +
|
|
|
|
|
el motion de dos momentos) **no puede quedarse en sRGB de 2020**. Este RFC sube el
|
|
|
|
|
techo de fidelidad y, **como subproducto**, absorbe el deseo legítimo del «modelo de
|
|
|
|
|
color más eficiente» (menos superficie de autoría — ver §13).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. Principios de diseño
|
|
|
|
|
|
|
|
|
|
1. **Generador isomórfico, salida estática.** El generador es matemática pura (sin
|
|
|
|
|
DOM) y corre en build (temas estáticos) **o** en runtime JS (white-label). En
|
|
|
|
|
ambos escupe valores ya resueltos → conserva TSC, purge, introspección (pick
|
|
|
|
|
APCA), alpha compositing-inverse y soporte universal **en todos los modos**. El
|
|
|
|
|
sacrificio solo lo impone resolver el color en CSS puro, que no usamos como motor.
|
|
|
|
|
2. **Aditivo y backwards-compatible.** Las 12-hex `ColorScale` siguen siendo válidas
|
|
|
|
|
verbatim. La semilla es la forma *nueva y ergonómica*, no la única.
|
|
|
|
|
3. **Zero-downstream.** Los nombres de vars no cambian; los recipes/CSS/componentes
|
|
|
|
|
no se tocan (mismo contrato que prometió `COLOR_MODEL_RFC §6`).
|
|
|
|
|
4. **OKLCH como espacio de autoría y de fuente.** sRGB+P3 son **salida**, no autoría.
|
|
|
|
|
5. **Fidelidad sobre fórmula.** El generador reusa las 33 escalas afinadas como
|
|
|
|
|
plantillas de curva (no inventa una rampa lineal naíf — el error que comete la
|
|
|
|
|
propuesta de relative-colors en runtime).
|
|
|
|
|
6. **Verificable por fase.** Cada fase aporta valor y se valida sola (§11).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. Estado actual (anclas en código)
|
|
|
|
|
|
|
|
|
|
| Pieza | Hoy | Archivo |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Forma de escala | `ColorScale = Record<ColorScaleStep, string>` (12 hex) | `config-types.ts:27` |
|
|
|
|
|
| Paleta | 33 escalas hex (muchas sembradas desde Radix) | `themes/base.ts` + `color-scales.ts` |
|
|
|
|
|
| Roles | jerarquía explícita + intents auto-derivados | `config-types.ts:78-94` |
|
|
|
|
|
| Slots | 13 slots `track1·bg2·element3·hover4·active5·separator6·border7·borderHover8·solid9·solidHover10·text11·textStrong12·contrast(on-solid)` | `render-css.ts` `DEFAULT_COLOR_ROLE_SLOT_STEPS` |
|
|
|
|
|
| Emisión color | `renderThemeCss` → `--scale-*`, `--primitive-*`, `--color-{role}-{slot}` | `render-css.ts:288-380` |
|
|
|
|
|
| Contraste | WCAG2 gamma-lin, swap a `onSolidContrast` si `<3:1` | `render-css.ts:343-363,1629` |
|
|
|
|
|
| Alpha | compositing-inverse (genera `aN` que sobre el fondo reproduce el sólido N) | `render-css.ts:1647-1700` |
|
|
|
|
|
| Salida | sRGB hex en bloques `[data-theme='…']` (light+dark ambos shipped, uno activo) | `render-css.ts:379` |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 4. Arquitectura propuesta (capas del color, revisadas)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌─ AUTORÍA (nuevo) build-time
|
|
|
|
|
│ seed OKLCH | oklch() string | 12-hex verbatim (legacy)
|
|
|
|
|
│ │
|
|
|
|
|
│ ▼ generador (morph de plantilla) §6
|
|
|
|
|
├─ FUENTE: escala de 12 pasos en OKLCH build-time
|
|
|
|
|
│ │
|
|
|
|
|
│ ▼ proyección de gamut §7
|
|
|
|
|
├─ SALIDA: --scale-{name}-{step} = hex sRGB + oklch() override
|
|
|
|
|
│ --scale-{name}-a{step} = alpha compositing-inverse (P3-aware)
|
|
|
|
|
│ │
|
|
|
|
|
│ ▼ (SIN CAMBIOS — capas 2..7 del §25)
|
|
|
|
|
├─ --primitive-{role}-{step} alias rol→escala
|
|
|
|
|
├─ --color-{role}-{slot} slots (pick de contraste = APCA) §8
|
|
|
|
|
└─ recipes / TSC / eidos CSS INTACTO
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Solo se inserta una etapa **delante** (autoría OKLCH → generación → proyección de
|
|
|
|
|
gamut). De `--scale-*` hacia abajo, todo es idéntico a hoy.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. Autoría OKLCH + tipos (aditivo)
|
|
|
|
|
|
|
|
|
|
`config-types.ts` (las formas existentes se conservan; se añaden las nuevas):
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
/** OKLCH triple: L (0..1), C (0..~0.4), H (0..360 deg). */
|
|
|
|
|
export type Oklch = readonly [l: number, c: number, h: number]
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A scale authored as ONE seed. The generator expands the functional 12-step
|
|
|
|
|
* ramp by morphing a template scale (Radix's generateRadixColors method) and
|
|
|
|
|
* re-hueing it to the seed. Generated per-mode (the mode's surface anchors the
|
|
|
|
|
* low steps). See §6.
|
|
|
|
|
*/
|
|
|
|
|
export interface ColorScaleSeed {
|
|
|
|
|
/** Brand/source color: any CSS color (hex, rgb, `oklch(...)`) OR an Oklch triple. */
|
|
|
|
|
readonly seed: string | Oklch
|
|
|
|
|
/**
|
|
|
|
|
* Curve donor. Name of an existing scale whose perceptual L-curve and chroma
|
|
|
|
|
* profile are borrowed and re-hued. Default: nearest template by step-9
|
|
|
|
|
* lightness + hue. Selecting the nearest template guarantees step 9 lands on
|
|
|
|
|
* the seed.
|
|
|
|
|
*/
|
|
|
|
|
readonly template?: string
|
|
|
|
|
/** Step the seed lands on exactly. Default `'9'` (the solid). */
|
|
|
|
|
readonly solidStep?: ColorScaleStep
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** A scale is EITHER 12 explicit entries (verbatim) OR a seed (generated). */
|
|
|
|
|
export type ColorScaleSource = ColorScale | ColorScaleSeed
|
|
|
|
|
|
|
|
|
|
/** Map of scale name → source. Widens `ColorScales` for authoring. */
|
|
|
|
|
export type ColorScaleSourceMap = Record<string, ColorScaleSource>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`ThemeColorSet.scales` se ensancha de `ColorScales` a `ColorScaleSourceMap`
|
|
|
|
|
(retro-compatible: un `Record<string, ColorScale>` lo satisface). El motor, al
|
|
|
|
|
renderizar, expande las semillas antes del bucle de emisión.
|
|
|
|
|
|
|
|
|
|
### Ejemplo de autoría — antes vs después
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// HOY (untitled-ui / grafito): 12 hex × 2 modos a mano, por escala.
|
|
|
|
|
violet: s('#fcfaff','#f9f5ff','#f4ebff','#e9d7fe','#d6bbfb','#c3a5f7',
|
|
|
|
|
'#b692f6','#9e77ed','#7f56d9','#6941c6','#5b34b5','#42307d'),
|
|
|
|
|
|
|
|
|
|
// PROPUESTA: una semilla. light y dark se generan del MISMO seed contra el
|
|
|
|
|
// surface de cada modo.
|
|
|
|
|
violet: { seed: '#7f56d9' } // hex
|
|
|
|
|
violet: { seed: [0.556, 0.196, 296.5] } // OKLCH directo
|
|
|
|
|
violet: { seed: '#7f56d9', template: 'iris' } // forzar donante de curva
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
El override por componente (`<Button color="grass">`) y los roles (`primary:
|
|
|
|
|
'violet'`) **no cambian** — siguen referenciando escalas por nombre.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 6. El generador 1-seed → 12 pasos (algoritmo)
|
|
|
|
|
|
|
|
|
|
**Método: morph de plantilla** (lo que hace `@radix-ui/colors`'
|
|
|
|
|
`generateRadixColors`). No es una rampa paramétrica naíf — reusa la forma
|
|
|
|
|
perceptual de una escala afinada. Vive en build-time, p.ej.
|
|
|
|
|
`lib/themes/generate-scale.ts`.
|
|
|
|
|
|
|
|
|
|
Dado `seed → OKLCH (Ls, Cs, Hs)`, modo `m`, y su fondo `bg = mode.surface.default`:
|
|
|
|
|
|
|
|
|
|
1. **Selección de plantilla.** Si no se da `template`, elegir la escala de la
|
|
|
|
|
librería que minimice `|L9_template − Ls|` ponderado por proximidad de hue.
|
|
|
|
|
*Clave*: al elegir la plantilla cuyo step-9 tiene la luminosidad más cercana al
|
|
|
|
|
seed, preservar la curva-L de la plantilla **aterriza el step 9 sobre el seed**.
|
|
|
|
|
2. **Re-tintado.** Para cada step `i`, tomar `H_i := Hs` (+ opcional el *drift* de
|
|
|
|
|
hue relativo de la plantilla: `H_i := Hs + (H_i_tpl − H9_tpl)`).
|
|
|
|
|
3. **Luminosidad.** Conservar la curva-L de la plantilla **verbatim** (`L_i :=
|
|
|
|
|
L_i_tpl`). Son los hitos perceptuales (1-2 fondo casi-surface, 9 sólido, 11-12
|
|
|
|
|
texto). Como la plantilla se eligió por `L9 ≈ Ls`, el step 9 ya cae en el seed.
|
|
|
|
|
4. **Croma.** Re-escalar el perfil de croma para que el step 9 alcance `Cs`:
|
|
|
|
|
`C_i := C_i_tpl × (Cs / C9_tpl)`, con tope de gamut (§7). Los steps 1-2, de
|
|
|
|
|
croma bajísimo en la plantilla, quedan casi-grises tras el re-tintado → los
|
|
|
|
|
fondos siguen pegados al surface **sin** casos especiales.
|
|
|
|
|
5. **Anclaje exacto.** En `solidStep` (9 por defecto) forzar el resultado = seed
|
|
|
|
|
exacto (`L9:=Ls, C9:=Cs, H9:=Hs`) para fidelidad de marca pura en el botón.
|
|
|
|
|
6. **Alpha.** Reusar el compositing-inverse existente sobre `bg` (ya implementado,
|
|
|
|
|
`render-css.ts:1647`) — ahora con entrada OKLCH→sRGB, salida P3-aware (§7).
|
|
|
|
|
|
|
|
|
|
Resultado: 12 OKLCH por modo, fieles a la marca en el step 9, armónicos en el resto,
|
|
|
|
|
gamut-safe. **Las 33 escalas dejan de ser «768 vars muertas» (la queja del doc de
|
|
|
|
|
bloat) y pasan a ser la librería de plantillas del generador** — su valor se
|
|
|
|
|
multiplica.
|
|
|
|
|
|
|
|
|
|
> **Calibración (obligatoria antes de mergear, §11.4).** El generador debe
|
|
|
|
|
> reproducir las escalas Radix originales dentro de tolerancia (ΔE2000 por paso +
|
|
|
|
|
> delta APCA del par de contraste). Donde no llegue, la escala se queda **verbatim**
|
|
|
|
|
> (las 31 hex actuales son ground-truth). El generador es para *marcas nuevas*, no
|
|
|
|
|
> para regenerar lo ya afinado.
|
|
|
|
|
|
|
|
|
|
### 6.1 — El generador es isomórfico (build + runtime, mismo código)
|
|
|
|
|
|
|
|
|
|
La introspección (pick de contraste) y el alpha compositing-inverse **solo necesitan
|
|
|
|
|
el valor numérico del color al decidir** — no exigen build-time, exigen **JS, no CSS**.
|
|
|
|
|
La matemática de color (OKLCH↔sRGB↔Display-P3, gamut-map, APCA, inverse-alpha) es
|
|
|
|
|
**pura y determinista, sin DOM**, así que el mismo módulo corre en los dos sitios:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// lib/color/engine.ts — PURO, isomórfico. Sin imports de DOM.
|
|
|
|
|
export function generateScale(seed: Oklch, mode: ModeAnchors): ResolvedScale
|
|
|
|
|
export function pickOnSolid(solid: Oklch, candidates: OnSolidPair): string // APCA
|
|
|
|
|
export function alphaOverBackground(solid: Oklch, bg: Oklch): string // inverse
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Modo | Quién llama | Qué hace con el resultado |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| **Build** | `render-css.ts` (temas estáticos del framework + app) | concatena strings CSS (camino actual) |
|
|
|
|
|
| **Runtime JS** | `buildScheme(seed)` (composición pura; white-label / editor de temas) | `ActiveEidos.applyColorScheme(seed)` escribe el bloque scheme (hex + `oklch()`) |
|
|
|
|
|
|
|
|
|
|
En **ambos** la salida son **valores estáticos resueltos** (el `contrast` ya elegido
|
|
|
|
|
por APCA, el `aN` ya invertido). Por eso **no se sacrifica introspección ni alpha en
|
|
|
|
|
ningún modo** — se calcularon en JS *antes* de escribir.
|
|
|
|
|
|
|
|
|
|
**Coste de mantener ambas en runtime**: al cambiar la marca en vivo se reescriben
|
|
|
|
|
**también los slots del rol resueltos** (`--color-{role}-contrast`, `-surface`,
|
|
|
|
|
`-surface-hover`), no solo la escala cruda — para que el pick APCA y el alpha sigan
|
|
|
|
|
correctos con la nueva luminancia. Son ~24-48 vars/color, math sub-ms. El único peso
|
|
|
|
|
añadido es **shippear el módulo de color-math al cliente, y solo si la app usa
|
|
|
|
|
generación runtime** (tree-shakeable; las apps con temas estáticos no lo cargan).
|
|
|
|
|
|
|
|
|
|
**Promoción a `uix.color`**: el generador deja de ser un script de build y pasa a ser
|
|
|
|
|
un *art* isomórfico (paralelo a `uix.motion`): servicio puro que consumen el build
|
|
|
|
|
(`render-css`) y el runtime (`ActiveEidos`). Esto absorbe el sueño white-label / live
|
|
|
|
|
del documento de bloat **sin** la regresión de CSS-relative-colors.
|
|
|
|
|
|
|
|
|
|
**Estado 2026-06-29 — realizado.** `uix.color` existe como accessor *stateless* en
|
|
|
|
|
`ActiveUix` (tipo `EngineColor` — sin estado, por eso `Engine*` y no `Active*`),
|
|
|
|
|
descubrible junto a `uix.motion` / `uix.timers`; `eidos` sigue importando `$color`
|
|
|
|
|
directo para build/SSR. El consumidor recíproco —resolver un token de tema a un
|
|
|
|
|
color concreto en JS, sin probe `getComputedStyle`— es `eidos.resolveToken(token)`
|
|
|
|
|
(config + `$color`).
|
|
|
|
|
|
|
|
|
|
> **Frontera CSS-nativa (watch, no solución).** `contrast-color()` (CSS Color 5)
|
|
|
|
|
> haría el pick de contraste en CSS puro algún día — pero no está listo (prototipo
|
|
|
|
|
> Safari 18, nada en Chrome/Firefox en 2026), solo elige blanco/negro puro (no tus
|
|
|
|
|
> tokens `onSolid`/`onSolidContrast`) y no controlas el criterio. Para el
|
|
|
|
|
> alpha-inverse **no hay primitiva CSS** — JS es el único camino. Por eso el motor es
|
|
|
|
|
> JS isomórfico, no CSS.
|
|
|
|
|
|
|
|
|
|
### 6.2 — Derivación de esquema (theme builder · fórmula Material 3)
|
|
|
|
|
|
|
|
|
|
Encima de `generateScale` (un seed → 12 pasos) vive **`deriveScheme`** (un seed → los
|
|
|
|
|
SEEDS de los roles de jerarquía). Es el núcleo de un theme builder:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
seed de marca → deriveScheme(seed, variant) → { primary, secondary, tertiary, neutral, neutralVariant }
|
|
|
|
|
→ generateScale(cada seed) → escalas de 12 pasos (todo dentro de buildScheme)
|
|
|
|
|
→ applyColorScheme(seed) → tema en vivo (bloque hex + oklch)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**La fórmula** es la de Material 3 (`CorePalette` HCT) portada a OKLCH:
|
|
|
|
|
|
|
|
|
|
| rol | hue | chroma (OKLCH calibrado) | regla M3 (HCT) |
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
| primary | H (seed) | el del seed (verbatim) | `max(C, 48)` |
|
|
|
|
|
| secondary | H | `0.04` (bajo) | `16` |
|
|
|
|
|
| **tertiary** | **H + 60°** | `0.09` | `+60 / 24` |
|
|
|
|
|
| neutral | H | `0.008` (casi gris) | `4` |
|
|
|
|
|
| neutral-variant | H | `0.016` | `8` |
|
|
|
|
|
|
|
|
|
|
La **estructura** (mismo-hue-desaturado para secondary · +60° para tertiary) es
|
|
|
|
|
model-agnóstica, así que porta exacta; solo los números de croma se recalibran (HCT
|
|
|
|
|
`0..120` ≠ OKLCH `0..0.37`). El truco **tone→contraste** de HCT NO se porta — el
|
|
|
|
|
contraste lo decide **APCA** (§8).
|
|
|
|
|
|
|
|
|
|
**Variantes** (`SchemeVariant`) — el "estilo" del builder:
|
|
|
|
|
- `tonal` (default) — la tabla (look M3 clásico).
|
|
|
|
|
- `vibrant` — más croma + pequeña rotación en secondary; tertiary saturado.
|
|
|
|
|
- `monochrome` — croma 0 en todo: la jerarquía **colapsa a una tinta neutra** (look
|
|
|
|
|
Vercel / Linear); se diferencia por tono + énfasis, no por hue (colisión *por
|
|
|
|
|
diseño*, a diferencia del bug del base).
|
|
|
|
|
|
|
|
|
|
Estructurado para añadir `expressive` / `neutral` / `content` como ~15 líneas de
|
|
|
|
|
reglas, sin tocar nada más.
|
|
|
|
|
|
|
|
|
|
**Override por rol** — `deriveScheme` da DEFAULTS, no una jaula. El diseñador puede
|
|
|
|
|
**fijar** cualquier rol a su color exacto (reemplaza el seed de ese rol; el resto se
|
|
|
|
|
sigue derivando del seed base, y cambiar el seed re-deriva solo los no fijados). Es
|
|
|
|
|
el patrón de Radix/M3 (colores custom por rol) y del camino hand-authored (grafito
|
|
|
|
|
mapea cada rol explícito: `secondary: 'violet'`). El builder de `/temas/color` lo
|
|
|
|
|
expone con un input de color por fila + «auto» para volver a derivado.
|
|
|
|
|
|
|
|
|
|
**Los 6 intents NO se derivan** — son hues canónicos del libro (un error es rojo
|
|
|
|
|
siempre). Para que no **desentonen** con la marca se **afinan** con
|
|
|
|
|
`temper(color, reference, amount)`: mantiene el **hue** (rojo sigue rojo) y solo
|
|
|
|
|
acerca **croma + luminosidad** al perfil de la marca — la *temperatura perceptual*.
|
|
|
|
|
Eso es lo que cohesiona una paleta; **rotar el hue erosiona el significado** (un rojo
|
|
|
|
|
deja de leerse como error). Un valor **sutil (~10-15%)** basta; el builder de
|
|
|
|
|
`/temas/color` lo usa como default (slider en *Roles canónicos*, 0 = canónico puro →
|
|
|
|
|
fuerte). `harmonize(color, toward, amount)` (M3 `blend.harmonize`, **rota hue**) sigue
|
|
|
|
|
en el motor para **acentos de marca** custom, NO para intents semánticos.
|
|
|
|
|
|
|
|
|
|
**Caveat del +60°**: la rotación de Material puede caer cerca de un intent según el
|
|
|
|
|
primary (p. ej. `purple + 60° = H6 ≈ red/threat`). Por eso el tema base afinó su
|
|
|
|
|
`tertiary` a `indigo` (−60°, frío, libre de intents) **a mano**. Un builder debería
|
|
|
|
|
ofrecer override del hue del tertiary o esquivar la banda de los intents (red 25° ·
|
|
|
|
|
orange 55° · amber 75° · green 158° · teal 182° · plum 330°).
|
|
|
|
|
|
|
|
|
|
**Blast radius: cero sobre componentes.** `deriveScheme` produce VALORES que entran
|
|
|
|
|
por el contrato congelado `--color-{role}-{slot}` (§10). Ningún componente, recipe,
|
|
|
|
|
CSS ni el TSC cambian — una variante es "otro tema", como `base` ↔ `grafito`. El
|
|
|
|
|
**número de variantes es decisión de catálogo del builder, no coste arquitectónico**
|
|
|
|
|
(no se shippean N CSS; se computa un tema a la vez, build o runtime).
|
|
|
|
|
|
|
|
|
|
Vive en `uix.color` (`scheme.ts`): matemática pura, isomórfica. La COMPOSICIÓN de
|
|
|
|
|
`deriveScheme` + `generateScale` + APCA + alpha en el mapa de tokens
|
|
|
|
|
`--primitive-{role}-*` es `buildScheme(seed, opts)` (`eidos/lib/build-scheme.ts`,
|
|
|
|
|
pura). El método runtime **`eidos.applyColorScheme(seed, opts)`** (ActiveEidos)
|
|
|
|
|
resuelve las escalas-donantes + background del tema activo, escribe el bloque de
|
|
|
|
|
estilo y **sigue light/dark** (re-deriva al cambiar de modo); devuelve un
|
|
|
|
|
`BuildSchemeResult` (steps hex + `stepsOklch` + solid / on-solid por rol) para
|
|
|
|
|
introspección. `eidos.clearColorScheme()` revierte. El bloque apila **hex + `oklch()`**
|
|
|
|
|
por paso (wide-gamut, §7) y `generateScale` retiene el OKLCH raw sin clamp, así que un
|
|
|
|
|
seed vívido (croma > sRGB) sale P3 (demo: slider *vivacidad*). **Estado: implementado**
|
|
|
|
|
(Fase 4) — ver THEMING.md §26; tests en `build-scheme.test.ts` + `active-eidos.test.ts`.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const result = eidos.applyColorScheme('#8e4ec6', {
|
|
|
|
|
variant: 'tonal', // 'tonal' | 'vibrant' | 'monochrome'
|
|
|
|
|
temper: 0.12, // cohesión de intents (mantiene hue)
|
|
|
|
|
overrides: { tertiary: '#3e63dd' } // fija un rol; el resto deriva
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 7. Salida wide-gamut (P3 + sRGB)
|
|
|
|
|
|
|
|
|
|
**Estado: estrategia A implementada, default-on** (2026-06-04). `render-css` emite por
|
|
|
|
|
cada paso de paleta el **hex (fallback universal)** + un hermano **`oklch()`** que gana
|
|
|
|
|
donde se soporta (`appendColorScaleDeclarations`). SIN flag de config: es el
|
|
|
|
|
comportamiento por defecto. La estrategia B (P3 explícito vía `@media`) NO está
|
|
|
|
|
implementada (se añadiría como opción si una app necesita afinar valores P3 a mano).
|
|
|
|
|
|
|
|
|
|
**Honestidad sobre el efecto visible**: la paleta por defecto (Radix) está autorada en
|
|
|
|
|
**hex sRGB**, así que su `oklch()` es **sRGB-equivalente** — no hay datos P3 que
|
|
|
|
|
recuperar de un sRGB, se ve idéntico hoy (verificado: `--scale-purple-9` →
|
|
|
|
|
`oklch(0.5556 0.1829 305.86)` pinta `#8e4ec6`). El valor es que el token-layer es ahora
|
|
|
|
|
**OKLCH-nativo y wide-gamut-ready**: un tema autorado en OKLCH, o un esquema generado
|
|
|
|
|
con un seed vívido, renderiza más saturado en P3 **sin trabajo extra**. Hacer la paleta
|
|
|
|
|
SHIPPED visiblemente wide-gamut es Fase 3 (autorar/generar la paleta en OKLCH).
|
|
|
|
|
|
|
|
|
|
Dos estrategias (histórico de diseño):
|
|
|
|
|
|
|
|
|
|
### A. OKLCH-directo + fallback hex (recomendado · estilo Tailwind v4)
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
[data-theme='brand-light'] {
|
|
|
|
|
--scale-violet-9: #7f56d9; /* fallback universal (gamut-mapped sRGB) */
|
|
|
|
|
--scale-violet-9: oklch(0.556 0.196 296.5); /* gana donde OKLCH existe → wide-gamut AUTOMÁTICO */
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **Wide-gamut gratis**: `oklch()` usa el gamut del display; en pantallas P3 el color
|
|
|
|
|
sale más saturado que el hex sin un `@media`.
|
|
|
|
|
- **Universal**: navegadores pre-OKLCH (raros en 2026) usan el hex.
|
|
|
|
|
- **Byte-light**: dos líneas por token, sin duplicar bloques `@media`. Gzip las come.
|
|
|
|
|
- Soporte OKLCH: Chrome 111+ / Safari 15.4+ / Firefox 113+ (amplio desde 2023).
|
|
|
|
|
|
|
|
|
|
### B. P3 explícito vía `@media (color-gamut: p3)` (fidelidad-máx · estilo Radix)
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
[data-theme='brand-light'] { --scale-violet-9: #7f56d9; }
|
|
|
|
|
@supports (color: color(display-p3 0 0 0)) {
|
|
|
|
|
@media (color-gamut: p3) {
|
|
|
|
|
[data-theme='brand-light'] { --scale-violet-9: color(display-p3 0.45 0.34 0.83); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Más control (valores P3 distintos por paso) a costa de más bytes. Para apps que
|
|
|
|
|
afinan el gamut a mano.
|
|
|
|
|
|
|
|
|
|
### Gamut-mapping del fallback sRGB
|
|
|
|
|
|
|
|
|
|
El hex de fallback se obtiene por **gamut-mapping CSS Color 4** (reducción de croma
|
|
|
|
|
preservando L y H hasta entrar en sRGB), **no** por clip de canales (que desplaza el
|
|
|
|
|
hue). Implementación: reducir `C` iterativamente hasta que el OKLCH→sRGB esté
|
|
|
|
|
in-gamut. (Reusable también para el step 9 anclado si el seed es P3-only.)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 8. Contraste APCA
|
|
|
|
|
|
|
|
|
|
Reemplazar `wcagContrastRatio` (`render-css.ts:1629`) por **APCA (Lc)** para el pick
|
|
|
|
|
texto-on-solid (`render-css.ts:343-363`):
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
/** APCA lightness contrast, −108..+106. Polarity-aware (text vs bg). */
|
|
|
|
|
function apcaLc(text: Srgb, bg: Srgb): number { /* APCA-W3 0.1.9 */ }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **Pick**: para cada rol, calcular `Lc(onSolid, solid9)` y `Lc(onSolidContrast,
|
|
|
|
|
solid9)`; elegir el de mayor `|Lc|`.
|
|
|
|
|
- **Suelo**: exigir `|Lc| ≥ 60` (texto normal) / `≥ 45` (UI / texto grande). Si
|
|
|
|
|
ninguno llega, fallback al step-12 (como hoy) + warning de validación.
|
|
|
|
|
- **Por qué APCA**: WCAG2 sobre/infra-estima el contraste en mid-tones (el caso
|
|
|
|
|
exacto de `risk`=naranja que se coló, audit P2-2). APCA modela la percepción real.
|
|
|
|
|
- **Honestidad**: APCA es **borrador WCAG3**, no conformidad legal. Mantener un
|
|
|
|
|
**cross-check WCAG2 ≥ 3:1** como red de seguridad y dejarlo documentado; si APCA y
|
|
|
|
|
WCAG2 discrepan fuerte, ganar el más conservador.
|
|
|
|
|
|
|
|
|
|
Efecto: el pick mejora en sólidos claros (amber/yellow/lime/mint) sin regresión en
|
|
|
|
|
los oscuros (purple/red/blue mantienen blanco).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 9. Saneo del modelo actual (incluido en el giro)
|
|
|
|
|
|
|
|
|
|
| Defecto | Fix | Ancla |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `primary` ≡ `loss` = purple en el base | Migrar base a semillas; `loss` → su seed propio (plum). Auto-deriva si se omite. | `themes/base.ts:18,31` |
|
|
|
|
|
| `tertiary` ≡ `neutral` = gray | `tertiary` → un hue distinto (p.ej. indigo) o quitarlo del default. | `themes/base.ts:20,21` |
|
|
|
|
|
| Doc-drift `primary: indigo` (doc) vs `purple` (código) | Alinear doc + base tras decidir el primary del base. | `THEMING.md §4/§9` |
|
|
|
|
|
| **P3-3** slot→step apretado abajo, 7-8 infrautilizados, `border=6` lavado | *Conservador*: `border` 6→7; evaluar `border-strong`=8. **Gated tras probe visual** — no romper armonía. | `render-css.ts:71-81` |
|
|
|
|
|
|
|
|
|
|
El saneo del modelo no es el corazón del RFC pero entra «de gratis» al migrar el
|
|
|
|
|
base a semillas.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 10. Compatibilidad / invariantes (qué NO cambia)
|
|
|
|
|
|
|
|
|
|
**Contrato de nombres — congelado.** El generador y la proyección de gamut producen
|
|
|
|
|
exactamente las mismas vars que hoy:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
--scale-{name}-{1..12} --scale-{name}-a{1..12}
|
|
|
|
|
--primitive-{role}-{1..12} --primitive-{role}-a{1..12}
|
|
|
|
|
--color-{role}-{slot} --color-{role}-surface --color-{role}-surface-hover
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Por tanto **no cambian**: recipes (`lib/recipes/base.ts`), TSC, eidos CSS de
|
|
|
|
|
componentes, demos, el purge, el contrato público (`contract.ts`), ni la API de
|
|
|
|
|
componentes (`data-color`, prop `color`). Es el mismo blindaje que prometió
|
|
|
|
|
`COLOR_MODEL_RFC §6`: «aguas abajo, cero cambios; solo cambia *cómo* se producen esas
|
|
|
|
|
vars».
|
|
|
|
|
|
|
|
|
|
**Sema canon — intacta.** Las 6 intents, las 8 families, la doctrina «el color
|
|
|
|
|
expresa el intent, no lo define». El color sigue aportando solo la identidad de hue.
|
|
|
|
|
|
|
|
|
|
**Variants canon — intacta** (§19). El theme retinta; no añade variants ni redefine
|
|
|
|
|
cascadas.
|
|
|
|
|
|
|
|
|
|
**Persistencia** — el envelope versionado (`toDocument()`) gana un `version` bump si
|
|
|
|
|
la forma de `scales` autoradas cambia; las escalas 12-hex viejas se leen igual.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 11. Plan de fases (cada una verificable y mergeable sola)
|
|
|
|
|
|
|
|
|
|
### Fase 0 — Tipos + generador (build-time lib), sin cambio de comportamiento
|
|
|
|
|
- Añadir `Oklch`, `ColorScaleSeed`, `ColorScaleSource`, ensanchar `ThemeColorSet.scales`.
|
|
|
|
|
- `lib/themes/generate-scale.ts` (morph de plantilla) + conversión OKLCH↔sRGB↔P3 +
|
|
|
|
|
gamut-mapping. Sin consumir todavía — las escallas existentes siguen verbatim.
|
|
|
|
|
- **Verifica**: tests unitarios del generador (round-trip, gamut, anclaje step 9).
|
|
|
|
|
|
|
|
|
|
### Fase 1 — APCA contrast
|
|
|
|
|
- Swap `wcagContrastRatio` → `apcaLc` en el pick on-solid; suelo + cross-check WCAG2.
|
|
|
|
|
- **Verifica**: re-asertar AA/Lc de los 9 roles × 2 modos (cubre el punto ciego
|
|
|
|
|
P1-6); probe en navegador de button/badge solid de cada rol.
|
|
|
|
|
|
|
|
|
|
### Fase 2 — Salida wide-gamut (estrategia A)
|
|
|
|
|
- Emitir cada token de color como `hex; oklch()`-override. Sin `@media` (default A).
|
|
|
|
|
- **Verifica**: hex idéntico al actual (cero regresión sRGB); en display P3 el color
|
|
|
|
|
satura. Test de que el fallback existe siempre.
|
|
|
|
|
|
|
|
|
|
### Fase 3 — Migrar base + grafito a semillas
|
|
|
|
|
- Reautorar `themes/base.ts` y `_lib/grafito.ts` con `seed`. Arreglar `primary≡loss`.
|
|
|
|
|
- **Verifica**: ΔE2000 por paso vs las hex actuales dentro de tolerancia; probe
|
|
|
|
|
visual de las páginas `/uix/components/*` + `/temas/grafito` (light+dark).
|
|
|
|
|
|
|
|
|
|
### Fase 4 — Generador 1-seed público + demo
|
|
|
|
|
- Exponer en `ActiveEidos` / `defineEidosConfig` y documentar.
|
|
|
|
|
- Demo en `/temas` (o `/uix/lib`): un input de color → escala completa + dark + P3
|
|
|
|
|
live (build-time vía un endpoint o precomputado).
|
|
|
|
|
- **Verifica**: una marca arbitraria genera escala AA sin autorar hex.
|
|
|
|
|
|
|
|
|
|
### Fase 5 — Afinado slot→step (P3-3), opcional, gated tras probe
|
|
|
|
|
- Solo si el probe visual lo respalda. Conservador (`border` 6→7).
|
|
|
|
|
|
|
|
|
|
### Fase 6 — Docs
|
|
|
|
|
- `THEMING.md`: §25 gana una subsección «capa física» que apunta aquí; §22 marca P3-1
|
|
|
|
|
resuelto. Marcar `COLOR_MODEL_RFC §5` fase 3 como resuelta por este RFC.
|
|
|
|
|
|
|
|
|
|
### Fase 4-bis — Generación en runtime (white-label / live theming) — ✅ IMPLEMENTADO
|
|
|
|
|
- `ActiveEidos.applyColorScheme(seed, opts)` corre el **mismo generador isomórfico**
|
|
|
|
|
(§6.1) en JS (vía el helper puro `buildScheme`) y escribe la escala + **slots de rol
|
|
|
|
|
resueltos** (`--color-{role}-contrast`) como un **bloque de estilo gestionado** (hex
|
|
|
|
|
fallback + `oklch()` wide-gamut). Conserva pick APCA + alpha (se calculan antes de
|
|
|
|
|
escribir) y **sigue light/dark** (re-deriva al cambiar de modo). Cubre el white-label
|
|
|
|
|
**sin** CSS-relative. Ver §6.2 + THEMING.md §26.
|
|
|
|
|
- **Verifica**: un hex de marca elegido en vivo produce escala AA + dark + P3
|
|
|
|
|
coherentes; cambiar de marca reescribe solo las vars del rol afectado.
|
|
|
|
|
|
|
|
|
|
### Azúcar opcional (sin fase fija) — relative colors en CSS
|
|
|
|
|
- **Solo** para derivaciones triviales donde el valor numérico no hace falta
|
|
|
|
|
(`oklch(from var(--color-x-solid) calc(l - .05) c h)` para un hover). Siempre con
|
|
|
|
|
`@supports` + fallback al token estático. **Nunca** genera escalas ni decide
|
|
|
|
|
contraste/alpha (eso lo hace el motor JS, no el CSS).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 12. Riesgos y preguntas abiertas
|
|
|
|
|
|
|
|
|
|
| Riesgo | Mitigación |
|
|
|
|
|
|---|---|
|
|
|
|
|
| **Fidelidad del generador < hand-tuned** | Calibrar contra Radix (ΔE + APCA); donde no llegue, dejar verbatim. Las 31 hex son ground-truth, no se borran. |
|
|
|
|
|
| **APCA es borrador** | Mantener suelo WCAG2 ≥3:1 como cross-check; ganar el conservador. |
|
|
|
|
|
| **Suelo OKLCH (pre-2023)** | El fallback hex es universal — cero pérdida. |
|
|
|
|
|
| **P3 duplica bytes (estrategia B)** | Default = estrategia A (OKLCH-directo, sin `@media`); B solo opt-in. |
|
|
|
|
|
| **Gamut-map mal hecho desplaza hue** | Usar reducción de croma CSS Color 4, no clip de canales. |
|
|
|
|
|
| **Coste de build del generador** | Memoizar por seed; es build-time, no runtime. |
|
|
|
|
|
| **Drift doc/código** | Fase 6 cierra; §10 congela el contrato de nombres. |
|
|
|
|
|
|
|
|
|
|
**Preguntas abiertas** (a decidir antes de Fase 0):
|
|
|
|
|
|
|
|
|
|
1. **Estrategia de salida default**: A (OKLCH-directo + hex) vs B (P3 `@media`).
|
|
|
|
|
*Recomiendo A* (simple, byte-light, wide-gamut automático).
|
|
|
|
|
2. **Algoritmo**: morph-de-plantilla (recomendado, reusa las 31) vs curva paramétrica.
|
|
|
|
|
*Recomiendo morph*.
|
|
|
|
|
3. **¿Las 31 plantillas siguen shippeando por defecto** (status quo + purge) o pasan a
|
|
|
|
|
ser **data build-time** que solo emite lo referenciado? *Recomiendo status quo +
|
|
|
|
|
purge* (conserva el override `data-color="grass"`); las plantillas son la misma data.
|
|
|
|
|
4. **`primary`/`tertiary` del base**: ¿qué hues? (purple→¿indigo/violet? · tertiary→¿?).
|
|
|
|
|
5. **Umbrales APCA** (60/45) — calibrar contra el catálogo real.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 13. Cómo este RFC absorbe el «modelo de color más eficiente»
|
|
|
|
|
|
|
|
|
|
El documento de «reducción de bloat» pedía tres cosas. Este RFC entrega su parte
|
|
|
|
|
buena **sin** sus regresiones (ver el análisis de runtime-vs-build):
|
|
|
|
|
|
|
|
|
|
| Deseo del doc | Cómo lo entrega este RFC | Sin pagar |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| «1 variable base en vez de 12» | **Generador 1-seed** (§6) isomórfico: autoras un color, salen los 12 — en build **o** runtime JS | …la fidelidad (curva afinada), el contraste introspectable, el alpha, el soporte universal |
|
|
|
|
|
| «menos bytes» | Apps de marca authoran N seeds (no N×12 hex) + **purge** + OKLCH-directo compacto | …el override `data-color` (las escalas siguen disponibles) |
|
|
|
|
|
| «reducción semántica a ~6 estados» | **Ya existe**: los 9 slots `--color-{role}-{slot}` (§25). No se toca. | — |
|
|
|
|
|
| OKLCH / vanguardia | **Espacio de autoría y fuente** + salida wide-gamut | …romper el cálculo estático |
|
|
|
|
|
|
|
|
|
|
Acuerdo en el objetivo (OKLCH, menos autoría); el mecanismo correcto es **build-time**,
|
|
|
|
|
estrictamente superior porque es un superconjunto: la ergonomía del runtime *más* la
|
|
|
|
|
fidelidad, correctitud y robustez que el runtime sacrifica.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 14. Comparación con referentes
|
|
|
|
|
|
|
|
|
|
| Capacidad | activeUIX (post-RFC) | Radix Colors v3 | Tailwind v4 | Chakra Panda | Material 3 |
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
|
|
| Espacio de autoría | **OKLCH** | sRGB+P3 (tool OKLCH) | OKLCH | token | HCT |
|
|
|
|
|
| Generador 1-color→escala | **✅ isomórfico (build+runtime), morph** | ✅ (CLI/tool, build) | ❌ | ❌ | ✅ (HCT) |
|
|
|
|
|
| Wide-gamut P3 | **✅** | ✅ | ✅ (oklch+fallback) | ⚠️ | ⚠️ |
|
|
|
|
|
| Contraste | **APCA + suelo WCAG2** | APCA | — | — | tone-based |
|
|
|
|
|
| Scope-as-contract (TSC) | **✅ único** | ❌ | ❌ | ⚠️ build | ❌ |
|
|
|
|
|
| Capa perceptual (sema) | **✅ único** | ❌ | ❌ | ❌ | ⚠️ |
|
|
|
|
|
| Salida estática introspectable | **✅** | ✅ | ✅ | ✅ | varía |
|
|
|
|
|
|
|
|
|
|
El RFC pone a eidos a la **par de Radix/M3 en fidelidad de color** (OKLCH+P3+APCA+
|
|
|
|
|
generador) manteniendo lo que ya tenía **en exclusiva** (TSC + capa sema). Ese es el
|
|
|
|
|
«siguiente nivel».
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
**Última revisión**: 2026-06-04. Si algo aquí contradice el código tras
|
|
|
|
|
implementar, gana el código — abrir issue para sincronizar.
|
|
|
|
|
# RFC — Next-generation color engine (moved)
|
|
|
|
|
|
|
|
|
|
✅ Implemented through phase 4-bis. The **physical layer** of color: OKLCH
|
|
|
|
|
authoring (`ColorScaleSeed`), the isomorphic 1-seed → 12-step generator
|
|
|
|
|
(template morph, promoted to `uix.color`), wide-gamut output (hex +
|
|
|
|
|
`oklch()` sibling, default-on), APCA contrast with a WCAG2 floor,
|
|
|
|
|
`deriveScheme` (M3 formula) + `buildScheme` + runtime
|
|
|
|
|
`eidos.applyColorScheme(seed, opts)` / `resolveToken`. The conceptual model
|
|
|
|
|
stays closed in the theming reference §25 — this RFC changes how the vars
|
|
|
|
|
are produced, never their names.
|
|
|
|
|
|
|
|
|
|
**The RFC moved to the docs corpus:**
|
|
|
|
|
[`docs/rfcs/rfc-color-engine.md`](../../../docs/rfcs/rfc-color-engine.md)
|
|
|
|
|
— TL;DR, principles, generator algorithm, isomorphism (§6.1), scheme
|
|
|
|
|
derivation (§6.2), wide-gamut strategies, APCA, invariants, phases, risks.
|
|
|
|
|
|
|
|
|
|
Canonical reference: [`docs/theming/reference.md`](../../../docs/theming/reference.md) §25–§27.
|
|
|
|
|
|