docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente

Barrido de lo que la sesion cambio y la documentacion todavia no decia.

`testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que
atrapa cada script», con su reparto explicito: `layer:check` mira el valor
computado (quien gana la cascada) y declara su hueco (la geometria, porque
`getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles);
`shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador
no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio.

`component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la
regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la
pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha
a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar.

`canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda
`--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no
portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist
de recetas decia «si flota → una rung de overlay», que era incompleto.

`eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba
escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y
`affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en
el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un
contrato ajeno), un eje = token publico + ranura, el puente reafirma `position`
si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna
basta sola.

`audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original
debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia
tabla resumen como «pendiente de doctrina explicita». Ya no lo esta.

`PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo
interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron
de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba
prevista en el plan; todas salieron de auditar lo construido.

docs:check 0/627.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 7ea913ab11
commit a11805d434

@ -45,13 +45,13 @@ Las debilidades reales no están en la disciplina de valores sino en **cuatro
frentes**: frentes**:
1. **Huecos de enforcement** — el escáner de recetas solo lee `{kebab}.css` 1. **Huecos de enforcement** — el escáner de recetas solo lee `{kebab}.css`
(los 6 CSS secundarios escapan a R-*, y dentro hay violaciones reales); un (los 6 CSS secundarios escapan a R-\*, y dentro hay violaciones reales); un
componente sin morfo (`card-group`) es **invisible** para toda la matriz; componente sin morfo (`card-group`) es **invisible** para toda la matriz;
la propia fundación (`archetypes.css`) usa literales de opacidad que la propia fundación (`archetypes.css`) usa literales de opacidad que
prohíbe a las recetas. prohíbe a las recetas.
2. **Contrato sema declarado pero no ejecutado** — deuda ya registrada en 2. **Contrato sema declarado pero no ejecutado** — deuda ya registrada en
`book-deviations.md` (D.6/D.11) pero sustantiva: 6 superficies de `book-deviations.md` (D.6/D.11) pero sustantiva: 6 superficies de
menú/árbol con packs *dormant* y 5 pickers con `close` polimórfico menú/árbol con packs _dormant_ y 5 pickers con `close` polimórfico
inerte; a lo que esta pasada añade `field` (interactivo con 0 eventos, inerte; a lo que esta pasada añade `field` (interactivo con 0 eventos,
error A-3.1) y `float-panel` (28 acciones de teclado vs 6 eventos, A-3.7). error A-3.1) y `float-panel` (28 acciones de teclado vs 6 eventos, A-3.7).
3. **Incoherencias puntuales de a11y/theming** — dos políticas divergentes de 3. **Incoherencias puntuales de a11y/theming** — dos políticas divergentes de
@ -75,12 +75,12 @@ escape), no de usuario final.
## 2. Alcance y método ## 2. Alcance y método
| Fase | Qué se hizo | | Fase | Qué se hizo |
| --- | --- | | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Lectura completa del corpus: E0–E4 + RFCs ×7 + `book-deviations.md` + READMEs in-code (uix, eidos/components, arts del grafo: color, prefs, active-app, mapa arts). ~45 documentos. | | 1 | Lectura completa del corpus: E0–E4 + RFCs ×7 + `book-deviations.md` + READMEs in-code (uix, eidos/components, arts del grafo: color, prefs, active-app, mapa arts). ~45 documentos. |
| 2 | Auditoría de código del núcleo: `morfo` (compile, selectors, tipos), `sema` (engine, canales visual/sound/announce, holds), `soma` (runtime completo, Soma), `active-uix` (boot dual, impl, services), `eidos` (ActiveEidos, archetypes/events.css, themes/base, spot-checks de render-css). | | 2 | Auditoría de código del núcleo: `morfo` (compile, selectors, tipos), `sema` (engine, canales visual/sound/announce, holds), `soma` (runtime completo, Soma), `active-uix` (boot dual, impl, services), `eidos` (ActiveEidos, archetypes/events.css, themes/base, spot-checks de render-css). |
| 3 | Guards del repo (`eidos-lint-all`, `component:audit`, `vitest src/uix/eidos`, `docs:check`) + barridos grep propios sobre los 130 CSS mantenidos + matriz transversal por componente. | | 3 | Guards del repo (`eidos-lint-all`, `component:audit`, `vitest src/uix/eidos`, `docs:check`) + barridos grep propios sobre los 130 CSS mantenidos + matriz transversal por componente. |
| 4 | Gap analysis de personalización por eje (síntesis en §7). | | 4 | Gap analysis de personalización por eje (síntesis en §7). |
**Límites declarados**: `morfo:check`, `perm:check` y `smoke` no se **Límites declarados**: `morfo:check`, `perm:check` y `smoke` no se
ejecutaron (requieren dev server activo); no hubo verificación visual en ejecutaron (requieren dev server activo); no hubo verificación visual en
@ -167,9 +167,9 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
- **SEM-1 · P1 — dos políticas divergentes de prioridad de live-region.** El - **SEM-1 · P1 — dos políticas divergentes de prioridad de live-region.** El
runtime marca `assertive` solo si `family === 'signal' ∧ intent ∈ runtime marca `assertive` solo si `family === 'signal' ∧ intent ∈
{threat, loss}` (`soma/runtime.svelte.ts:725-728`); `AnnounceChannel` mira {threat, loss}` (`soma/runtime.svelte.ts:725-728`); `AnnounceChannel` mira
solo el intent (`sema/chans/announce.ts:55-56`). Un `commit.fail + threat` solo el intent (`sema/chans/announce.ts:55-56`). Un `commit.fail + threat`
se anuncia *polite* por la vía `sources.announce` y *assertive* por la vía se anuncia _polite_ por la vía `sources.announce` y _assertive_ por la vía
canal. Mismo concepto, dos reglas — hay que unificar (y decidir cuál es la canal. Mismo concepto, dos reglas — hay que unificar (y decidir cuál es la
canónica; el libro sugiere intent). canónica; el libro sugiere intent).
- **SEM-2 · P2 — política de errores frágil en los bordes.** (a) Los fallos - **SEM-2 · P2 — política de errores frágil en los bordes.** (a) Los fallos
@ -186,7 +186,7 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
tope cuando `finished` gana (callback fantasma de hasta 1500 ms en tope cuando `finished` gana (callback fantasma de hasta 1500 ms en
`uix.timers`, `chans/visual.ts:113-126`). `uix.timers`, `chans/visual.ts:113-126`).
- **SEM-4 · P2 — deuda declarada pero perceptualmente relevante**: los packs - **SEM-4 · P2 — deuda declarada pero perceptualmente relevante**: los packs
de menú/árbol están *dormant* (los 6 morfos no emiten vía de menú/árbol están _dormant_ (los 6 morfos no emiten vía
`runtime.trigger`; `book-deviations.md` D.6 caveat) y los 5 pickers tienen `runtime.trigger`; `book-deviations.md` D.6 caveat) y los 5 pickers tienen
el `close` polimórfico declarado pero los providers solo togglean `open` el `close` polimórfico declarado pero los providers solo togglean `open`
(D.11 "eventos inertes"). Resultado: la firma perceptiva de ~11 componentes (D.11 "eventos inertes"). Resultado: la firma perceptiva de ~11 componentes
@ -246,8 +246,8 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
- **EID-1 · P1 — el anillo de foco de la fundación contradice el §32 - **EID-1 · P1 — el anillo de foco de la fundación contradice el §32
canonizado.** `archetypes.css:162-167` implementa el anillo universal con canonizado.** `archetypes.css:162-167` implementa el anillo universal con
`box-shadow`, con esta justificación en comentario: *"the project's `box-shadow`, con esta justificación en comentario: _"the project's
layout.css sets `outline: none !important` globally"*. Pero layout.css sets `outline: none !important` globally"_. Pero
`theming/reference.md` §32 canonizó (2026-07-07) **outline como modelo `theming/reference.md` §32 canonizó (2026-07-07) **outline como modelo
único de todo el catálogo** (HCM-safe, sin flicker de segmentos, paridad único de todo el catálogo** (HCM-safe, sin flicker de segmentos, paridad
con referencias). El agujero de HCM está parcheado (forced-colors emite con referencias). El agujero de HCM está parcheado (forced-colors emite
@ -259,16 +259,22 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
reset, o anotar la excepción en §32. reset, o anotar la excepción en §32.
- **EID-2 · P2 — la fundación viola su propia disciplina de opacidad.** - **EID-2 · P2 — la fundación viola su propia disciplina de opacidad.**
`archetypes.css` usa `opacity: 0.5` (líneas 52, 111, 222) y `opacity: `archetypes.css` usa `opacity: 0.5` (líneas 52, 111, 222) y `opacity:
0.85` (129) sin token ni anotación — exactamente lo que R-4.2 declara 0.85` (129) sin token ni anotación — exactamente lo que R-4.2 declara
error en recetas ("nunca 0.4/0.5/0.6 a mano; `var(--opacity-disabled)`"). error en recetas ("nunca 0.4/0.5/0.6 a mano; `var(--opacity-disabled)`").
Además ignora `--opacity-disabled` existiendo el token. Además ignora `--opacity-disabled` existiendo el token.
- **EID-3 · P3 — ✅ RESUELTO 2026-08-15.** La excepción se cerró con doctrina
explícita en vez de con una nota: hay DOS rejillas nombradas en
`eidos/lib/types.ts` — `Position` (física, no espeja) y `LogicalPosition`
(`start`/`end`, espeja) —, ambas generadas a `canon/vocabularies.md`, y la
regla de elección es de comportamiento: ¿tiene que voltearse para un lector de
derecha a izquierda? El hallazgo original, para el registro:
- **EID-3 · P3 — `data-side` físico vs mandato "logical RTL".** dialog (24), - **EID-3 · P3 — `data-side` físico vs mandato "logical RTL".** dialog (24),
drawer (16), toast (6), box (6) y media-player (4) usan drawer (16), toast (6), box (6) y media-player (4) usan
`left:`/`right:` físicos como implementación de APIs de colocación por `left:`/`right:` físicos como implementación de APIs de colocación por
lado físico de viewport (convención floating-ui). No es deuda de flujo de lado físico de viewport (convención floating-ui). No es deuda de flujo de
contenido (paddings/margins están en lógicos), pero convendría que el contenido (paddings/margins están en lógicos), pero convendría que el
mandato "logical RTL — never left/right" (cabecera de component-guide) mandato "logical RTL — never left/right" (cabecera de component-guide)
explicitara la excepción de *placement*. explicitara la excepción de _placement_.
- **EID-4 · P3 — comentarios con recuento de paleta desactualizado**: "the - **EID-4 · P3 — comentarios con recuento de paleta desactualizado**: "the
31-scale palette" (`active-eidos.svelte.ts:653`, `arts/color/README.md:65`) 31-scale palette" (`active-eidos.svelte.ts:653`, `arts/color/README.md:65`)
vs las 33 canónicas de `PALETTE_SCALES`/`docs:check`. vs las 33 canónicas de `PALETTE_SCALES`/`docs:check`.
@ -315,14 +321,14 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
- **THM-1 · P1 — enforcement parcial: los CSS secundarios no se escanean.** - **THM-1 · P1 — enforcement parcial: los CSS secundarios no se escanean.**
`component-audit` lee únicamente `{kebab}.css` `component-audit` lee únicamente `{kebab}.css`
(`scripts/component-audit.ts:698,756`). Quedan fuera de TODAS las reglas (`scripts/component-audit.ts:698,756`). Quedan fuera de TODAS las reglas
R-*: `calendar/calendar-select.css`, `color-picker/color-picker-spectrum.css`, R-_: `calendar/calendar-select.css`, `color-picker/color-picker-spectrum.css`,
`date-range-picker/date-range-picker-time.css`, `date-range-picker/date-range-picker-time.css`,
`field/field-control-trigger.css`, `field/field-segment-state.css`, `field/field-control-trigger.css`, `field/field-segment-state.css`,
`picker-shell/picker-time-row.css`. Y no es teórico: dentro hay `picker-shell/picker-time-row.css`. Y no es teórico: dentro hay
`hsl()` ×8 sin anotar (spectrum — físicamente fijo, pero la regla exige `hsl()` ×8 sin anotar (spectrum — físicamente fijo, pero la regla exige
anotación), `opacity: 0.6` (calendar-select.css:27) y un fallback mágico anotación), `opacity: 0.6` (calendar-select.css:27) y un fallback mágico
`var(--calendar-month-gap, 36px)` (date-range-picker-time.css:9). El `var(--calendar-month-gap, 36px)` (date-range-picker-time.css:9). El
escáner debería iterar `*.css` del directorio. escáner debería iterar `_.css` del directorio.
- **THM-2 · P1 — la paleta por instancia está al 12 % del catálogo.** De las - **THM-2 · P1 — la paleta por instancia está al 12 % del catálogo.** De las
**17 superficies** que emiten cascada `data-color` (avatar+badge, badge, **17 superficies** que emiten cascada `data-color` (avatar+badge, badge,
button, card, checkbox, editable, file-upload, radio-group, button, card, checkbox, editable, file-upload, radio-group,
@ -351,11 +357,11 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
- **THM-5 · P2 — `!important` sin doctrina.** 17 usos en el catálogo - **THM-5 · P2 — `!important` sin doctrina.** 17 usos en el catálogo
mantenido. Los de color-picker (4) están justificados en comentario mantenido. Los de color-picker (4) están justificados en comentario
(inline styles de soma) y los de group (8) implementan la fusión de radios; (inline styles de soma) y los de group (8) implementan la fusión de radios;
drawer/float-panel/tabs/select van sin anotación. No hay regla R-* que los drawer/float-panel/tabs/select van sin anotación. No hay regla R-\* que los
gobierne — conviene o anotarlos como los literales o darles guard. gobierne — conviene o anotarlos como los literales o darles guard.
- **THM-6 · P3 — fallback sospechoso**: `var(--radius-full, 1px)` en - **THM-6 · P3 — fallback sospechoso**: `var(--radius-full, 1px)` en
`menu-dial/menu-dial.css:293` (sus hermanos usan `9999px`; 1px como `menu-dial/menu-dial.css:293` (sus hermanos usan `9999px`; 1px como
fallback de *full* huele a errata). fallback de _full_ huele a errata).
- **THM-7 · P2 — el ejemplo del doc contra su propia regla**: `sema.md` - **THM-7 · P2 — el ejemplo del doc contra su propia regla**: `sema.md`
§packs ilustra el pack de dialog con un selector **manual** §packs ilustra el pack de dialog con un selector **manual**
(`'[data-dialog-content][data-event-intent="threat"]'`) cuando la regla del (`'[data-dialog-content][data-event-intent="threat"]'`) cuando la regla del
@ -367,14 +373,14 @@ impacto real · P2 deuda/corrección · P3 menor/observación). Evidencia en
Lectura correcta: heredar de la fundación es cumplimiento — la columna mide Lectura correcta: heredar de la fundación es cumplimiento — la columna mide
**uso propio en la receta**, no cobertura efectiva. **uso propio en la receta**, no cobertura efectiva.
| Sistema | Recetas que lo consumen en propio CSS | Lectura | | Sistema | Recetas que lo consumen en propio CSS | Lectura |
| --- | --- | --- | | ------------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| state-layer (`var(--state-*)`) | 20/130 | El resto lo hereda de `archetypes.css` vía arquetipo; el único hover fuera de contrato que queda es badge (R-4.3, `color-mix(currentColor)`), confirmado por la matriz de aceptación. | | state-layer (`var(--state-*)`) | 20/130 | El resto lo hereda de `archetypes.css` vía arquetipo; el único hover fuera de contrato que queda es badge (R-4.3, `color-mix(currentColor)`), confirmado por la matriz de aceptación. |
| depth/shadow tokens | 10/130 | Coherente con el modelo `data-depth` (el bundle lo pinta la fundación); no hay sombras literales. | | depth/shadow tokens | 10/130 | Coherente con el modelo `data-depth` (el bundle lo pinta la fundación); no hay sombras literales. |
| focus propio | 73/130 | El resto cae al anillo de fundación (ver EID-1). 7 warns R-1.5 pendientes en la matriz. | | focus propio | 73/130 | El resto cae al anillo de fundación (ver EID-1). 7 warns R-1.5 pendientes en la matriz. |
| `data-size` en CSS | 78/130 | Consistente con los subsets declarados; 10 fallos de paridad de chips (D-7.4) son de demo, no de receta. | | `data-size` en CSS | 78/130 | Consistente con los subsets declarados; 10 fallos de paridad de chips (D-7.4) son de demo, no de receta. |
| motion tokens propios | 37/130 | El resto usa presets/signatures generados — el diseño esperado. | | motion tokens propios | 37/130 | El resto usa presets/signatures generados — el diseño esperado. |
| props físicas left/right | 5/130 | Todas son APIs de colocación (EID-3). | | props físicas left/right | 5/130 | Todas son APIs de colocación (EID-3). |
Estado por reglas de la matriz de aceptación (fresco, 2026-07-10): Estado por reglas de la matriz de aceptación (fresco, 2026-07-10):
**R-4.x prácticamente limpio** (1 sola violación viva: badge R-4.3) y **R-4.x prácticamente limpio** (1 sola violación viva: badge R-4.3) y
@ -419,27 +425,27 @@ ancha** (pocas librerías exponen color+tipografía+espacio+forma+profundidad+
gradientes con builders runtime y contrato introspectable). Las carencias son gradientes con builders runtime y contrato introspectable). Las carencias son
periféricas pero reales: periféricas pero reales:
| Eje | Estado | Carencia concreta | | Eje | Estado | Carencia concreta |
| --- | --- | --- | | ----------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Color (escalas/roles/slots/alphas) | ✅ completo + runtime + CSS-only themes | **THM-2**: paleta 33 por instancia solo en 2/17 componentes. Layer 4 (`--{c}-{role}-{slot}`) sigue sin consumidores — la propia FAQ la marca candidata a colapso "si en 6 meses nadie la usa": toca decidir. | | Color (escalas/roles/slots/alphas) | ✅ completo + runtime + CSS-only themes | **THM-2**: paleta 33 por instancia solo en 2/17 componentes. Layer 4 (`--{c}-{role}-{slot}`) sigue sin consumidores — la propia FAQ la marca candidata a colapso "si en 6 meses nadie la usa": toca decidir. |
| Tipografía | ✅ fluid + tracking/leading/features/measure + fonts config | Motor de fuentes variables listo pero **assets aún TTF estáticos** (capacidad no ejercida, rfc-typography §5); `--size-{k}-font-letter-spacing` emitido y sin consumir por recetas de control (pendiente declarado); divergencia de 2 px en label `lg` segmentado (pendiente documentado en §5). | | Tipografía | ✅ fluid + tracking/leading/features/measure + fonts config | Motor de fuentes variables listo pero **assets aún TTF estáticos** (capacidad no ejercida, rfc-typography §5); `--size-{k}-font-letter-spacing` emitido y sin consumir por recetas de control (pendiente declarado); divergencia de 2 px en label `lg` segmentado (pendiente documentado en §5). |
| Espaciado / densidad / scaling | ✅ 3 ejes compuestos + applySpacing | Densidad = 3 niveles fijos (retunables en magnitud, no ampliables en número); cambiar la participación de scaling exige config+regeneración (no runtime) — ambas son decisiones de diseño declaradas, no bugs. | | Espaciado / densidad / scaling | ✅ 3 ejes compuestos + applySpacing | Densidad = 3 niveles fijos (retunables en magnitud, no ampliables en número); cambiar la participación de scaling exige config+regeneración (no runtime) — ambas son decisiones de diseño declaradas, no bugs. |
| Forma | ✅ smoothing/familias/nesting + applyShape | Nesting a `full` solo esquinas superiores (límite geométrico documentado). | | Forma | ✅ smoothing/familias/nesting + applyShape | Nesting a `full` solo esquinas superiores (límite geométrico documentado). |
| Profundidad | ✅ planes config-driven + frost + applyDepth | Token `scrim` emitido **sin regla** que lo consuma (backdrop sigue per-component); `--depth-{plane}-z` expuesto con 0 consumidores (open cage declarado — vigilar que no fosilice). | | Profundidad | ✅ planes config-driven + frost + applyDepth | Token `scrim` emitido **sin regla** que lo consuma (backdrop sigue per-component); `--depth-{plane}-z` expuesto con 0 consumidores (open cage declarado — vigilar que no fosilice). |
| Motion | ✅ dos momentos + presets tipados extensibles + loops + reduce | Presets JS no serializables (registro directo, documentado). Sin carencia real. | | Motion | ✅ dos momentos + presets tipados extensibles + loops + reduce | Presets JS no serializables (registro directo, documentado). Sin carencia real. |
| Sonido / háptica | ✅ tunings + packs + preferences reduce/off | **Un tema no puede re-sonorizar sin código**: los packs son TS, no config del tema (coherente con D.7 "samples = recursos", pero significa que la personalización sonora es de app/desarrollador, no de theme distribuible). | | Sonido / háptica | ✅ tunings + packs + preferences reduce/off | **Un tema no puede re-sonorizar sin código**: los packs son TS, no config del tema (coherente con D.7 "samples = recursos", pero significa que la personalización sonora es de app/desarrollador, no de theme distribuible). |
| Focus ring | ✅ `--focus-ring-*` parametrizado | EID-1 (modelo dividido fundación vs canon §32). | | Focus ring | ✅ `--focus-ring-*` parametrizado | EID-1 (modelo dividido fundación vs canon §32). |
| State layer | ✅ config `primitives.state` | — | | State layer | ✅ config `primitives.state` | — |
| Touch target | Constante 44 px | **No temable por diseño declarado** (§37: "hard ergonomic constant"). Coherente; anotar en gap solo como decisión. | | Touch target | Constante 44 px | **No temable por diseño declarado** (§37: "hard ergonomic constant"). Coherente; anotar en gap solo como decisión. |
| Z-index / bandas | ✅ config + banda overlay | — | | Z-index / bandas | ✅ config + banda overlay | — |
| Breakpoints / container | ✅ config vía ActiveDom → tokens + `@container` | Container queries: eje temable con **0 consumidores** ("open cage", tsc.md §container) — mismo riesgo de fosilización que scrim/depth-z. | | Breakpoints / container | ✅ config vía ActiveDom → tokens + `@container` | Container queries: eje temable con **0 consumidores** ("open cage", tsc.md §container) — mismo riesgo de fosilización que scrim/depth-z. |
| Gradientes | ✅ tokens + angles + applyGradients + capstone | Solo deriva documental: reference §10 y channels.md §5 omiten el eje `gradient` que `ThemeSeed` ya compone (`active-eidos.svelte.ts:219-232`). | | Gradientes | ✅ tokens + angles + applyGradients + capstone | Solo deriva documental: reference §10 y channels.md §5 omiten el eje `gradient` que `ThemeSeed` ya compone (`active-eidos.svelte.ts:219-232`). |
| Iconografía | ✅ `--icon-size-*`/stroke + glifos spin-field temables | No hay noción de "icon set" temable por config (el tema no puede sustituir el set de iconos; decisión razonable, registrar como límite). | | Iconografía | ✅ `--icon-size-*`/stroke + glifos spin-field temables | No hay noción de "icon set" temable por config (el tema no puede sustituir el set de iconos; decisión razonable, registrar como límite). |
| Scrollbars | Parcial | `scroll-area` tiene receta propia, pero no hay theming sistémico del scrollbar nativo global (colores de `scrollbar-color`/webkit fuera del contrato). | | Scrollbars | Parcial | `scroll-area` tiene receta propia, pero no hay theming sistémico del scrollbar nativo global (colores de `scrollbar-color`/webkit fuera del contrato). |
| Selección de texto / caret / cursor | ❌ | La fundación no emite `::selection`, `caret-color` ni tokens de cursor; los cursores están cableados por arquetipo (pointer/grab/not-allowed). Si "todos los aspectos" es la vara: son los tres huecos sistémicos visibles. | | Selección de texto / caret / cursor | ❌ | La fundación no emite `::selection`, `caret-color` ni tokens de cursor; los cursores están cableados por arquetipo (pointer/grab/not-allowed). Si "todos los aspectos" es la vara: son los tres huecos sistémicos visibles. |
| Dark/light/HCM/contraste | ✅ | — | | Dark/light/HCM/contraste | ✅ | — |
| RTL | ✅ lógico en flujo + `getDirectionalKeys` | EID-3 (placement físico — pendiente de doctrina explícita). | | RTL | ✅ lógico en flujo + `getDirectionalKeys` | ~~EID-3 (placement físico — pendiente de doctrina explícita)~~ → **cerrado 2026-08-15**: dos rejillas nombradas (`Position` física / `LogicalPosition` lógica) con regla de elección en sus typedoc y en el canon generado. |
| Tema distribuible | ✅ CSS-only vía contrato | **Límite deliberado** (§19): `ThemeDefinition` no expone `recipes` — un tema no puede ajustar tokens per-component (solo el app en boot). Está razonado (portabilidad perceptual); consecuencia práctica: un "marketplace de temas" no podría re-tunear componentes sin pasar por el app. | | Tema distribuible | ✅ CSS-only vía contrato | **Límite deliberado** (§19): `ThemeDefinition` no expone `recipes` — un tema no puede ajustar tokens per-component (solo el app en boot). Está razonado (portabilidad perceptual); consecuencia práctica: un "marketplace de temas" no podría re-tunear componentes sin pasar por el app. |
--- ---
@ -491,34 +497,34 @@ sistemáticas y vale la pena listarlas:
## 9. Tabla de severidades ## 9. Tabla de severidades
| ID | Severidad | Resumen | Dónde | | ID | Severidad | Resumen | Dónde |
| --- | --- | --- | --- | | ----------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| MOR-4 | **P1** | card-group sin morfo → invisible a toda la maquinaria de aceptación | `eidos/components/card-group/` · `component-audit.ts:192` | | MOR-4 | **P1** | card-group sin morfo → invisible a toda la maquinaria de aceptación | `eidos/components/card-group/` · `component-audit.ts:192` |
| SEM-1 | **P1** | Dos políticas de prioridad de live-region divergentes | `runtime.svelte.ts:725` vs `chans/announce.ts:55` | | SEM-1 | **P1** | Dos políticas de prioridad de live-region divergentes | `runtime.svelte.ts:725` vs `chans/announce.ts:55` |
| SOM-5 | **P1** | field 0 eventos (A-3.1) · float-panel 28 acciones/6 eventos (A-3.7) | matriz de aceptación 2026-07-10 | | SOM-5 | **P1** | field 0 eventos (A-3.1) · float-panel 28 acciones/6 eventos (A-3.7) | matriz de aceptación 2026-07-10 |
| EID-1 | **P1** | Anillo de foco de fundación en box-shadow vs §32 outline canónico; condicionado por reset del app | `archetypes.css:138-167` | | EID-1 | **P1** | Anillo de foco de fundación en box-shadow vs §32 outline canónico; condicionado por reset del app | `archetypes.css:138-167` |
| THM-1 | **P1** | 6 CSS secundarios fuera del escaneo R-* (con violaciones dentro) | `component-audit.ts:698,756` | | THM-1 | **P1** | 6 CSS secundarios fuera del escaneo R-\* (con violaciones dentro) | `component-audit.ts:698,756` |
| THM-2 | **P1** | Paleta 33-escalas por instancia solo en button+toggle (2/17) | `generated/base.css` (census) | | THM-2 | **P1** | Paleta 33-escalas por instancia solo en button+toggle (2/17) | `generated/base.css` (census) |
| DOC-1 | **P1** | Canon E1/E2 dependiente de docs audit/deprecated | CANON §3 · component-guide cabecera | | DOC-1 | **P1** | Canon E1/E2 dependiente de docs audit/deprecated | CANON §3 · component-guide cabecera |
| DOC-2 | **P1** | Ancla editorial (PDF del libro) inexistente; edición FINAL sin reconciliar | `docs:check` I6 · git status | | DOC-2 | **P1** | Ancla editorial (PDF del libro) inexistente; edición FINAL sin reconciliar | `docs:check` I6 · git status |
| SEM-2 | P2 | Errores de canales silenciosos sin logger; unhandled rejections en `void trigger` | `engine.ts` · `runtime.svelte.ts:670` | | SEM-2 | P2 | Errores de canales silenciosos sin logger; unhandled rejections en `void trigger` | `engine.ts` · `runtime.svelte.ts:670` |
| SEM-4 | P2 | Packs dormant (6) + close polimórfico inerte (5) — firma declarada, no emitida | book-deviations D.6/D.11 | | SEM-4 | P2 | Packs dormant (6) + close polimórfico inerte (5) — firma declarada, no emitida | book-deviations D.6/D.11 |
| SOM-1 | P2 | `await handler()` contradice el contrato de handler síncrono | `runtime.svelte.ts:743` | | SOM-1 | P2 | `await handler()` contradice el contrato de handler síncrono | `runtime.svelte.ts:743` |
| SOM-2 | P2 | `keydown` corta en match sin handler | `runtime.svelte.ts:562` | | SOM-2 | P2 | `keydown` corta en match sin handler | `runtime.svelte.ts:562` |
| SOM-3 | P2 | 7 setTimeout + 1 addEventListener fuera de la doctrina gestionada | soma layers/providers | | SOM-3 | P2 | 7 setTimeout + 1 addEventListener fuera de la doctrina gestionada | soma layers/providers |
| SOM-4 | P2 | Backlog de providers re-declarando attrs morfo, sin censo ni guard | component-guide §Part props | | SOM-4 | P2 | Backlog de providers re-declarando attrs morfo, sin censo ni guard | component-guide §Part props |
| MOR-1 | P2 | semaSelector sin escape/validación en matchers string | `selectors.ts:141-155` | | MOR-1 | P2 | semaSelector sin escape/validación en matchers string | `selectors.ts:141-155` |
| EID-2 | P2 | Literales de opacidad en la fundación sin token/anotación | `archetypes.css:52,111,129,222` | | EID-2 | P2 | Literales de opacidad en la fundación sin token/anotación | `archetypes.css:52,111,129,222` |
| THM-3 | P2 | Suite eidos en rojo (canario censo toggle sin voltear) | `lint.test.ts:110` | | THM-3 | P2 | Suite eidos en rojo (canario censo toggle sin voltear) | `lint.test.ts:110` |
| THM-4 | P2 | 1150 selectores de contrato sin consumidor CSS, sin doctrina | eidos-lint-all | | THM-4 | P2 | 1150 selectores de contrato sin consumidor CSS, sin doctrina | eidos-lint-all |
| THM-5 | P2 | 17 `!important` sin regla que los gobierne | catálogo | | THM-5 | P2 | 17 `!important` sin regla que los gobierne | catálogo |
| THM-7 | P2 | Ejemplo de sema.md con selector manual contra su propia regla | `sema.md` §packs | | THM-7 | P2 | Ejemplo de sema.md con selector manual contra su propia regla | `sema.md` §packs |
| DEP-1 | P2 | clsx fantasma (importado, no declarado) | `soma/props/props.ts:1` | | DEP-1 | P2 | clsx fantasma (importado, no declarado) | `soma/props/props.ts:1` |
| DEP-2 | P2 | ogl declarado como dependencia y sin un solo import | `package.json:60` | | DEP-2 | P2 | ogl declarado como dependencia y sin un solo import | `package.json:60` |
| SEC-1 | P2 | Endurecer serializador de valores en renderCssVariables (`;}` embebidos) | `lib/` render de variables | | SEC-1 | P2 | Endurecer serializador de valores en renderCssVariables (`;}` embebidos) | `lib/` render de variables |
| DOC-3/4 | P2 | Enlaces muertos ×15 (WARN tolerado) + secciones stale (holds, event:*, gradient, paletteScaleDecls) | docs:check · sema.md · notes.md | | DOC-3/4 | P2 | Enlaces muertos ×15 (WARN tolerado) + secciones stale (holds, event:\*, gradient, paletteScaleDecls) | docs:check · sema.md · notes.md |
| MOR-2/3 · SEM-3 · AUX-1/2 · EID-3/4 · THM-6 · DOC-5/6 | P3 | Menores: duplicación marker, no-op cache, timer fantasma, triple live-region, disabledDom mixto, placement físico, recuentos fósiles, radius-full 1px, §4.11 duplicado, TODOs huérfanos | ver secciones | | MOR-2/3 · SEM-3 · AUX-1/2 · EID-3/4 · THM-6 · DOC-5/6 | P3 | Menores: duplicación marker, no-op cache, timer fantasma, triple live-region, disabledDom mixto, placement físico, recuentos fósiles, radius-full 1px, §4.11 duplicado, TODOs huérfanos | ver secciones |
| GAP-* | — | Carencias de personalización de §7 (selection/caret/cursor/scrollbar global, sonido por tema, layer-4, container/scrim/depth-z sin consumidores, assets variables) | §7 | | GAP-\* | — | Carencias de personalización de §7 (selection/caret/cursor/scrollbar global, sonido por tema, layer-4, container/scrim/depth-z sin consumidores, assets variables) | §7 |
**Sin P0.** Nada de lo encontrado rompe el contrato público ni expone al **Sin P0.** Nada de lo encontrado rompe el contrato público ni expone al
usuario final; los P1 son incoherencias internas con impacto real en a11y, usuario final; los P1 son incoherencias internas con impacto real en a11y,

@ -35,16 +35,16 @@ Every recipe declares its knobs in `lib/recipes/base.ts` with this
vocabulary. **The names are canon**, just like the color slots are in vocabulary. **The names are canon**, just like the color slots are in
THEMING §6: THEMING §6:
| Concept | Canonical name | Forbidden | | Concept | Canonical name | Forbidden |
| --- | --- | --- | | ----------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| Control height per size | `control-height-{size}` (or `{part}-height-{size}` when the part is not the control) | inventing a third name per component | | Control height per size | `control-height-{size}` (or `{part}-height-{size}` when the part is not the control) | inventing a third name per component |
| Padding per axis | `padding-inline[-{size}]` / `padding-block[-{size}]` | **`padding-x` / `padding-y`** AND the abbreviated segments **`px` / `py`** (physical axes) | | Padding per axis | `padding-inline[-{size}]` / `padding-block[-{size}]` | **`padding-x` / `padding-y`** AND the abbreviated segments **`px` / `py`** (physical axes) |
| Margin per axis | `margin-inline[-{size}]` / `margin-block[-{size}]` | `margin-x/-y`, `mx` / `my` | | Margin per axis | `margin-inline[-{size}]` / `margin-block[-{size}]` | `margin-x/-y`, `mx` / `my` |
| Internal separation | `gap[-{size}]` | — | | Internal separation | `gap[-{size}]` | — |
| Radius | `radius[-{size}]` → `var(--radius-*)` | px that equal a step of the scale | | Radius | `radius[-{size}]` → `var(--radius-*)` | px that equal a step of the scale |
| Control font | `font-size-{size}` = `var(--font-size-{size})` (**1:1**, THEMING §5) | literal px/rem (guarded in `recipe-css-contract`) | | Control font | `font-size-{size}` = `var(--font-size-{size})` (**1:1**, THEMING §5) | literal px/rem (guarded in `recipe-css-contract`) |
| Icon | `icon-size-{size}` = `var(--icon-size-{size})` | hand px; size from `--control-height-*` unless the element IS a control | | Icon | `icon-size-{size}` = `var(--icon-size-{size})` | hand px; size from `--control-height-*` unless the element IS a control |
| Field label | one typographic step below the input (THEMING §5) | — | | Field label | one typographic step below the input (THEMING §5) | — |
Inherited naming rules (THEMING §6): public `--{c}-{slot}`, private Inherited naming rules (THEMING §6): public `--{c}-{slot}`, private
`--_{c}-{slot}`, no `color-` segment, no kebab abbreviation, no `--_{c}-{slot}`, no `color-` segment, no kebab abbreviation, no
@ -55,7 +55,7 @@ Inherited naming rules (THEMING §6): public `--{c}-{slot}`, private
- A recipe token VALUE may reference only vocabulary that **exists** in the - A recipe token VALUE may reference only vocabulary that **exists** in the
emitted contract — a no-fallback `var(--x)` pointing at a name the emitted contract — a no-fallback `var(--x)` pointing at a name the
generators never emit is a *phantom* (it freezes the token generators never emit is a _phantom_ (it freezes the token
guaranteed-invalid; `--color-content-tertiary` shipped a month that way). guaranteed-invalid; `--color-content-tertiary` shipped a month that way).
`var(--x, fallback)` is runtime-optional by construction. `var(--x, fallback)` is runtime-optional by construction.
- A value referencing a component **private** (`--_{c}-*`, declared in the - A value referencing a component **private** (`--_{c}-*`, declared in the
@ -88,34 +88,35 @@ Inherited naming rules (THEMING §6): public `--{c}-{slot}`, private
The table IS the contract: **concept → canonical mechanism → the rule that The table IS the contract: **concept → canonical mechanism → the rule that
guards it**. A recipe reimplements none of these concepts on its own. guards it**. A recipe reimplements none of these concepts on its own.
| Concept | Canonical mechanism | How it is consumed | Rule | | Concept | Canonical mechanism | How it is consumed | Rule |
| --- | --- | --- | --- | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| **Neutral hover / press / selected** | the state-layer (rules in `archetypes.css`; magnitudes are config data `primitives.state` — changelog §40) | transparent background → `background: var(--state-hover)`; filled background → the overlay `background-image: linear-gradient(var(--state-hover), var(--state-hover))` | R-4.3 | | **Neutral hover / press / selected** | the state-layer (rules in `archetypes.css`; magnitudes are config data `primitives.state` — changelog §40) | transparent background → `background: var(--state-hover)`; filled background → the overlay `background-image: linear-gradient(var(--state-hover), var(--state-hover))` | R-4.3 |
| **Valenced hover** (solid / soft per color) | the recipe's palette swap | `background: var(--{c}-solid-hover)` etc. — tokens, never hand-rolled `color-mix(currentColor …)` | R-4.3 | | **Valenced hover** (solid / soft per color) | the recipe's palette swap | `background: var(--{c}-solid-hover)` etc. — tokens, never hand-rolled `color-mix(currentColor …)` | R-4.3 |
| **Elevation** | the scale + the depth channel | `box-shadow: var(--shadow-*)` or compose `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`; never literal shadows | R-4.1 | | **Elevation** | the scale + the depth channel | `box-shadow: var(--shadow-*)` or compose `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`; never literal shadows | R-4.1 |
| **Crisp inner ring** | the inset-ring (THEMING §29) | `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, …)` — the expression lives at the point of use | R-4.1 (passes: it carries `var()`) | | **Crisp inner ring** | the inset-ring (THEMING §29) | `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, …)` — the expression lives at the point of use | R-4.1 (passes: it carries `var()`) |
| **Focus** | per archetype (THEMING §32) | fields → the two-ring `--focus-ring-*` (box-shadow); surfaces/controls → their own `outline` (HCM-safe); declaring nothing falls back to `archetypes.css` | R-1.5 | | **Focus** | per archetype (THEMING §32) | fields → the two-ring `--focus-ring-*` (box-shadow); surfaces/controls → their own `outline` (HCM-safe); declaring nothing falls back to `archetypes.css` | R-1.5 |
| **Disabled** | the opacity token | `opacity: var(--opacity-disabled)` + `cursor: not-allowed` — never hand `0.4`/`0.5`/`0.6` | R-4.2 | | **Disabled** | the opacity token | `opacity: var(--opacity-disabled)` + `cursor: not-allowed` — never hand `0.4`/`0.5`/`0.6` | R-4.2 |
| **Typography** | 1:1 with the scale | see §1; canonical exceptions: avatar/marker (glyph ∝ diameter), accordion (prose scale) | R-2.7 + guard | | **Typography** | 1:1 with the scale | see §1; canonical exceptions: avatar/marker (glyph ∝ diameter), accordion (prose scale) | R-2.7 + guard |
| **Motion — state transitions** | tokens | `transition: X var(--duration-*) var(--ease-*)` | THEMING §35 block C | | **Motion — state transitions** | tokens | `transition: X var(--duration-*) var(--ease-*)` | THEMING §35 block C |
| **Motion — event signatures** | `EidosConfig.motion.{keyframes,signatures}` (generated, themeable) | NO local `@keyframes` for perceptual reactions; a local `@keyframes` only when **functional** (spin/shimmer/continuous period) and annotated | R-4.5 | | **Motion — event signatures** | `EidosConfig.motion.{keyframes,signatures}` (generated, themeable) | NO local `@keyframes` for perceptual reactions; a local `@keyframes` only when **functional** (spin/shimmer/continuous period) and annotated | R-4.5 |
| **Spacing** | `var(--space-*)` | density × scaling arrive composed from the foundation — **never** multiply by `--density-*`/`--scaling` in the recipe | R-2.3 | | **Spacing** | `var(--space-*)` | density × scaling arrive composed from the foundation — **never** multiply by `--density-*`/`--scaling` in the recipe | R-2.3 |
| **Touch target** | archetype (`trigger`/`close`/`action`) + list-surface | free via `@media (pointer: coarse)` in `archetypes.css` — don't declare your own 44px minimums | — | | **Touch target** | archetype (`trigger`/`close`/`action`) + list-surface | free via `@media (pointer: coarse)` in `archetypes.css` — don't declare your own 44px minimums | — |
| **List/menu rows** | the list-surface (`--list-item-*`) | list surfaces bridge `--list-item-height`/`-py`, not their own row heights | — | | **List/menu rows** | the list-surface (`--list-item-*`) | list surfaces bridge `--list-item-height`/`-py`, not their own row heights | — |
| **Floating z-index** | the `--z-index-overlay-*` band | each portaled overlay consumes its rung; raw integers `0..5` only for intra-component order | guard in contracts.test | | **Floating z-index** | the `--z-index-overlay-*` band | each PORTALED overlay consumes its rung; raw integers `0..5` only for intra-component order | guard in contracts.test |
| **Color** | role slots / recipe tokens | `var(--color-{role}-{slot})` or `var(--{c}-*)`; never direct `--scale-*`/`--primitive-*` nor hex/rgb | R-2.1/2.6, R-4.6 | | **Viewport-fixed chrome** (not portaled) | the `--z-index-*` ladder, `affix` rung | a consent strip / FAB / floating bar is page chrome, not an overlay: it rides `affix` (150) — above `sticky`, below every menu. Two elements in the SAME band tie, and DOM order breaks the tie (measured: a notice correctly first in the source lost to a sticky header, 30.7px of overlap) | THEME-SYS-1 + `layer:check` |
| **Color** | role slots / recipe tokens | `var(--color-{role}-{slot})` or `var(--{c}-*)`; never direct `--scale-*`/`--primitive-*` nor hex/rgb | R-2.1/2.6, R-4.6 |
## 3. Exceptions — how they are declared ## 3. Exceptions — how they are declared
A deviation is valid **only** when annotated on the same line. The audit A deviation is valid **only** when annotated on the same line. The audit
honors annotations; an unannotated deviation is drift. honors annotations; an unannotated deviation is drift.
| Annotation | When | Example | | Annotation | When | Example |
| --- | --- | --- | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `/* literal: <reason> */` | a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` | | `/* literal: <reason> */` | a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` |
| `/* functional: <reason> */` | a local `@keyframes` that is NOT a perceptual signature (continuous period, machinery) | `/* functional: continuous spin period, not an event signature */` `@keyframes spin { … }` | | `/* functional: <reason> */` | a local `@keyframes` that is NOT a perceptual signature (continuous period, machinery) | `/* functional: continuous spin period, not an event signature */` `@keyframes spin { … }` |
| physically-fixed | colors that must not follow the theme (the QR's white, the natural clock's skies) — hex allowed WITH a comment | `--_qr-bg: #ffffff; /* literal: QR quiet zone must be true white */` | | physically-fixed | colors that must not follow the theme (the QR's white, the natural clock's skies) — hex allowed WITH a comment | `--_qr-bg: #ffffff; /* literal: QR quiet zone must be true white */` |
| `/* important: <reason> */` | an `!important` that must win EVERY cascade fight — the three legitimate shapes: bypassing a soma inline style, cross-recipe fusion (the group's radius flattening), reduced-motion kills | `cursor: nwse-resize !important; /* important: wins over the header's inherited grab cursor */` | | `/* important: <reason> */` | an `!important` that must win EVERY cascade fight — the three legitimate shapes: bypassing a soma inline style, cross-recipe fusion (the group's radius flattening), reduced-motion kills | `cursor: nwse-resize !important; /* important: wins over the header's inherited grab cursor */` |
What does **not** pass as an exception: "it's faster this way", "the token What does **not** pass as an exception: "it's faster this way", "the token
didn't exist" (create it), "it's just a hover" (that is exactly the didn't exist" (create it), "it's just a hover" (that is exactly the
@ -123,15 +124,15 @@ contract's case).
## 4. Enforcement ## 4. Enforcement
| Rule | Guards | Severity | | Rule | Guards | Severity |
| --- | --- | --- | | ----- | ----------------------------------------------------------------------------------------------------- | -------- |
| R-4.1 | `box-shadow` without `var(` (elevation/ring outside tokens) | error | | R-4.1 | `box-shadow` without `var(` (elevation/ring outside tokens) | error |
| R-4.2 | literal `opacity: 0<N<1` outside `@keyframes` | error | | R-4.2 | literal `opacity: 0<N<1` outside `@keyframes` | error |
| R-4.3 | `background*` in `:hover` rules without a token (`var(`) or with manual `color-mix(… currentColor …)` | error | | R-4.3 | `background*` in `:hover` rules without a token (`var(`) or with manual `color-mix(… currentColor …)` | error |
| R-4.4 | recipe tokens with physical axes `padding-x/-y`, `margin-x/-y` | error | | R-4.4 | recipe tokens with physical axes `padding-x/-y`, `margin-x/-y` | error |
| R-4.5 | a local `@keyframes` without a `/* functional: … */` annotation | error | | R-4.5 | a local `@keyframes` without a `/* functional: … */` annotation | error |
| R-4.6 | direct `var(--scale-*)` / `var(--primitive-*)` in component CSS | error | | R-4.6 | direct `var(--scale-*)` / `var(--primitive-*)` in component CSS | error |
| R-4.7 | `!important` without a same-line `/* important: <reason> */` annotation (THM-5, 2026-07-11) | error | | R-4.7 | `!important` without a same-line `/* important: <reason> */` annotation (THM-5, 2026-07-11) | error |
**All R-4.x are `error`** (graduated 2026-07-02: the five mechanical ones **All R-4.x are `error`** (graduated 2026-07-02: the five mechanical ones
after the backfill; R-4.5 after the motion migration emptied its 15 after the backfill; R-4.5 after the motion migration emptied its 15
@ -178,8 +179,9 @@ The WIP tracks `words` / `palabras` / `chronos` are excluded from R-4.x
7. Transitions with `--duration-*`/`--ease-*`; no perceptual-signature 7. Transitions with `--duration-*`/`--ease-*`; no perceptual-signature
`@keyframes`. `@keyframes`.
8. Spacing with `--space-*`, no hand-multiplied density. 8. Spacing with `--space-*`, no hand-multiplied density.
9. If it's a list/menu → list-surface; if it floats → a 9. If it's a list/menu → list-surface; if it PORTALS → a `--z-index-overlay-*`
`--z-index-overlay-*` rung. rung; if it is viewport-fixed page chrome → the `affix` rung, via the
shared layer rather than a private copy of the geometry.
10. Every deviation carries its annotation (§3). 10. Every deviation carries its annotation (§3).
11. `node --import tsx/esm scripts/component-audit.ts --only {kebab}` with no 11. `node --import tsx/esm scripts/component-audit.ts --only {kebab}` with no
new R-4.x. new R-4.x.

@ -24,22 +24,22 @@ design.** All axes are LIVE — the phased rollout the 2026-06-19 audit
planned (A3–A5) landed during 2026-06/07; each axis names the guard that planned (A3–A5) landed during 2026-06/07; each axis names the guard that
defends it today. defends it today.
| Axis | Canon — WHAT to consume | NOT this (drift) | Guard | | Axis | Canon — WHAT to consume | NOT this (drift) | Guard |
| --- | --- | --- | --- | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Surface / elevation** | `data-depth='overlay'\|'modal'\|…` → the full bundle (surface·shadow·halo·border·blur·z) | hand-picked `surface-raised`/`-default`; own `--{c}-overlay-z`; arbitrary frost | `elevation-plane.test.ts` · THEME-SYS-1 | | **Surface / elevation** | `data-depth='overlay'\|'modal'\|…` → the full bundle (surface·shadow·halo·border·blur·z) | hand-picked `surface-raised`/`-default`; own `--{c}-overlay-z`; arbitrary frost | `elevation-plane.test.ts` · THEME-SYS-1 |
| **Radius** | `--radius-default` / global factor + `[data-shape-nest]` concentric | `calc(--radius-md − space)` by hand; fixed px | R-2.x + shape engine | | **Radius** | `--radius-default` / global factor + `[data-shape-nest]` concentric | `calc(--radius-md − space)` by hand; fixed px | R-2.x + shape engine |
| **State (hover/active)** | `--state-{hover,press,selected}` layer (neutral tier; per-variant accent stays in the recipe) | ad-hoc `color-mix`; per-component `--x-hover-bg` | R-4.3 | | **State (hover/active)** | `--state-{hover,press,selected}` layer (neutral tier; per-variant accent stays in the recipe) | ad-hoc `color-mix`; per-component `--x-hover-bg` | R-4.3 |
| **Focus** | `outline` + `--focus-ring-*` (§32 — ONE model, HCM-safe; the foundation fallback is `:where()`-wrapped so recipes win) | own focus tokens; box-shadow rings (die in HCM) | R-1.5 + forced-colors floor | | **Focus** | `outline` + `--focus-ring-*` (§32 — ONE model, HCM-safe; the foundation fallback is `:where()`-wrapped so recipes win) | own focus tokens; box-shadow rings (die in HCM) | R-1.5 + forced-colors floor |
| **Field label** | the canonical label role (size-relative, one step below the input; unified weight/color) | redefining `--{c}-label-*` | Field doctrine 2026-07-05 | | **Field label** | the canonical label role (size-relative, one step below the input; unified weight/color) | redefining `--{c}-label-*` | Field doctrine 2026-07-05 |
| **Size (controls)** | the `--size-{k}-*` bundle (height·font·padding·gap·radius·icon) | re-deriving size→font; consuming none of the bundle | size-bundle test (recipe-css-contract) | | **Size (controls)** | the `--size-{k}-*` bundle (height·font·padding·gap·radius·icon) | re-deriving size→font; consuming none of the bundle | size-bundle test (recipe-css-contract) |
| **Touch hit-area** | §37: `--touch-target` (44px) under `pointer: coarse` only — AREA ≠ VISUAL (`::before` slop for bare markers) | targets <44 on touch without slop; growing the visual | archetypes.css coarse rules | | **Touch hit-area** | §37: `--touch-target` (44px) under `pointer: coarse` only — AREA ≠ VISUAL (`::before` slop for bare markers) | targets <44 on touch without slop; growing the visual | archetypes.css coarse rules |
| **Portal typography** | anchor `font-family`+`line-height`+`color` on the portaled content root | inheriting (falls to serif in the portal) | rule (LIVE) | | **Portal typography** | anchor `font-family`+`line-height`+`color` on the portaled content root | inheriting (falls to serif in the portal) | rule (LIVE) |
| **RTL** | **logical** properties (`inline/block`, `inset-inline`) for flow; physical `left/right` ONLY as floating *placement* APIs (EID-3 exception) or where the geometry itself is physical (compass handles, polar arcs, a JS-measured offset); branch on direction with **`:dir(rtl)`** — and then the provider MUST stamp the raw `dir`, or `:dir()` only ever sees the inherited direction ([direction contract](../canon/direction-contract.md)) | physical `padding-left`/… in content flow; a logical anchor paired with a physical `translateX` — the anchor flips, the transform does not; `[dir='rtl'] …`, which misses the common no-attribute case and ignores any nearer re-declaration; accepting the prop, running the chain and never stamping — the maths moves, the paint stays behind; a `:dir()` rule that turns one logical face off and repaints the other — the property had ALREADY mirrored, so that cancels it | RTL-1 · RTL-2 · `npm run rtl:check` | | **RTL** | **logical** properties (`inline/block`, `inset-inline`) for flow; physical `left/right` ONLY where the geometry itself is physical (compass handles, polar arcs, a JS-measured offset) or as a **placement grid that must NOT mirror** — and that is now a named choice, not an exception: `Position` (physical) vs `LogicalPosition` (`start`/`end`, mirrors), both consts in `eidos/lib/types.ts`, both in [`canon/vocabularies.md`](../canon/vocabularies.md) §Placement grids. **Ask: must it flip for a right-to-left reader?** A strip pinned to `bottom-end` belongs on the trailing edge in both directions; a panel that opens to the physical right because that is where the space is does not. Narrow with `Extract<>`, never re-declare a grid (the logical one was hand-written five times until 2026-08-15). This closes EID-3, which recorded the physical exception in July 2026 and left its doctrine pending; branch on direction with **`:dir(rtl)`** — and then the provider MUST stamp the raw `dir`, or `:dir()` only ever sees the inherited direction ([direction contract](../canon/direction-contract.md)) | physical `padding-left`/… in content flow; a logical anchor paired with a physical `translateX` — the anchor flips, the transform does not; `[dir='rtl'] …`, which misses the common no-attribute case and ignores any nearer re-declaration; accepting the prop, running the chain and never stamping — the maths moves, the paint stays behind; a `:dir()` rule that turns one logical face off and repaints the other — the property had ALREADY mirrored, so that cancels it | RTL-1 · RTL-2 · `npm run rtl:check` |
| **RTL · SVG** | a graphic with a READING axis mirrors (invert the scale's pixel range); a RADIAL one does not. `text-anchor` is LOGICAL: leave it alone when the composition mirrors, force the physical one when it does not — see `eidos/components/chart/README.md` §Direction | mirroring *and* flipping the anchor (they cancel); flipping the anchor on a gutter that never moves (the label walks across the graphic); mirroring y values | eye, in RTL — RTL-1 reads CSS text and cannot see SVG attrs or JS-written inline geometry | | **RTL · SVG** | a graphic with a READING axis mirrors (invert the scale's pixel range); a RADIAL one does not. `text-anchor` is LOGICAL: leave it alone when the composition mirrors, force the physical one when it does not — see `eidos/components/chart/README.md` §Direction | mirroring _and_ flipping the anchor (they cancel); flipping the anchor on a gutter that never moves (the label walks across the graphic); mirroring y values | eye, in RTL — RTL-1 reads CSS text and cannot see SVG attrs or JS-written inline geometry |
| **i18n** | `eidos.langs.ts('#?key\|fallback')` + key in the catalog | hardcoded strings / `aria-label`s | rule (LIVE) | | **i18n** | `eidos.langs.ts('#?key\|fallback')` + key in the catalog | hardcoded strings / `aria-label`s | rule (LIVE) |
| **Color (values)** | role tokens `--color-*` / recipe tokens | raw hex/rgb/hsl/oklch | R-2.1/2.6 · R-4.6 | | **Color (values)** | role tokens `--color-*` / recipe tokens | raw hex/rgb/hsl/oklch | R-2.1/2.6 · R-4.6 |
| **Density / spacing** | `--space-*` · `--control-height-*` | fixed px (bypasses density/scaling) | R-2.x | | **Density / spacing** | `--space-*` · `--control-height-*` | fixed px (bypasses density/scaling) | R-2.x |
| **Composition** | compose the existing `Button`/`Field`/`Icon`/`Select` | re-implementing primitives inline | §4 + review | | **Composition** | compose the existing `Button`/`Field`/`Icon`/`Select` | re-implementing primitives inline | §4 + review |
**Update rule (so the guide can never reference a nonexistent token):** a new **Update rule (so the guide can never reference a nonexistent token):** a new
axis enters this table WITH its guard in the same pass — the table, the axis enters this table WITH its guard in the same pass — the table, the
@ -67,12 +67,12 @@ Document what soma includes and what it skips (with reason).
Do not treat an empty `events` array as correct by default. Classify the Do not treat an empty `events` array as correct by default. Classify the
component first: component first:
| Shape | Sema expectation | | Shape | Sema expectation |
| ----------- | ----------------------------------------------------------------------------- | | ----------- | ---------------------------------------------------------------------------- |
| Passive | `0 events` is valid when the component only projects external state. | | Passive | `0 events` is valid when the component only projects external state. |
| Interactive | User decisions usually need discrete events. | | Interactive | User decisions usually need discrete events. |
| Continuous | Do not emit every frame/pixel; model start/confirmed drag/commit boundaries. | | Continuous | Do not emit every frame/pixel; model start/confirmed drag/commit boundaries. |
| Mixed | Passive display may stay silent, but user actions still need events. | | Mixed | Passive display may stay silent, but user actions still need events. |
For every real user action decide: For every real user action decide:
@ -141,8 +141,8 @@ never by guessing:
`scope: ['eidos']` and 0 events, plus the `## Passive justification` `scope: ['eidos']` and 0 events, plus the `## Passive justification`
section in its README (machine rule F-1.5). The reference exemplars: section in its README (machine rule F-1.5). The reference exemplars:
`color-swatch` (minimal eidos-scope morfo) and `radio-cards` (composition `color-swatch` (minimal eidos-scope morfo) and `radio-cards` (composition
whose morfo header explains the delegation: *"declaring them here would whose morfo header explains the delegation: _"declaring them here would
duplicate the contract"*). duplicate the contract"_).
A composite that delegates behavior to embedded components documents that A composite that delegates behavior to embedded components documents that
delegation in its morfo header and, when sema-scoped, declares delegation in its morfo header and, when sema-scoped, declares
@ -275,7 +275,7 @@ sanctioned ways to apply them:
})); }));
``` ```
Override a morfo attr only when soma genuinely owns the *value* (formatting, Override a morfo attr only when soma genuinely owns the _value_ (formatting,
stringifying an ARIA boolean). Never override it just to repeat it. stringifying an ARIA boolean). Never override it just to repeat it.
> **Anti-pattern**: `{ ...this.runtimePart.props, role: 'spinbutton', 'aria-disabled': ... }` > **Anti-pattern**: `{ ...this.runtimePart.props, role: 'spinbutton', 'aria-disabled': ... }`
@ -577,9 +577,9 @@ N1–N10; census and evidence in
2. **Binaries speak their ARIA**: `checked`/`onCheckedChange`, 2. **Binaries speak their ARIA**: `checked`/`onCheckedChange`,
`pressed`/`onPressedChange`, `indeterminate` — never `value: boolean`. `pressed`/`onPressedChange`, `indeterminate` — never `value: boolean`.
3. **Overlays**: `open`/`onOpenChange`/`onOpenChangeComplete` (post-animation) 3. **Overlays**: `open`/`onOpenChange`/`onOpenChangeComplete` (post-animation)
+ `side`/`align`/`forceMount`/`modal` + `onInteractOutside`/ - `side`/`align`/`forceMount`/`modal` + `onInteractOutside`/
`onFocusOutside`. Hover timing: `openDelay`/`closeDelay` (+ `onFocusOutside`. Hover timing: `openDelay`/`closeDelay` (+
`groupSkipDelay` for tooltip groups). `groupSkipDelay` for tooltip groups).
4. **Capability booleans**: plain positive adjective first (`deselectable`, 4. **Capability booleans**: plain positive adjective first (`deselectable`,
`dismissible`, `loop`); `allowX` only when no natural adjective exists `dismissible`, `loop`); `allowX` only when no natural adjective exists
(`allowHalf`, `allowCustomValue`); never `allowsX`. (`allowHalf`, `allowCustomValue`); never `allowsX`.
@ -603,7 +603,7 @@ N1–N10; census and evidence in
## Checklist ## Checklist
> This is the **build checklist** — the ordered authoring steps to take a > This is the **build checklist** — the ordered authoring steps to take a
> component from nothing to shipped. For the *acceptance* criteria (the > component from nothing to shipped. For the _acceptance_ criteria (the
> machine-audited rules that decide when a component counts as done across all > machine-audited rules that decide when a component counts as done across all
> four layers + recipe CSS + demo), see > four layers + recipe CSS + demo), see
> [`completion-checklist.md`](./completion-checklist.md). > [`completion-checklist.md`](./completion-checklist.md).

@ -303,3 +303,70 @@ Separado en dos elementos.
- **`banner`**: envía `affix="top" | "bottom"` + `affixOffset`. Su README tenía - **`banner`**: envía `affix="top" | "bottom"` + `affixOffset`. Su README tenía
la disposición contraria («app-land, el canon ya trae `Sticky`») y se corrigió: la disposición contraria («app-land, el canon ya trae `Sticky`») y se corrigió:
nombraba un componente que NO puede hacerlo. nombraba un componente que NO puede hacerlo.
---
## 7. Lo que vino DESPUÉS de cerrar el plan (2026-08-14 → 15)
El §6 se escribió al cerrar. Lo que sigue salió de auditar lo construido, y
ninguna de las cuatro cosas estaba prevista aquí.
### La capa se cobró sus tres consumidores
`Fab` (2026-08-14) y `MenuDial` (2026-08-15) borraron sus copias privadas — 4 y
9 zonas — e importan la capa estampando `data-affix-placement`. Con eso las TRES
copias de la geometría de viewport (`air/layout/float` la tuvo primero) son una.
Dos aprendizajes que sólo aparecen migrando:
- **El puente reafirma `position` si el primitivo compuesto declara uno.** La
base de la capa es (0,1,0) y `button.css` declara `position: relative` sobre
`[data-button]` al mismo peso, con los dos ficheros code-split: el orden de
carga decide. `Fab` lo debe; `MenuDial` no, porque no compone nada en el marco.
- **`data-placement` no siempre es del posicionado.** En `MenuDial` tiene un
segundo lector —el arco deriva de la zona su apertura y su span— así que se
queda, y el gancho de capa viaja aparte. En `Fab`, que no tenía ese segundo
lector, desapareció.
### Un peldaño nuevo en el canon de z
`--z-index-affix: 150`. Con la tira en `top`, la banda `sticky` EMPATA con la de
`Sticky`, y el empate lo rompe el orden del DOM: un aviso va antes que la
cabecera en el fuente, luego perdía — 30,7px de solape, el cromo encima, o sea
el fallo del §1 reproducido por su propio sustituto. ⚠️ Añadir un peldaño toca
DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`.
### Un eje de capa = un token público + una ranura
Mientras el prop escribía el MISMO nombre que la capa lee, `offset="var(--affix-offset)"`
producía una custom property auto-referencial: ciclo, _guaranteed-invalid_,
`calc()` muerto, insets a `auto` — la caja a 324px del borde que debía tocar, y
la demo ofrecía ese valor como uno de sus chips. La forma correcta ya la hablaba
el árbol (`code.css` ×6, `display.css` ×6): `var(--_x, var(--x))`. Al separarlos
se retiraron `--fab-offset` y `--fab-z`, cuyo único lector era el puente que los
traducía de vuelta. **Regla**: un consumidor escribe la ranura, no acuña un token
propio.
### La rejilla de colocación se canonizó — y cerró EID-3
`lib/types.ts` ya tenía `Position`, la rejilla FÍSICA. La LÓGICA estaba escrita
a mano **cinco** veces (affix · fab · menu-dial · onion-menu · badge de avatar),
coincidiendo por mantenimiento y no por contrato. Ahora hay `LogicalPosition`
junto a ella, ambas consts y ambas en `canon/vocabularies.md`. **No son dos
grafías: son dos comportamientos** — `top-left` nunca espeja, `top-start` sí
(medido: `left: 0` → `right: 0`). Eso cierra EID-3, que el audit de julio dejó
como excepción «pendiente de doctrina explícita».
### Y la capa dejó de no tener guard
`shared-layer-contract.test.ts` (texto) + `npm run layer:check` (valor
computado). Ambos verificados **por mutación** — ocho defectos inyectados, ocho
detectados —, porque un test en verde no prueba nada hasta que falla sobre lo que
dice atrapar. El hueco que NO cubren está escrito en la cabecera del script:
la geometría, porque `getComputedStyle` da el valor usado y un `inset: auto` se
lee como píxeles.
### Lo que sigue abierto
- **Test de geometría de la capa** — diferido al tercer consumidor, que ya está.
- Ajenos a este eje: los 6 fallos previos de `contracts.test.ts`, y `morfo:check`
en `fab` (`data-fab-size` sin declarar) y `menu-dial` (`data-state` ausente).

@ -10,7 +10,7 @@ status: current
The cross-cutting story of how the framework stays correct: the test suite, the The cross-cutting story of how the framework stays correct: the test suite, the
contract validators, what is generated vs authored, and the SSR posture. The contract validators, what is generated vs authored, and the SSR posture. The
commands are the `scripts` in `package.json`; this groups them by what they are commands are the `scripts` in `package.json`; this groups them by what they are
*for*. _for_.
## The verification loop (the short version) ## The verification loop (the short version)
@ -51,16 +51,18 @@ inside soma. See
These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss. These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss.
| Command | Catches | | Command | Catches |
| --- | --- | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run morfo:check` | the **real DOM vs the morfo contract** — navigates each demo and validates emitted `data-*` against the declaration. | | `npm run morfo:check` | the **real DOM vs the morfo contract** — navigates each demo and validates emitted `data-*` against the declaration. |
| `npm run morfo:vocabulary` | **canonical-verb / vocabulary drift** in morfo events (e.g. `open\|closed`, declared verbs not in `SEMA_VERBS`). Hard-fails CI on declared-verb drift. | | `npm run morfo:vocabulary` | **canonical-verb / vocabulary drift** in morfo events (e.g. `open\|closed`, declared verbs not in `SEMA_VERBS`). Hard-fails CI on declared-verb drift. |
| `npm run component:audit` | the **acceptance matrix** of [`completion-checklist.md`](./guides/completion-checklist.md) — per-rule severity/applicability across all four layers + recipe + demo. | | `npm run component:audit` | the **acceptance matrix** of [`completion-checklist.md`](./guides/completion-checklist.md) — per-rule severity/applicability across all four layers + recipe + demo. |
| `npm run perm:check` | **state-transition** bugs — cycles a component through its declared states (via `data-perm-step` annotations) and re-validates morfo after each. Catches reactivity loops and transition-time drift `morfo:check` can't. | | `npm run perm:check` | **state-transition** bugs — cycles a component through its declared states (via `data-perm-step` annotations) and re-validates morfo after each. Catches reactivity loops and transition-time drift `morfo:check` can't. |
| `npm run smoke` | **runtime / hydration** errors — walks every `+page.svelte` with Playwright and surfaces `pageerror`, `console.error`, missing translation keys, `Context "X" not found`, and `__uix_lang_missing__` markers. Needs `npm run dev` running. HTTP 200 is SSR only; smoke exercises client hydration. | | `npm run smoke` | **runtime / hydration** errors — walks every `+page.svelte` with Playwright and surfaces `pageerror`, `console.error`, missing translation keys, `Context "X" not found`, and `__uix_lang_missing__` markers. Needs `npm run dev` running. HTTP 200 is SSR only; smoke exercises client hydration. |
| `npm run translations:check` | missing / malformed translation keys. | | `npm run layer:check` | a **shared visual layer losing a cascade fight** — for every element carrying a layer hook (`data-affix-placement`), asserts the computed `position`, that the stacking token resolved, and that no override slot declared inline computes to nothing (the signature of a custom-property CYCLE). Needs `npm run dev`. Its consumer list is DERIVED from who imports the layer, so a component joins the day it migrates. **Deliberate hole, stated in the script**: geometry. `getComputedStyle` reports the USED value, so an `inset: auto` reads back as pixels (measured: `-1976.7px`) — there is no property-level way to tell a dead `calc()` from an intended value. |
| `npm run docs:check` | **doc-corpus drift** — copied vocabulary counts vs the source consts, phantom fields (the legacy morfo text field; rejected API shapes), dependency claims vs `package.json`, the variant-vocab mirror in `component-audit.ts`, checklist↔audit rule-ID sync, and relative links (warn severity). Guards the "link the canon, never copy it" law of [`docs/authoring.md`](./authoring.md). | | `npm run translations:check` | missing / malformed translation keys. |
| `scripts/eidos-lint.ts` · `eidos-lint-all.ts` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. (Opt-in safety net; the architectural defense is the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md.) | | `npm run docs:check` | **doc-corpus drift** — copied vocabulary counts vs the source consts, phantom fields (the legacy morfo text field; rejected API shapes), dependency claims vs `package.json`, the variant-vocab mirror in `component-audit.ts`, checklist↔audit rule-ID sync, and relative links (warn severity). Guards the "link the canon, never copy it" law of [`docs/authoring.md`](./authoring.md). |
| `scripts/eidos-lint.ts` · `eidos-lint-all.ts` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. (Opt-in safety net; the architectural defense is the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md.) |
| `src/uix/eidos/shared-layer-contract.test.ts` (vitest) | the **text half** a browser cannot cover for a shared layer: every zone has a rule, `stretch` stays on the axes it was scoped to, no axis is read without its override slot, no geometry rule keys on the component identity, and each consumer imports the layer / stamps the hook / mints no parallel token / re-asserts `position` when the primitive it composes declares one. It exists for ONE thing the computed check is blind to: `getComputedStyle` of a safe-area slot returns `"0px"` on desktop, so a `:dir(rtl)` remap with one half flipped reads identical to a correct one on every CI machine and only surfaces on a notched phone, sideways, in RTL. |
| `src/uix/contracts.test.ts` (vitest, via `npm run test`) | **catalogue invariants** that keep declarations / recipes coherent as the framework grows. Each fails on drift naming the offender, and excludes the active-dev-track set so it stays green for the maintained catalogue: **VG-8** every morfo is `as const satisfies Morfo`, never `: Morfo` (a `: Morfo` annotation widens the literal so the schema can't check it); **SYS-1 scope-drift** a component shipping an `eidos/components/{c}/` recipe declares `'eidos'` in `scope`; **A31** no per-item membership predicate (`isSelected` / `isItemPressed` / …) doing `.current.includes` (O(N²) — lift a `Set`, use `.has()`); **A30** `inputId` registered with the parent Field in the constructor, not wrapped in a `$effect`; **THEME-SYS-1** overlay z-index references the named `--z-index-overlay-*` scale, never a raw integer. | | `src/uix/contracts.test.ts` (vitest, via `npm run test`) | **catalogue invariants** that keep declarations / recipes coherent as the framework grows. Each fails on drift naming the offender, and excludes the active-dev-track set so it stays green for the maintained catalogue: **VG-8** every morfo is `as const satisfies Morfo`, never `: Morfo` (a `: Morfo` annotation widens the literal so the schema can't check it); **SYS-1 scope-drift** a component shipping an `eidos/components/{c}/` recipe declares `'eidos'` in `scope`; **A31** no per-item membership predicate (`isSelected` / `isItemPressed` / …) doing `.current.includes` (O(N²) — lift a `Set`, use `.has()`); **A30** `inputId` registered with the parent Field in the constructor, not wrapped in a `$effect`; **THEME-SYS-1** overlay z-index references the named `--z-index-overlay-*` scale, never a raw integer. |
## Codegen — what is generated vs authored ## Codegen — what is generated vs authored
@ -68,8 +70,8 @@ These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss.
Some surfaces are produced from a source of truth, not hand-maintained. Don't Some surfaces are produced from a source of truth, not hand-maintained. Don't
edit the output; edit the source and regenerate. edit the output; edit the source and regenerate.
| Command | Generates from | | Command | Generates from |
| --- | --- | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `npm run generate:eidos-css` | the eidos foundation/recipe CSS, from `EidosConfig.recipes`. (`generated/base.css` is output — regenerate, don't hand-edit.) | | `npm run generate:eidos-css` | the eidos foundation/recipe CSS, from `EidosConfig.recipes`. (`generated/base.css` is output — regenerate, don't hand-edit.) |
The `data-*` contract is **not** a generated doc — it is the morfo itself, validated The `data-*` contract is **not** a generated doc — it is the morfo itself, validated
@ -93,7 +95,7 @@ and headless tests working:
- **`dom:false`** injects a shared `disabledDom` no-op consumed by Soma / Sema / - **`dom:false`** injects a shared `disabledDom` no-op consumed by Soma / Sema /
Eidos — there is no silent fallback to direct DOM writes (see Eidos — there is no silent fallback to direct DOM writes (see
[`arts/adom/README`](../src/arts/adom/README.md) and `src/uix/contracts.ts`). [`arts/adom/README`](../src/arts/adom/README.md) and `src/uix/contracts.ts`).
- **`ActiveDom`** resolves the *owner* `document` / `window` (`getDocument(node)` - **`ActiveDom`** resolves the _owner_ `document` / `window` (`getDocument(node)`
/ `getWindow(node)`), so it is correct under iframes, popups and happy-dom — / `getWindow(node)`), so it is correct under iframes, popups and happy-dom —
not bound to the global `document`. not bound to the global `document`.
- **Sema is ornamental** — `ActiveUix.events` (the perceptual engine) is optional; - **Sema is ornamental** — `ActiveUix.events` (the perceptual engine) is optional;

@ -290,6 +290,40 @@ export type DateFieldVariant = ControlVariant;
export type DateFieldColor = ColorRole; export type DateFieldColor = ColorRole;
``` ```
### Capas compartidas (shared layers)
Cuando VARIOS componentes necesitan la misma geometría, no se copia: se declara
una vez y se engancha por un **attr de capa** que cada consumidor estampa sobre
el elemento que ya tiene. Sin nodo envoltorio, sin anidar componentes.
Ejemplares vivos: `lib/list-surface.css` (ritmo de menú/listbox, consumido por
dropdown-menu · context-menu · menubar · select · combobox · command) y
`components/affix/affix.css` (colocación contra el viewport, consumida por
`Affix` · `Fab` · `MenuDial`).
Las reglas, y las tres primeras se aprendieron rompiéndose:
1. **La geometría engancha en el attr de CAPA, nunca en la identidad del
componente.** `morfo-check` selecciona `[data-{kebab}]` en toda la página y
valida cada coincidencia contra ese morfo — si un consumidor estampara la
identidad para heredar la geometría, quedaría soldado a un contrato ajeno.
2. **Un eje = un token público + una ranura privada**, consumido como
`var(--_x, var(--x))`. La capa posee el default; el consumidor escribe la
ranura si ha evaluado el eje y aterriza en otro sitio. Un consumidor **no
acuña `--{componente}-{eje}`**: sería un vocabulario paralelo para valores que
la capa ya posee. Y si el prop escribiera el mismo nombre que la capa lee, un
valor que referencie el token se vuelve una custom property cíclica —
_guaranteed-invalid_, `calc()` muerto, insets a `auto`.
3. **El puente reafirma `position` si el primitivo compuesto declara uno.** La
regla base de la capa tiene especificidad (0,1,0) y los ficheros van
code-split, así que un `position` del primitivo al mismo peso decide por orden
de carga. `Fab` lo debe (compone `<Button>`); `MenuDial` no (no compone nada
en el marco).
4. **Se guarda con dos redes**, porque ninguna basta sola: el texto
(`shared-layer-contract.test.ts`) ve lo que el navegador no puede — un
`env()` ya sustituido —, y `npm run layer:check` ve lo que el texto no puede
— quién gana la cascada de verdad.
### Partes Eidos-only ### Partes Eidos-only
Algunas partes existen solo para componer la superficie visual: `Header`, Algunas partes existen solo para componer la superficie visual: `Header`,

Loading…
Cancel
Save

Powered by TurnKey Linux.