From 458137c0274a5eb87deeb2c3fdad19af6243abf0 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 5 Jun 2026 01:33:16 +0200 Subject: [PATCH] docs(color): sync arts/color README + COLOR_ENGINE_RFC with implemented API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found stale color docs while verifying currency: - arts/color/README.md: said "Status — Phase 0 ... nothing consumes it yet" (false — consumed at build via render-css + runtime via applyColorScheme) and used `ActiveEidos.setCssVariables` as the theme-builder mechanism (the real API is applyColorScheme; setCssVariables is for contract knobs). Updated status, added deriveScheme/temper/harmonize to the API table, documented temper as the canonical intent-cohesion tool (vs harmonize for brand accents), fixed the builder pipeline to buildScheme + applyColorScheme, and corrected the wide-gamut note (strategy A is live + default-on, not "deferred"). - COLOR_ENGINE_RFC.md: 4 remaining `setCssVariables` references for the runtime white-label builder -> applyColorScheme (only §6.2 was fixed earlier). Marked Fase 4-bis (runtime generation) as IMPLEMENTED. COLOR_MODEL_RFC.md verified current (RESUELTO; loss->plum correct; the anchor-hex examples are the documented-discarded proposal = history). THEMING/audit/README were already synced in their own commits. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/arts/color/README.md | 43 ++++++++++++++++++++----------- src/uix/eidos/COLOR_ENGINE_RFC.md | 21 ++++++++------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/arts/color/README.md b/src/arts/color/README.md index 0cbab2f0f..cbe9d03e3 100644 --- a/src/arts/color/README.md +++ b/src/arts/color/README.md @@ -2,13 +2,14 @@ Isomorphic **color math engine**. Pure, deterministic, DOM-free — the same functions run at **build** (eidos `render-css` emits static CSS) and at **runtime** -(`ActiveEidos.setCssVariables` for live / white-label theming). Because every +(`ActiveEidos.applyColorScheme` for live / white-label theming). Because every decision is computed in JS **before** a value is written, **introspection (APCA on-solid pick) and alpha fidelity are preserved in every mode**. -> **Status — Phase 0** of [`COLOR_ENGINE_RFC.md`](../../uix/eidos/COLOR_ENGINE_RFC.md). -> This module + its tests exist; **nothing consumes it yet** (zero behavior change, -> safe to merge alone). The eidos wiring (build + runtime) lands in later phases. +> **Status — consumed** ([`COLOR_ENGINE_RFC.md`](../../uix/eidos/COLOR_ENGINE_RFC.md) +> Phases 0/1/2/4). eidos's `render-css` consumes it at build (APCA on-solid pick + +> OKLCH-native palette output); `ActiveEidos.applyColorScheme` consumes it at runtime +> (the live theme builder). Wide-gamut output (`oklch()` + hex fallback) is default-on. ## Why an art (and not an eidos lib) @@ -44,6 +45,9 @@ shifts the hue). The `oklch(...)` string keeps the full gamut for the browser. | `generateScale(seed, template, { solidStep? })` | seed → 12 OKLCH steps (morph; solid anchored to seed) | | `pickOnSolid(solid, { onSolid, onSolidContrast }, floor?)` | APCA pick of on-solid text + `passes` | | `alphaOverBackground(solid, background)` | translucent fill `(rgb, alpha)` that over `background` = solid | +| `deriveScheme(seed, variant?)` | brand seed → hierarchy role seeds (Material 3 `CorePalette`) | +| `temper(color, reference, amount?)` | match a reference's L+C **keeping hue** (cohere intents) | +| `harmonize(color, toward, amount?)` | rotate hue toward a reference (M3 blend; brand accents) | ## The generator (template morph) @@ -63,19 +67,28 @@ brand seed → the hierarchy role seeds (`primary` / `secondary` / `tertiary` / chroma · **neutral** = same hue, near-zero chroma. (Structure is exact; chroma is recalibrated HCT→OKLCH. Tone→contrast is NOT ported — APCA decides contrast.) - **Variants** (`SchemeVariant`): `tonal` (default), `vibrant`, `monochrome`. -- The **6 canonical intents are NOT derived** (an error is always red). - `harmonize(color, toward, amount?)` (M3 `blend.harmonize`) nudges a hue toward the - brand for cohesion — opt-in, keeps L + C. - -Feed each derived seed to `generateScale`. **Live builder**: seed → `deriveScheme` → -`generateScale` → `ActiveEidos.setCssVariables` — same code at build or runtime. It -produces only VALUES behind the frozen `--color-{role}-{slot}` contract, so it -touches **no component**. Full spec: `../../uix/eidos/COLOR_ENGINE_RFC.md` §6.2. +- The **6 canonical intents are NOT derived** (an error is always red). To cohere them + with a brand WITHOUT losing meaning, `temper(color, reference, amount?)` matches the + brand's **perceptual temperature** (L + C) while **keeping the hue** (red stays red) — + the right tool for intents. `harmonize` (rotates hue) also exists, but for brand + ACCENTS, not semantic intents (rotating an intent's hue erodes its meaning). + +**Composition + apply**: `buildScheme(seed, opts)` (in `eidos/lib`, pure) composes +`deriveScheme` + `generateScale` + APCA + alpha into the `--primitive-{role}-*` token +map; **`ActiveEidos.applyColorScheme(seed, opts)`** writes it as a managed style block +(hex fallback + `oklch()` wide-gamut), follows light/dark, and returns the scheme for +introspection. Same math at build or runtime; only VALUES behind the frozen +`--color-{role}-{slot}` contract, so it touches **no component**. `opts`: `variant` +(tonal/vibrant/monochrome) + `temper` (intent cohesion) + per-role `overrides`. Full +spec: `../../uix/eidos/COLOR_ENGINE_RFC.md` §6.2 + `THEMING.md` §26. ## Notes - **APCA is a WCAG 3 draft**, not legal conformance. `pickOnSolid` decides by APCA but `onSolidWcagRatio` is available as a documented cross-check / safety floor. -- Display-P3 explicit output (`color(display-p3 …)`) is **deferred** — the default - output strategy is `oklch()` + hex fallback, which gets wide-gamut automatically - and needs no P3 conversion. P3-explicit lands only if strategy B is chosen (RFC §7). +- **Wide-gamut output is live + default-on** (RFC §7 strategy A): the palette emits a + hex fallback + an `oklch()` sibling that wins where supported → wide-gamut on P3 with + no `@media`. Explicit `color(display-p3 …)` (strategy B) stays deferred — only if an + app needs hand-tuned P3 values. NOTE: an sRGB-authored palette renders identically in + `oklch()` (no P3 data to recover); real P3 needs a wide-gamut SOURCE (a vivid seed / + OKLCH-authored theme), which the generator preserves (raw OKLCH, never gamut-clamped). diff --git a/src/uix/eidos/COLOR_ENGINE_RFC.md b/src/uix/eidos/COLOR_ENGINE_RFC.md index aaff71294..b9735cf9b 100644 --- a/src/uix/eidos/COLOR_ENGINE_RFC.md +++ b/src/uix/eidos/COLOR_ENGINE_RFC.md @@ -50,7 +50,8 @@ inverse-alpha) es **pura, sin DOM**, así que corre idéntica en build **y** en - **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 + P3 y la escribe vía `setCssVariables(...)`. + 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 @@ -256,7 +257,7 @@ export function alphaOverBackground(solid: Oklch, bg: Oklch): string // i | 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** | `uix.color.generate(seed)` (white-label / editor de temas) | escribe vía `ActiveEidos.setCssVariables(...)` | +| **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 @@ -288,8 +289,8 @@ 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 - → setCssVariables(...) → tema en vivo + → 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: @@ -527,11 +528,13 @@ la forma de `scales` autoradas cambia; las escalas 12-hex viejas se leen igual. - `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), pilar, no azúcar -- `uix.color.generate(seed)` corre el **mismo generador isomórfico** (§6.1) en JS y - escribe la escala + dark + P3 + **slots de rol resueltos** vía - `ActiveEidos.setCssVariables(...)`. Conserva pick APCA + alpha (se calculan antes de - escribir). Cubre el caso white-label del documento de bloat **sin** CSS-relative. +### 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.