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/fable-eidos-audit.md

774 lines
54 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Auditoría Fable — Eidos y el sistema de theming
> **Fecha**: 2026-07-01 · **Rama**: `alpha-0.1-sec-dom` · **Auditor**: Claude Fable 5
> **Alcance**: la capa eidos completa — foundation generada, colores, superficies,
> tipografía, sizes/espaciado/densidad/scaling, elevación (shadow/depth), forma, motion,
> focus/estado/touch, el theme builder runtime (`apply*`) y su documentación (THEMING.md,
> TSC.md, THEMING_GUIDE, components/README, eidos-motion.md).
> **Método**: lectura directa de THEMING.md (secciones canónicas + §20–§38),
> `lib/render-css.ts` (parcial), `lib/recipes/base.ts` (4.459 líneas, por barridos),
> `lib/themes/base.ts`, `lib/primitives/{static,typography}.ts`, `lib/build-scheme.ts`
> (completo), `archetypes.css` (completo), `events.css` (completo),
> `active-eidos.svelte.ts` (auditado en `fable_audit.md`), más **barridos cuantitativos
> por grep sobre los 135 CSS de componentes y el foundation generado**. Cada cifra de
> este informe sale de un grep reproducible, no de la documentación.
> Complementa (no repite) los hallazgos E1–E6 de [`fable_audit.md`](fable_audit.md).
---
## 1. Resumen ejecutivo — la hipótesis se confirma, con matiz
La sospecha del encargo era: *"hay incoherencias, no un esquema coherente; se ha ido
desviando con cada modificación y conceptualmente no hay una guía con la que se
implementen los componentes"*. El diagnóstico tras la evidencia:
**El núcleo está bien y está protegido.** El modelo de color (33 escalas → 9 roles →
slots → palette por componente), el TSC, la tokenización de ejes crudos (blur, border,
z-index de overlays, tracking) y los guards mecánicos que existen (`recipe-css-contract`,
guard tipográfico anti-literal, guard de z crudo, lint de variants) funcionan: en 135
ficheros CSS de componentes hay **0 usos de `--focus-ring` legacy, 1 solo fichero con
hex crudos, 0 blur literales, 5 duraciones ms sueltas, 1 uso directo de `--scale-*`**.
Donde hay guard, no hay deriva.
**La deriva es real, pero no está en los tokens: está en los SISTEMAS.** Eidos es hoy una
federación de **~14 subsistemas canónicos añadidos por sprints fechados** (densidad,
scaling, depth, shape, structure, focus de dos anillos, state-layer, touch-target,
list-surface, gap flotante, motion de dos momentos, tipografía 1:1, opacidad unificada,
gradientes) donde **cada uno declara ser "el canon" y ninguno alcanzó adopción completa**:
| Sistema canónico (fecha) | Promesa | Adopción real medida |
|---|---|---|
| State-layer `--state-*` (§38, 06-28) | sustituir ~168 hovers bespoke | **21/135** CSS lo usan; **22 tokens `*-bg-hover`** siguen declarados en recipes; el propio `archetypes.css` mantiene el idiom viejo en `close:hover` (opacity-dim 0.85) |
| Depth channel `--depth-*` (§29, 06-05) | "canal unificado, no tres sistemas sueltos" | 7 CSS + 6 refs en recipes componen el canal; el resto sigue en la escala `--shadow-*` directa; adopción por `data-depth` = "opción futura" → **dos sistemas conviven** (corrección 2026-07-02: los `box-shadow` de componentes SÍ están tokenizados — R-4.1 = 0 literales; el hallazgo es la partición escala-vs-canal, no literales) |
| Bundle `--size-{k}-*` (§5) | "map global coordinado" | **0 consumidores** (confirmado hoy por grep; el doc lo admite: "huérfano") |
| Opacidad unificada (§35, 06-15) | "~10 valores de disabled → 1" | 28 reglas usan `--opacity-disabled` (0.4)… y `archetypes.css` hardcodea **0.5 tres veces**, contradiciendo su propio token (corrección 2026-07-02: fuera de `@keyframes` solo quedan **2 componentes** con literal — el grueso de los 20 matches iniciales eran fades de keyframes, exentos) |
| list-surface (06-21) | ritmo único de menú/lista | **4/135** adoptantes; select/combobox "recipe-level pending" |
| Motion 2 momentos (F1–F7) | keyframes/firmas en config, themeables | **49 `@keyframes` locales en 25 CSS** fuera del canal; 14 componentes usan presets (`data-animation-style` — corrección 2026-07-02; dropdown/context-menu están en AMBAS listas: presets Y keyframe local). El repositorio (verificado): ~40 keyframes + 12 firmas + 13 presets de estado con `bySide`/`reduce` + 4 loops — cubre lo que los 15 del backlog re-implementan a mano (`slide-fade`, `collapse`, `shared-axis-x`) |
| Alpha scales a1..a12 (README) | tintas translúcidas para overlays/rings/hovers | **0 consumos directos** de `--primitive-*-aN`/`--scale-*-aN` en recipes+componentes; solo a2/a3 viven indirectamente vía los slots `surface`/`surface-hover` → **~500 tokens emitidos sin consumidor** |
| Slots de color emitidos | 14 slots × rol | `text-strong`, `separator`, `border-hover` = **0 usos**; `hover` = 8, `active` = 1 (sobre 36 emisiones cada uno) |
**Y la causa raíz es exactamente la que el encargo intuía**: no existe un **contrato por
componente** que diga "una recipe DEBE consumir estos sistemas" — el `component-audit`
mecánico cubre 10 reglas (R-1.x a R-3.x) anteriores a junio y **ninguna de las nuevas
capas canónicas** (state-layer, focus, touch, depth, list-surface). Cada sistema nuevo se
desplegó como sprint manual (pilot + N componentes) que se detuvo dejando una sección
"Pendiente", y THEMING.md creció como **changelog fechado disfrazado de referencia**
(§20–§38 son sprints; §5 contiene dos doctrinas revocadas inline; §13–§14 están
"superseded" pero siguen dentro; §1 dice "5 layers de CSS" y §2 dice "6 capas").
**Veredicto**: coherencia del foundation **8.5/10** · coherencia de sistemas transversales
**5/10** · theme builder **7/10** (bugs concretos abajo) · documentación como guía de
implementación **4/10**. No hace falta rediseñar: hace falta **cerrar adopciones,
canonizar el contrato de recipe como regla mecánica, y partir THEMING.md**.
---
## 2. El patrón de deriva (diagnóstico estructural)
Los cinco mecanismos que producen la incoherencia, todos observables en el repo:
1. **Canon por acreción, no por sustitución.** Cada sprint añade un sistema "canónico"
sin retirar el anterior: elevación tiene hoy `--shadow-{1..6}` + aliases semánticos +
`--depth-{plane}-*` conviviendo. Feedback neutro tiene state-layer + surface-swap +
opacity-dim + 22 tokens bespoke. Translucidez tiene alpha-scales + `color-mix(...%,
transparent)` + `--opacity-*`. **Varios idiomas por concepto es la norma, no la
excepción.**
2. **Rollouts que se detienen en el "Pendiente".** §38 ("Pendiente: poda de bg-hover +
tabs + color-picker"), §29 ("adopción plena vía data-depth queda como opción futura"),
§5 ("el refactor a consumir el bundle queda como follow-up"), list-surface ("select/
combobox pending"). El parking-lot es sistemático y nada lo re-visita.
3. **El doc de referencia es un changelog.** THEMING.md §20–§38 son informes de sprint
con fechas, incidentes (¡el de PowerShell!), commits y doctrinas revocadas conservadas
inline. Un implementador nuevo no puede extraer de ahí "qué debe hacer mi recipe hoy"
sin arqueología.
4. **Guard-coverage asimétrico.** Donde hay guard mecánico (tipografía literal, z crudo,
variants, recipe-contract) la deriva es ~0. Donde el canon es prosa (state-layer,
depth, opacidad en componentes, padding axes) la deriva es del 30–85%. La lección está
en el propio repo y no se aplicó a los sistemas de junio.
5. **Sin plantilla de recipe.** No hay un esqueleto canónico de recipe (qué ejes declara,
con qué nombres, qué sistemas consume). Resultado medible: los ejes de padding se
nombran de dos formas al 50% (`padding-x/y` ×18 vs `padding-inline/block` ×18), las
alturas de tres (`height-*` ×50, `control-height-*` ×15, `trigger-height-*` ×11), y el
vocabulario de slots dimensionales no existe (el §6 solo fija slots de color).
---
## 3. Hallazgos por subsistema
Severidades: **P0** bug visible/corrección · **P1** incoherencia estructural ·
**P2** mejora clara · **P3** higiene.
### 3.1 Color — núcleo sólido, periferia con huérfanos y docs desalineados
Lo bueno: la cadena escala→primitive→slot→palette se respeta en la práctica (609 refs
`--color-{role}-{slot}` en recipes, 1 solo bypass a `--scale-*`, 0 a `--primitive-*` en
CSS de componentes). El slot vocabulary REAL usado es coherente (border/solid/text/track/
element/solid-hover/contrast dominan).
- **C1 · P1 — Tres listas de slots, ninguna coincide.** §3 declara "13 slots" (incluye
`bg2`); el generador emite **14** (sin `bg2`, con `surface`/`surface-hover`); §6 regla 7
fija un "vocabulario fijo" de **9** que no incluye `surface`, `surface-hover`,
`separator`, `border-hover` ni `text-strong`. La fuente ejecutable
(`DEFAULT_COLOR_ROLE_SLOT_STEPS`, render-css.ts:93) no está citada por ninguna de las
tres. *Acción*: una sola tabla generada desde el código.
- **C2 · P1 — Slots emitidos sin consumidor.** `text-strong`, `separator`, `border-hover`:
0 usos; `hover`: 8; `active`: 1 (cada uno se emite 36 veces en generated). Son bytes del
"foundation floor" que el purge nunca podrá quitar (foundation siempre kept). Decidir:
consumir (p. ej. `separator` es exactamente lo que muchos recipes resuelven con
`border-subtle`) o dejar de emitir.
- **C3 · P1 — La tabla de mapeo de §4 miente.** Doc: `primary→indigo`, `risk→amber`.
Código (`THEME_BASE_COLOR_ROLES`): `primary→purple`, `tertiary→indigo`, `risk→orange`.
Tres celdas estables desde hace semanas — el doc canónico de roles describe un tema que
ya no existe.
- **C4 · P1 — Tres idiomas de translucidez.** (a) alpha-scales `aN` (emitidas, huérfanas
salvo a2/a3 indirectas); (b) `color-mix(in srgb, X N%, transparent)` — el idiom real
dominante (focus ring, state-layer, backdrops); (c) `--opacity-*`. No hay doctrina de
cuándo usar cada uno. La inversión (≈500 tokens alpha) no la usa nadie mientras el
código produce las mismas tintas con color-mix ad-hoc.
- **C5 · P3 — `natural-time-picker` conserva 6 hex crudos** (cielos del reloj natural +
`--_ntp-ink: #ffffff`). Los cielos son candidatos legítimos a "físicamente fijos", pero
la excepción exige comentario justificativo (doctrina "no intentional sin anotar") y no
lo llevan todos.
### 3.2 Theme builder (`buildScheme` / `applyColorScheme` / `applyTheme`)
- **TB1 · P0 — El scheme deja las alphas viejas.** `buildScheme` solo emite
`--primitive-{role}-a2/a3` (build-scheme.ts:155-158). El foundation emite a1..a12 por
rol. Tras `applyColorScheme(seed)`, **a1 y a4..a12 siguen apuntando al color del tema
anterior** — cualquier consumidor futuro de esas alphas (o un tema externo que las use,
están en el contrato público) mezclará el morado viejo con el brand nuevo. Emitir las
12 (la matemática `alphaOverBackground` ya existe) o retirar a4..a12 del contrato (ver
C4).
- **TB2 · P0 — Donores de intent ignoran el tema activo.** Con `temper > 0`, los intents
se derivan desde `CANONICAL_INTENT_SCALES` (build-scheme.ts:184): `risk` siempre parte
de **amber**, aunque el tema base mapea `risk→orange`. Consecuencia: al subir el slider
de temper, `risk` **cambia de familia de hue** (naranja→ámbar) además de temperarse — un
salto perceptivo que no es el contrato del knob ("mantiene su hue"). El donor debe salir
del role-map del tema activo (`getColorRoleScale('risk')`), con el canónico como
fallback.
- **TB3 · P1 — Background de compositing hardcodeado.** `#buildSchemeResult` usa
`mode === 'dark' ? '#111111' : '#ffffff'` (active-eidos.svelte.ts:845) mientras el
fondo real del tema es `--color-surface-default = neutral-1` (`#fcfcfc` light; el dark
de gray-1 no es `#111111`). Las a2/a3 generadas quedan compositadas contra un fondo que
no es el del tema — error pequeño en el base, grande en temas tintados (cristal, brand
oscuro azulado). Resolver el token vía `resolveToken('--color-surface-default')`.
- **TB4 · P2 — `applyTheme` no es atómico en coste** (ya en fable_audit E1): cada builder
se construye dos veces y `apply()` regenera TODO el static CSS. Para el caso de uso
anunciado (sliders del theme builder en vivo) es cuadrático respecto a lo necesario.
- **TB5 · P2 — Literales duplicados**: `onSolidContrast: '#1c1917'` vive como default en
build-scheme.ts:106 Y en los dos bloques de semantics de themes/base.ts; el par
`#ffffff/#1c1917` es una constante de sistema sin nombre.
- **TB6 · P3 — `#renderSchemeCss` traga errores** (fable_audit E3): un seed que rompe en
el re-derive de modo hace desaparecer el scheme sin log.
### 3.3 Superficies y elevación — el canal "unificado" que no unificó
- **E1 · P1 — Dos sistemas de elevación conviven.** (a) escala `--shadow-{1..6}` +
aliases semánticos (13 refs `--shadow-subtle` en recipes…) consumida directamente por
la mayoría; (b) canal depth `--depth-{plane}-{shadow|halo|blur|translucency}` —
compuesto solo por ~13 consumidores (popover, dialog, menús, tooltip, card…). §29
declara "canal unificado" — la frase describe la intención, no el estado: la
unificación por atributo (`data-depth`) quedó explícitamente como "opción futura" y el
z-index sigue por libre en cada componente (con su banda `--z-index-overlay-*`, esa sí
guardeada). *(Corrección 2026-07-02: la versión inicial de este informe contaba
"23 box-shadow literales" — la regla R-4.1 implementada demostró que todos llevan
`var()` (patrones ring / tokens de recipe); literales reales = 0. El hallazgo queda
reducido a la partición escala-vs-canal y la adopción parcial del halo.)*
- **E2 · P2 — El halo/frost es opt-in y casi nadie lo pide.** `[data-frost]` no aparece
en los recipes del catálogo (solo demos/tema cristal). Correcto como diseño gated, pero
conviene documentar en el contrato de recipe cuándo un overlay DEBE ofrecerlo.
- **E3 · P3 — Ladder de superficies OK** (default<raised<muted<overlay en ambos modos,
verificado en themes/base.ts) — sin hallazgos.
### 3.4 Tipografía — diseño de dos anclas correcto, tres fuentes de verdad de facto
El modelo documentado (escala numérica para recipes + named styles para primitivas de
contenido, aliases foundation leyendo del named style) es defendible y está razonado.
Pero:
- **T1 · P1 — Los named styles re-declaran line-heights que la escala ya fija.**
`sizes.xxl.lineHeight = '1.05'` Y `styles.h1.lineHeight = '1.05'` (typography.ts:65,89).
Retocar la escala NO propaga a los headings — dos fuentes para el mismo número, sin
guard que las mantenga iguales. Igual en h2/xl (1.1 vs 1.2 — ya divergen), h3/lg
(1.2 vs 1.35 — divergen). ¿Intencional (los styles ajustan óptica) o deriva? No está
documentado cuál manda.
- **T2 · P1 — Tercer sistema tipográfico en accordion.** El doc del 1:1 (§5) reconoce que
accordion usa una progresión propia `--text-N-size` ("sistema de prosa"). Con words/
palabras/chronos excluidos, hay **tres vocabularios** activos: `--font-size-{t-shirt}`,
`--style-{name}-*`, `--text-{N}-*`. El tercero no aparece en la tabla de naming §6.
- **T3 · P2 — `semanticTracking` es un eje muerto**: 6 claves, todas `'0'`
(typography.ts:221-228). Config + tokens emitidos que no expresan nada. Usarlo (caps
de menús → `--tracking-caps`?) o retirarlo.
- **T4 · P2 — Colisión de namespace `--leading-*`**: `leading` (none/tight/…) y
`semanticLeading` (ui/prose/text/…) emiten al MISMO prefijo desde dos mapas distintos
sin validación de claves disjuntas — un `semanticLeading.normal` pisaría
`--leading-normal` silenciosamente.
- **T5 · P2 — El pendiente del label `lg`** (divergencia 2px entre label segmentado por
`calc` y label por token, §5) sigue abierto — es exactamente el tipo de asimetría que
luego se copia a un componente nuevo.
- **T6 · P3 — 12 `font-size: Npx` crudos** en 8 ficheros; la mayoría en tracks excluidos
(words, timeline) pero `fab`, `field`, `password-field`, `command`, `date/time-range-field`
no son excluidos — el guard tipográfico solo cubre TOKENS de recipe, no la propiedad
CSS directa. Ampliar el guard o anotar.
- **T7 · P3 — Dos escalas comparten camiseta con rangos distintos**: el `Size` canónico
termina en `xxl` (+ `full` de layout) y la tipográfica añade `xxxl`. Documentado, pero
es una fuente recurrente de confusión (¿`--size-xxl-font-size` vs `--font-size-xxl`?)
que el naming no desambigua.
### 3.5 Sizes, espaciado, densidad, scaling
- **S1 · P1 — El bundle `--size-{k}-*` sigue con 0 consumidores** (verificado hoy). Está
"realineado al 1:1" pero nadie lo lee: cada recipe re-declara su mapeo size→fuente/
altura/padding. Es EL síntoma de la falta de plantilla de recipe: la coordinación
existe como datos y se re-implementa a mano 90 veces. Decidir de una vez: consumo real
(refactor grande pero mecánico) o degradarlo a documentación y dejar de emitirlo.
- **S2 · P1 — Idioma de ejes partido al 50%**: `padding-x/-y` (18 tokens) vs
`padding-inline/-block` (18). Mismo concepto, dos gramáticas, cero doctrina en §6.
Con RTL en el roadmap (memoria: "rtl" en reference-grade remaining), los `-x/-y` son
además la mitad equivocada.
- **S3 · P2 — Alturas con tres nombres** (`height-*`, `control-height-*`,
`trigger-height-*`) para el mismo concepto de "altura del control por size".
- **S4 · P3 — 17 paddings/gaps/margins en px crudos** en CSS de componentes (fuera de
tokens); pocos, pero sin guard equivalente al tipográfico.
- **S5 — Densidad y scaling: OK.** La composición `calc(raw × density × scaling)` vive en
el foundation (render-css:2397) y los recipes heredan vía `var(--space-*)` — no
encontré doble-escalado en componentes. El diseño post-§20/§23 es limpio.
### 3.6 Estado e interacción (state-layer)
- **I1 · P1 — Adopción 21/135 + 22 tokens huérfanos.** El state-layer es la decisión
correcta (MD3, theme-adaptive) pero quedó a ~30% del catálogo interactivo; los tokens
`*-bg-hover` sobreviven en recipes (22 declarados, 5 CSS aún los consumen) y la poda +
guard están "diferidos por entanglement". Mientras el guard no exista, cada componente
nuevo puede volver al idiom viejo sin que nada falle.
- **I2 · P1 — `archetypes.css` contradice su propio canon**: `close:hover` usa
opacity-dim `0.85` — el idiom que §38 declaró retirado para trigger ("el dim atenuaba
también el texto; el tinte no"). El archivo que define `--state-*` mantiene el
anti-patrón tres líneas más abajo.
- **I3 · P2 — El selector del highlight de item/option** son 10 selectores × 4 `:not()`
repetidos a mano (archetypes.css:208-217). Funciona, pero es la clase de bloque que
`:is()` reduce a 2 líneas legibles y que hoy nadie se atreve a tocar.
### 3.7 Focus
- **F1 · P1 — Dos mecanismos + una justificación stale.** El ring universal de archetypes
es `box-shadow` "porque layout.css pone `outline: none !important` global" — ese reset
vive en `web/routes/layout.css` (la shell de docs), **no en el framework**: la
arquitectura de a11y del framework quedó condicionada por su demo. En paralelo, §32
migró superficies a `outline` (HCM/overflow-safe) y quedan **5 componentes box-shadow
pendientes de migrar** (inventario reference-grade). Resultado: campos con box-shadow
de dos anillos (correcto y deliberado), superficies con outline, archetype-fallback con
box-shadow, y forced-colors emitiendo outline por encima. El modelo final es razonable —
pero nadie lo describe completo en un solo sitio, y el comentario fundacional es falso
para el framework standalone.
- **F2 · P3 — Buena praxis verificada**: exclusiones de item/option/content del ring
universal bien razonadas; `:where()` discipline del resto de archetypes correcta.
### 3.8 Opacidad
- **O1 · P1 — El token dice 0.4; el canon transversal usa 0.5.** `--opacity-disabled =
0.4` (static.ts:294). `archetypes.css` hardcodea `opacity: 0.5` en trigger/thumb/item
disabled (3 sitios). La §35 proclama la unificación; el fichero más transversal del
sistema no la consume. Fix trivial + guard (mismo patrón que el tipográfico).
*(Corrección 2026-07-02: en CSS de componentes, fuera de `@keyframes`, solo quedan
2 con literal (R-4.2) — el conteo inicial de 20 incluía fades de keyframes, que son
animación, no estado, y quedan exentos.)*
### 3.9 Motion
- **M1 · P1 — 49 keyframes locales en 25 CSS, fuera del canal.** La doctrina (events.css
header + eidos-motion §15) dice que firmas y keyframes viven en
`EidosConfig.motion.{keyframes,signatures}` (themeables, generados). Los 49 locales son
invisibles para temas y para el motor — no se pueden retunear ni desactivar
coherentemente. Habrá funcionales legítimos (spin de spinner), pero no hay
clasificación: ni doctrina "cuándo un keyframe puede ser local" ni inventario.
- **M2 · P2 — El cap de reduced-motion solo frena `animation-duration`** (events.css:24,28)
— las transiciones de estado (`transition:`) bajo `data-motion='reduce'` no se capan en
este archivo (THEMING §13 aún muestra una versión con `transition-duration`). Verificar
dónde vive hoy el cap de transiciones o añadirlo.
- **M3 · P3 — Sano**: uso de `--duration-*`/`--ease-*` amplio (40 CSS), `--ease-default`
domina (100 usos), 5 duraciones crudas restantes con excepciones razonadas en §35.
### 3.10 Touch-target y list-surface
- **TT1 — Touch-target: bien diseñado** (gated por `pointer: coarse`, doble función del
`:not()`, exclusiones razonadas). Pendiente reconocido: fila etiquetada de
checkbox/switch (tarea de Field).
- **LS1 · P2 — list-surface con 4 adoptantes** y select/combobox pendientes — otro canon
a medio desplegar; mientras tanto item/option cae al fallback font-driven.
### 3.11 Documentación del theming
- **D1 · P1 — THEMING.md es referencia + changelog fusionados.** 2.583 líneas; §20–§38
son sprints fechados con incidentes y commits; §5 conserva DOS doctrinas revocadas
narradas inline ("supersede… quedó revocada"); §13–§14 están marcados "superseded" pero
ocupan 100 líneas del doc "current"; §21 es un RFC resuelto conservado. La contradicción
interna es inmediata: **§1 dice "5 layers de CSS", §2 dice "Las 6 capas"** (y enumera 6).
- **D2 · P1 — La "guía de implementación por componente" está fragmentada en 6 documentos**
(THEMING §5/§6 + TSC.md + THEMING_GUIDE + components/README + COMPONENT_COMPLETION_CHECKLIST
+ eidos-motion §13) y NINGUNO enumera los sistemas transversales que una recipe debe
consumir (state-layer, focus, elevación, touch, list-surface, 1:1, motion channel). Esa
lista hoy solo existe implícita en los "Pendiente" de cada sprint. **Es la carencia
exacta que el encargo señalaba.**
- **D3 · P2 — Secciones "estado actual" congeladas** en docs atemporales:
eidos/README "Estado actual (2026-05-17)" (wrappers: lista ~20 vs 137 reales),
components/README "Estado de la migración (2026-05-20)". Contradicen docs/authoring.md.
- **D4 · P3 — §4 tabla de mapeo stale** (ver C3) y §3 "13 slots" vs generador (ver C1).
---
## 4. Lista consolidada de bugs y fixes puntuales (accionables ya)
| # | Sev | Qué | Dónde | Fix |
|---|---|---|---|---|
| B1 | P0 | Scheme deja a1/a4..a12 del tema anterior | build-scheme.ts:155 | emitir las 12 alphas o retirar a4+ del contrato |
| B2 | P0 | `temper` cambia el hue de `risk` (amber vs orange del tema) | build-scheme.ts:184 | donor desde el role-map del tema activo |
| B3 | P1 | Alpha compositado contra `#fff`/`#111` en vez del surface real | active-eidos:845 | `resolveToken('--color-surface-default')` |
| B4 | P1 | `opacity: 0.5` ×3 en archetypes vs `--opacity-disabled` 0.4 | archetypes.css:54,113,224 | usar el token + guard |
| B5 | P1 | `close:hover` opacity-dim contra doctrina state-layer | archetypes.css:131 | `background-image: linear-gradient(var(--state-hover)…)` |
| B6 | P1 | "5 layers" vs "6 capas" en el mismo doc | THEMING §1 vs §2 | corregir §1 |
| B7 | P1 | Tabla roles §4 desalineada del código (primary/risk/tertiary) | THEMING §4 | regenerar desde `THEME_BASE_COLOR_ROLES` |
| B8 | P2 | reduce-cap sin `transition-duration` | events.css | añadir o documentar dónde vive |
| B9 | P2 | `#1c1917`/on-solid duplicado en 3 sitios | build-scheme, themes/base ×2 | constante nombrada |
| B10 | P3 | hex sin anotación en natural-time-picker | ntp.css:15-20 | comentario "physically-fixed" o tokenizar ink |
| B11 | P3 | 12 `font-size:` px fuera del guard (fab/field/command…) | varios css | ampliar guard R-2.7 a la propiedad |
---
## 5. Lo que está bien (y hay que proteger al refactorizar)
- **TSC + pipeline de defensas** — intacto y ejemplar (ya valorado en fable_audit).
- **El foundation de color** — cadena de capas respetada en la práctica; el modelo §25
(paleta rica + roles alias + intents derivados) es superior al de las referencias.
- **Los guards existentes** — cada uno tiene deriva ≈0 en su dominio. La plantilla del
éxito ya existe; solo falta replicarla.
- **`archetypes.css` como pieza** — la disciplina `:where()` con las 3 excepciones
intencionales documentadas es de las mejores prácticas del repo (pese a I2/B4).
- **Densidad × scaling** — composición limpia post-§20/§23, sin doble escalado.
- **Los builders puros** (`build-*` testeados, sin DOM) — la separación matemática/
aplicación es correcta; los bugs TB1–TB3 son de datos, no de arquitectura.
- **Touch-target y focus-exclusions** — decisiones a11y razonadas contra referencias
(React Spectrum, WCAG) y verificadas.
- **eidos:purge** — con resultados medidos honestos.
---
## 6. Valoración del sistema de theming frente a los existentes
> Fundada en la comparativa propia del repo (THEMING_NOTES.md, que es honesta y sigue
> vigente) + los hallazgos verificados de esta auditoría. La pregunta que responde:
> *¿qué vale este sistema de theming puesto al lado de Radix Themes, Panda/Chakra,
> Mantine, Tailwind v4, shadcn y Material 3?*
### 6.1 La frase que lo resume
> **Eidos tiene el motor de theming más capaz del campo y la gobernanza de consumo más
> débil respecto a su propia ambición. Radix Themes es exactamente lo contrario: un motor
> modesto con gobernanza perfecta.**
Radix Themes ofrece ~un tercio de los ejes de Eidos (accent + gray + radius + scaling),
pero el 100% de sus componentes consume el 100% de su sistema — no existe "adopción
parcial" porque los componentes y el sistema se diseñaron juntos y no hay vía de escape.
Eidos ofrece el triple de ejes y sus componentes los consumen al 30–85% según el
subsistema. La calidad percibida de un theming no la da el motor sino la **uniformidad
del consumo** — y ahí es donde Eidos pierde hoy contra referencias objetivamente menos
capaces.
### 6.2 Dónde gana (capacidades que nadie más tiene, verificadas)
| Capacidad | Eidos | Mejor alternativa del campo |
|---|---|---|
| **Scope de tokens como contrato validado** (TSC: álgebra + colisión cross-axis + composition) | ✅ único | Panda valida parcialmente en build; Radix/Mantine/shadcn: nada — el bug del custom-property congelado es invisible para todos ellos |
| **Builders runtime semilla→sistema en 6 ejes** (color · type · depth · shape · space · gradient, `applyTheme` atómico) | ✅ único | Material 3 lo hace SOLO para color (dynamic color); Radix Themes ni eso (su panel es token-level) |
| **Color science**: OKLCH wide-gamut dual-stack default-on, APCA on-solid, alpha por compositing-inverse, `temper` de intents | ✅ | Radix ships hex sRGB; M3 usa HCT (comparable en rigor, sin P3 dual-stack en web) |
| **Roles evaluativos del libro** (affirm/fulfill/risk/threat/loss ≠ semáforo Bootstrap) | ✅ único | Todos los demás: success/warning/danger/info |
| **Densidad × scaling ortogonales en runtime** | ✅ | Radix tiene scaling; NINGUNO tiene ambos ejes componiendo |
| **Integración perceptiva** (tokens/motion que reaccionan a `data-event-*` de sema) | ✅ único | No existe el concepto fuera de este framework |
| **Contrato CSS introspectable + themes CSS-only + persistencia versionada** (`getCssContract` / `renderContractCss` / envelope) | ✅ | Panda introspecta en build; Mantine expone el theme object; nadie publica el contrato como CSS vacío para temas externos |
| **a11y del theming**: forced-colors, prefers-contrast, touch-target gated por puntero, reduced-motion cross-modal | ✅ | React Spectrum es el único comparable en touch/contrast; ninguna lib de theming CSS lo integra |
| **Purge con cierre transitivo** (13.6 KB gz para 10 componentes) | ✅ competitivo | Tailwind 10 / Panda 12 — misma liga |
Esto no es marketing propio: cada fila de arriba la contrasté contra el código, no contra
los docs. Como **motor**, el sistema está 2–3 años por delante de lo publicado.
### 6.3 Dónde pierde (contra referencias concretas)
1. **Contra Radix Themes — uniformidad de consumo.** Es el hallazgo central de esta
auditoría (§1): en Radix no puede existir un componente con 23 sombras literales o un
hover que ignora el state-layer, porque no hay superficie donde escribirlo. En Eidos
las recipes son CSS plano + un mapa de tokens sin forma: la vía de escape está abierta
en cada fichero.
2. **Contra Panda/Chakra v3 — la recipe tipada.** Panda tiene `defineRecipe` /
`defineSlotRecipe`: la forma de una recipe (ejes, variants, slots, compoundVariants)
es un TIPO, y desviarse no compila. Eidos tiene el TSC (que valida *scope*, mejor que
Panda) pero **no valida la forma**: qué ejes existen, cómo se llaman
(`padding-x` vs `padding-inline`), qué sistemas consume. Es la única dimensión del
theming donde Panda es estructuralmente superior hoy — y es exactamente el hueco que
la Fase A propone cerrar.
3. **Contra Tailwind/shadcn — coste de entrada.** 7 capas de tokens, 2.583 líneas de doc
de referencia con secciones revocadas inline, y "cómo hago un componente" repartido en
6 documentos. La comparativa del propio repo lo admite ("Eidos NO es más simple") —
correcto, pero la deriva documental (§3.11) lo empeora más allá de lo necesario.
4. **Contra todos — madurez del catálogo de temas.** 2 temas shipped (base + cristal) y
un builder con 3 bugs P0/P1 en el camino crítico del retint (TB1–TB3). El sistema
*permite* más que ninguno; lo *demostrado* en producción es menos que cualquiera de
las referencias.
5. **Rendimiento del builder en vivo** — `apply()` regenera el foundation entero por
cambio de eje (fable_audit E1); ninguna referencia con theme runtime (Mantine,
Chakra) paga ese coste por knob.
### 6.4 Puntuaciones (escala honesta)
| Dimensión | Nota | Ancla |
|---|---|---|
| Arquitectura de foundation (capas, roles, escalas, naming de color) | **9** | Mejor que Radix (3 capas) y Panda; C1–C4 son periferia |
| Capacidades del motor (TSC + builders 6 ejes + color science) | **9.5** | Nadie tiene el conjunto |
| Implementación del motor | **7.5** | TB1–TB3 (retint incompleto/incoherente) + E1 (perf) descuentan |
| **Disciplina de consumo del catálogo** | **5** | 21/135 state-layer · 3 sistemas de elevación · 0 consumidores de `--size-*` · 49 keyframes fuera del canal |
| Ergonomía para el autor de componentes | **5** | Sin plantilla ni recipe tipada; el contrato vive en 6 docs + "pendientes" |
| Ergonomía para el autor de temas | **7.5** | Contract introspection + CSS-only + envelope excelentes; solo 2 temas lo validan |
| a11y del theming | **8** | forced-colors/contrast/touch/reduced-motion; F1 y B4 descuentan |
| Documentación como guía | **4** | Referencia=changelog; contradicciones internas verificadas |
| **Global** | **7** | Motor de referencia mundial · gobernanza por debajo de su propia doctrina |
La nota global no es la media: está dominada por la disciplina de consumo, porque es lo
que el usuario final del framework *ve*. Un tema aplicado sobre componentes que bypassean
los canales (sombras literales, hovers bespoke, keyframes locales) produce un retint
incompleto — y eso convierte la ventaja del motor en una promesa, no en un resultado.
---
## 7. ¿Basta con el contrato por componente? (respuesta directa)
**Sí, el contrato de recipe es la pieza clave — pero solo si se entiende como tres cosas
a la vez, y hay una cuarta parte del problema que el contrato no toca.**
1. **El contrato como documento** (la "guía conceptual" que falta) es necesario pero, por
sí solo, insuficiente. La evidencia está en este mismo repo: los cánones que viven en
prosa derivan al 30–85% (state-layer, depth, opacidad); los que tienen guard mecánico
derivan ~0% (tipografía-literal, z crudo, variants, TSC). Un `RECIPE_CONTRACT.md` sin
enforcement sería el decimoquinto sistema canónico parcialmente adoptado.
2. **El contrato como guard** (`component-audit` R-4.x — Fase A2) es lo que congela la
deriva futura: cada componente nuevo nace obligado. Es barato (el script y los
precedentes de regla existen) y es el paso de mayor palanca.
3. **El contrato como tipo** es el destino natural y la versión superior: una
`defineRecipe` de eidos con ejes canónicos tipados (dimensiones con nombres fijos,
sistemas transversales como flags declarativos, variants contra `EIDOS_VARIANTS`),
igual que Panda — y coherente con la doctrina ya escrita del propio framework
("types over lint": el precedente de `semaSelector` frente al eidos-lint). Las recipes
CSS planas son hoy **la última gran superficie no tipada** del sistema. No hace falta
empezar aquí (es la versión cara), pero el contrato-documento y el contrato-guard
deben redactarse de forma que migren limpio a contrato-tipo.
4. **Lo que el contrato NO arregla** — y por eso el plan tiene 5 fases y no 1:
- **La deuda ya emitida**: los 135 CSS existentes hay que backfillearlos (Fase B);
si no, el guard nace en rojo o con una allowlist eterna que lo desactiva de facto.
- **Los bugs del motor** (TB1–TB3: alphas parciales, donors que ignoran el tema,
background hardcodeado): son del builder, no de las recipes — ningún contrato por
componente los toca (Fase C).
- **Las decisiones de inventario** (alphas huérfanas, slots muertos, bundle
`--size-*`, `semanticTracking` cero): son decisiones de sistema que alguien tiene
que tomar una vez (Fase D). El contrato las *consume*, no las *decide*.
- **La documentación** (Fase E): el contrato debe ser LA página que un implementador
lee, y eso exige que THEMING.md deje de ser un changelog donde esa página no cabe.
En una línea: **el contrato por componente es la llave, pero la cerradura tiene cuatro
piezas — contrato ejecutable + backfill + motor corregido + inventario decidido.** El
orden correcto es A (contrato+guard en WARN) → B (backfill) → guard a FAIL, con C y D en
paralelo, porque redactar el contrato ANTES del backfill es lo que da al backfill su
lista de tareas exacta.
---
## 8. Plan de acción
### Fase A — El contrato de recipe (la "guía" que falta, como regla mecánica)
**A1.** Redactar `RECIPE_CONTRACT.md` (1 página, E2): el esqueleto canónico de una recipe
— ejes y nombres de tokens dimensionales (**canon: `padding-inline/-block`**,
`control-height-*`), qué sistemas transversales consume obligatoriamente (state-layer
para hover neutro · focus según su arquetipo (campo=2 anillos / superficie=outline) ·
elevación SOLO vía `--shadow-*`/`--depth-*` · tipografía 1:1 · `--opacity-disabled` ·
touch-target implícito por archetype · list-surface si es lista) y las excepciones
válidas con anotación.
**A2.** Convertirlo en reglas `R-4.x` de `component-audit`: (a) ningún `box-shadow`
literal; (b) ningún `opacity` disabled literal; (c) ningún `:hover` con `background`
que no sea state-layer o palette-swap declarado; (d) ejes físicos `-x/-y` prohibidos en
tokens nuevos; (e) `@keyframes` local exige comentario `/* functional: … */`. WARN
primero, FAIL tras la Fase B.
### Fase B — Cerrar las adopciones a medias (mecánico, por barridos)
| | Acción | Cierra |
|---|---|---|
| B-1 | Completar state-layer (tabs, color-picker + resto neutro), podar los 22 `*-bg-hover` huérfanos de recipes, fix `close:hover` y opacities de archetypes | I1, I2, B4, B5 |
| B-2 | Decidir la unificación escala-vs-canal depth (adopción `data-depth` o doctrina de convivencia explícita) — los box-shadow ya están tokenizados (R-4.1 = 0) | E1 |
| B-3 | Terminar la migración focus box-shadow→outline en los 5 pendientes + un doc-mapa único del modelo de focus (campos/superficies/fallback/HCM) + corregir el comentario stale de archetypes | F1 |
| B-4 | list-surface a select/combobox (pendiente declarado) | LS1 |
| B-5 | Codemod `padding-x/y → padding-inline/block` + unificar naming de alturas | S2, S3 |
| B-6 | Inventariar los 49 keyframes: mover firmas al config, anotar los funcionales | M1 |
### Fase C — Theme builder correcto
C1: B1+B2+B3 (alphas completas, donors del tema, background real) + tests de "retint
completo" (aplicar scheme → grep de tokens del color viejo en el CSS resultante = 0).
C2: memoización de `renderStaticCss` + `apply(scope)` (fable_audit E1). C3: log en
`#renderSchemeCss`.
### Fase D — Decisiones de inventario (una sesión de decisiones, no de código)
- Alpha scales: ¿consumir (focus rings, scrims, hovers → sustituyendo color-mix ad-hoc)
o recortar a a1–a3? (C4/TB1 se resuelven juntos).
- Slots `text-strong`/`separator`/`border-hover`/`hover`/`active`: consumir o dejar de
emitir (C2).
- Bundle `--size-*`: refactor de consumo o degradar a doc y retirar (S1) — lleva un año
en el limbo; cualquiera de las dos opciones es mejor que el limbo.
- `semanticTracking` todo-cero: usar o retirar (T3).
- `tertiary`: sigue "RESERVED" sin consumidor — fijar fecha de revisión.
### Fase E — Documentación
E1: Partir THEMING.md — `THEMING.md` (referencia atemporal ≤600 líneas: mental model,
capas, roles+slots generados desde código, sizes, naming con los ejes dimensionales,
punteros a TSC/GUIDE) + `THEMING_CHANGELOG.md` (§20–§38 tal cual). E2: retirar §13/§14/§21
al changelog. E3: borrar las secciones "estado actual" congeladas o regenerarlas por
script. E4: tabla única de slots/roles/mapeos autogenerada (`generate:contracts-docs` ya
existe como precedente).
---
## 9. Nota final
La frase del encargo — *"se ha ido desviando con cada modificación"* — es correcta como
descripción del **proceso**, pero la desviación no es entropía aleatoria: es una serie de
mejoras genuinas (state-layer, depth, 1:1, opacidad, touch) que se quedaron sin las dos
piezas que el propio repo demostró que funcionan: **el guard mecánico y el cierre del
rollout**. La materia prima para el "esquema coherente" ya existe; lo que falta es
promover el contrato de recipe a regla ejecutable (Fase A) y pagar la deuda de adopción
(Fase B). Con eso, eidos pasa de "federación de sprints" a el sistema que sus propios
docs describen.
*Cifras verificadas por grep a 2026-07-01 sobre `alpha-0.1-sec-dom` (y recalibradas el
2026-07-02 con las reglas R-4.x, que parsean declaraciones completas en vez de líneas):
135 CSS de componentes; state-layer 21; `*-bg-hover` 22 declarados / 5 consumidores;
box-shadow literales **0** (los 23 matches iniciales llevaban `var()`); keyframes
locales 49 en 25 ficheros — 23 componentes sin anotación funcional (R-4.5); `--size-*`
0 consumidores; alphas directas 0; slots huérfanos text-strong/separator/border-hover
0 usos; padding-x/y 18 vs inline/block 18 (2 componentes, R-4.4); opacity literales
fuera de `@keyframes` 2 componentes (R-4.2) + 3 en archetypes.css; hex crudos 6
(1 fichero); `var(--scale-*)`/`--primitive-*` directos 2 componentes (R-4.6);
duraciones crudas 5.*
---
## Addendum 2026-07-02 — Fase A ejecutada + desfase del tooling
1. **El Recipe Contract existe**: [`src/uix/eidos/RECIPE_CONTRACT.md`](src/uix/eidos/RECIPE_CONTRACT.md)
(canon E2 — esqueleto de recipe, sistemas transversales obligatorios, excepciones
anotadas) + sección **C4 / reglas R-4.1–R-4.6** en `COMPONENT_COMPLETION_CHECKLIST.md`
y `scripts/component-audit.ts` (severidad WARN; graduarán a error tras el backfill).
Referenciado desde THEMING §8, eidos/README y docs/README (mapa E2). Tracks
`words`/`palabras`/`chronos` excluidos.
2. **Línea base R-4.x** (primer run): R-4.1 = 0 · R-4.2 = 2 · R-4.3 = 0 · R-4.4 = 2 ·
R-4.5 = **23** · R-4.6 = 2. El grueso del backfill de Fase B es el canal de motion
(keyframes sin clasificar), no la elevación ni los hovers — mejor de lo estimado.
3. **Backfill Fase B (mecánico) ejecutado — 2026-07-02.** R-4.1/4.2/4.3/4.4/4.6 = **0**:
- R-4.4: `padding-x/y-*` → `padding-inline/block-*` en popover + tooltip
(recipes/base.ts + popover.css + tooltip.css + natural-time-picker.css + regen;
diff del generated = exactamente los 14 tokens). **Verificado en navegador**:
popover viewport `padding: 12px`, tooltip `6px 10px`, token viejo vacío, 0 errores
de consola.
- R-4.2: scroll-frames `0.5` → `var(--opacity-50)`, spinner ring-track `0.6` →
`var(--opacity-60)` (value-preserving, la escala numérica dual de §35 existía para
esto).
- R-4.6: menu-dial scrim → `var(--color-neutral-text-strong)` (primer consumidor del
slot huérfano C2, value-preserving); media-player amber anotado
`/* literal: theme-stable accent over arbitrary video */`.
- R-4.5: los 13 keyframes de **maquinaria continua** (spinner ×3, progress ×3,
shimmer ×2, spin ×4, toast-pulse) anotados `/* functional: … */`. Quedan **15
componentes** como backlog real de migración al canal de motion — entradas/salidas
de overlays (link-preview, nav-menu, menús, select, combobox, tabs, tooltip,
collapsible, card, onion/menu-dial, clipboard-pop, timeline-reveal) y la reacción a
`data-event-*` de metrics-flash. Migrarlos exige verificación visual por componente
(presets `data-animation-style` / firmas en config) — deliberadamente NO se hizo en
esta pasada mecánica.
- Tests eidos: los 15 fallos existentes son WIP paralelo pre-existente (palabras,
natural-time-picker, calendar view-switch, paleta 31→33) — ninguno menciona los
tokens renombrados; `recipe-css-contract` pasa limpio sobre popover/tooltip.
3.ter **Migración R-4.5 — segunda tanda (2026-07-02): 7/15.** Tres patrones más,
todos verificados en navegador:
- **tooltip** (máquina tri-estado `delayed-open`/`instant-open`): tras la
objeción del usuario ("¿sale fuera del canon?") la solución intermedia
(keyframes del canal sin preset) se sustituyó por la **plenamente canónica**:
el generador gana `PRESET_STATE_ALIASES` (render-css.ts) — `delayed-open` es
alias de apertura del trigger de presets (selector propio, bySide y reglas
reduce incluidos), e `instant-open` deliberadamente NO lo es (su semántica ES
"aparecer sin entrada"). Tooltip estampa `motion='scale-fade'` como cualquier
overlay (preset conmutables + política reduce del preset) conservando sus
duraciones vía hooks. Verificado: `delayed-open → scale-in, fade-in` 0.18s;
`instant-open → none`; cero keyframes locales; generated emite los alias.
El alias sirve a cualquier futura máquina soma con vocabulario de apertura
más rico.
- **link-preview**: el caso de libro — sus 8 keyframes por-lado eran la
re-implementación manual del `bySide`; ahora preset `slide-fade`. Verificado
con `side='top' → slide-from-bottom` (la side-awareness que costaba 8 keyframes).
- **collapsible** (momento-EVENTO, no estado): sus keyframes reaccionaban a
`data-event='expand/collapse'` de sema → migrados a **`BUILTIN_SIGNATURES`**
(firma genérica por verbo emerge.expand/collapse, como present/dismiss) +
keyframes `expand-reveal`/`collapse-conceal` en el canal. Beneficia a CUALQUIER
emisor futuro de esos verbos. Verificado en pleno hold: `expand-reveal` 0.24s /
`collapse-conceal` forwards.
- Fix de la regla R-4.5: el scanner ahora blanquea comentarios antes de buscar
`@keyframes` (un comentario con la palabra daba falso positivo); el lookback de
la anotación sigue sobre el texto original.
**Backlog restante: 8** — navigation-menu (≈shared-axis-x), tabs (content
fade/slide ×3), card (card-emerge≈scale-fade/present), clipboard-pop, metrics-flash
(reacción a data-event → candidata a signature per-intent), timeline-reveal,
onion-menu y menu-dial (radial/backdrop — preset propio o excepción razonada).
3.bis **Migración R-4.5 en curso (2026-07-02) — 4/15 hechos, patrón validado.**
`dropdown-menu` · `context-menu` · `select` · `combobox` migrados al preset
canónico `slide-fade`: prop `motion` (default `'slide-fade'`, `'none'` opt-out)
en el Content/SubContent eidos → `data-animation-style`; keyframes locales
retirados del recipe. **Los hooks `--motion-duration-enter/-exit` del preset
permiten migración value-preserving**: select conserva sus
`--select-open/close-duration` alimentándolos al hook (verificado en navegador:
0.18s exactos); combobox ídem con `--duration-normal`. Los 4 verificados en
navegador (attr + `animationName: slide-from-top, scale-in, fade-in` side-aware +
0 errores de consola) y PASS en el audit. Ganancia colateral: los paneles ganan
exit animation + política reduced-motion que sus keyframes locales no tenían.
**Backlog restante: 11** (tooltip, link-preview, navigation-menu, tabs,
collapsible→preset `collapse`, card, clipboard, metrics, timeline, onion-menu,
menu-dial). **Hallazgo pre-existente** (no regresión, detectado al verificar):
el stagger de items de menú (`[data-stagger][data-state='open'] > [data-animation-style]`)
exige hijo DIRECTO — los items dentro de `[data-*-group]` nunca matchean; el
cascade solo anima items sin agrupar. Candidato a fix en el generador de
`renderMotionBlocks` (selector descendant o variante group-aware).
3.quater **Migración R-4.5 — tercera tanda (2026-07-02): 10/15.** Tres casos de
MONTAJE, cada uno con su mecanismo correcto:
- **clipboard** (bubble "Copied!"): entrada de contenido pura →
**`motionAttrs('scale-fade')`** en el wrapper del Indicator (el mecanismo
canónico de mount). Hooks preservan el pop original (`--motion-scale-enter:
0.85` + `fast`). Verificado con sonda MutationObserver (el bubble vive
`timeout` ms y el click sintético no da user-activation a la Clipboard API —
hizo falta click CDP real): `scale-in, fade-in` a 0.12s, scale 0.85.
- **card** (emerge de montaje): NO puede usar `motionAttrs` — la card tiene
`data-state` SEMÁNTICO (máquina selected) que el `data-state='open'` constante
pisaría. **Patrón materiales**: la regla del recipe consume los keyframes
registrados (`slide-from-bottom` + `fade-in`) con sus tokens; el token de
distancia alimenta `--motion-distance-md` scoped. Verificado por inyección:
0.32s (slow preservado), 8px.
- **timeline** (reveal de live-feed): el trigger es CONDICIONAL al flag
`data-live` del ROOT — un stamp incondicional animaría timelines no-live.
Patrón materiales con la regla container-gated. Verificado por inyección en
su ruta (recipes code-split: el CSS solo carga donde el componente monta):
0.24s, distancia 12px del token vía override scoped.
Doctrina emergente de la tanda: **preset stamp cuando el elemento no tiene
data-state semántico ni trigger condicional; patrón materiales (keyframes
registrados + razón documentada) cuando lo tiene**. Hallazgo colateral
preexistente (chip creado): el demo de card anida `<button>` en `<button>`
(Card interactiva + Buttons hijos) — `node_invalid_placement_ssr` en consola.
**Backlog restante: 5** — navigation-menu, tabs, metrics, onion-menu, menu-dial.
3.quinquies **Migración R-4.5 COMPLETADA (2026-07-02): 15/15 — R-4.5 graduada a
`error`.** Cuarta tanda (5 componentes), verificada en navegador por inyección:
- **onion-menu** y **menu-dial**: sus keyframes eran fades puros → `fade-in`
registrado (trivial).
- **tabs**: sus 3 sabores (`data-motion` fade/slide × orientación — eje
per-componente que el trigger genérico no modela) → materials: `fade-in` /
`slide-from-bottom`+`fade-in` / `slide-from-right`+`fade-in` con el nudge de
4px preservado vía `--motion-distance-sm`. Verificado los tres.
- **navigation-menu**: enter → `slide-from-top`+`scale-in`+`fade-in` (panel
CSS-posicionado sin `data-side`); los 4 swaps direccionales → el par
shared-axis M3 **completado en el canal** (`slide-axis-x-in/out-reverse`,
adición genérica: la dirección backward faltaba) con travel de 12px vía
`--space-3`. Verificados los 5 triggers.
- **metrics**: keyframe genérico **`value-flash`** registrado (tinte vía hook
`--motion-flash-tint`); el recipe conserva su graduación por intent y el
trigger (materialización por-componente de la señal, más rica que el pulse
genérico de announce).
**Doctrina final consolidada en RECIPE_CONTRACT §4**: preset stamp / firma de
evento / patrón materiales — los tres modos legítimos de consumir el canal, con
el criterio de cuándo aplica cada uno. Verificación de cierre: audit global
estable (75/50/5, la graduación costó cero), `generated-css` +
`recipe-css-contract` con solo los 3 fallos pre-existentes ajenos, `npm run
check` con la línea base intacta (61 pre-existentes, 0 propios).
3.sexies **Fase C — theme builder corregido (2026-07-02): TB1 + TB2 + TB3 cerrados.**
`buildScheme` reescrito en dos fases (planificar escalas → emitir):
- **TB1**: emite la rampa alpha COMPLETA `a1..a12` por rol (antes solo a2/a3 —
el resto quedaba apuntando al tema anterior tras `applyColorScheme`).
Verificado en vivo en `/temas/color`: el scheme aplicado pasa de 18 a
**108 alphas** (9 roles × 12).
- **TB2**: nueva opción `intentSeeds` — la base del `temper` es el step-9 del
tema ACTIVO por intent (`ActiveEidos.#resolveIntentSeeds`), con la convención
canónica solo como fallback. El knob ya no cambia la familia de hue de `risk`
(naranja del tema vs ámbar de la convención). Test dedicado.
- **TB3**: sin background hardcodeado — las alphas compositan sobre el step 1
del neutral DERIVADO (la superficie real de la página bajo el scheme,
mode-correcta gratis porque los donors son por-modo). `options.background`
queda como pin explícito. Test de no-op contra el neutral derivado.
- Bonus: `options.mode` ahora fuerza también el DONOR (vía el theme resolver) —
antes solo tocaba el background (gap doc↔impl preexistente); y el demo
`/temas/color` dejó de pasar el `#fff/#111` heredado (dogfooding del fix).
Verificación: build-scheme 10/10 + resolve-token 16/16; suite eidos con los
mismos 15 fallos pre-existentes (cero nuevos); `npm run check` en línea base
(61, 0 propios).
3.septies **Fase D — decisiones de inventario tomadas y ejecutadas (2026-07-02).**
Las 4 decisiones (usuario eligió la recomendada en todas) + `tertiary` resuelta
por los hechos (el builder la deriva vía M3 → se queda, documentada como slot de
jerarquía para apps):
- **semanticTracking BORRADO** (eje todo-cero): typography.ts + config-types +
validación + emisión; su único consumidor (`tabs.trigger-letter-spacing`) pasó
a `--tracking-normal` (value-preserving). 0 tokens `--tracking-{role}` en
generated.
- **Slot `border-hover` RETIRADO** del generador y del contrato (0 consumidores;
los 19 `--{c}-*-border-hover` restantes son tokens bespoke de recipe,
legítimos). Comentario tombstone en `COLOR_ROLE_SLOTS` por si vuelve un caso.
- **`separator` ADOPTADO**: sweep de los divisores de línea (5 tokens de recipe
+ combobox/command css + el componente Separator) de `border-subtle` (step 4,
lavado) al slot canónico (step 6 — tono Radix de divider; Separator standalone
queda idéntico porque 6==border-default). Verificado en navegador (dropdown:
`oklch(0.3485)` = slot) + screenshot.
- **Alphas: contrato completo + workstream de adopción abierto** — sustituir
color-mix ad-hoc por steps alpha donde el semántico coincida (candidatos:
tintas soft, rings, scrims). Sin código en esta pasada más allá del TB1 que ya
las emite bien.
- **Bundle `--size-*`: refactor de consumo APROBADO + patrón establecido con el
pilot `toggle`** — consumir la coordenada canónica donde el valor coincide
(height/font-size, alias idénticos, verificado 36px/16px en navegador) y
conservar las desviaciones deliberadas (px/gap) visibles. THEMING §5
actualizado; el barrido de ~90 recipes es el workstream abierto (por tandas
verificadas).
Verificación: regen limpio, recipe-css-contract con solo los 3 fallos
pre-existentes, guard R-4.x en verde.
4. **Tracks WIP excluidos consistentemente (2026-07-02).** `words` / `palabras` /
`chronos` son componentes SIN TERMINAR (vías de desarrollo paralelas) — pero solo el
guard tipográfico los excluía; el resto de checks de `recipe-css-contract.test.ts` y
el `component-audit` completo los contaban como catálogo roto. Unificado en un set
`WIP_TRACKS` compartido: el audit los saca del catálogo y los veredictos (130
componentes; BROKEN 7→5; auditables a demanda vía `--only words`), y las 3
suites del contrato que fallaban por ellos ahora señalan solo deuda REAL de otros
frentes en curso: `navigation-menu-indicator-h` sin declarar, los hex de
`natural-time-picker` (C5, pendiente de decisión physically-fixed) y los 14 tokens
huérfanos de drawer/color-field/calendar (WIP del view-switch del calendario).
5. **El tooling también había derivado** (hipótesis del encargo extendida a los
scripts, confirmada): `component-audit` auditaba contra la arquitectura anterior —
E-2.2 exigía el import del CSS desde `eidos/index.css` (el patrón actual es el
import del wrapper) y fallaba en 120/132; D-1.2 exigía el template de 6 tabs
(el canónico v2 es de 9) y **fallaba precisamente en las demos ya migradas**;
D-3.1 exigía `somaSnippet` (v2 usa snippet único); `TabsVariant` en el vocabulario
espejo no tenía `segmented`. **Corregido en la misma pasada.** Veredictos antes/después
del fix del tooling: 0 PASS / 117 NEEDS-WORK / 15 BROKEN → **75 / 50 / 7**.
Moraleja para la Fase E: los espejos manuales dentro de scripts
(`SHARED_VARIANT_VOCAB`, templates de demo) necesitan el mismo tratamiento
anti-drift que los docs — generarlos o testearlos contra el canon.

Powered by TurnKey Linux.