You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/theming/reference.md

1893 lines
102 KiB

---
title: Eidos Theming — Architecture Reference
type: reference
audience: human + agent
authority: E1 reference — the theming system: mental model, token layers, roles, sizes, naming, tooling
status: current
source: migrated from src/uix/eidos/THEMING.md (2026-07-02, docs-book F7.3)
---
# Eidos Theming — Architecture Reference
> **Audience**: any dev opening the repo who needs to understand how theming
> works in UIX. It covers the mental model, the contracts, the tooling and
> the traps. If after reading it you still don't know where a new token
> goes, this doc failed — open an issue.
**TL;DR**:
- **9 canonical color roles** (`primary`, `secondary`, `tertiary`,
`neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`).
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **7 canonical sizes** + `full` (`xxs..xxl`).
- **3 public token levels**: foundation (stable), per-component recipe
(overrideable), private (`--_*`, no external contract).
- The **Token Scope Contract (TSC)** decides WHERE each token is emitted
(`:root` / `[data-{c}]` / `[data-{c}][data-color='X']` / etc.) and
validates transitivity at generation time.
- **226 KB raw / 25 KB gzip** of foundation CSS by default. Use
`npm run eidos:purge` for production apps → −46 to −55%.
- **The color model**: a palette of 33 scales (designable) → hierarchy
roles (explicit aliases) → intents (auto-derived by the book's
convention, identity = step 9). See §25.
- **Compatible with** versioned persistence, CSS-only themes, runtime
overrides, dark/light, density (compact/comfortable/spacious), scaling
(zoom 90–110, a separate axis), reduced motion, multi-axis breakpoints.
---
## Table of contents
1. [Mental model](#1-mental-model)
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
1bis. [Theming lives in Eidos, not in Morfo (by design)](#1bis-theming-lives-in-eidos-not-in-morfo-by-design)
2. [The layers of Eidos's CSS](#2-the-layers-of-eidoss-css)
3. [The 7 token layers](#3-the-7-token-layers)
4. [The 9 canonical color roles](#4-the-9-canonical-color-roles)
5. [The size canon](#5-the-size-canon)
6. [Naming conventions](#6-naming-conventions)
7. [Token Scope Contract (TSC)](#7-token-scope-contract-tsc)
8. [How to add a new component](#8-how-to-add-a-new-component)
9. [How to define a theme](#9-how-to-define-a-theme)
10. [How to override tokens at runtime](#10-how-to-override-tokens-at-runtime)
11. [Bundle strategy + `eidos:purge`](#11-bundle-strategy--eidospurge)
12. [Validation tooling](#12-validation-tooling)
13. [Sema integration (`event:*` scope)](#13-sema-integration-event-scope)
14. [Motion](#14-motion)
15. [Comparison with reference libraries](#15-comparison-with-reference-libraries)
16. [Anti-patterns you must NOT commit](#16-anti-patterns-you-must-not-commit)
17. [FAQ — controversial decisions](#17-faq--controversial-decisions)
18. [Universal TSC coverage](#18-universal-tsc-coverage)
19. [Variants are eidos canon, NOT the theme's](#19-variants-are-eidos-canon-not-the-themes)
20. [Theming-engine corrections (2026-06-01)](#20-theming-engine-corrections-2026-06-01)
21. [Two-level color model (RFC — RESOLVED in §25)](#21-two-level-color-model-rfc--resolved-in-25)
22. [Pending theming improvements](#22-pending-theming-improvements)
23. [The `scaling` axis (global zoom)](#23-the-scaling-axis-global-zoom--2026-06-02)
24. [P2 engine corrections (2026-06-02)](#24-p2-engine-corrections-2026-06-02)
25. [The color model — palette + derived roles/intents](#25-the-color-model--palette--derived-rolesintents-2026-06-02)
26. [Runtime theme builder — `eidos.applyColorScheme`](#26-runtime-theme-builder--eidosapplycolorscheme-2026-06-04)
27. [Wide-gamut OKLCH output (default-on)](#27-wide-gamut-oklch-output-default-on-2026-06-04)
28. [Forced-colors accessibility + the border ramp](#28-forced-colors-accessibility--the-border-ramp-2026-06-05)
29. [Depth — the unified, eventful channel](#29-depth--the-unified-eventful-channel-2026-06-05)
30. [Shape — continuity + families + nesting](#30-shape--continuity--families--nesting--eventful-2026-06-05)
31. [Structure (space · density · scale)](#31-structure-space--density--scale--space-as-rhythm-2026-06-05)
32. [Focus ring — the parameterized two-ring model](#32-focus-ring--the-parameterized-two-ring-model-2026-06-11)
33. [Themeable stepper glyphs (`spin-field`)](#33-themeable-stepper-glyphs-spin-field--2026-06-11)
34. [`spin-field` — the stepper-field's shared visual](#34-spin-field--the-stepper-fields-shared-visual-number-field--css-field--2026-06-11)
35. [The scale canon — the theming audit (2026-06-15)](#35-the-scale-canon--the-theming-audit-2026-06-15)
36. [The canonical trigger→panel gap — a token-driven offset (2026-06-22)](#36-the-canonical-triggerpanel-gap--a-token-driven-offset-2026-06-22)
37. [Touch-target — 44px on touch, pointer-gated (2026-06-28)](#37-touch-target--44px-on-touch-pointer-gated-2026-06-28)
38. [The state layer — unified neutral feedback (2026-06-28)](#38-the-state-layer--unified-neutral-feedback-2026-06-28)
---
## 1. Mental model
Eidos is UIX's **visual layer**. It owns NO behavior and NO state. It reads
from the DOM what the previous layers wrote, and applies styles.
```
Morfo declares the genetics (which attrs / events / parts exist)
↓
Soma transcribes behavior (data-state, data-color, aria-*, focus, …)
↓
Sema emits signals (data-event-* during the perceptual hold)
↓
Eidos applies the visual (tokens, themes, recipes, archetypes, motion)
```
**What Eidos owns**:
- The `--*` custom-property namespace.
- The entrypoint's CSS layers (§2: generated foundation, archetypes,
events, recipes) + the theme blocks `ActiveEidos` injects.
- The `ActiveEidos` runtime that injects foundation + theme CSS.
- Tooling: generation, validation, purge, lint.
**What Eidos does NOT own**:
- Components' logical state (that's soma).
- The definition of which events exist (that's morfo).
- Firing perceptual signals (that's sema).
**The 2-of-3 rule**: a system extension (an attribute, a token, a
convention) is only justified when **at least two of the three layers**
(soma, sema, eidos) consume it. The extensions that entered with eidos's
vote: `archetype`, `events[].semantic.{family,intent}`,
`events[].prewrite[]`, `data-starting-style` / `data-ending-style`.
---
## 1.bis Theming lives in Eidos, not in Morfo (by design)
> **This is the most frequent architectural question — and the most
> important answer for not breaking the system.**
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
A new dev's reasonable intuition is: _"if morfo is the cross-layer source
of truth, visual tokens should live in morfo too"_. **NO.** UIX's explicit
design says the opposite. This section exists to close the case with
citations, before the confusion drags a PR into violating the architecture.
### The two canonical quotes in the repo
**[`architecture/active-architecture.md`](../architecture/active-architecture.md) §9 (What this architecture is NOT)**:
> **Not a classic design system.** Tokens, themes and recipes belong to
> Eidos, not to the core.
**[`architecture/overview.md`](../architecture/overview.md) §2 (Eidos)**:
> The visual layer: **tokens, themes, per-component CSS recipes**, archetype
> rules, event reactions and Svelte wrappers over soma's headless
> providers…
>
> Eidos reads from the DOM what the other layers write — **it never imports
> soma or sema internals**.
Those two sentences, by themselves, close any debate about where theming
lives. If a future proposal contradicts them, the proposal must be rejected
or the canonical doc must be updated first — not after.
### The 2-of-3 rule derives it mechanically
**`active-architecture.md` §7 #12** and **`overview.md` §5** say the same
thing:
> A morfo extension is only justified when **at least two of the three
> layers** (soma, sema, eidos) consume it.
Applied to theming:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Who consumes the visual tokens? | |
| ------------------------------- | ---------- |
| Soma (the behavior runtime) | ❌ no |
| Sema (perceptual signals) | ❌ no |
| Eidos (the visual layer) | ✅ yes |
| **Count** | **1-of-3** |
**1-of-3 ≠ 2-of-3 → tokens do NOT go in morfo, by rule**. The visual
integration falls into eidos automatically through the 2-of-3 discipline,
with nobody having to decide it case by case.
### What is the morfo↔theming relationship, then?
Morfo is the source of truth of the **cross-layer contract**:
- Parts (which parts exist)
- Events (which events it may fire)
- Attrs and their enumerated values (which attrs appear in the DOM, with
which values)
- Archetypes (the transversal classification)
- Declarative states
**Theming integrates with morfo in ONE PRECISE SENSE**: eidos recipes
target DOM attrs that morfo declares. Without morfo, the attrs wouldn't
exist in the DOM and the eidos selectors would be dead.
```
MORFO declares data-color.values = ['primary', 'affirm', 'threat', ...]
↓
SOMA emits <button data-color="affirm"> (to the DOM)
↓
EIDOS recipe targets scope: 'color:affirm' → [data-toggle][data-color='affirm']
↓
BROWSER cascade resolves the CSS rule
```
The channel between morfo and eidos is **the DOM**, not TypeScript objects.
This is critical and armored by hard rule #6:
**`active-architecture.md` §7 #6**:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Eidos consumes DOM and data-\*, not Soma/Sema internals.** If it needs
> something, it must be declared in morfo or emitted in a sema signal.
**`overview.md` §5** repeats it verbatim.
### What eidos must NEVER do
```ts
// ❌ ARCHITECTURAL VIOLATION — Eidos importing morfo at runtime
import { toggleMorfo } from '$uix/morfo/components/toggle';
const validValues = toggleMorfo.parts
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
.find((p) => p.kebab === 'provider')!
.data.find((d) => d.attr === 'data-color')!.values;
// using validValues to validate TSC scope:'color:X'
```
Even with good intent (validating that `scope: 'color:affirm'` matches a
morfo-declared value), **this import violates rule #6** and breaks the
morfo↔eidos boundary. If you want that validation, the correct defense is
eidos-lint at the DOM/CSS level, not TS coupling.
### The correct defense: eidos-lint at the DOM/CSS level
Validating that eidos recipes target values morfo declares IS DONE, but at
the DOM/CSS level:
```bash
node scripts/eidos-lint.ts toggle
```
It classifies every `[data-*]` selector as:
- **morfo-backed** — declared in morfo; soma emits it with a valid value
- **eidos-only** — an attr added by the wrapper (data-variant, data-size)
- **invalid** — references a morfo-backed attr with a value outside the
enum → a bug
This closes the loop architecturally without cross-layer imports.
### Recap of the canonical split
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Concept | Source of truth | Justification |
| ------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------- |
| Parts (which parts exist) | **Morfo** | Cross-layer: soma emits, eidos selects, sema references |
| Events + semantic | **Morfo** | Cross-layer: soma triggers, sema dispatches, eidos reacts |
| Archetypes | **Morfo** | Cross-layer: soma emits, sema cascades, eidos selects |
| Attr values (`data-color.values`) | **Morfo** | Cross-layer: soma validates, eidos targets, sema references |
| Declarative states | **Morfo** | Cross-layer: soma emits data-state, eidos selects |
| **The 9 systemic canonical roles** | **Eidos** (`lib/themes/base.ts`) | Only eidos materializes them |
| **The 7 canonical sizes** | **Eidos** (`lib/config-types.ts`) | Only eidos coordinates them |
| **Visual variants** (solid/outline/ghost) | **The eidos wrapper** | Only eidos renders them |
| **Tokens** (`--toggle-solid-on-bg`) | **Eidos** (`lib/recipes/base.ts`) | Only eidos consumes them |
| **Themes** (light/dark/custom) | **Eidos** (`lib/themes/`) | Only eidos composes them |
| **Theme persistence** | **Eidos** (`toDocument()`) | Only eidos serializes it |
| **TSC scope axes** (color, state, variant, size, event) | **Eidos** (they refer to morfo-emitted attrs) | Hardcoded in the TSC because they are axes of the DOM contract |
### The canonical sentence
> **Morfo declares the contract. Eidos declares the theming. The DOM
> connects them.**
This is NOT a compromise. It IS the design. Morfo's purity (declarative TS,
no runtime, no visual-layer imports) DEPENDS on theming living outside.
### Future proposals that MUST be rejected citing this section
1. **"Let's put the Toggle's visual tokens in its morfo so morfo is the
source of truth of everything"** — violates §9 and the 2-of-3 rule.
2. **"Let's make the TSC validate `color:affirm` by importing
`toggleMorfo.data['data-color'].values`"** — violates rule #6 (eidos
does not import morfo internals in TS).
3. **"Let's put `variant: 'solid' | 'outline'` in the Toggle's morfo"** —
violates 2-of-3 (variants are consumed by eidos alone).
4. **"Let's define `size` in the morfo with its 6 canonical values"** —
violates 2-of-3 (the 6 sizes are the visual system; only eidos
materializes them with coordinated tokens).
If the proposal has merit, the right move is to **update the canonical doc
(`active-architecture.md` §9) FIRST** — never afterwards.
### What SHOULD enter morfo regarding theming
- A component exposing `color` as a prop → must declare
`data-color.values: ['primary', 'affirm', ...]` in its morfo. Those
values are cross-layer (eidos targets, soma emits, sema could reference).
- A component exposing `state` (open/closed) → declares
`data-state.values: ['open', 'closed']`. Same.
- A component adding purely visual attrs (`data-variant`, `data-size`) that
NOBODY else needs → they do **NOT** go in morfo; the eidos wrapper adds
them directly.
### Consistency with the canonical docs
This section **introduces no new doctrine**. It gathers and consolidates
what was scattered across:
- [`architecture/active-architecture.md`](../architecture/active-architecture.md)
§3 (Morfo = the single cross-layer articulation point), §7 #6 (Eidos
imports no internals), §7 #12 (the 2-of-3 rule), §9 (tokens belong to
Eidos).
- [`architecture/overview.md`](../architecture/overview.md) §2 (Eidos = the
visual layer with tokens), §4 (not a classic design system), §5 (Eidos
consumes DOM and data-\*).
- This same reference, §1 (Mental model) and §7 (TSC).
If any of those canonical docs contradicts this section, **the canonical
doc wins**. This section consolidates; it doesn't decide.
---
## 2. The layers of Eidos's CSS
`src/uix/eidos/index.css` is the entrypoint (the source of truth for the
order is the file itself). It imports, in order:
```
1. generated/base.css ← foundation + recipe tokens + @font-face (generated)
2. archetypes.css ← transversal rules per data-archetype
3. events.css ← reactions to data-event-* (sema)
4. components/{c}/{c}.css ← aggregated recipes: layout primitives + spin-field
```
Outside the entrypoint but part of the visual layer:
- **Code-split recipes**: most components are NOT in `index.css` — each
`.svelte` imports its own CSS and Vite emits a per-component chunk. The
ABSENCE of an `@import` is deliberate; re-adding it would double-load.
- **Shared partials** (`lib/menu-indicator.css`): imported by the component
that uses them, not by the entrypoint.
- **Themes**: CSS blocks injected at runtime by `ActiveEidos`
(`uix-eidos-theme`), not a static `@import`. `themes/fonts.css` is
superseded — the `@font-face` live in `EidosConfig` and come out in
`generated/base.css`.
### Why this order matters
- `generated/base.css` declares tokens (it doesn't style). If recipes
loaded first, the tokens wouldn't be available.
- `archetypes.css` sets the interactive baseline (cursor, hover, focus
ring). Specific recipes override.
- `events.css` reacts to `data-event-*` with `animation: @keyframes` (not
`transition`) because signals are transient and the animation must
complete independently of the signal's lifetime.
- Specific recipes come last → higher cascade priority in equal-specificity
conflicts.
### What each layer concretely does
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Layer | Kind | Purpose |
| ------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `generated/base.css` | `:root` + some `[data-{c}]` blocks + `@font-face` | Foundation tokens (scale, primitive, color, size, density, typography, recipe tokens) + fonts (config-driven) |
| `archetypes.css` | `[data-archetype='X']` selectors | Transversal baseline styling per archetype (the inventory is `ARCHETYPE_VOCABULARY`, morfo/types.ts) |
| `events.css` | global `[data-event-*]` hints | The compositor hint + the reduced-motion cap (the signatures live in `EidosConfig.motion` — see [motion.md](./motion.md) §15) |
| `components/{c}/{c}.css` | `[data-{c}-*]` selectors | The component's own recipe (aggregated or code-split; partials like `lib/menu-indicator.css` are imported by their consumer) |
### Rules for touching each layer
- **`generated/base.css`**: **NEVER hand-edit**. It is generator output. To
change it, edit `lib/recipes/base.ts` or `lib/themes/base.ts` and run
`npm run generate:eidos-css`.
- **`archetypes.css`**: add entries only when the archetype is declared in
some morfo. Low-specificity rules (one attribute).
- **`events.css`**: add reactions only for signals sema emits. Use
`animation: @keyframes`, NOT `transition`. Read `data-event-intent`
(signal-bound), NEVER `data-intent` (state-bound).
- **`components/{c}/{c}.css`**: owned by whoever maintains the component.
Follows the naming convention (§6).
---
## 3. The 7 token layers
Eidos composes an element's final color by crossing 7 levels of
indirection. Each level serves a distinct purpose:
```
┌─ Layer 1: --scale-{name}-{step} :root (stable)
│ Radix physical scales (12 steps + alpha): --scale-teal-9 = #12a594
│
├─ Layer 2: --primitive-{role}-{step} :root (stable)
│ Role → scale mapping: --primitive-affirm-9 = var(--scale-teal-9)
│
├─ Layer 3: --color-{role}-{slot} :root (stable)
│ Semantic slot: --color-affirm-solid = var(--primitive-affirm-9)
│
├─ Layer 4: --{component}-{role}-{slot} :root (stable)
│ Per-component alias: --button-affirm-solid = var(--color-affirm-solid)
│ (NOTE: the "color-" segment was dropped on 2026-05-27)
│
├─ Layer 5: --{component}-palette-{slot} [data-{c}] (DYNAMIC)
│ Per-instance dynamic palette: changes with data-color
│
├─ Layer 6: --{component}-{variant}-{slot} [data-{c}] (host) (DYNAMIC)
│ The variant × palette combination
│
└─ Layer 7: --_{component}-{slot} [data-{c}] (private)
Private token consumed directly by the recipe CSS
```
**Scope rules**:
- Layers 1-4 are constant → `:root`.
- Layer 5 changes per instance → `[data-{c}]` and
`[data-{c}][data-color='X']`.
- Layer 6 depends on 5 → it MUST live at `[data-{c}]` (the TSC enforces
it).
- Layer 7 is private → always at `[data-{c}]`.
### Why so many layers
**Not accidental.** Each hop serves an extension point:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Layer | Overriding it allows | Usage example |
| ----- | ----------------------------------- | --------------------------------------------------- |
| 1 | Changing the Radix physical scale | The brand wants its own teal |
| 2 | Changing which scale a role maps to | "Affirm" uses green instead of teal |
| 3 | Changing the per-role slot mapping | Affirm's "solid" uses step 10 instead of 9 |
| 4 | Changing a component-specific token | Toggle wants its affirm distinct from the global |
| 5 | The per-instance runtime | `<Toggle color="affirm" />` swaps the palette |
| 6 | Combining variant × color | The toggle's solid variant with the affirm color |
| 7 | Recipe-internal | The recipe decides which internal token serves what |
In practice, **most apps ONLY touch layers 1-3** (brand customization).
Layers 4-7 belong to the component catalog.
### When to create a new token at each layer
- **Layer 1** (scale): an app rarely; a **brand theme** DOES bring or
extend its own palette (§25.7). The default **33 scales** cover the
general case.
- **Layer 2** (primitive): rarely. Only if you add a new canonical role
(which would change the book canon — don't).
- **Layer 3** (color): if you add a new `{slot}` (rare). The canonical slot
inventory and its default step live in the code — **single source**:
`COLOR_ROLE_SLOTS` (`lib/config-types.ts`, with each slot's rationale in
its JSDoc) + `DEFAULT_COLOR_ROLE_SLOT_STEPS` (`lib/render-css.ts`). The
list is not copied here: it already drifted twice (border-hover retired;
bg2/separator/text-strong added).
- **Layer 4** (component-color): when adding color support to a new
component. Generated automatically by `lib/recipes/base.ts`.
- **Layer 5** (palette): when the component accepts a `data-color` prop and
needs a dynamic palette. TSC `scope: 'host'` + `scope: 'color:X'`
overrides.
- **Layer 6** (variant): when a variant (`solid`, `outline`, etc.) combines
the palette + something specific. TSC `scope: 'host'`.
- **Layer 7** (private): the recipe consumes it. Convention: the `_`
prefix.
---
## 4. The 9 canonical color roles
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
The roles come from the book _Diseñando lo que ocurre_. THEY ARE CANON. Do
NOT invent new ones.
```
HIERARCHY (no evaluative) INTENT (evaluative)
───────────────────────────── ───────────────────────────
primary — brand main affirm — turning ON something positive
secondary — brand support fulfill — completion / success
tertiary — brand tertiary risk — moderate negative consequence
neutral — gray default threat — active negative consequence
loss — irreversible negative outcome
```
**Strict rules**:
- The 9 names are the only valid ones. Do NOT use `success`, `warning`,
`danger`, `info` — those belong to other models (Bootstrap, etc.).
- **`primary`/`secondary`/`tertiary`** are **hierarchical**: use them when
the difference is "more vs less prominent". No evaluative load.
- **`neutral`** is the default. No semantic load.
- **`affirm`/`fulfill`/`risk`/`threat`/`loss`** are **evaluative**: they
communicate what happens with the action.
- If `intent === 'neutral'`, `color` (the hierarchy override) may apply. If
`intent` is evaluative, the `intent` WINS and `color` is ignored.
**Mapping to physical scales** (in the base theme): the standing assignment
lives in the code — **single source**: `THEME_BASE_COLOR_ROLES`
(`lib/themes/base.ts`), with each choice's rationale in its comments (e.g.
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara `ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica: ni el commit (fb5dd0919, «Advance Eidos demos and form validation docs»), ni un comentario, ni un documento. No fue una decisión de excluirlo: fue una omisión, y el 2026-06-27 se fosilizó al añadir HierarchyColorRole AL LADO en vez de arreglarla. Qué fichero importara de dónde decidía si tertiary era legal. La deriva era SÓLO de tipos: el runtime nunca divergió. resolveComponentColor une COLOR_ROLES + PALETTE_SCALES (42), la capa compartida emite la fila [data-color='tertiary'] iterando COLOR_ROLES, y 94 ficheros pasan por ese resolvedor. O sea que el tipo rechazaba en compilación un valor que el motor resuelve. Por eso ensanchar 8 -> 9 es aditivo: check queda en la línea base exacta (73 errores / 53 warnings) y los 362 tests de eidos pasan. - eidos/lib/types.ts re-exporta ambos tipos de config-types, donde derivan del const. El docblock deja escrito el porqué para que no vuelva por ignorancia. - Guard nuevo en recipe-css-contract.test.ts: falla si un fichero de eidos/lib vuelve a deletrear >=2 nombres de rol como literales. Verificado con control negativo — caza el texto del bug original y el HierarchyColorRole a mano; permite el Extract<> legítimo, el re-export y uniones ajenas al color. Documentación del color, reconciliada contra el código: - reference.md §4 «Per-component subset» seguía enseñando los subconjuntos por componente que §25 derogó el 2026-07-18, con tabla y justificación, sin marca. Marcada REVOKED; conserva el arbitraje (intent evaluativo gana), que es lo que sigue vivo. - `tertiary` figuraba como RESERVADO y no consumido en dos sitios (reference §4 y el comentario de themes/base.ts) cuando entra en ComponentColor, lo acepta todo prop color abierto y tiene su fila en la capa compartida. - rfc-color-engine §13 decía «the 9 --color-{role}-{slot} slots»; son 12 (COLOR_ROLE_SLOTS). Ahora enlaza el const en vez de copiar el número. - El frontmatter del mismo RFC daba la fase 5 por «gated» cuando su `border` 6->7 aterrizó el 2026-06-05 (reference §28); y §9 leía como lista de defectos vivos cuando sus cuatro filas están hechas. - reference §40 se contradecía en una línea: llamaba a CONTRAST_PAIRS fuente compartida «con el Stage-2 solver» y 40 líneas después declaraba que Stage 2 cerró SIN solver. - 8 citas a «THEMING §25.5» apuntadas a §25: esa numeración sobrevivió en la crónica (changelog §25.5) pero no en la referencia tras la escisión. - JSDoc de avatar y card documentaba su prop como «Canonical ColorRole» (8) cuando el tipo real es ComponentColorProp (42 + CSS crudo) — justo lo que lee un consumidor. - «8 roles» hardcodeado en callout/README.md (error de docs:check), callout.svelte y mark/README.md: enlazan el const, como manda authoring.md. docs:check baja de 2 errores a 1. Y una anotación, no un arreglo: banner/types.ts declara `BannerIntent = ColorRole`, fundiendo el eje evaluativo con el de pintura (primary/secondary no son evaluativos), y Banner no tiene prop `color` en absoluto. Queda marcado en el docblock como decisión pendiente con sus citas — resolverlo es el otro frente (un solo eje de color arbitrado por intent), y no está decidido. Fuera de este commit: src/uix/blocks/banner/ es un directorio entero sin trackear (trabajo en vuelo), y ahí quedan las dos últimas instancias del conteo hardcodeado, incluido el error que le queda a docs:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`tertiary: 'indigo'` is the saturated third hierarchy accent, in a hue band
no intent occupies; `loss: 'plum'` to avoid colliding with
`primary: 'purple'`). This
table was copied here twice and diverged both times (primary, risk) — hence
it is now a pointer.
> **Convention ≠ authorship.** `CANONICAL_INTENT_SCALES`
> (`lib/config-types.ts`) is the book's **convention** for auto-deriving
> intents from a palette (identity = step 9; e.g. `risk→amber`). The **base
> theme** is authorship and may deviate (e.g. `risk: 'orange'`). The
> hierarchy (`primary`/`secondary`/`tertiary`) is always the theme's
> choice. The complete model is **§25**.
The **palette** is **33 scales** of 12 steps + 12 alpha = 24 tokens each
(**792 `--scale-*` tokens** — the bulk of the foundation's bloat). On top of
it, the 9 roles alias via `--primitive-{role}-{step}` (9 × 24 = **216
primitives**).
### Why 9 roles and not 4 (like shadcn) or 14 (like Mantine)
The 9 are the result of the book's perceptual analysis:
- 3 hierarchy roles cover the "visual prominence" dimension.
- 1 neutral covers the unloaded default.
- 5 intent roles cover the five distinct evaluative valences.
Any system with fewer loses perceptual resolution. Any system with more
falls into redundancy (success vs fulfill, danger vs threat — they are not
the same).
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara `ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica: ni el commit (fb5dd0919, «Advance Eidos demos and form validation docs»), ni un comentario, ni un documento. No fue una decisión de excluirlo: fue una omisión, y el 2026-06-27 se fosilizó al añadir HierarchyColorRole AL LADO en vez de arreglarla. Qué fichero importara de dónde decidía si tertiary era legal. La deriva era SÓLO de tipos: el runtime nunca divergió. resolveComponentColor une COLOR_ROLES + PALETTE_SCALES (42), la capa compartida emite la fila [data-color='tertiary'] iterando COLOR_ROLES, y 94 ficheros pasan por ese resolvedor. O sea que el tipo rechazaba en compilación un valor que el motor resuelve. Por eso ensanchar 8 -> 9 es aditivo: check queda en la línea base exacta (73 errores / 53 warnings) y los 362 tests de eidos pasan. - eidos/lib/types.ts re-exporta ambos tipos de config-types, donde derivan del const. El docblock deja escrito el porqué para que no vuelva por ignorancia. - Guard nuevo en recipe-css-contract.test.ts: falla si un fichero de eidos/lib vuelve a deletrear >=2 nombres de rol como literales. Verificado con control negativo — caza el texto del bug original y el HierarchyColorRole a mano; permite el Extract<> legítimo, el re-export y uniones ajenas al color. Documentación del color, reconciliada contra el código: - reference.md §4 «Per-component subset» seguía enseñando los subconjuntos por componente que §25 derogó el 2026-07-18, con tabla y justificación, sin marca. Marcada REVOKED; conserva el arbitraje (intent evaluativo gana), que es lo que sigue vivo. - `tertiary` figuraba como RESERVADO y no consumido en dos sitios (reference §4 y el comentario de themes/base.ts) cuando entra en ComponentColor, lo acepta todo prop color abierto y tiene su fila en la capa compartida. - rfc-color-engine §13 decía «the 9 --color-{role}-{slot} slots»; son 12 (COLOR_ROLE_SLOTS). Ahora enlaza el const en vez de copiar el número. - El frontmatter del mismo RFC daba la fase 5 por «gated» cuando su `border` 6->7 aterrizó el 2026-06-05 (reference §28); y §9 leía como lista de defectos vivos cuando sus cuatro filas están hechas. - reference §40 se contradecía en una línea: llamaba a CONTRAST_PAIRS fuente compartida «con el Stage-2 solver» y 40 líneas después declaraba que Stage 2 cerró SIN solver. - 8 citas a «THEMING §25.5» apuntadas a §25: esa numeración sobrevivió en la crónica (changelog §25.5) pero no en la referencia tras la escisión. - JSDoc de avatar y card documentaba su prop como «Canonical ColorRole» (8) cuando el tipo real es ComponentColorProp (42 + CSS crudo) — justo lo que lee un consumidor. - «8 roles» hardcodeado en callout/README.md (error de docs:check), callout.svelte y mark/README.md: enlazan el const, como manda authoring.md. docs:check baja de 2 errores a 1. Y una anotación, no un arreglo: banner/types.ts declara `BannerIntent = ColorRole`, fundiendo el eje evaluativo con el de pintura (primary/secondary no son evaluativos), y Banner no tiene prop `color` en absoluto. Queda marcado en el docblock como decisión pendiente con sus citas — resolverlo es el otro frente (un solo eje de color arbitrado por intent), y no está decidido. Fuera de este commit: src/uix/blocks/banner/ es un directorio entero sin trackear (trabajo en vuelo), y ahí quedan las dos últimas instancias del conteo hardcodeado, incluido el error que le queda a docs:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Per-component subset — REVOKED (2026-07-18)
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara `ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica: ni el commit (fb5dd0919, «Advance Eidos demos and form validation docs»), ni un comentario, ni un documento. No fue una decisión de excluirlo: fue una omisión, y el 2026-06-27 se fosilizó al añadir HierarchyColorRole AL LADO en vez de arreglarla. Qué fichero importara de dónde decidía si tertiary era legal. La deriva era SÓLO de tipos: el runtime nunca divergió. resolveComponentColor une COLOR_ROLES + PALETTE_SCALES (42), la capa compartida emite la fila [data-color='tertiary'] iterando COLOR_ROLES, y 94 ficheros pasan por ese resolvedor. O sea que el tipo rechazaba en compilación un valor que el motor resuelve. Por eso ensanchar 8 -> 9 es aditivo: check queda en la línea base exacta (73 errores / 53 warnings) y los 362 tests de eidos pasan. - eidos/lib/types.ts re-exporta ambos tipos de config-types, donde derivan del const. El docblock deja escrito el porqué para que no vuelva por ignorancia. - Guard nuevo en recipe-css-contract.test.ts: falla si un fichero de eidos/lib vuelve a deletrear >=2 nombres de rol como literales. Verificado con control negativo — caza el texto del bug original y el HierarchyColorRole a mano; permite el Extract<> legítimo, el re-export y uniones ajenas al color. Documentación del color, reconciliada contra el código: - reference.md §4 «Per-component subset» seguía enseñando los subconjuntos por componente que §25 derogó el 2026-07-18, con tabla y justificación, sin marca. Marcada REVOKED; conserva el arbitraje (intent evaluativo gana), que es lo que sigue vivo. - `tertiary` figuraba como RESERVADO y no consumido en dos sitios (reference §4 y el comentario de themes/base.ts) cuando entra en ComponentColor, lo acepta todo prop color abierto y tiene su fila en la capa compartida. - rfc-color-engine §13 decía «the 9 --color-{role}-{slot} slots»; son 12 (COLOR_ROLE_SLOTS). Ahora enlaza el const en vez de copiar el número. - El frontmatter del mismo RFC daba la fase 5 por «gated» cuando su `border` 6->7 aterrizó el 2026-06-05 (reference §28); y §9 leía como lista de defectos vivos cuando sus cuatro filas están hechas. - reference §40 se contradecía en una línea: llamaba a CONTRAST_PAIRS fuente compartida «con el Stage-2 solver» y 40 líneas después declaraba que Stage 2 cerró SIN solver. - 8 citas a «THEMING §25.5» apuntadas a §25: esa numeración sobrevivió en la crónica (changelog §25.5) pero no en la referencia tras la escisión. - JSDoc de avatar y card documentaba su prop como «Canonical ColorRole» (8) cuando el tipo real es ComponentColorProp (42 + CSS crudo) — justo lo que lee un consumidor. - «8 roles» hardcodeado en callout/README.md (error de docs:check), callout.svelte y mark/README.md: enlazan el const, como manda authoring.md. docs:check baja de 2 errores a 1. Y una anotación, no un arreglo: banner/types.ts declara `BannerIntent = ColorRole`, fundiendo el eje evaluativo con el de pintura (primary/secondary no son evaluativos), y Banner no tiene prop `color` en absoluto. Queda marcado en el docblock como decisión pendiente con sus citas — resolverlo es el otro frente (un solo eje de color arbitrado por intent), y no está decidido. Fuera de este commit: src/uix/blocks/banner/ es un directorio entero sin trackear (trabajo en vuelo), y ahí quedan las dos últimas instancias del conteo hardcodeado, incluido el error que le queda a docs:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **There are no per-component subsets.** `color` accepts the FULL system on
> **every** component — role / intent / 33 donor scales / raw CSS value
> (`ComponentColorProp`) — per the design decision in **§25, "Reversión de los
> subconjuntos"**. This section used to publish a subset table (Toggle
> excluding fulfill/loss, Badge excluding tertiary) and argue that exposing
> them "would be semantically wrong". That argument is precisely what §25
> revoked, and the guard *"keeps every component `*Color` prop open"*
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
(`recipe-css-contract.test.ts`) now breaks the build on any narrowing.
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara `ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica: ni el commit (fb5dd0919, «Advance Eidos demos and form validation docs»), ni un comentario, ni un documento. No fue una decisión de excluirlo: fue una omisión, y el 2026-06-27 se fosilizó al añadir HierarchyColorRole AL LADO en vez de arreglarla. Qué fichero importara de dónde decidía si tertiary era legal. La deriva era SÓLO de tipos: el runtime nunca divergió. resolveComponentColor une COLOR_ROLES + PALETTE_SCALES (42), la capa compartida emite la fila [data-color='tertiary'] iterando COLOR_ROLES, y 94 ficheros pasan por ese resolvedor. O sea que el tipo rechazaba en compilación un valor que el motor resuelve. Por eso ensanchar 8 -> 9 es aditivo: check queda en la línea base exacta (73 errores / 53 warnings) y los 362 tests de eidos pasan. - eidos/lib/types.ts re-exporta ambos tipos de config-types, donde derivan del const. El docblock deja escrito el porqué para que no vuelva por ignorancia. - Guard nuevo en recipe-css-contract.test.ts: falla si un fichero de eidos/lib vuelve a deletrear >=2 nombres de rol como literales. Verificado con control negativo — caza el texto del bug original y el HierarchyColorRole a mano; permite el Extract<> legítimo, el re-export y uniones ajenas al color. Documentación del color, reconciliada contra el código: - reference.md §4 «Per-component subset» seguía enseñando los subconjuntos por componente que §25 derogó el 2026-07-18, con tabla y justificación, sin marca. Marcada REVOKED; conserva el arbitraje (intent evaluativo gana), que es lo que sigue vivo. - `tertiary` figuraba como RESERVADO y no consumido en dos sitios (reference §4 y el comentario de themes/base.ts) cuando entra en ComponentColor, lo acepta todo prop color abierto y tiene su fila en la capa compartida. - rfc-color-engine §13 decía «the 9 --color-{role}-{slot} slots»; son 12 (COLOR_ROLE_SLOTS). Ahora enlaza el const en vez de copiar el número. - El frontmatter del mismo RFC daba la fase 5 por «gated» cuando su `border` 6->7 aterrizó el 2026-06-05 (reference §28); y §9 leía como lista de defectos vivos cuando sus cuatro filas están hechas. - reference §40 se contradecía en una línea: llamaba a CONTRAST_PAIRS fuente compartida «con el Stage-2 solver» y 40 líneas después declaraba que Stage 2 cerró SIN solver. - 8 citas a «THEMING §25.5» apuntadas a §25: esa numeración sobrevivió en la crónica (changelog §25.5) pero no en la referencia tras la escisión. - JSDoc de avatar y card documentaba su prop como «Canonical ColorRole» (8) cuando el tipo real es ComponentColorProp (42 + CSS crudo) — justo lo que lee un consumidor. - «8 roles» hardcodeado en callout/README.md (error de docs:check), callout.svelte y mark/README.md: enlazan el const, como manda authoring.md. docs:check baja de 2 errores a 1. Y una anotación, no un arreglo: banner/types.ts declara `BannerIntent = ColorRole`, fundiendo el eje evaluativo con el de pintura (primary/secondary no son evaluativos), y Banner no tiene prop `color` en absoluto. Queda marcado en el docblock como decisión pendiente con sus citas — resolverlo es el otro frente (un solo eje de color arbitrado por intent), y no está decidido. Fuera de este commit: src/uix/blocks/banner/ es un directorio entero sin trackear (trabajo en vuelo), y ahí quedan las dos últimas instancias del conteo hardcodeado, incluido el error que le queda a docs:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
What survives is the **arbitration**, not the narrowing: identity (`color`)
stays decoupled from evaluation, so an evaluative `intent` still wins over
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`color` (the rule above). A component does not decide _which_ colors exist for
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara `ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica: ni el commit (fb5dd0919, «Advance Eidos demos and form validation docs»), ni un comentario, ni un documento. No fue una decisión de excluirlo: fue una omisión, y el 2026-06-27 se fosilizó al añadir HierarchyColorRole AL LADO en vez de arreglarla. Qué fichero importara de dónde decidía si tertiary era legal. La deriva era SÓLO de tipos: el runtime nunca divergió. resolveComponentColor une COLOR_ROLES + PALETTE_SCALES (42), la capa compartida emite la fila [data-color='tertiary'] iterando COLOR_ROLES, y 94 ficheros pasan por ese resolvedor. O sea que el tipo rechazaba en compilación un valor que el motor resuelve. Por eso ensanchar 8 -> 9 es aditivo: check queda en la línea base exacta (73 errores / 53 warnings) y los 362 tests de eidos pasan. - eidos/lib/types.ts re-exporta ambos tipos de config-types, donde derivan del const. El docblock deja escrito el porqué para que no vuelva por ignorancia. - Guard nuevo en recipe-css-contract.test.ts: falla si un fichero de eidos/lib vuelve a deletrear >=2 nombres de rol como literales. Verificado con control negativo — caza el texto del bug original y el HierarchyColorRole a mano; permite el Extract<> legítimo, el re-export y uniones ajenas al color. Documentación del color, reconciliada contra el código: - reference.md §4 «Per-component subset» seguía enseñando los subconjuntos por componente que §25 derogó el 2026-07-18, con tabla y justificación, sin marca. Marcada REVOKED; conserva el arbitraje (intent evaluativo gana), que es lo que sigue vivo. - `tertiary` figuraba como RESERVADO y no consumido en dos sitios (reference §4 y el comentario de themes/base.ts) cuando entra en ComponentColor, lo acepta todo prop color abierto y tiene su fila en la capa compartida. - rfc-color-engine §13 decía «the 9 --color-{role}-{slot} slots»; son 12 (COLOR_ROLE_SLOTS). Ahora enlaza el const en vez de copiar el número. - El frontmatter del mismo RFC daba la fase 5 por «gated» cuando su `border` 6->7 aterrizó el 2026-06-05 (reference §28); y §9 leía como lista de defectos vivos cuando sus cuatro filas están hechas. - reference §40 se contradecía en una línea: llamaba a CONTRAST_PAIRS fuente compartida «con el Stage-2 solver» y 40 líneas después declaraba que Stage 2 cerró SIN solver. - 8 citas a «THEMING §25.5» apuntadas a §25: esa numeración sobrevivió en la crónica (changelog §25.5) pero no en la referencia tras la escisión. - JSDoc de avatar y card documentaba su prop como «Canonical ColorRole» (8) cuando el tipo real es ComponentColorProp (42 + CSS crudo) — justo lo que lee un consumidor. - «8 roles» hardcodeado en callout/README.md (error de docs:check), callout.svelte y mark/README.md: enlazan el const, como manda authoring.md. docs:check baja de 2 errores a 1. Y una anotación, no un arreglo: banner/types.ts declara `BannerIntent = ColorRole`, fundiendo el eje evaluativo con el de pintura (primary/secondary no son evaluativos), y Banner no tiene prop `color` en absoluto. Queda marcado en el docblock como decisión pendiente con sus citas — resolverlo es el otro frente (un solo eje de color arbitrado por intent), y no está decidido. Fuera de este commit: src/uix/blocks/banner/ es un directorio entero sin trackear (trabajo en vuelo), y ahí quedan las dos últimas instancias del conteo hardcodeado, incluido el error que le queda a docs:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
it; it decides nothing — the system is open and the intent arbitrates.
---
## 5. The size canon
```
xxs · xs · sm · md · lg · xl · xxl | full
───────────────────────────────────── ───────
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
7 canonical sizes (physical) 1 layout size
```
`md` is the default. `full` is not physical — it is layout semantics
(`100%` / `100vw` / `100dvh` depending on context). It generates no fixed
tokens.
Each canonical size generates coordinated tokens:
```
--size-md-control-height: 36px
--size-md-font-size: 16px /* = var(--font-size-md), 1:1. Bundle IN ADOPTION (Phase-D decision 2026-07-02; pilot: toggle) */
--size-md-font-line-height: 1.45
--size-md-icon-size: 18px /* = --icon-size-md */
--size-md-padding-inline: 12px
--size-md-padding-block: 8px
--size-md-gap: 8px
--size-md-radius: 6px
```
> **There is only one `md`** (1:1 — the 2026-06-17 override, below): the
> control bundle's `--size-md-font-size` = `var(--font-size-md)` = 16px,
> identical to the typographic scale. The earlier "two `md`s" doctrine
> (compact 14px control vs 16px body) was **revoked** — control text
> follows the typographic scale 1:1.
### The key rule: `md` does NOT change per viewport
Responsiveness decides **which active size is used**; it does NOT redefine
the tokens. If your Toggle uses `sm` on mobile and `md` on desktop, both
tokens are available and the wrapper picks one.
```svelte
<!-- Correct -->
<Toggle size={{ base: 'sm', md: 'md' }} />
<!-- Incorrect -->
@media (max-width: 768px) {
:root { --toggle-height-md: 32px; } /* do NOT redefine a size's token per viewport */
}
```
### Per-component subset
As with color, each component exposes the size subset its recipe supports.
Categories:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Category | Subset | Examples |
| --------------------------- | -------- | ---------------------------------------------------- |
| Form controls + text inputs | `xs..xl` | input, select, switch, slider, checkbox |
| Nav controls | `xs..lg` | breadcrumb, pagination, tag-group, toolbar |
| Composed panels | `sm..lg` | calendar, date-picker, file-upload, stepper, tooltip |
The declaration itself is each component's recipe + types (demos mirror it
1:1 — the parity rule is
[`demo-authoring.md`](../guides/demo-authoring.md) §6).
### Container→part derivation: capped at `md` (2026-06-19 norm)
When a part derives its `size` from its **container** (e.g. `Dialog.Close`
inherits the dialog's size), the part **follows the container's size only
on the steps below `md`**; at `md` and above (`lg` / `xl` / `full`) it
**caps at the normal `md` density**. A wider container — or `full` — does
NOT fatten its controls: `full` is layout (it fills the viewport), not a
bigger control size.
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| container size | part size |
| --------------------------- | ------------------------------ |
| `xs` / `sm` | `xs` / `sm` (follows the step) |
| `md` / `lg` / `xl` / `full` | `md` (normal) |
This matches the references: Radix separates `size` (density) from `width`
(full); Mantine's `fullScreen` **ignores** `size`; Material 3's full-screen
is a **layout type** (top app bar) with standard controls. None enlarges
controls because the dialog is `full`.
First consumer: `Dialog.Close` — `components/dialog/context.ts` publishes
the dialog's size and Close derives it. An explicit `size` prop on the part
always wins.
### The size→font mapping: universal 1:1 (2026-06-17 override)
> **Supersedes Phase 7's `control`/`compact`/`dense` archetypes.** The user
> decided an enterprise-grade framework needs **one source of truth**:
> control text follows the typographic scale **1:1** — `font-size-{size}` =
> `var(--font-size-{size})` — in ALL components. So changing
> `--font-size-md` to 15px re-adapts the whole theme without touching a
> single recipe. The `md` control is **16px** (NOT 14 anymore: the "compact
> control md=14" doctrine is revoked).
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**The scale** (xxs/xs/sm/md/lg/xl/xxl/xxxl): **10 · 12 · 14 · 16 · 18→20 ·
24→28 · 32→48 · 40→80** (`lib/primitives/typography.ts`; lg…xxxl are fluid
`clamp()`).
Recipes brought to 1:1 (2026-06-17): button, badge, breadcrumb, calendar,
pagination, radio-group, toolbar, file-upload, tag-group, stepper, toggle,
tooltip — plus the whole field family (field, spin/date/time/color-field,
search/password-field, select, editable, tags-input), already 1:1 since the
fields sprint.
**Legitimate exceptions** (NOT control text → 1:1 doesn't apply):
- **avatar/marker** — the font is the initial inside the circle, scaled to
the **diameter** (24→96px): a proportional glyph, not a control.
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **accordion** — _(re-joined 1:1 on 2026-07-06)_: its former prose-scale
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
aliases (`--text-N-size`) died in the token-alias purge (changelog §39)
and the trigger/content now consume the size-bundle coordinates
(`--size-{k}-font-size`) like every control.
- **words / palabras / chronos** — excluded WIP tracks.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
The `--size-*` bundle (above) is **adopted** — the C7 sweep (2026-07-03,
`notes.md`¹) pointed the 34 sized recipes at the bundle coordinates
(`--size-{k}-control-height` / `-font-size` / `-icon-size`) instead of the
raw primitives, and `recipe-css-contract` **forbids the raw primitive** in
recipes. A recipe keeps its own value only where it **deliberately
deviates** — the deviation stays visible instead of buried in a parallel
re-declaration. The bundle also carries the typographic pair of each size:
`--size-{k}-font-line-height` and `--size-{k}-font-letter-spacing` (the
per-size optical tracking travels with the coordinate). Realigned to 1:1 on
2026-06-29 (`--size-md-font-size` = `var(--font-size-md)` = 16px).
**Hard rule (the coherence guard)** — `recipe-css-contract.test.ts`:
> No recipe `font-size-*` / `icon-size-*` token may be a **px/rem literal**
> — it MUST reference `--font-size-*` / `--icon-size-*` (otherwise the text
> stops following the typographic scale, `--scaling` and
> `applyTypeScale()`).
It closes the hole the earlier (CSS-only) guard left — recipe-token values
are custom properties, not the `font-size:` property.
(`words`/`palabras`/`chronos` excluded — an active track.) The refactor to
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_consume_ the archetype bundle (instead of re-declaring the mapping) stays
a follow-up; the guard is what **prevents the drift**.
### Fields: input 1:1 + the label one step below (2026-06-17 override)
The **field** family applies the **universal 1:1** (above) to the
**input**, and adds a rule of its own for the **label**:
- **Input/control → 1:1** (like every control): **12 · 14 · 16 · 18→20 ·
24→28**.
- **Label → one typographic step below the input**: **10 · 12 · 14 · 16 ·
18**. The label is the **only** element in the system that deliberately
steps down — label↔input hierarchy, not the collapsed drift that was
rejected.
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Recipe | input | label |
| -------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------- |
| `field` (generic) | `control-font-size-*` 1:1 | `label-font-size-*` one step below → covers everything `<Field>` wraps |
| `spin-field` (number/css), `date/time/color-field` | `font-size-*` 1:1 | date/time/color: own label via CSS calc; spin: the Field's label |
| `search-field`, `password-field`, `select`, `editable`, `tags-input` | `font-size-*` 1:1 | the Field's label |
**Segmented-field labels** (only date/time/color have
`[data-X-field-label]`):
`font-size: calc(1em - (var(--font-size-md) - var(--font-size-sm)))` — the
input (inherited `1em`) minus one scale step (md−sm = 2px, tokenized). The
generic `Field` uses per-size `label-font-size-*` tokens.
**Untouched** (already coherent): `pin-input` (`cell-font-size-*` already
scales), `combobox` (no font of its own). **Excluded**: `words`, `chronos`.
Non-fields (button/calendar/badge…) keep their archetype.
**Pending**: at `lg` the segmented label (calc → 18) and the `Field`'s
(token → 16) diverge by 2px because the input's `lg` is **fluid** (18→20).
At xs/sm/md (fixed) they agree. Resolve by giving the segmented ones
per-size tokens, or by dropping the field input's fluid `lg`.
### The typographic scale ↔ the icon scale
They are two parallel scales — `--font-size-{name}`
(`lib/primitives/typography.ts`) and `--icon-size-{name}`
(`lib/primitives/static.ts` → `STATIC_ICON.size`) — both scaled by density
(`× --scaling`). **They are not independent: the icon accompanies the
text.**
The optical rule was validated **by eye, not by formula** (the test bench
[`/uix/icon-scale-study`](../../web/routes/uix/icon-scale-study/+page.svelte)):
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
the icon weighs _one point above the text_ — `≈ font + 2` at body sizes,
growing toward `≈ line-height` at the large ones (an icon **tied to
`font-size` below, to the `line-height` above**). It is not a constant:
| size | `--font-size` (px) | line-height (px) | `--icon-size` (px) | icon − font |
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| ---- | ------------------ | ---------------- | ------------------ | ----------- |
| xxs | 10 | 15 | 12 | +2 |
| xs | 12 | 18 | 14 | +2 |
| sm | 14 | 20 | 16 | +2 |
| md | 16 | 23 | 18 | +2 |
| lg | 20 | 27 | 20 | 0 |
| xl | 24→28 | 34 | 32 | +4 |
| xxl | 32→48 | 50 | 52 | +4 |
(The `lg`–`xxl` headings are fluid `clamp()`; the `font-size` column shows
the desktop max. The icon jump `lg 20 → xl 32` is faithful to the text jump
`20 → 28` — there is no in-between size because the typography has none
either.)
That is why a component **never invents icon sizes in px**: it declares
`var(--icon-size-{name})` and inherits this correlation. The canonical
`size` already pairs both axes (`--size-md-font-size` +
`--size-md-icon-size`). Archetype nuances:
- **Control icons follow the font 1:1** (the 2026-06-17 override): each
control recipe's `icon-size-{size}` references `var(--icon-size-{size})`,
parallel to the 1:1 font. The icon scale preserves `icon ≈ font+2` at
body (md: font 16 → icon 18). Applied to **button** + **search-field**.
**Exceptions**: `password-field` — its `icon-size-*` is NOT a glyph but
the **visibility-trigger button's box** (the glyph is 65% of it),
control-coupled on purpose; `radio-cards` — the icon follows the card's
**title** font.
- **Density ⊥ typography** (compact/comfortable/spacious): density scales
ONLY layout — `space` (`× --density-space-scale`) + `control-height`
(`× --density-control-scale`). **`--font-size-*` and `--icon-size-*`
carry no density** — only the global zoom `--scaling` (Radix parity)
touches them. Consequence: at 1:1 the icon stays **coupled to the text at
every density** (font 16 / icon 18 constant; only the control's box
tightens: 32.4 / 36 / 40.3). That is why an icon sized from
`--control-height-*` (density-coupled) decouples from the text — an
anti-pattern unless the element IS a control (e.g. the password-field's
trigger).
- **Cards / titles** (radio-cards, empty-states): the icon **accompanies
the title's font**, never a hand-inflated size — e.g. radio-cards =
`16/16/18/18/20` (follows its title). To emphasize, raise the **title's
font** (the icon follows); don't inflate the icon.
Changing the scale = editing `STATIC_ICON` +
`components/icon/create-icon.ts` + regenerating
(`npm run generate:eidos-css`). Nothing else consumes it raw.
---
## 6. Naming conventions
### Public tokens (consumable)
```
--{prefix}-{slot}
```
Where `{prefix}` is one of:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Prefix | Meaning | Example |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `--scale-{name}-{step}` | Radix physical scale | `--scale-teal-9` |
| `--primitive-{role}-{step}` | Role → step | `--primitive-affirm-9` |
| `--color-{role}-{slot}` | Color role × slot | `--color-affirm-solid` |
| `--font-{kind}-{key}` | Typography | `--font-family-primary` |
| `--size-{key}-{slot}` | Size primitive | `--size-md-control-height` |
| `--space-{n}` | Spacing scale | `--space-3` |
| `--radius-{key}` | Radius scale | `--radius-md` |
| `--border-width-{key}` | Border-width scale (linear `none·thin·medium·thick·heavy` = 0/1/2/3/4) — §35 | `--border-width-thick` |
| `--ring-inset-width` | The inset-ring's default width (the inner `box-shadow` ring) — §35 | `--ring-inset-width` |
| `--shadow-{n}` | Shadow scale (drop) | `--shadow-3` |
| `--shadow-inset-{key}` | Inner-shadow / recessed (`subtle·deep`, mode-aware) — §35 | `--shadow-inset-subtle` |
| `--blur-{key}` | Blur scale (backdrop/frost, `none·sm·md·lg·xl·xxl`) — §35 | `--blur-lg` |
| `--depth-{plane}-translucency` | Per-plane frost opacity = **a function of elevation** (higher = more opaque) — §29 | `--depth-modal-translucency` |
| `--gradient-{name}` | A named, themeable gradient — a token or **role-derived** via `buildGradient`/`applyGradients` (the 6th builder) — §29 | `--gradient-aurora` |
| `--gradient-angle-{dir}` | Gradient direction (8 compass points) — §35 | `--gradient-angle-to-r` |
| `--breakpoint-{key}` | Responsive breakpoint (source = ActiveDom) — §35 | `--breakpoint-md` |
| `--z-index-{key}` | Z-index layer (depth planes) | `--z-index-modal` |
| `--z-index-overlay-{key}` | The flat overlay micro-band (portaled overlays + modals) — §35 | `--z-index-overlay-floating` |
| `--opacity-{key}` | Opacity — a dual numeric (`0..100`) + semantic (`disabled·muted·…`) scale — §35 | `--opacity-disabled` |
| `--tracking-{key}` | Letter-spacing scale (incl. `caps` for UPPERCASE) — §35 | `--tracking-caps` |
| `--leading-{key}` | Line-height scale | `--leading-ui` |
| `--duration-{key}` | Motion duration | `--duration-fast` |
| `--ease-{key}` | Motion ease | `--ease-out` |
| `--motion-distance-{key}` / `--motion-scale-{key}` / `--motion-stagger` / `--motion-loop-{name}` | Motion metrics, event-scale factors, stagger step, loop periods — §14 | `--motion-scale-press` |
| `--control-height-{key}` | Control-height scale (density × scaling composed) — §5 | `--control-height-md` |
| `--icon-size-{key}` / `--icon-stroke-width-{key}` | Icon scale + stroke widths — §5 | `--icon-size-md` |
| `--container-width-{key}` / `--content-width-{key}` / `--aspect-ratio-{key}` / `--container-padding-inline` | Layout family (container widths reference `--breakpoint-{key}` — 2026-07-06) — §35 | `--container-width-xl` |
| `--density-{level}-{space·control}-scale` + `--density-{space·control}-scale` | Density scalars per level + the active bindings — §31 | `--density-compact-space-scale` |
| `--scaling` / `--scaling-{90..120}` | The global zoom axis (per-family participation map — §23) | `--scaling-110` |
| `--state-{hover·press·selected}` | State-layer veils over `currentColor` (config `primitives.state` — §38 + changelog §40) | `--state-hover` |
| `--floating-gap` / `--floating-gap-{menu·panel}` | Trigger→panel gap canon (config `primitives.floating` — §36 + changelog §40) | `--floating-gap-menu` |
| `--radius-factor` / `--radius-default` | The roundness multiplier + the default step (config — changelog §40) | `--radius-factor` |
| `--shape-{key}` | Shape family (squircle smoothing, nest gap) — §30 | `--shape-smoothing` |
| `--depth-{plane}-{cue}` | Depth cues per plane (`surface·border·shadow·halo·z·blur·translucency`) — §29 | `--depth-modal-shadow` |
| `--focus-ring` / `--focus-ring-{color,width,offset,inner-width,error}` | The two-ring focus family — §32 | `--focus-ring-color` |
| `--measure-{key}` | Line-length (measure) scale — §35 | `--measure-narrow` |
| `--font-feature-{key}` | `font-feature-settings` presets — §35 | `--font-feature-tabular` |
| `--style-{name}-*` | Typography named style | `--style-h1-font-size` |
| `--{c}-{slot}` | Component recipe token | `--toggle-height-md` |
| `--{c}-{role}-{slot}` | Component color | `--toggle-affirm-solid` |
| `--{c}-palette-{slot}` | Component runtime palette | `--toggle-palette-solid` |
### Private tokens (component-internal)
```
--_{c}-{slot}
```
The `_` prefix means: do NOT consume this from outside the component's
recipe. It is internal. Example:
```css
[data-toggle] {
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
--_toggle-bg: var(--toggle-solid-bg); /* private */
--_toggle-on-bg: var(--toggle-palette-solid); /* private */
}
```
### Strict rules
1. **All public Eidos tokens carry the bare `--` prefix**, with no layer
sub-prefix. Reason: debug clarity. See `--toggle-bg` and you know it is
Eidos. See `--bg` and you don't know where it came from.
2. **NEVER use `--eidos-`** as a prefix. The layer is already implicit in
the `$uix/eidos/components/{c}` path.
3. **NEVER use `--soma-`, `--air-` or `--terra-`**. Those layers are dead
or own no tokens.
4. **Component tokens follow the pattern** `--{component-kebab}-...`. The
component kebab is the directory name.
5. **Don't abbreviate component names**. `dropdown-menu` does not become
`ddmenu`. Authorship clarity is worth 6 chars.
6. **Do NOT include the intermediate "color-" segment** in color tokens.
`--toggle-affirm-solid` (correct), `--toggle-color-affirm-solid`
(deprecated 2026-05-27).
7. **Slots follow a fixed vocabulary**: for layers 3-5 the canonical
inventory is `COLOR_ROLE_SLOTS` (`lib/config-types.ts` — see §3, layer
3; not copied here). For layers 6-7 (recipe-level):
`bg, fg, border, on-bg, on-fg, on-border, hover-bg, on-hover-bg`.
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Guarded since 2026-08-20 (D-TH.6).** This rule and rule 6 spent months
> as documentation without a guard, and the catalogue drifted to 196
> `-color` keys against 55 `fg`, with the modifier landing on both sides
> (`hover-bg` ×13, `bg-hover` ×22). A codemod normalised 269 keys and
> **R-5.3** (`component-audit`) now holds the shape. Note what the slot
> list above already showed: `hover-bg` puts the modifier IN FRONT. The
> full sentence — _in front what is interactive, behind what is dimensional
> or contextual_ — plus the two families with a grammar of their own
> (role/palette, where `COLOR_ROLE_SLOTS` carries the modifier behind by
> construction, and the system tokens a recipe consumes but never mints)
> lives in [`canon/recipe-contract.md`](../canon/recipe-contract.md) §1.
> The platform principle of the px/py normalisation ("a token is named
> after the property it feeds") governs the DIMENSIONAL axes only: if it
> governed colour, `bg` would have to be `background-color`.
### Generated tokens vs authorship
Tokens in `generated/base.css` are **output**. To add a new one, you edit:
- `lib/themes/base.ts` for primitives, scales, theme variants.
- `lib/recipes/base.ts` for component tokens.
And run `npm run generate:eidos-css`.
---
## 7. Token Scope Contract (TSC)
> **Moved to [`canon/tsc.md`](../canon/tsc.md)** (the visual canon, E2).
> The Token Scope Contract — the available scopes, the three ways to
> declare a token, the `scopeCovers` algebra, cross-axis collision
> detection, multi-part scope and cross-recipe composition (v2.2), and the
> 5-layer defense pipeline — lives there as its own chapter. Summary: the
> TSC decides WHERE each token is emitted (root, per component, per color,
> per event) and validates at generation time that every dependency is
> available in the consumer's scope.
---
## 8. How to add a new component
> **Moved to [`guide.md`](./guide.md)** (the E4 guide). The steps to add a
> new component — deciding which tokens it needs, the recipe, the scope and
> validation — live there next to the define-a-theme guide.
>
> **Which transversal systems the recipe MUST consume** (state-layer,
> focus, tokenized elevation, 1:1 typography, opacity, motion, logical
> axes) is its own canon:
> [`canon/recipe-contract.md`](../canon/recipe-contract.md), enforced by
> `component-audit`'s R-4.x rules.
---
## 9. How to define a theme
> **Moved to [`guide.md`](./guide.md)** (the E4 guide).
---
## 10. How to override tokens at runtime
`ActiveEidos.setCssVariables()` allows contract-aware runtime overrides:
```ts
activeEidos.setCssVariables({
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
'--color-primary-solid': 'rebeccapurple',
'size-md-control-height': '40px', // works without -- too
'shadow-3': '0 10px 28px rgb(20 20 20 / 0.16)'
});
```
Eidos:
1. **Validates** each name against `getCssContract()`. Tokens outside the
contract throw in `strict` mode (the default).
2. **Renders** transactionally: first render + validate, then replace the
runtime `<style>` block.
3. **Applies** the overrides under `:root` (or the selector you pass).
For variables outside the contract (the app's locals):
```ts
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
activeEidos.setCssVariables({ '--my-app-custom': 'value' }, { strict: false });
```
### Whole-system builders
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Above `setCssVariables` sit **six builders** that each derive one axis of
the system from a seed and write it as a managed block (they follow the
active light/dark theme), plus the capstone that composes them
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_(count corrected 2026-07-07 — this section used to list two)_:
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **`eidos.applyColorScheme(seed, opts)`** — the 33 scales + 9 roles from a
brand color (`buildScheme`).
- **`eidos.applyTypeScale(seed, opts)`** — the `--font-size-*` scale from a
modular ratio + base (`buildTypeScale`), optionally fluid.
- **`eidos.applyDepth(planes, opts)`** — per-plane depth cue overrides.
- **`eidos.applyShape(seed, opts)`** — squircle smoothing / shape family.
- **`eidos.applySpacing(seed)`** — modular/fluid space scale
(`buildSpaceScale`).
- **`eidos.applyGradients(seeds, opts)`** — named, role-derived gradients
(`buildGradient`, the 6th builder — changelog §29).
- **`eidos.applyTheme(seed, opts)`** — the capstone: `{ color?, type?,
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
depth?, shape?, space?, gradient? }` (all six axes) in ONE atomic managed
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
write; `clearTheme()` reverts everything.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Each has its `clear*()`. All are pure in `eidos/lib` (`build-*`) + an
application method on `ActiveEidos`. Live demos under `/temas/*`.
---
## 11. Bundle strategy + `eidos:purge`
`generated/base.css` contains the tokens of EVERY component in the catalog
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
(≈140 components, 2026-07 — dates arbitrate). In production a typical app
uses 5-20.
### The tool
```bash
npm run eidos:purge -- \
--src 'src/**/*.svelte' \
--src 'src/**/*.ts' \
--src 'src/**/*.css' \
--output dist/eidos.purged.css \
--verbose
```
### How it decides what to keep
1. **Foundation always kept**: scale, primitive, color, size, opacity,
z-index, shadow, border, radius, space, density, motion, icon,
typography, layout. ~1100 tokens (~95 KB raw / ~11 KB gzip).
2. **Source-scanned tokens**: every `var(--XXX)` and `--XXX:` declaration
found in source → `XXX` pinned.
3. **Component-import detection**: every `from '...components/{c}'` →
`{c}`'s full recipe pinned.
4. **Data-attr detection**: every `data-{c}=` (filtered against the
canonical recipe registry) → `{c}`'s full recipe pinned.
5. **Transitive closure**: if X is pinned and X→`var(--Y)`, Y is pinned.
Iterated to a fixed point.
### Measured results
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Measured 2026-06 (62-component catalog, 217.7 KB base). The catalog has
since grown (≈140 components / 345 KB base, 2026-07): the PROPORTIONS hold;
the absolute figures are dated.
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Profile | Components | Raw before | Raw after | Reduction | Gzip after |
| ----------------------------- | ---------- | ---------- | --------- | --------- | ---------- |
| Minimal (toggle+button+badge) | 3 | 217.7 KB | 97.4 KB | **−55%** | 11.1 KB |
| Typical SaaS (10 components) | 10 | 217.7 KB | 116.3 KB | **−46%** | 13.6 KB |
| 5 UIX demo pages | 7 | 217.7 KB | 107.3 KB | −51% | 12.4 KB |
| Exhaustive (all) | 62 | 217.7 KB | ~217 KB | −0.4% | ~25 KB |
**The architectural floor**: ~95 KB raw / ~11 KB gzip (the foundation every
app needs).
### When to use it
- **In production**: ALWAYS. Integrate it into your build pipeline.
- **In dev**: optional. The raw 226 KB is fine for local iteration.
- **In SSR**: pre-purge once per build, not per request.
### Known limitations
- **Dynamic component selection**: if your app imports components
dynamically (`await import(...)`), the scanner can miss them. Mitigation:
pass the names via `--keep my-component`.
- **`var()` in dynamic strings**: if you build `var(--${name})` at runtime,
the scanner doesn't see it. Mitigation: declare the names statically in
some scannable file.
---
## 12. Validation tooling
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Tool | Command | What it validates |
| -------------------------- | --------------------------------------- | ------------------------------------------------------------------------ |
| **morfo:check** | `npm run morfo:check` | DOM contracts vs the morfo declarations (a Playwright walk of the demos) |
| **eidos-lint** | `node scripts/eidos-lint.ts {c}` | Recipe CSS selectors vs the morfo enum values |
| **eidos-lint-all** | `node scripts/eidos-lint-all.ts` | Same, all components |
| **TSC validation** | `npm run generate:eidos-css` (implicit) | Scope algebra + cross-axis collision detection |
| **recipe-css-contract** | `npm test -- recipe-css-contract` | Consumed recipe tokens + the TSC v2 scenarios |
| **component-api-contract** | `npm test -- component-api-contract` | Each component's public API surface |
| **component-visual-attrs** | `npm test -- component-visual-attrs` | The visual data-attrs the wrapper emits |
| **generated-css** | `npm test -- generated-css` | The generated CSS's structure |
feat(theming)!: R-5.1/5.2 a error contra el LEDGER DE DEUDA - el ratchet es por clave La pieza grande del CIERRE, con la forma firmada hoy: la deuda de alcance no se tolera en warn ni se disfraza de excepcion - se REGISTRA, clave a clave, y desde ahi la regresion es imposible y la mejora queda contada. - scripts/theming-census-debt.ts (NUEVO): 1088 claves (763 global + 325 literal, 74 componentes), clave `{clase} - {fichero} - {selector} - {propiedad}` INDEPENDIENTE de linea (mover una regla no fabrica regresion), comparacion MULTISET, generacion reproducible (dos corridas = mismo sha256), nace prettier-limpio. NO es un fichero de excepciones: es deuda registrada, la otra clase de acta - la valvula R-5.x exception de los README sigue intacta y NUNCA ciega el ratchet. Los carriles WIP ENTRAN (palabras 359 + chronos 209 = 568): la deuda es real viva donde viva, y dejarla fuera haria del gate una afirmacion sobre dos tercios del arbol. Con la salvedad MEDIDA de palabras escrita: sus nombres --palabras-* son canal de VALOR del scheme del documento, no contrato de tema - sus 103 "public" del censo estan en cuestion. - theming-census.ts: censusAudit() -> {newDebt, stale} + CLI --debt [--write] que imprime el delta que va a cometer (regenerar en masa borra el ratchet: el escritor grita y la cabecera lo prohibe sin firma). - theming-reach-floor.test.ts (reescrito): newDebt=0 y stale=0 con las claves NOMBRADAS; los techos burdos maxLiteral/maxGlobal RETIRADOS (superseded por el por-clave: 5 regresiones ya no se esconden bajo 5 arreglos); reachPct sube a 69 como ratchet grueso - y cubre el hueco nombrado: los 318 privados no-derivados siguen SIN ratchet por clave (acotado por la firma a literal|global; pendiente de firma propia); atHundred corrige su criterio (public>0, 14 -> 45: los 31 de diferencia eran denominadores vacios, ninguno un avance real). - component-audit.ts: filas R-5.1 y R-5.2 a ERROR consumiendo censusAudit() (dos implementaciones de una medida son dos medidas); R-5.2 honesto sobre los 18 sin-contrato (11 nada-que-declarar all-system/0-knobs; field-langs cubierto POR el ledger - la entrada ES su registro; 3 consumidores de capa calendar; mockup y text-scramble PASS con nota del idioma var(..,fallback) sin contrato - forma real sin nombre, pendiente de decision; palabras fuera del catalogo del audit). R-5.3 YA estaba en error (verificado, --names 0 desviadas). El skip por censo roto ahora GRITA por consola (la leccion del prepareWith: un guard saltado nunca es mudo - y el suelo de vitest queda de red mecanica). - docs: canon/recipe-contract.md SS4 y theming/reference.md SS12 reflejan la ley (gate F3 = censo 100% ADJUDICADO); completion-checklist gana las dos filas (exigido por el guard I5); el stub RECIPE_CONTRACT.md solo actualiza su linea de enforcement. Mutaciones, todas mordiendo: literal nuevo en mark -> newDebt lo nombra, suelo rojo, R-5.1 falla; clave de aura tokenizada -> STALE rojo hasta borrar la linea; literal sin registrar en field-langs -> R-5.2 muerde. Guards en HEAD: component:audit 162 PASS (cero flips; los 4 NEEDS-WORK son R-1.x ajenos), suelo 5/5, docs:check 0/0. BREAKING: los techos maxLiteral/maxGlobal del suelo desaparecen; anadir un literal o un global crudo a una receta exige desde ahora tokenizar, anotar /* literal: */ o firmar la entrada en el ledger de deuda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| **theming-census** | `node --import tsx/esm scripts/theming-census.ts` | How much of each recipe's appearance a theme can reach through the component's OWN public tokens (`--report` writes `docs/audit/theming/`, `--names` the naming grammar, `--debt` the ledger) |
| **theming-reach-floor** | `npm test -- theming-reach-floor` | The catalogue ratchet: no unregistered debt, no stale ledger entry, reach ≥ 69 % |
| **theming:sentinel** | `npm run theming:sentinel -- {c} {url}` | R-5.4: every public token moves a computed value in the live demo, or carries a written adjudication |
### The debt ledger, and what «adjudicated» means (firma 2026-08-25)
A recipe declaration that reaches no theme is a fact, not an opinion, and the
census counts 1088 of them. They are registered ONE BY ONE in
[`scripts/theming-census-debt.ts`](../../scripts/theming-census-debt.ts) —
**deuda registrada, not an exceptions file**: an exception asserts «this is
fine» and every entry there asserts the opposite. It is the sibling of
`theming-sentinel-exceptions.ts` in mechanism (one written line per key, a
STALE detector on top) and its opposite in meaning.
The ratchet is therefore PER KEY, not per total. Until this landed the floor
was two coarse ceilings (`maxLiteral` / `maxGlobal`), which only see a NET
move: five regressions hiding under five unrelated fixes read as green. Now a
literal or a raw global outside the ledger is a regression that arrives NAMED,
and a ledger entry whose knob is no longer debt — tokenized, annotated,
dead — is **STALE** and stays red until the line is deleted. The debt only
shrinks, key by key. Same doctrine as R-5.4 («a public token that moves
nothing and carries no act, lies») and the same mechanism.
The exception valve is untouched and lives where it always did: the
`/* literal: <reason> */` annotation on the declaration (recipe-contract §3)
and the component-level `R-5.1 exception:` of a README. Precedence between the
two acts: a README valve softens the component's audit ROW; the catalogue
floor reads only the ledger, so a valve never blinds the ratchet.
With that, F3's gate reads honestly — **«census 100 % ADJUDICATED»**, not
«census 100 %»: every knob is public, private, system, structural, an
annotated exception, or a ledger entry.
### The recommended pre-commit validation pipeline
```bash
npm run generate:eidos-css # if you touched recipes/themes
npm test -- src/uix/eidos # the eidos suite
npm run check # TS check
npm run morfo:check # DOM contracts (needs the dev server)
node scripts/eidos-lint-all.ts # the CSS-drift safety net
```
---
## 13. Sema integration (`event:*` scope)
> ⚠️ **Superseded.** The current motion model is the **two-moment** one
> documented in [`motion.md`](./motion.md) (F1–F7): the `--event` moment
> (the perceptual signature) is declared in `motion.signatures` and
> generated as CSS against `data-event-*` directly — without the TSC
> `event:*` scope or the `data-motion-ref` this section used to discuss.
> The engine (`EngineMotion`) is a service in `arts/motion` (`uix.motion`).
> The original body (the decision's historical context) lives in
> [`changelog.md §13`](./changelog.md).
---
## 14. Motion
Eidos's motion system — the **two-moment model** (the perceptual `--event`
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
during a signal's _hold_ + `--state` for persistent transitions), the
`EngineMotion` engine (relocated to `arts/motion`, exposed as `uix.motion`
and consumed by soma and eidos) and how a preset is authored — is its own
system and lives in [`motion.md`](./motion.md). Roadmap F1–F7 implemented
(2026-06-04). This section covers only what touches **theming**.
> **Status note.** Earlier versions of this doc described motion as
> "deferred" and pointed at `data-motion-ref` / the "TSC `event:*` scope"
> (§13) as its future. That is **obsolete**: the perceptual signature
> migrated into the `signatures` registry (not `events.css`) and the engine
> is a service in `arts/motion` today. [`motion.md`](./motion.md) is the
> canonical, current reference.
### Draggable surfaces: the "pickup" lift
When the user **grabs and drags** a surface, it must **rise toward them**
(depth: "I picked it up"). It is a transversal cue — NOT component-specific
— so the scale factor is a **global motion token**, not a per-recipe
literal:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Token | Value | Use |
| --------------------- | ------ | -------------------------------------------------------------------------------- |
| `--motion-scale-lift` | `1.02` | the pickup scale while dragging (the only `>1` in the `--motion-scale-*` family) |
**The pattern (every draggable applies it the same way)** — gated by the
drag data-attr its morfo declares (`data-dragging`, `data-grabbed`, …),
paired with a shadow elevation, and suppressed under reduced motion:
```css
/* float-panel, a dragging slider thumb, a sortable item, a drawer… */
[data-x][data-dragging] {
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
scale: var(--motion-scale-lift); /* rises toward the user */
box-shadow: var(--…-shadow-active); /* + elevates the shadow */
}
@media (prefers-reduced-motion: reduce) {
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
[data-x][data-dragging] {
scale: 1;
transition: none;
}
}
```
Rules:
- **`scale` (not `transform`)** to compose with the position, which travels
via `translate` (distinct properties) → the lift never fights the 1:1
drag.
- The `scale` change is **transitioned** (lift on grab / settle on
release); during the move it stays **static** (composited, no per-frame
cost).
- `will-change: translate, scale` for the duration of the gesture.
- A theme re-themes the lift in `STATIC_MOTION.scale.lift`
(`primitives/static.ts`) — every draggable inherits it. Don't redefine
the 1.02 per component.
Today `float-panel` consumes it
(`[data-float-panel-content][data-dragging]`); a slider/sortable adding
drag must read the SAME token, not invent its own.
---
## 15. Comparison with reference libraries
> **Moved to [`notes.md`](./notes.md)** (E3). The comparison of eidos with
> Radix, Ark, Mantine and others lives there, next to the decisions FAQ.
---
## 16. Anti-patterns you must NOT commit
### A. Declaring derived tokens at `:root`
```ts
// ❌ WRONG — the pre-TSC Toggle bug
'palette-solid': { value: '...', scope: 'host' },
'solid-on-bg': 'var(--my-component-palette-solid)' // implicit 'root' scope
```
The TSC throws on regeneration. Fix: `scope: 'host'` on the consumer.
### B. Names with the redundant "color-" segment
```ts
// ❌ DEPRECATED (2026-05-27)
'color-affirm-solid': 'var(--color-affirm-solid)'
// ✅ CORRECT
'affirm-solid': 'var(--color-affirm-solid)'
```
### C. Inventing roles outside the canon
```ts
// ❌ NO — success/danger/warning/info belong to other models
'success': 'green',
'danger': 'red'
// ✅ Use the 9 canonical ones
'fulfill': 'green', // success → fulfill
'threat': 'red' // danger → threat
```
### D. Media queries that change canonical tokens
```css
/* ❌ NO — md changes meaning per viewport */
@media (max-width: 768px) {
:root { --size-md-control-height: 32px; }
}
/* ✅ The component picks which size applies per viewport */
<Toggle size={{ base: 'sm', md: 'md' }} />
```
### E. Importing `$libs/dom` directly in eidos
```ts
// ❌ NO
import { foo } from '$libs/dom';
// ✅ Eidos consumes via ActiveEidos.dom
const eidos = ActiveEidos.require();
eidos.dom.apply(...);
```
### F. Hand-editing `generated/base.css`
It is output. Any change is overwritten on regeneration. To change
something, edit `lib/themes/base.ts` or `lib/recipes/base.ts`.
### G. Creating loose physical scales inside an app
The default **33 scales** cover the reasonable palettes. A **brand theme**
DOES bring its own palette as scales (§25.7) — that is legitimate. What you
must NOT do is add a one-off scale inside an app when remapping a role to
an existing scale already solves the case.
### H. Re-exporting between layers
```ts
// ❌ NO — eidos does not re-export soma
export * from '$soma/components/toggle';
// ✅ Each layer exposes its own API
```
---
## 17. FAQ — controversial decisions
> **Moved to [`notes.md`](./notes.md)** (E3).
---
## References
- [`architecture/eidos.md`](../architecture/eidos.md) — the visual layer as
a module (the living architecture chapter).
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- `THEMING_AUDIT_2026-06-01.md` — the theming audit (the journal of how we
got here; removed from the tree — its chronicle survives in
[`changelog.md §22`](./changelog.md)).
- [`motion.md`](./motion.md) — the motion system (the two-moment model,
F1–F7; see §14).
- [`src/uix/eidos/lib/config-types.ts`](../../src/uix/eidos/lib/config-types.ts) —
the source of truth of the TSC type.
- [`src/uix/eidos/lib/render-css.ts`](../../src/uix/eidos/lib/render-css.ts) —
the generator (parsing, scope algebra, cross-axis detection).
- [`src/uix/eidos/lib/recipes/base.ts`](../../src/uix/eidos/lib/recipes/base.ts) —
the per-component token catalog.
- [`src/uix/eidos/lib/themes/base.ts`](../../src/uix/eidos/lib/themes/base.ts) —
the base theme (primitives + semantics + themes).
- [`scripts/eidos-purge.ts`](../../scripts/eidos-purge.ts) — the purge
tool.
- [`src/uix/eidos/recipe-css-contract.test.ts`](../../src/uix/eidos/recipe-css-contract.test.ts) —
the test guard.
- [`architecture/active-architecture.md`](../architecture/active-architecture.md) —
the full UIX context.
- [`guia-semantica-historica.md`](../decisions/guia-semantica-historica.md) —
the sema/perceptual doctrine (the historical seed of the 9-role canon;
[`CANON.md`](../CANON.md) rules today).
---
## 18. Universal TSC coverage
> **Moved to [`canon/tsc.md`](../canon/tsc.md)**. The v2.2 extensions
> (multi-part scope and cross-recipe composition) that take the TSC to
> universal coverage live with the rest of the contract there.
---
## 19. Variants are eidos canon, NOT the theme's
**A firmly-held architectural position**: the variant vocabulary (`solid`,
`outline`, `ghost`, `soft`, `surface`, `line`, `pills`, etc.) is **fixed in
the system**. A theme can NOT:
- Add a new variant (there is no `branded`, `bubble`, `corporate` each
theme invents).
- Redefine an existing variant's visual cascade (`outline` means "bordered
restraint" in every theme — only the border's COLOR changes, not its
geometry).
### The 3 layers of the onion
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Layer | What it is | Who changes it |
| --------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| **Sema families / intents** | the book's canonical perceptual vocabulary (8 families × 6 intents) | NOBODY — fixed |
| **Eidos variants** | perceptual visual archetypes (5 shared archetypes + component-specific variants) | NOBODY — fixed |
| **Eidos color roles** | the 9 canonical roles (`primary`/`affirm`/`risk`/…) | NOBODY — fixed |
| **The eidos color palette** | which hex each role is | **THE THEME** |
| **Recipe-internal tokens** | `--toggle-solid-bg` etc. | **The app** (a targeted override in `EidosConfig.recipes`) |
**Theming = retinting the perceptually fixed.** The theme changes WHICH
color `affirm` is, not WHAT `outline` means.
### Why fixed
1. **Component portability.** `<Toggle variant="outline">` must render
coherently in any theme. Theme-defined variants would break that
silently — a component assuming `outline` wouldn't work in a theme that
doesn't declare it.
2. **Type safety = part of the contract.** Consumers need
`SelectionVariant = 'solid' | 'outline' | 'ghost'` for autocomplete and
TS errors. An extensible `Record<string, …>` would lose that guarantee.
Radix Themes 3.x, Chakra v3, Mantine — every serious reference keeps
variants fixed per component.
3. **Variants are perceptual archetypes, parallel to sema families.**
`solid` = "filled emphasis", `outline` = "bordered restraint",
`ghost` = "ambient transparency", `soft` = "tinted background". That is
the framework's perceptual vocabulary — not a theme decision.
4. **There are 5 archetypes, not infinitely many.** The catalog closes; TS
rejects non-canonical ones. If a genuinely perceptual new archetype
emerges, it is added to `EIDOS_VARIANTS` — at the framework level, not
the theme's.
### The single source of truth — `EIDOS_VARIANTS`
`src/uix/eidos/lib/types.ts` declares the constant:
```ts
export const EIDOS_VARIANTS = {
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
control: ['surface', 'outline', 'ghost'],
selection: ['solid', 'outline', 'ghost'],
chip: ['soft', 'solid', 'outline', 'ghost'],
marker: ['solid', 'soft', 'outline'],
tabs: ['line', 'surface', 'pills', 'segmented']
} as const satisfies Readonly<Record<string, readonly string[]>>;
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
export type ControlVariant = (typeof EIDOS_VARIANTS.control)[number];
export type SelectionVariant = (typeof EIDOS_VARIANTS.selection)[number];
// …
```
The 5 unions are DERIVED from the const — the value and the type cannot
desynchronize. Each component narrows to the appropriate archetype:
```ts
// components/toggle/types.ts
export type ToggleVariant = SelectionVariant;
// components/accordion/types.ts
export type AccordionVariant = ControlVariant;
```
For finer narrowing within an archetype:
```ts
export type AlertDialogCancelVariant = Extract<ButtonVariant, ControlVariant>;
```
### Component-specific variants
Some components have genuinely unique vocabularies:
- `Banner`: `inline | overlay | persistent` (positioning, not perceptual
treatment).
- `Spinner`: `bars | dots | ring` (the indicator's geometry).
- `Button`: adds `'plain'` for inline/link-like — it doesn't deserve its
own archetype because it only appears in Button + Code.
These live in each `components/{c}/types.ts`. The lint
`recipe-css-contract.test.ts > variant CSS selectors per component match
the declared type union` validates bidirectionally:
- The CSS uses `[data-{c}][data-variant='X']` → X must be in the union.
- The type union declares `'X'` → the CSS should have entries (advisory).
### What a theme CAN do
- Change palettes (`ThemeColorSet.scales`, `ThemeColorSet.roles`).
- Change shadows (`ShadowScale`).
- Change typography styles (`TypographyPrimitiveSet.styles`).
### What a theme can NOT do
- Add variants. (`ThemeDefinition` exposes no `recipes`.)
- Redefine visual cascades. (Recipes are `EidosConfig.recipes`, part of the
app's bootstrap, not the theme's.)
- Change color roles. (The 9 roles are canon.)
- Change the size canon. (`SIZE_PRIMITIVE_KEYS` is fixed.)
### What the app CAN do (at boot, not per theme)
- Override tokens in `EidosConfig.recipes` — it changes the visual
cascade's RESULT; it adds no new variant.
- Create its own wrapper components composing eidos primitives with custom
className/style.
- Change tokens at runtime via `ActiveEidos.setCssVariables()` /
`clearCssVariables()` (per-app CSS variables).
### When to add a new archetype
Only when a repeated perceptual pattern EMERGES in ≥3 components and fits
none of the 5 existing archetypes. Procedure:
1. Document the archetype with 1 paragraph describing the perceptual
affordance (parallel to "solid = filled emphasis").
2. Add it to `EIDOS_VARIANTS` in `lib/types.ts`.
3. Export the derived type.
4. Migrate the consuming components to reference it.
5. Update this section.
### Comparison with the references
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Lib | Theme-extensible variants | App-extensible variants |
| ------------------------ | --------------------------------- | --------------------------------------------------------------------------- |
| **Radix Themes 3.x** | ❌ | ❌ (fixed per component) |
| **Mantine 7+** | ❌ | ❌ (defaultProps + styles override) |
| **Chakra UI v3 (Panda)** | ❌ | ⚠️ via recipes config (compound variants) |
| **Ark UI** | n/a (100% headless, no opinion) | n/a |
| **shadcn/ui** | n/a (copy-paste, not a framework) | ✓ (copy + edit) |
| **activeUIX** | ❌ | ⚠️ via an `EidosConfig.recipes` override (changes tokens; adds no variants) |
activeUIX aligns with Radix Themes and Mantine: a framework with a fixed
contract, a theme with flexibility bounded to color/spacing. Extreme
extensibility (Tailwind, plain CSS-in-JS) is deliberately NOT the goal —
because the framework's promise is perceptual portability across apps and
themes.
---
> **§20–§38 — the chronicle moved to [`changelog.md`](./changelog.md).**
> These sections were dated sprint records (corrections, incidents,
> commits) mixed with doctrine. The full chronicle now lives in the
> changelog **with the same §N numbering**; below, each section keeps the
> standing decision in a sentence + the pointer to the living source (RFC /
> config / generator). Historical `§N` citations across the corpus and the
> code keep resolving here.
## 20. Theming-engine corrections (2026-06-01)
Chronicle in [`changelog.md §20`](./changelog.md). Standing: density emits
real per-level scalars (`data-density` drives `--density-space-scale` /
`--density-control-scale`) and the `contrast` slot resolves legibly on
solids (APCA on-solid with a flip; see §25 and `lib/render-css.ts`).
## 21. Two-level color model (RFC — RESOLVED in §25)
The RFC is resolved — chronicle in [`changelog.md §21`](./changelog.md).
The standing model is §25 +
[`rfc-color-model.md`](../rfcs/rfc-color-model.md).
## 22. Pending theming improvements
A historical, resolved backlog — chronicle in
[`changelog.md §22`](./changelog.md). The prioritized audit that absorbed
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO F2 — lote mecánico (13 ítems): - DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString propio + suite de contrato (props.test.ts; soma.md §12 cerrado). - THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector (los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la deriva que el builder previene, demostrada en el propio doc). - MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9 tests (selectors.test.ts, matches() real con comillas/corchetes) · MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad · MOR-3 _resetCompileCache borrado (0 usos). - SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo · SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin. - SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de applyDominance → skip defensivo + timer tope de awaitExpression cancelado · SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11) + pin del path de VALOR. - accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) — verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33. F3 — censos con guard: - SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred (consumidores cableados: date/time-field vía soma.uix.timers; avatar/image vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige .schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo). - THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo comment-blanked) + las 15 declaraciones anotadas con su razón + canon recipe-contract §3/§4. - SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/ timeline pinneados (overrides documentados en call-site); media-player Batch-4 (35 hits, cero renderProps) = único batch restante, registrado. - THM-4 doctrinado en eidos.md §unused (comportamiento/composición = legítimo; deuda = eje visual sin consumidor; hotspots por lotes). F4-C — corpus documental (decisiones de usuario aplicadas): - DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL trackeada / des-link históricos) · docs:check I6-links WARN→ERROR. - DOC-1: tabla «Build contract» MIGRADA a component-guide con estados modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil. - DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures · gradient añadido a los DOS capstones (sextet real) · nota de paleta de demo-authoring corregida (universalPaletteDecls + decisión THM-2 = mecanismo universal como sucesor del tracker borrado). - DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado · EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado. SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11): - Reconciliación: los morfos ya no declaran close (delegated al Popover, de-dialoged 06-27); el agujero real era el cierre programático bypaseando dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS. - Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5 providers (14 sitios; select/commit → 'save' = commit.save+fulfill, cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito (ya suena commit-set/cancel por diseño S9). - Verificado en vivo (date-picker): Done → close·commit·fulfill·active · Cancel → close·emerge · cierre real. Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 · docs:check 0/0 con I6 en error · baseline propio 57. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
it was `THEMING_AUDIT_2026-06-01.md` (the full scorecard; removed from the
tree).
## 23. The `scaling` axis (global zoom) — 2026-06-02
Chronicle in [`changelog.md §23`](./changelog.md). Standing: `data-scaling`
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
(`90`–`110`) is the **global zoom**, and WHICH families it multiplies is
**declared per element** by the participation map
(`STATIC_SCALING_PARTICIPATION`, `primitives/static.ts` — the axis
definition, 2026-07-06): metric families scale (`space`, `control-height`,
`font-size`, `icon-size`, `blur`); chrome families stay crisp (`radius` —
which keeps its own `--radius-factor` magnitude knob —, `border-width`,
`shadow`). Orthogonal to density (which does NOT touch typography) and
multiplies with it. Full design:
[`rfc-scaling.md`](../rfcs/rfc-scaling.md).
## 24. P2 engine corrections (2026-06-02)
Chronicle in [`changelog.md §24`](./changelog.md). Standing: the per-role
`surface`/`soft` tints are **translucent by construction**
(`--color-{role}-surface` = `a2`, `-surface-hover` = `a3`) — they compose
over non-uniform backgrounds.
## 25. The color model — palette + derived roles/intents (2026-06-02)
Chronicle in [`changelog.md §25`](./changelog.md). Standing (the canonical
color model, three layers):
- **The palette** — functional 12-step + 12-alpha scales, theme-designable
(`lib/themes/color-scales.ts` + `base.ts`); a scale's identity = step 9
(the solid). Directly usable: `var(--scale-{name}-{step})`.
- **Hierarchy roles** (`primary`/`secondary`/`tertiary`) — explicit aliases
to a scale: `THEME_BASE_COLOR_ROLES` (`lib/themes/base.ts`), see §4.
- **Intents** — auto-derived from the palette by the book's convention
(`CANONICAL_INTENT_SCALES`, `lib/config-types.ts`); the theme may deviate
(convention ≠ authorship, §4).
The per-role slots are `COLOR_ROLE_SLOTS` (§3, layer 3 — the single
source). Detail and rationale:
[`rfc-color-model.md`](../rfcs/rfc-color-model.md) +
[`rfc-color-engine.md`](../rfcs/rfc-color-engine.md).
feat(theming): THM-2 — paleta universal por instancia (capa compartida + forward), 5/17 superficies El mecanismo genérico ya existía (universalPaletteDecls auto-completaba las 33 escalas para recetas con tokens palette-*), pero con PREFIJO de componente (--button-palette-*) — cada superficie pagaba ~21 KB, por eso solo button/toggle lo tenían (rollout a las 17 = ~+315 KB). Colapso al modelo compartido: - renderSharedPaletteLayer: UNA cascada component-agnóstica [data-color='{role|scale}'] { --palette-{slot}: … } — 8 roles + 33 escalas, emitida una vez (~19 KB). Roles 1:1 a --color-{role}-*; escalas con el step map + on-solid contrast, valor idéntico a la cascada per-component retirada. - renderRecipePaletteForward: toda receta con palette-* (público) o _palette-* (privado) recibe [data-{c}][data-color] { --{c}-palette-{slot}: var(--palette-{slot}, host-default) }. La guarda de PRESENCIA es la seguridad de anidamiento — un hijo sin color dentro de un ancestro coloreado matchea solo [data-{c}] (su default) y nunca lee el --palette-* heredado. - Generalización a la paleta PRIVADA: checkbox/radio-group/switch ganan las 33 escalas con CERO cambio de CSS (ya consumen --_{c}-palette-*). - universalPaletteDecls eliminado; 2 guards migrados al modelo compartido (recipe-css-contract "shared layer + forward"; active-eidos-config contrast en la capa compartida). DONE (per-instance): button, toggle, checkbox, radio-group, switch (5/17). Rollout de las 12 restantes en docs/process/continue-cleanroom-fixes §THM-2. Doctrina en theming/reference §25 ("The per-instance palette layer"). Verificado en vivo (button + checkbox): color="teal" → escala teal exacta · affirm → rol exacto · primary → primary · nested-nocolor en ancestro teal → primary (sin contaminación). Peso 345K → 329K. Gates: matriz 141/141 · eidos 314 · recipe-css-contract + active-eidos-config 98/98 · contracts 38/38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### The per-instance palette layer — `--palette-*` (THM-2, 2026-07-12)
`<X color="teal">` — the promise that a component's `color` prop accepts ANY
of the 33 donor scales (not only roles) — is served by ONE shared,
component-agnostic layer, not a per-component copy of the cascade.
- **The shared layer** (`renderSharedPaletteLayer`, `lib/render-css.ts`):
`[data-color='{role|scale}'] { --palette-{slot}: … }` for all roles + all
`PALETTE_SCALES`, emitted ONCE (~19 KB). Role rows mirror the role tokens
1:1; scale rows use the `PALETTE_SLOT_STEP` map + the on-solid contrast
criterion (`lib/on-solid.ts`), so the value is identical to the retired
per-component cascade.
- **The per-recipe forward** (`renderRecipePaletteForward`): a recipe that
declares `palette-{slot}` (public) or `_palette-{slot}` (private) tokens
feat(theming)!: FIRMA B' - la cascada de paleta es una ESCALERA sin empates suelo :where([data-{c}]) (0,0,0) < forward [data-{c}]:where([data-color], [data-color-custom]) (0,1,0) < tono [data-{c}][data-color='X'] (0,2,0). El orden de emision deja de decidir: la especificidad ES el contrato, legible en el artefacto (doctrina hermana de la FIRMA 12.9). - Emisor (render-css.ts): isPaletteSlotToken PARTE el bucket host - solo las ranuras de paleta bajan al suelo, el chasis del componente queda a (0,1,0). 49 suelos + 49 forwards reescritos + 0 pares viejos; bloques por tono (112), capa compartida y gradient finish byte-identicos. - Neutra en pixel, MEDIDO: ~297.000 valores computados antes/despues (105.248 planos lotes A+B, 57.720 planos lote C, 134.464 con los tonos ESTAMPADOS en las 49 unidades, incluidos 5 @active), 0 diffs reales. Los 17 crudos, probados ruido por reproduccion sobre codigo identico. - Guarda por MUTACION (active-eidos-config.test.ts): suelo con contenido, forward de un peldano, par viejo ausente, tono sin envolver, particion del host, cierre estructural (toda declaracion de paleta vive en un peldano) - 6 mutaciones inyectadas, 6 mordidas. El contrato del forward pasa de vigilar 2 recetas a 49 (privadas incluidas, multi-parte bien). - Ledger: PALETTE_SUPERSEDED muere (describia el mundo viejo); 207 de sus 353 claves pasan a VIVAS y las 146 restantes quedan bajo PALETTE_FLOOR (el tono-default estampado resuelve por el forward: quitar el atributo ES hablar en silencio) o TONE_UNREACHED (ningun paso del guard estampa tono Y hoverea), con razon medida. stepper y tag-group, barridos por primera vez. El detector STALE es CIEGO a patrones: la retirada fue manual y por estrechamiento, guiada por medida. - Docs: changelog 53 - reference THM-2 - next-features 13 (RESUELTA) - CONTINUE-theming. Supervision: 4 agentes Opus en 5 fases (baseline plano + baseline estampado, emisor, guarda, verificacion de parque completo, docs), con refutacion del supervisor sobre censo, emisor y guarda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
gets `[data-{c}]:where([data-color], [data-color-custom]) { --{c}-palette-{slot}: var(--palette-{slot}, <host default>) }`.
feat(theming): THM-2 — paleta universal por instancia (capa compartida + forward), 5/17 superficies El mecanismo genérico ya existía (universalPaletteDecls auto-completaba las 33 escalas para recetas con tokens palette-*), pero con PREFIJO de componente (--button-palette-*) — cada superficie pagaba ~21 KB, por eso solo button/toggle lo tenían (rollout a las 17 = ~+315 KB). Colapso al modelo compartido: - renderSharedPaletteLayer: UNA cascada component-agnóstica [data-color='{role|scale}'] { --palette-{slot}: … } — 8 roles + 33 escalas, emitida una vez (~19 KB). Roles 1:1 a --color-{role}-*; escalas con el step map + on-solid contrast, valor idéntico a la cascada per-component retirada. - renderRecipePaletteForward: toda receta con palette-* (público) o _palette-* (privado) recibe [data-{c}][data-color] { --{c}-palette-{slot}: var(--palette-{slot}, host-default) }. La guarda de PRESENCIA es la seguridad de anidamiento — un hijo sin color dentro de un ancestro coloreado matchea solo [data-{c}] (su default) y nunca lee el --palette-* heredado. - Generalización a la paleta PRIVADA: checkbox/radio-group/switch ganan las 33 escalas con CERO cambio de CSS (ya consumen --_{c}-palette-*). - universalPaletteDecls eliminado; 2 guards migrados al modelo compartido (recipe-css-contract "shared layer + forward"; active-eidos-config contrast en la capa compartida). DONE (per-instance): button, toggle, checkbox, radio-group, switch (5/17). Rollout de las 12 restantes en docs/process/continue-cleanroom-fixes §THM-2. Doctrina en theming/reference §25 ("The per-instance palette layer"). Verificado en vivo (button + checkbox): color="teal" → escala teal exacta · affirm → rol exacto · primary → primary · nested-nocolor en ancestro teal → primary (sin contaminación). Peso 345K → 329K. Gates: matriz 141/141 · eidos 314 · recipe-css-contract + active-eidos-config 98/98 · contracts 38/38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
It consumes its own `--{c}-palette-*` unchanged — a **private-palette recipe
gains all 33 scales with ZERO CSS change**.
feat(theming)!: FIRMA B' - la cascada de paleta es una ESCALERA sin empates suelo :where([data-{c}]) (0,0,0) < forward [data-{c}]:where([data-color], [data-color-custom]) (0,1,0) < tono [data-{c}][data-color='X'] (0,2,0). El orden de emision deja de decidir: la especificidad ES el contrato, legible en el artefacto (doctrina hermana de la FIRMA 12.9). - Emisor (render-css.ts): isPaletteSlotToken PARTE el bucket host - solo las ranuras de paleta bajan al suelo, el chasis del componente queda a (0,1,0). 49 suelos + 49 forwards reescritos + 0 pares viejos; bloques por tono (112), capa compartida y gradient finish byte-identicos. - Neutra en pixel, MEDIDO: ~297.000 valores computados antes/despues (105.248 planos lotes A+B, 57.720 planos lote C, 134.464 con los tonos ESTAMPADOS en las 49 unidades, incluidos 5 @active), 0 diffs reales. Los 17 crudos, probados ruido por reproduccion sobre codigo identico. - Guarda por MUTACION (active-eidos-config.test.ts): suelo con contenido, forward de un peldano, par viejo ausente, tono sin envolver, particion del host, cierre estructural (toda declaracion de paleta vive en un peldano) - 6 mutaciones inyectadas, 6 mordidas. El contrato del forward pasa de vigilar 2 recetas a 49 (privadas incluidas, multi-parte bien). - Ledger: PALETTE_SUPERSEDED muere (describia el mundo viejo); 207 de sus 353 claves pasan a VIVAS y las 146 restantes quedan bajo PALETTE_FLOOR (el tono-default estampado resuelve por el forward: quitar el atributo ES hablar en silencio) o TONE_UNREACHED (ningun paso del guard estampa tono Y hoverea), con razon medida. stepper y tag-group, barridos por primera vez. El detector STALE es CIEGO a patrones: la retirada fue manual y por estrechamiento, guiada por medida. - Docs: changelog 53 - reference THM-2 - next-features 13 (RESUELTA) - CONTINUE-theming. Supervision: 4 agentes Opus en 5 fases (baseline plano + baseline estampado, emisor, guarda, verificacion de parque completo, docs), con refutacion del supervisor sobre censo, emisor y guarda. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- **The ladder** (firma B′, 2026-08-24 — changelog §53): the three writers of
`--{c}-palette-*` sit on three rungs and **no rung ties another**, so the
order of emission never decides:
| rung | selector | specificity |
| --- | --- | --- |
| floor | `:where([data-{c}])` | `(0,0,0)` |
| forward | `[data-{c}]:where([data-color], [data-color-custom])` | `(0,1,0)` |
| tone | `[data-{c}][data-color='X']` | `(0,2,0)` |
Only the palette SLOTS ride the floor (`isPaletteSlotToken`); every other
host-scoped token of the recipe stays at `(0,1,0)`. Guarded by
`active-eidos-config.test.ts` ("palette cascade is a ladder").
- **Nesting safety**: the `:where([data-color], [data-color-custom])`
**presence** guard means a nested component with no color of its own matches
only the floor (its host default) and never reads a `--palette-*` a colored
ancestor set — the isolation the per-component prefix used to give, kept
without the ~21 KB × N it cost (the per-component rollout to all 17 surfaces
would have been ≈ +315 KB; the shared layer is ~19 KB once).
feat(theming): THM-2 — paleta universal por instancia (capa compartida + forward), 5/17 superficies El mecanismo genérico ya existía (universalPaletteDecls auto-completaba las 33 escalas para recetas con tokens palette-*), pero con PREFIJO de componente (--button-palette-*) — cada superficie pagaba ~21 KB, por eso solo button/toggle lo tenían (rollout a las 17 = ~+315 KB). Colapso al modelo compartido: - renderSharedPaletteLayer: UNA cascada component-agnóstica [data-color='{role|scale}'] { --palette-{slot}: … } — 8 roles + 33 escalas, emitida una vez (~19 KB). Roles 1:1 a --color-{role}-*; escalas con el step map + on-solid contrast, valor idéntico a la cascada per-component retirada. - renderRecipePaletteForward: toda receta con palette-* (público) o _palette-* (privado) recibe [data-{c}][data-color] { --{c}-palette-{slot}: var(--palette-{slot}, host-default) }. La guarda de PRESENCIA es la seguridad de anidamiento — un hijo sin color dentro de un ancestro coloreado matchea solo [data-{c}] (su default) y nunca lee el --palette-* heredado. - Generalización a la paleta PRIVADA: checkbox/radio-group/switch ganan las 33 escalas con CERO cambio de CSS (ya consumen --_{c}-palette-*). - universalPaletteDecls eliminado; 2 guards migrados al modelo compartido (recipe-css-contract "shared layer + forward"; active-eidos-config contrast en la capa compartida). DONE (per-instance): button, toggle, checkbox, radio-group, switch (5/17). Rollout de las 12 restantes en docs/process/continue-cleanroom-fixes §THM-2. Doctrina en theming/reference §25 ("The per-instance palette layer"). Verificado en vivo (button + checkbox): color="teal" → escala teal exacta · affirm → rol exacto · primary → primary · nested-nocolor en ancestro teal → primary (sin contaminación). Peso 345K → 329K. Gates: matriz 141/141 · eidos 314 · recipe-css-contract + active-eidos-config 98/98 · contracts 38/38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Wiring a component to the scales = declare `palette-*` / `_palette-*` tokens
(host default) and consume `--{c}-palette-*` in its recipe CSS. Guarded by
`recipe-css-contract.test.ts` ("emits the shared palette layer + a forward for
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
every palette-\* recipe"). Rollout status: the plan
feat(theming): THM-2 — paleta universal por instancia (capa compartida + forward), 5/17 superficies El mecanismo genérico ya existía (universalPaletteDecls auto-completaba las 33 escalas para recetas con tokens palette-*), pero con PREFIJO de componente (--button-palette-*) — cada superficie pagaba ~21 KB, por eso solo button/toggle lo tenían (rollout a las 17 = ~+315 KB). Colapso al modelo compartido: - renderSharedPaletteLayer: UNA cascada component-agnóstica [data-color='{role|scale}'] { --palette-{slot}: … } — 8 roles + 33 escalas, emitida una vez (~19 KB). Roles 1:1 a --color-{role}-*; escalas con el step map + on-solid contrast, valor idéntico a la cascada per-component retirada. - renderRecipePaletteForward: toda receta con palette-* (público) o _palette-* (privado) recibe [data-{c}][data-color] { --{c}-palette-{slot}: var(--palette-{slot}, host-default) }. La guarda de PRESENCIA es la seguridad de anidamiento — un hijo sin color dentro de un ancestro coloreado matchea solo [data-{c}] (su default) y nunca lee el --palette-* heredado. - Generalización a la paleta PRIVADA: checkbox/radio-group/switch ganan las 33 escalas con CERO cambio de CSS (ya consumen --_{c}-palette-*). - universalPaletteDecls eliminado; 2 guards migrados al modelo compartido (recipe-css-contract "shared layer + forward"; active-eidos-config contrast en la capa compartida). DONE (per-instance): button, toggle, checkbox, radio-group, switch (5/17). Rollout de las 12 restantes en docs/process/continue-cleanroom-fixes §THM-2. Doctrina en theming/reference §25 ("The per-instance palette layer"). Verificado en vivo (button + checkbox): color="teal" → escala teal exacta · affirm → rol exacto · primary → primary · nested-nocolor en ancestro teal → primary (sin contaminación). Peso 345K → 329K. Gates: matriz 141/141 · eidos 314 · recipe-css-contract + active-eidos-config 98/98 · contracts 38/38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
[`process/continue-cleanroom-fixes-2026-07.md`](../process/continue-cleanroom-fixes-2026-07.md)
§THM-2.
uix(color): Fase 5 — guard estructural + docs (CIERRA la iniciativa) Guard: test nuevo "keeps every component `*Color` prop open (THM-2 — the cage stays open)" en recipe-css-contract.test.ts — escanea los alias `export type XColor = …` de components/*/types.ts y FALLA si alguno se estrecha por debajo de ComponentColorProp. Uniones aditivas ('muted', 'inherit', 'absent'…) pasan; alias resuelven transitivamente (DatePickerColor = CalendarColor); WIP_TRACKS (chronos) re-entra con su track; exento comentado: OnionColor = string (MAS ancho que la jaula, normalizacion pendiente). Verde con el inventario real: 60+ alias abiertos. Docs (todo lo aplazado del track): - reference.md §25: reversion de los subconjuntos THM-2 registrada como doctrina (color = sistema completo en TODOS; identidad ≠ evaluacion; enforcement = el guard). - Tracker clean-room §THM-2: nota de reversion (los narrows que THM-2 dejo "por diseño" quedan abiertos, Avatar.Badge incluido). - changelog.md §43: cronica de la iniciativa completa (mecanica, fases, huecos señalados). - Handoff open-color-cage-2026-07.md → CERRADO con el resumen de la tercera sesion (cola completa, F4, F5, commits) y los 2 huecos pendientes de decision: chart (11 props ColorRole) y qr-code (color inline semi-abierto, invisible para el guard). El cuerpo queda como referencia de patrones A–E. - 13 READMEs con afirmaciones ya falsas corregidos (roles-only, subsets, "Locking to ColorRole", tablas de props): badge, css-field, field-langs, float-panel, link, listbox, mark, metrics (solo el icon; Chart delega en chart y queda como esta), password-field, proof-of-human (incl. la fila de decision "descartar" → "hecho (reversion)"), range-calendar, textarea, timeline. banner NO se toca (su ColorRole es del eje intent, otra cosa). Contract test: 29/29 verde. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Reversión de los subconjuntos (decisión de diseño, 2026-07-18).** THM-2
originalmente dejaba los controles semánticos en subconjuntos por componente
(`AffirmativeColorRole`, `ProgressiveColorRole`, …). La decisión de 2026-07-18
lo revierte: **`color` acepta el sistema completo — rol / intent / 33 escalas
donantes / valor CSS crudo (`ComponentColorProp`) — en TODOS los componentes,
sin excepciones** (incluida la tinta de contenido, que conserva su eje como
feat(color)!: una palabra, un significado - primary/secondary son SIEMPRE jerarquia La homonimia muere: `primary`/`secondary` significan JERARQUIA DE MARCA en el catalogo ENTERO, tambien dentro de los seis primitivos de tinta (text, heading, display, code, label, s-text). El paso de tinta del 82 % se llama `subtle` - PROP y TOKEN, sin excepcion: el nombre es el contrato en el artefacto (la misma ley que la FIRMA B' y la 12.9; se rechazo por escrito la via "solo el prop con tabla de traduccion" - las excepciones legitimas son las FIRMADAS con razon, no las inventadas al decidir para abaratar). - Los seis CONTENT_INK, IDENTICOS: {subtle, muted, disabled, on-solid}; el nivel-1 es el DEFAULT sin prop. `label` gana `on-solid` (cierra la divergencia de familia registrada en e4fa616e5). - Renombre completo: --color-content-secondary -> --color-content-subtle y la clave de config content.secondary -> content.subtle (RUPTURA de API de tema). 355 referencias por patrones acotados; la raiz SIN prefijo (cssVar('color-content-secondary'), render-css.ts) incluida; en grafito.ts conviven tres claves `subtle` (content/border/shadow) y solo cambio content. El generado, reproducido BYTE A BYTE aplicando solo el renombre al anterior. - Codemod value-preserving: 11 call sites (6 secondary->subtle incl. expresiones y ternarias; 5 primary->fuera, incl. TRES defaults de prop) con la condicion ROJA caracterizada (--style-caption-color es el UNICO estilo cuya tinta default no es primary; los 5 sitios caen en ramas body). alpha/ (untracked, TERMINATED) fuera. - Las 6 demos migradas al vocabulario nuevo (s-text: CERO cambios, no tiene lista de tinta); label arranca en el default y pinta IDENTICO (contrafactual medido: el estado viejo habria pintado el morado de marca). - Neutra en pixel, MEDIDO: nivel 1 ESTRICTO CERO en 71.200 valores (blocks enteros, s-text entero, subarboles data-*); nivel 2: 31 diffs, todos trazados a las ediciones firmadas del harness (anchos de chips, reindexado de la lista de label con las tintas supervivientes byte a byte). Total 171.712 valores + 59.072 de re-corridas a 0 de ruido. - Guards: CONTENT_INK identicos entre los seis y sin nombres de rol (mutacion: muerde), y el generado sin rastro del token viejo con las 6 definiciones del nuevo. Dos cronicas fechadas en docs/old-deprecated, intactas a proposito. - Docs: changelog 54 - reference (eje de tinta) - next-features 13 (divergencia de label RESUELTA). Supervision: verificador independiente pre-firma (dos claims MURIERON en verificacion antes de tocar codigo), tres agentes en fases (censo+baseline -> implementacion+codemod -> demos -> verificacion de dos niveles -> docs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
unión aditiva: `ComponentColorProp | 'subtle' | 'muted' | 'disabled' | 'on-solid'`).
**`primary` / `secondary` YA NO son tinta** (§54, 2026-08-24): significan
jerarquía de marca en todo el catálogo, el paso 82 % se llama `subtle` y el
nivel 1 de tinta es el DEFAULT, sin prop. La
uix(color): Fase 5 — guard estructural + docs (CIERRA la iniciativa) Guard: test nuevo "keeps every component `*Color` prop open (THM-2 — the cage stays open)" en recipe-css-contract.test.ts — escanea los alias `export type XColor = …` de components/*/types.ts y FALLA si alguno se estrecha por debajo de ComponentColorProp. Uniones aditivas ('muted', 'inherit', 'absent'…) pasan; alias resuelven transitivamente (DatePickerColor = CalendarColor); WIP_TRACKS (chronos) re-entra con su track; exento comentado: OnionColor = string (MAS ancho que la jaula, normalizacion pendiente). Verde con el inventario real: 60+ alias abiertos. Docs (todo lo aplazado del track): - reference.md §25: reversion de los subconjuntos THM-2 registrada como doctrina (color = sistema completo en TODOS; identidad ≠ evaluacion; enforcement = el guard). - Tracker clean-room §THM-2: nota de reversion (los narrows que THM-2 dejo "por diseño" quedan abiertos, Avatar.Badge incluido). - changelog.md §43: cronica de la iniciativa completa (mecanica, fases, huecos señalados). - Handoff open-color-cage-2026-07.md → CERRADO con el resumen de la tercera sesion (cola completa, F4, F5, commits) y los 2 huecos pendientes de decision: chart (11 props ColorRole) y qr-code (color inline semi-abierto, invisible para el guard). El cuerpo queda como referencia de patrones A–E. - 13 READMEs con afirmaciones ya falsas corregidos (roles-only, subsets, "Locking to ColorRole", tablas de props): badge, css-field, field-langs, float-panel, link, listbox, mark, metrics (solo el icon; Chart delega en chart y queda como esta), password-field, proof-of-human (incl. la fila de decision "descartar" → "hecho (reversion)"), range-calendar, textarea, timeline. banner NO se toca (su ColorRole es del eje intent, otra cosa). Contract test: 29/29 verde. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
identidad (`color`) sigue desacoplada de la evaluación (`invalid` + el
`intent` del evento). Enforcement estructural: el guard "keeps every component
`*Color` prop open" en `recipe-css-contract.test.ts` — un tipo `*Color` más
estrecho que `ComponentColorProp` rompe el build. Proceso completo:
[`process/open-color-cage-2026-07.md`](../process/open-color-cage-2026-07.md).
## 26. Runtime theme builder — `eidos.applyColorScheme` (2026-06-04)
Chronicle in [`changelog.md §26`](./changelog.md). Standing:
`buildScheme(seed, opts)` (pure, `lib/build-scheme.ts`) +
`ActiveEidos.applyColorScheme(seed, opts)` / `clearColorScheme()` — derives
a full scheme (roles + `a1..a12` alphas + APCA on-solid) from the active
theme and re-derives on mode change. Layers: `uix.color` = the math ·
`build-scheme` = pure composition · `ActiveEidos` = DOM application. API:
[`rfc-color-engine.md`](../rfcs/rfc-color-engine.md) §6.2/§7.
## 27. Wide-gamut OKLCH output (default-on) (2026-06-04)
Chronicle in [`changelog.md §27`](./changelog.md). Standing: every palette
step emits hex (the fallback) + an `oklch()` sibling that wins where
supported — default-on, no flag (`appendColorScaleDeclarations`,
`lib/render-css.ts`). The REAL wide gamut lives in the generator
(`buildScheme` keeps the unclamped OKLCH → `result.wideGamut`).
## 28. Forced-colors accessibility + the border ramp (2026-06-05)
Chronicle in [`changelog.md §28`](./changelog.md). Standing: the foundation
always emits `@media (forced-colors: active)` (focus via `outline` —
box-shadow dies in HCM) and `@media (prefers-contrast: more)` (borders and
de-emphasized text reinforced via `:root:root`); the `border` slot =
**step 7** of the scale (`DEFAULT_COLOR_ROLE_SLOT_STEPS`).
## 29. Depth — the unified, eventful channel (2026-06-05)
Chronicle in [`changelog.md §29`](./changelog.md). Standing: depth is ONE
channel — `data-depth='{plane}'` (`flush · raised · overlay · modal ·
recessed`) coheres surface + shadow + halo + z at rest, and the event
signature (`present-rise` / `press-squeeze`) moves it in the event-moment.
Config-driven planes (`EidosConfig.depth.planes`); overlays compose
`var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`. Canonical guide:
[`rfc-depth.md`](../rfcs/rfc-depth.md). Demo:
`/temas/profundidad`.
## 30. Shape — continuity + families + nesting + eventful (2026-06-05)
Chronicle in [`changelog.md §30`](./changelog.md). Standing: shape is a
channel — `--shape-smoothing` + `data-shape='{family}'`
(`rounded · continuous · cut · scoop`) over `corner-shape`, degrading to
the `border-radius` arc; nested harmony via `[data-shape-nest]`
(concentric); the squircle is the **surface tier's default**
(`renderShapeBlocks` `:where` + `--shape-surface-default`; `shape` =
opt-OUT). Runtime builder `applyShape(seed)`. Canonical guide:
[`rfc-shape.md`](../rfcs/rfc-shape.md). Demo:
`/temas/forma`.
## 31. Structure (space · density · scale) — space as rhythm (2026-06-05)
Chronicle in [`changelog.md §31`](./changelog.md). Standing: the three
structural axes are state-only — density (`data-density`), scaling
(`data-scaling`, §23) and space: `--space-{key}` =
`calc(value · var(--density-space-scale) · var(--scaling))`, regenerable
from a modular/fluid seed (`buildSpaceScale` + `applySpacing`). Canonical
guide:
[`rfc-structure.md`](../rfcs/rfc-structure.md).
Demo: `/temas/estructura`.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 32. Focus ring — the outline model (2026-06-11 · canonized 2026-07-07)
Chronicle in [`changelog.md §32`](./changelog.md). Standing: ONE focus
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
model — **`outline`** driven by the `--focus-ring-*` tokens
(`STATIC_FOCUS_RING`, `primitives/static.ts`); the foundation's ring excludes
field-internal elements. The box-shadow → outline migration for
surfaces/controls completed 2026-06-29; the component-audit checkpoint
(verdict S5, 2026-07-07,
[`audit/components/_veredictos.md`](../audit/components/_veredictos.md))
**canonized outline catalog-wide and retired the "fields stay on box-shadow"
clause** — the audit census found outline was already the de-facto model of
fix(cleanroom): P1s ejecutados (EID-1/EID-2/SEM-1/MOR-4/THM-1/THM-3) + auditoría + plan scene/ambient Clean-room fixes (2026-07-11, sobre los P1 de la auditoría): - EID-1: el anillo de foco de la fundación migra a `outline` (§32) — el último box-shadow del catálogo; :where()-wrapped para que las recetas siempre ganen; doctrina actualizada en theming/reference + changelog. - EID-2: opacidades literales de archetypes.css → var(--opacity-disabled/hover). - SEM-1: política de live-region unificada — la urgencia va SOLO por intent (threat/loss → assertive), runtime y AnnounceChannel ya coinciden. - MOR-4: card-group con morfo propio (scope eidos) + guard de enumeración. - THM-1: component-audit + recipe-css-contract escanean los *.css hermanos (calendar-select, color-picker-spectrum, date-range-picker-time…); R-2.1 caza también hsl()/oklch(); violaciones de los secundarios corregidas. - THM-3: canario de censo de toggle volteado (lint.test); commit-block promovido a firma global block-shake; regen de generated/base.css. Docs nuevos: - docs/audit-new-10-july/audit-active-uix.md — auditoría clean-room 2026-07-10 del ecosistema (hallazgos P1–P3 con evidencia ruta:línea, matriz y gap analysis). - docs/process/PLAN-scene-ambient-pack.md — plan por fases: motor arts/scene + pack Ambient (45 animaciones de demos) + vía de promoción agéntica (Aura); gate de decisiones D1–D9. - docs/Disenando_lo_que_ocurre_FINAL.{docx,pdf} — edición FINAL del libro. - README.md raíz (esqueleto de paquete). Excluidos deliberadamente de este commit: words/palabras/chronos (pista separada), web/routes/alpha (terminado, sin commitear por decisión), rt.css (volcado de referencia Radix Themes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
the entire catalog (the `field.css` remnant migrated in the fix phase; the
LAST box-shadow — the foundation's universal `[data-archetype]` fallback,
whose justifying app-layer `outline: none` reset no longer existed — migrated
2026-07-11, EID-1, now `:where()`-wrapped so recipes always win the shared
property). The reasons, now canonical: (1) `outline` survives
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
forced-colors/High Contrast Mode where box-shadow is stripped — the ring must
not vanish for the users who need it most; (2) segment-fields flicker with a
transitioned border/box-shadow on every increment (blur/refocus repaint) —
date-field's documented rationale; (3) alignment with the reference systems
(Radix Themes, Material 3 `md-focus-ring`, Chakra v3) — box-shadow rings were
the pre-2021 workaround for `outline` not following `border-radius`, fixed in
all modern browsers.
## 33. Themeable stepper glyphs (`spin-field`) — 2026-06-11
Chronicle in [`changelog.md §33`](./changelog.md). Standing: the stepper's
glyphs (`spin-field`) are themeable recipe tokens, not hardcoded SVG.
## 34. `spin-field` — the stepper-field's shared visual (`number-field` / `css-field`) — 2026-06-11
Chronicle in [`changelog.md §34`](./changelog.md). Standing: `number-field`
and `css-field` share ONE visual layer via the structural identity
`data-spin-field*` (`components/spin-field/spin-field.css`, aggregated in
`index.css`) — no clone.
## 35. The scale canon — the theming audit (2026-06-15)
Chronicle in [`changelog.md §35`](./changelog.md). Standing: every scale
axis (blur · inner-shadow · inset-ring · gradients · breakpoints ·
container · opacity — dual numeric+semantic · border-width · tracking) is
theme-retunable and **recipes consume the token, never a literal** (the
R-2.x/R-4.x guards). The inventory lives in `EidosConfig`
(`lib/primitives/*` + `config-types.ts`) and the naming table in §6;
breakpoints source from `ActiveDom`. The size→font mapping is the universal
1:1 of §5 (the audit's `control · compact · dense` size archetypes were
superseded by the 2026-06-17 override).
## 36. The canonical trigger→panel gap — a token-driven offset (2026-06-22)
Chronicle in [`changelog.md §36`](./changelog.md). Standing: the
trigger→panel gap is an OFFSET of the positioning engine via
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`@property --floating-gap` — menus `--space-1`, panels `--space-1-5`
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_(menus moved 0 → `--space-1` on 2026-06-28; this stub had kept the old
value — dates arbitrate)_. Since 2026-07-07 the two gaps are config data:
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`primitives.floating` (changelog §40).
## 37. Touch-target — 44px on touch, pointer-gated (2026-06-28 · rev. 2026-07-08 · confirmed 2026-07-10)
Chronicle in [`changelog.md §37`](./changelog.md). Standing: the 44px touch
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
target applies ONLY under `@media (pointer: coarse)` — desktop keeps its
compact density. The value is a single hard constant, `--touch-target: 44px`
(WCAG 2.5.5 AAA · Apple HIG), defined once in `archetypes.css` `:root` and
shared by every consumer (button-archetype floor, radio-group row, the
checkbox/switch slop).
**Área ≠ visual (rev. 2026-07-08).** The touch target is a property of the
interactive AREA, decoupled from the painted VISUAL — the reference-grade model
(React Aria's component-height box + full-fill input; Material Web's
`mdc-touch-target` pseudo-element). Two mechanisms by control shape:
- **Button-like controls** (`trigger`/`close`/`action`/`[data-button]`): the
control IS the target, so a `min-block-size`/`min-inline-size: var(--touch-target)`
floor on the box is correct (grows only xs/sm/md; lg/xl already ≥ 44).
- **Small markers** (`checkbox`/`switch`): these are `<button>`s that paint a
small visual (box/track), so a `min-*` floor would grow the VISUAL. Instead a
transparent `::before` grows only the AREA to `max(100%, var(--touch-target))`
— the painted box/track keeps its size-variant visual. Being absolute it adds
no layout (no neighbour-collision margin needed). `radio` keeps its labeled
ROW (`[data-radio-group-row]` `min-block-size`) — the label text supplies the
horizontal extent.
A single floor (44 AAA), not a per-size AA/AAA split: no reference framework
does the split, and the nearest analog (MUI's small variant falling below the
target) is treated as a defect.
**Confirmed 2026-07-10** (user decision — chronicle in changelog §37): the
`::before` slop is the standing mechanism for bare markers (the
Material/Android/iOS lineage). Two riders: (1) dense coarse stacks must keep
pitch ≥ `--touch-target` — WCAG excludes overlapped area from measurement, so
spacing is part of the contract (bare boxes still hold AA via the 2.5.8
Spacing exception in the worst case); (2) the labeled-row task on Field stays
open as the ADDITIVE improvement — once a marker has a labeled row, the ROW is
the real AAA target (2.5.5 Equivalent, the React Aria pattern) and the slop
remains as a harmless net.
## 38. The state layer — unified neutral feedback (2026-06-28)
Chronicle in [`changelog.md §38`](./changelog.md). Standing: neutral
interactive feedback (hover/press/selected) is the MD3 state layer — the
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`--state-hover` / `--state-press` / `--state-selected` veils composed as
`background-image: linear-gradient(...)`; bespoke per-component hovers are
deprecated (R-4.3). Since 2026-07-07 the magnitudes are **config data**
(`primitives.state`, generator-emitted — changelog §40); `archetypes.css`
keeps only the RULES.
eidos(gradient-finish): acabado anclado a la sombra de la tinta - v1 Button+Badge + dial temable + guard El gradiente entra al sistema como ACABADO (material) del fill, jamas como valor del eje de color: prop `gradient` -> attr eidos-only `data-gradient` (familia data-variant; NO morfo - el runtime lo resolveria desde props de soma y mergeProps clobberea el stamp del wrapper). - Fill por capas: background-color = base solida (degrada sola en forced-colors) + background-image = rampa derivada de los slots de LA instancia (roles + 33 escalas + custom gratis, tinta heredada). - Dial unico de tema: primitives.gradientFinish.lift (26%) -> --gradient-finish-lift (0% = apagado; override por instancia via cascada). - RAMPA ANCLADA a la sombra de la tinta ("la rampa huye de la tinta"): tinta blanca -> #000 fuerte abajo (CTA sombreado); tinta oscura -> #fff fuerte arriba (glossy). Ancla+angulo por color x modo con el MISMO flip del slot contrast. Rectificacion MEDIDA: el lift global hacia blanco rompia la tinta heredada en 52/84 combos a 26% (techo global 0%) - el contraste ahora solo puede mejorar: dial sin topes. - Guard ejecutable gradient-finish-guard.test.ts (3/3): no-regresion <=40% sobre 84 combos + set flat-fail clavado (cyan/orange, deuda on-solid preexistente). - Generador emite la VAR (--_{c}-fill-finish); el recipe pinta (solid + re-assert en hover: su shorthand background resetea el longhand). - Lab temas/gradientes: 9 casos con componentes reales sobre el token real (dial en vivo, polaridad observable, evidencia de pasos 7/11, forced-colors). - Docs: capitulo theming/gradient-finish.md (registro de decisiones D1-D9, alternativas rechazadas con evidencia, leccion de proceso: medir ANTES de fijar defaults) + reference.md paragrafo 39 + changelog paragrafo 42 + plan docs/process/gradient-finish-plan-2026-07.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
## 39. Gradient finish — the derived ramp treatment (2026-07-15)
Full chapter (design, alternatives rejected with evidence, the measured
rectification, decision register D1–D9):
[`theming/gradient-finish.md`](./gradient-finish.md). Standing summary:
**Doctrine: a gradient is a FINISH (material) of the fill, never a color
IDENTITY.** `color` keeps saying WHO the instance is (role / scale / custom);
the `gradient` prop says how its saturated fill is painted. The taxonomy:
color = identity · variant = wearing · depth/frost = material · state layer =
feedback → the gradient joins as MATERIAL. A gradient value must never enter
the `data-color` axis: an `<image>` cannot honor that axis' contract (10
mechanically-derived slots + every variant can express the value). The full
design history (the rejected color-value design, the reference-framework
research, the step-lattice analysis) lives in
`docs/process/gradient-finish-plan-2026-07.md`.
Mechanics (v1, Button pilots — `GRADIENT_FINISH_COMPONENTS` gate in
`render-css.ts`):
- **The layered fill.** `background-color` stays the solid base (it is what
`forced-colors` keeps: the UA strips every non-`url()` `background-image`,
so the finish degrades to today's solid button for free). The finish is a
`background-image` layer.
- **The ramp derives from the instance's own palette slots** — no per-color
catalog: `color-mix(in oklch, …)` over `palette-solid` /
`palette-solid-hover`. It re-tints with roles, the 33 donor scales and
light/dark automatically, and the INK stays the inherited
`palette-contrast` (steps 7/11 were rejected as endpoints: they are
border/text slots, mode-relative, and invert in dark).
- **The ramp is ANCHORED to the ink's shadow side** — the doctrine in one
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
sentence: _la rampa huye de la tinta_. White-ink colors deepen toward
eidos(gradient-finish): acabado anclado a la sombra de la tinta - v1 Button+Badge + dial temable + guard El gradiente entra al sistema como ACABADO (material) del fill, jamas como valor del eje de color: prop `gradient` -> attr eidos-only `data-gradient` (familia data-variant; NO morfo - el runtime lo resolveria desde props de soma y mergeProps clobberea el stamp del wrapper). - Fill por capas: background-color = base solida (degrada sola en forced-colors) + background-image = rampa derivada de los slots de LA instancia (roles + 33 escalas + custom gratis, tinta heredada). - Dial unico de tema: primitives.gradientFinish.lift (26%) -> --gradient-finish-lift (0% = apagado; override por instancia via cascada). - RAMPA ANCLADA a la sombra de la tinta ("la rampa huye de la tinta"): tinta blanca -> #000 fuerte abajo (CTA sombreado); tinta oscura -> #fff fuerte arriba (glossy). Ancla+angulo por color x modo con el MISMO flip del slot contrast. Rectificacion MEDIDA: el lift global hacia blanco rompia la tinta heredada en 52/84 combos a 26% (techo global 0%) - el contraste ahora solo puede mejorar: dial sin topes. - Guard ejecutable gradient-finish-guard.test.ts (3/3): no-regresion <=40% sobre 84 combos + set flat-fail clavado (cyan/orange, deuda on-solid preexistente). - Generador emite la VAR (--_{c}-fill-finish); el recipe pinta (solid + re-assert en hover: su shorthand background resetea el longhand). - Lab temas/gradientes: 9 casos con componentes reales sobre el token real (dial en vivo, polaridad observable, evidencia de pasos 7/11, forced-colors). - Docs: capitulo theming/gradient-finish.md (registro de decisiones D1-D9, alternativas rechazadas con evidencia, leccion de proceso: medir ANTES de fijar defaults) + reference.md paragrafo 39 + changelog paragrafo 42 + plan docs/process/gradient-finish-plan-2026-07.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
`#000` with the strong stop DOWN (the classic shaded CTA); dark-ink colors
(light solids: amber/yellow/lime…) lift toward `#fff` with the strong stop
UP (glossy). Anchor + direction resolve per color × mode by the SAME flip
the `contrast` slot rides (`--color-{role}-finish-anchor/-angle` at theme
time; light-solid membership for the 33 scales), and both stops move toward
the anchor — so the label's contrast can only IMPROVE over the base solid
pairing: **constructive safety, unbounded dial**. Why not a global
lift-toward-white: measured 2026-07-15 over 84 combos (9 roles + 33 scales
× light/dark, floors APCA ≥ 60 ∧ WCAG ≥ 3) — it breaks the inherited ink on
52/84 at 26% and the global safe ceiling is 0% (teal 1%, gray 15%,
dark-mode blues/greens 0%). The anchored form: 0 regressions up to 40%.
- **ONE theme dial**: `--gradient-finish-lift` (default `26%`), emitted from
`primitives.gradientFinish.lift` — config data like the state-layer
magnitudes (§38). `0%` ≈ finish off; per-instance override is free via the
custom-property cascade, and a theme overrides it by declaring
`gradientFinish.lift` in its `ThemeDefinition` (re-emitted in the theme
block — light/dark can run different intensities).
- **The `spread` kind (v1.5)**: `gradient="spread"` rotates hue
±`--gradient-finish-spread` (default `30`, UNITLESS — the relative-color
`h` channel is a `<number>`; `deg` computes `none`) at constant L/C, with
the ramp's weak anchor mix on both stops (pure constant-L broke grass/gold
— measured; anchored: 0 regressions ≤ ±45°, pinned in the guard). ≈Flat on
achromatic identities (grays). Full record: D10 in
eidos(gradient-finish): v2.2 named finishes + on/data-on - EL PLAN COMPLETO D11 - Named finishes (el diseno A rescatado como opt-in explicito): - gradientFinish.named opta gradientes del open cage como acabado, cada uno con su tinta AUTORADA obligatoria (--gradient-{name}-ink, override de la convencion --_{c}-fg a especificidad (0,3,0) sobre el slice de variante). - La base sigue siendo el solid de la identidad (fill por capas, D2) -> el aurora shipped son blobs de rol con alfa SIN color base final: background-image-valido POR ARQUITECTURA (el caveat del mesh disuelto, no exceptuado), y re-tine por tema y modo via referencias de rol. - Honestidad documentada: la validacion numerica al peor stop de un string CSS arbitrario no es implementable (stops desconocidos); posible solo para gradientes de modelo (buildGradient) - diferida. El guard clava la sanidad del config (named subconjunto de gradients + ink presente). D12 - on/data-on minimo (Surface on="light|dark"): - La foundation re-vincula --color-content-*/--color-border-default para el subarbol (dark -> tinta on-solid; light -> on-solid-contrast; mixes oklch 82/64/32%). Verificado EN VIVO: un parrafo muted dentro del aurora computa la tinta del contexto al 64%. - Limites POR CONSTRUCCION y documentados en cada consumidor: componentes anidados con tokens propios y contenido portaleado NO se re-entintan (la inversion completa sigue siendo iniciativa independiente). forced-colors: el contexto resuelve a CanvasText/GrayText (bloque extendido). Sin color-scheme a proposito (solo chrome UA). Ademas: prop gradient ampliada a (string & {}) para named en Button/Badge/ Surface; poda del orphan-guard (border/text de Surface: 3 slots x 8 colores, lo que las variantes consumen); guard 5/5; audit 145/145; lab CASO 07 con el aurora REAL (<Surface gradient="aurora" on="dark">) verificado por valores. Docs: capitulo D11+D12 + registro D1-D12 + roadmap COMPLETO; reference p39; changelog p42; plan cerrado (quedan declaradas: inversion completa, demo propia de Surface, validacion de modelo para tintas named). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
[`gradient-finish.md`](./gradient-finish.md).
- **Named finishes (v2.2, D11)**: `gradientFinish.named` opts open-cage
gradients in as finishes (`gradient="aurora"`), each with its REQUIRED
authored ink (`--gradient-{name}-ink`, overriding the recipe's `--_{c}-fg`
convention); the base stays the identity's solid — alpha-blob materials
need no trailing base and stay `background-image`-valid by architecture.
- **The `Surface` primitive + `on` (v2.1/v2.2, D12)**: the themeable canvas
(Box + treatment) with the MINIMAL subtree ink context — `on="light|dark"`
re-binds the global content/border roles for plain content (nested
components and portals excluded by construction; forced-colors →
`CanvasText`). The invariant is pinned executable in
eidos(gradient-finish): acabado anclado a la sombra de la tinta - v1 Button+Badge + dial temable + guard El gradiente entra al sistema como ACABADO (material) del fill, jamas como valor del eje de color: prop `gradient` -> attr eidos-only `data-gradient` (familia data-variant; NO morfo - el runtime lo resolveria desde props de soma y mergeProps clobberea el stamp del wrapper). - Fill por capas: background-color = base solida (degrada sola en forced-colors) + background-image = rampa derivada de los slots de LA instancia (roles + 33 escalas + custom gratis, tinta heredada). - Dial unico de tema: primitives.gradientFinish.lift (26%) -> --gradient-finish-lift (0% = apagado; override por instancia via cascada). - RAMPA ANCLADA a la sombra de la tinta ("la rampa huye de la tinta"): tinta blanca -> #000 fuerte abajo (CTA sombreado); tinta oscura -> #fff fuerte arriba (glossy). Ancla+angulo por color x modo con el MISMO flip del slot contrast. Rectificacion MEDIDA: el lift global hacia blanco rompia la tinta heredada en 52/84 combos a 26% (techo global 0%) - el contraste ahora solo puede mejorar: dial sin topes. - Guard ejecutable gradient-finish-guard.test.ts (3/3): no-regresion <=40% sobre 84 combos + set flat-fail clavado (cyan/orange, deuda on-solid preexistente). - Generador emite la VAR (--_{c}-fill-finish); el recipe pinta (solid + re-assert en hover: su shorthand background resetea el longhand). - Lab temas/gradientes: 9 casos con componentes reales sobre el token real (dial en vivo, polaridad observable, evidencia de pasos 7/11, forced-colors). - Docs: capitulo theming/gradient-finish.md (registro de decisiones D1-D9, alternativas rechazadas con evidencia, leccion de proceso: medir ANTES de fijar defaults) + reference.md paragrafo 39 + changelog paragrafo 42 + plan docs/process/gradient-finish-plan-2026-07.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
`src/uix/eidos/gradient-finish-guard.test.ts` (no-regression ≤ 40% + the
pre-existing flat-fail set — cyan/orange mid-tones, an on-solid reality —
must not grow).
- **Generator emits the VAR, the recipe paints.** `renderRecipeGradientFinish`
emits `--_{c}-fill-finish` under `[data-{c}][data-gradient]`; the recipe
paints it on its saturated fill (`[data-variant='solid']`) and re-asserts on
`:hover` (the hover rule uses the `background:` shorthand, which resets the
longhand). Inert on soft/outline/ghost — no saturated fill to finish, and
no broken contract because `gradient` never claimed to be the identity.
- **`data-gradient` is an eidos-only WRAPPER attr** (the
`data-variant`/`data-size` family), stamped by the eidos wrapper and NOT
morfo-declared. Found live: a morfo-declared attr resolves from SOMA's prop
space — the pure-visual `gradient` prop doesn't exist there, so the runtime
emits `undefined` and `mergeProps(restProps, state.props)` clobbers the
wrapper's stamp. The `data-color-custom` precedent (Card) differs because
its prop DOES flow through soma. Rule of thumb: morfo declares an attr only
when its driving prop crosses the soma boundary.
The NAMED gradients (`--gradient-{name}`, the open cage) remain a separate
axis: scenography material (v2 queue: named finishes with authored ink +
the `Surface` primitive for aurora/mesh). Live lab: `web/routes/temas/gradientes`.
## 40. Tonal-ramp contrast contract — the slot-pair floors (Stage 1, 2026-07-19)
Chronicle + the measured drift in [`changelog.md §44`](./changelog.md);
initiative registry [`next-features.md §1`](../next-features.md). Standing
doctrine — which slot pairs the framework promises to clear, and which are
intentionally subtle. The ratified pair table now lives as DATA (`$color` →
`CONTRAST_PAIRS`, `arts/color/contrast-contract.ts`), a single source shared by
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara `ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica: ni el commit (fb5dd0919, «Advance Eidos demos and form validation docs»), ni un comentario, ni un documento. No fue una decisión de excluirlo: fue una omisión, y el 2026-06-27 se fosilizó al añadir HierarchyColorRole AL LADO en vez de arreglarla. Qué fichero importara de dónde decidía si tertiary era legal. La deriva era SÓLO de tipos: el runtime nunca divergió. resolveComponentColor une COLOR_ROLES + PALETTE_SCALES (42), la capa compartida emite la fila [data-color='tertiary'] iterando COLOR_ROLES, y 94 ficheros pasan por ese resolvedor. O sea que el tipo rechazaba en compilación un valor que el motor resuelve. Por eso ensanchar 8 -> 9 es aditivo: check queda en la línea base exacta (73 errores / 53 warnings) y los 362 tests de eidos pasan. - eidos/lib/types.ts re-exporta ambos tipos de config-types, donde derivan del const. El docblock deja escrito el porqué para que no vuelva por ignorancia. - Guard nuevo en recipe-css-contract.test.ts: falla si un fichero de eidos/lib vuelve a deletrear >=2 nombres de rol como literales. Verificado con control negativo — caza el texto del bug original y el HierarchyColorRole a mano; permite el Extract<> legítimo, el re-export y uniones ajenas al color. Documentación del color, reconciliada contra el código: - reference.md §4 «Per-component subset» seguía enseñando los subconjuntos por componente que §25 derogó el 2026-07-18, con tabla y justificación, sin marca. Marcada REVOKED; conserva el arbitraje (intent evaluativo gana), que es lo que sigue vivo. - `tertiary` figuraba como RESERVADO y no consumido en dos sitios (reference §4 y el comentario de themes/base.ts) cuando entra en ComponentColor, lo acepta todo prop color abierto y tiene su fila en la capa compartida. - rfc-color-engine §13 decía «the 9 --color-{role}-{slot} slots»; son 12 (COLOR_ROLE_SLOTS). Ahora enlaza el const en vez de copiar el número. - El frontmatter del mismo RFC daba la fase 5 por «gated» cuando su `border` 6->7 aterrizó el 2026-06-05 (reference §28); y §9 leía como lista de defectos vivos cuando sus cuatro filas están hechas. - reference §40 se contradecía en una línea: llamaba a CONTRAST_PAIRS fuente compartida «con el Stage-2 solver» y 40 líneas después declaraba que Stage 2 cerró SIN solver. - 8 citas a «THEMING §25.5» apuntadas a §25: esa numeración sobrevivió en la crónica (changelog §25.5) pero no en la referencia tras la escisión. - JSDoc de avatar y card documentaba su prop como «Canonical ColorRole» (8) cuando el tipo real es ComponentColorProp (42 + CSS crudo) — justo lo que lee un consumidor. - «8 roles» hardcodeado en callout/README.md (error de docs:check), callout.svelte y mark/README.md: enlazan el const, como manda authoring.md. docs:check baja de 2 errores a 1. Y una anotación, no un arreglo: banner/types.ts declara `BannerIntent = ColorRole`, fundiendo el eje evaluativo con el de pintura (primary/secondary no son evaluativos), y Banner no tiene prop `color` en absoluto. Queda marcado en el docblock como decisión pendiente con sus citas — resolverlo es el otro frente (un solo eje de color arbitrado por intent), y no está decidido. Fuera de este commit: src/uix/blocks/banner/ es un directorio entero sin trackear (trabajo en vuelo), y ahí quedan las dos últimas instancias del conteo hardcodeado, incluido el error que le queda a docs:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
the audit and the CI guard (Stage 2 shipped no solver — see the closing note). Measured by `scripts/contrast-audit.ts` (WCAG
2 gate + APCA Lc, over the 33 scales × 2 modes, plus a morph-generated
regression bank), reusing the on-solid math (§8 of `rfc-color-engine.md`,
`lib/on-solid.ts`).
**Text.** Two tiers, by the inherited Radix contract:
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `text·11` is the **secondary / low-contrast** ink (≈APCA 60) — marginal
sub-4.5:1 on the muddy light-mode scales (bronze/orange/teal/gold…), by design.
- `text·12` (`text-strong`) is the **AA-guaranteed body ink** (≥4.5:1 on every
scale × mode). **For AA-critical text, consume `text-strong`, not `text`.**
**Borders — two tiers (WCAG 1.4.11).** The whole border vocabulary lives in the
subtle 4–8 range (Radix's border steps); only `solid·9` clears 3:1 by
construction.
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Decorative (EXEMPT — not a sole indicator):** the per-scale accent
`border·7` (§28), the semantic `subtle·4` / `default·6`, and any resting
border on a control that also carries a fill. Subtle on purpose; the fill +
content carry the boundary. Do NOT hold these to 3:1.
- **Load-bearing (3:1 target):** checked/selected (`solid·9` — clears it), the
error border and hover/ghost border (measure sub-3:1 but ride a redundant cue
— error text, background — so they're exempt where that cue exists), and the
focus ring (below).
**Focus — a config axis, single `outline` (§32).** The focus ring is the one
always-sole indicator. Its appearance is parameterised, NOT hardcoded:
`primitives.focusRing` (`{ offset, width, innerWidth }`, `primitives/static.ts`)
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `color.focus.{ring,ringError}` (the theme), emitted ONCE as `--focus-ring-*`
tokens (`render-css.ts`). `innerWidth: 0` = single ring (default); a theme raises
it for a second inset line. The shipped default (`primary·8 @ ~50%` translucent)
measures sub-3:1 as a raw ratio; hardening it (opaque color, `innerWidth > 0`,
offset) is a **default-VALUE decision via config, never a per-component CSS
change** — and it stays a single `outline` (§32 canonized outline over
box-shadow: it survives forced-colors/HCM, avoids segment-field flicker).
**Stage 2 (CLOSED 2026-07-20 — no solver).** The plan proposed a generator that
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_solves_ each step's luminance to satisfy this table for any seed. Execution
disproved the premise: the template morph **inherits** text contrast (steps 11/12
copy the donor's L-curve verbatim; L-driven contrast is ~chroma-invariant under
gamut-mapping), so any scale generated from a §40-compliant donor library clears
these floors **by construction** — verified across the authored base, a
leave-one-out regeneration, and 45 out-of-distribution seeds (0 hard-gate
failures, min WCAG 9.7:1). The luminance solver was dropped as speculative. What
shipped: this table as shared data (`CONTRAST_PAIRS`), the audit consuming it + a
morph-generated regression bank, and a CI guard (`eidos/lib/contrast-invariant.test.ts`)
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa El canon de nombres llevaba meses documentado y sin guard, y la medición del 2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %. Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que vuelva. R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`, modificador interactivo DELANTE, y detrás lo dimensional y contextual. No reimplementa la gramática: la consume de `theming-census --names`, que es la misma fuente sobre la que corrió el codemod. Dos implementaciones de una gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa factura con `commit-resize`, un hook muerto tres meses en una receta con todos los tests en verde. Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y renombrar lo condenado es churn. Muta-prueba de tres caras, que es lo único que distingue un guard de un guard que pasa sobre el vacío: `-bg-hover` con valor de acento → ROJO `trigger-color` → ROJO `primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone el modificador detrás por construcción) La tercera es la que importa: es el fallo que un codemod ingenuo habría cometido sobre las 47 claves de rol, `button` entero incluido. Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le faltaban (tinta y estado) más la frase que las gobierna y las dos familias con gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que acota el principio de plataforma del px/py a los ejes dimensionales. El checklist de cierre declara la regla — lo cazó `docs:check` con su propio guard I5, que exige que toda regla del audit esté declarada allí. Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde `PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando la migración a la capa de estado las vacíe; son dos líneas entonces. component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres sin tocar por esto, R-5.3 pasa en los 166) docs:check 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
that locks the inheritance. With D2 = _measured-pass_ the BASE stays **verbatim
ground-truth** (audit-guarded, no base→seeds migration). Plan + outcome:
[`process/contrast-stage2-plan-2026-07.md`](../process/contrast-stage2-plan-2026-07.md).
---
**Last revision**: 2026-07-19 (contrast contract §40). If anything
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
in this doc disagrees with the code, the code wins — but open an issue so we
update the doc.

Powered by TurnKey Linux.