# 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 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 `