From a11805d434d8303d4aa604d53596e5f1c5abc3ad Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 15 Aug 2026 02:15:58 +0200 Subject: [PATCH] =?UTF-8?q?docs(eidos):=20la=20doctrina=20alcanza=20a=20la?= =?UTF-8?q?=20jornada=20=E2=80=94=20dos=20guards=20nuevos,=20el=20patron?= =?UTF-8?q?=20de=20capa,=20y=20EID-3=20deja=20de=20estar=20pendiente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/audit-new-10-july/audit-active-uix.md | 158 +++++++++++---------- docs/canon/recipe-contract.md | 90 ++++++------ docs/guides/component-guide.md | 58 ++++---- docs/process/PLAN-affix.md | 67 +++++++++ docs/testing-and-tooling.md | 30 ++-- src/uix/eidos/components/README.md | 34 +++++ 6 files changed, 274 insertions(+), 163 deletions(-) diff --git a/docs/audit-new-10-july/audit-active-uix.md b/docs/audit-new-10-july/audit-active-uix.md index 0a3b88bf5..68b5069c1 100644 --- a/docs/audit-new-10-july/audit-active-uix.md +++ b/docs/audit-new-10-july/audit-active-uix.md @@ -45,13 +45,13 @@ Las debilidades reales no están en la disciplina de valores sino en **cuatro frentes**: 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; la propia fundación (`archetypes.css`) usa literales de opacidad que prohíbe a las recetas. 2. **Contrato sema declarado pero no ejecutado** — deuda ya registrada en `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, 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 @@ -75,12 +75,12 @@ escape), no de usuario final. ## 2. Alcance y método -| 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. | -| 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. | -| 4 | Gap analysis de personalización por eje (síntesis en §7). | +| 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. | +| 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. | +| 4 | Gap analysis de personalización por eje (síntesis en §7). | **Límites declarados**: `morfo:check`, `perm:check` y `smoke` no se 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 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` - 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 canónica; el libro sugiere intent). - **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 `uix.timers`, `chans/visual.ts:113-126`). - **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 el `close` polimórfico declarado pero los providers solo togglean `open` (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 canonizado.** `archetypes.css:162-167` implementa el anillo universal con - `box-shadow`, con esta justificación en comentario: *"the project's - layout.css sets `outline: none !important` globally"*. Pero + `box-shadow`, con esta justificación en comentario: _"the project's + layout.css sets `outline: none !important` globally"_. Pero `theming/reference.md` §32 canonizó (2026-07-07) **outline como modelo único de todo el catálogo** (HCM-safe, sin flicker de segmentos, paridad 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. - **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: - 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)`"). 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), drawer (16), toast (6), box (6) y media-player (4) usan `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 contenido (paddings/margins están en lógicos), pero convendría que el 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 31-scale palette" (`active-eidos.svelte.ts:653`, `arts/color/README.md:65`) 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.** `component-audit` lee únicamente `{kebab}.css` (`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`, `field/field-control-trigger.css`, `field/field-segment-state.css`, `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 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 - 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 **17 superficies** que emiten cascada `data-color` (avatar+badge, badge, 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 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; - 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. - **THM-6 · P3 — fallback sospechoso**: `var(--radius-full, 1px)` en `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` §packs ilustra el pack de dialog con un selector **manual** (`'[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 **uso propio en la receta**, no cobertura efectiva. -| 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. | -| 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. | -| `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. | -| props físicas left/right | 5/130 | Todas son APIs de colocación (EID-3). | +| 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. | +| 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. | +| `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. | +| 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): **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 periféricas pero reales: -| 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. | -| 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. | -| 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). | -| 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). | -| Focus ring | ✅ `--focus-ring-*` parametrizado | EID-1 (modelo dividido fundación vs canon §32). | -| 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. | -| 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. | -| 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). | -| 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. | -| Dark/light/HCM/contraste | ✅ | — | -| RTL | ✅ lógico en flujo + `getDirectionalKeys` | EID-3 (placement físico — pendiente de doctrina explícita). | -| 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. | +| 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. | +| 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. | +| 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). | +| 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). | +| Focus ring | ✅ `--focus-ring-*` parametrizado | EID-1 (modelo dividido fundación vs canon §32). | +| 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. | +| 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. | +| 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). | +| 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. | +| Dark/light/HCM/contraste | ✅ | — | +| 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. | --- @@ -491,34 +497,34 @@ sistemáticas y vale la pena listarlas: ## 9. Tabla de severidades -| 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` | -| 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 | -| 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-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-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-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-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-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` | -| 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-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-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-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 | -| 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 | -| 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 | +| 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` | +| 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 | +| 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-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-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-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-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-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` | +| 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-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-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-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 | +| 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 | +| 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 usuario final; los P1 son incoherencias internas con impacto real en a11y, diff --git a/docs/canon/recipe-contract.md b/docs/canon/recipe-contract.md index 3f6ce2137..a99e38785 100644 --- a/docs/canon/recipe-contract.md +++ b/docs/canon/recipe-contract.md @@ -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 THEMING §6: -| 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 | -| 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` | -| Internal separation | `gap[-{size}]` | — | -| 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`) | -| 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) | — | +| 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 | +| 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` | +| Internal separation | `gap[-{size}]` | — | +| 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`) | +| 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) | — | Inherited naming rules (THEMING §6): public `--{c}-{slot}`, private `--_{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 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). `var(--x, fallback)` is runtime-optional by construction. - 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 guards it**. A recipe reimplements none of these concepts on its own. -| 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 | -| **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 | -| **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 | -| **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 | -| **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 | -| **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 | — | -| **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 | -| **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 | +| 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 | +| **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 | +| **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 | +| **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 | +| **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 | +| **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 | — | +| **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 | +| **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 A deviation is valid **only** when annotated on the same line. The audit honors annotations; an unannotated deviation is drift. -| Annotation | When | Example | -| --- | --- | --- | -| `/* literal: */` | a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` | -| `/* functional: */` | 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 */` | -| `/* important: */` | 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 */` | +| Annotation | When | Example | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `/* literal: */` | a physically-fixed value or a justified optical tuning | `font-size: 13px; /* literal: tight icon affordance */` | +| `/* functional: */` | 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 */` | +| `/* important: */` | 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 didn't exist" (create it), "it's just a hover" (that is exactly the @@ -123,15 +124,15 @@ contract's case). ## 4. Enforcement -| Rule | Guards | Severity | -| --- | --- | --- | -| R-4.1 | `box-shadow` without `var(` (elevation/ring outside tokens) | error | -| R-4.2 | literal `opacity: 0 */` annotation (THM-5, 2026-07-11) | error | +| Rule | Guards | Severity | +| ----- | ----------------------------------------------------------------------------------------------------- | -------- | +| R-4.1 | `box-shadow` without `var(` (elevation/ring outside tokens) | error | +| R-4.2 | literal `opacity: 0 */` annotation (THM-5, 2026-07-11) | error | **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 @@ -178,8 +179,9 @@ The WIP tracks `words` / `palabras` / `chronos` are excluded from R-4.x 7. Transitions with `--duration-*`/`--ease-*`; no perceptual-signature `@keyframes`. 8. Spacing with `--space-*`, no hand-multiplied density. -9. If it's a list/menu → list-surface; if it floats → a - `--z-index-overlay-*` rung. +9. If it's a list/menu → list-surface; if it PORTALS → a `--z-index-overlay-*` + 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). 11. `node --import tsx/esm scripts/component-audit.ts --only {kebab}` with no new R-4.x. diff --git a/docs/guides/component-guide.md b/docs/guides/component-guide.md index 680e2cb82..5cea7a166 100644 --- a/docs/guides/component-guide.md +++ b/docs/guides/component-guide.md @@ -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 defends it today. -| 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 | -| **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 | -| **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 | -| **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 | -| **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 · 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) | -| **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 | -| **Composition** | compose the existing `Button`/`Field`/`Icon`/`Select` | re-implementing primitives inline | §4 + review | +| 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 | +| **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 | +| **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 | +| **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 | +| **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 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 | +| **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 | +| **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 | **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 @@ -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 component first: -| Shape | Sema expectation | -| ----------- | ----------------------------------------------------------------------------- | -| Passive | `0 events` is valid when the component only projects external state. | -| Interactive | User decisions usually need discrete events. | -| 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. | +| Shape | Sema expectation | +| ----------- | ---------------------------------------------------------------------------- | +| Passive | `0 events` is valid when the component only projects external state. | +| Interactive | User decisions usually need discrete events. | +| 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. | For every real user action decide: @@ -141,8 +141,8 @@ never by guessing: `scope: ['eidos']` and 0 events, plus the `## Passive justification` section in its README (machine rule F-1.5). The reference exemplars: `color-swatch` (minimal eidos-scope morfo) and `radio-cards` (composition - whose morfo header explains the delegation: *"declaring them here would - duplicate the contract"*). + whose morfo header explains the delegation: _"declaring them here would + duplicate the contract"_). A composite that delegates behavior to embedded components documents that 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. > **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`, `pressed`/`onPressedChange`, `indeterminate` — never `value: boolean`. 3. **Overlays**: `open`/`onOpenChange`/`onOpenChangeComplete` (post-animation) - + `side`/`align`/`forceMount`/`modal` + `onInteractOutside`/ - `onFocusOutside`. Hover timing: `openDelay`/`closeDelay` (+ - `groupSkipDelay` for tooltip groups). + - `side`/`align`/`forceMount`/`modal` + `onInteractOutside`/ + `onFocusOutside`. Hover timing: `openDelay`/`closeDelay` (+ + `groupSkipDelay` for tooltip groups). 4. **Capability booleans**: plain positive adjective first (`deselectable`, `dismissible`, `loop`); `allowX` only when no natural adjective exists (`allowHalf`, `allowCustomValue`); never `allowsX`. @@ -603,7 +603,7 @@ N1–N10; census and evidence in ## Checklist > 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 > four layers + recipe CSS + demo), see > [`completion-checklist.md`](./completion-checklist.md). diff --git a/docs/process/PLAN-affix.md b/docs/process/PLAN-affix.md index 1a9c443f9..91db0e7aa 100644 --- a/docs/process/PLAN-affix.md +++ b/docs/process/PLAN-affix.md @@ -303,3 +303,70 @@ Separado en dos elementos. - **`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ó: 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). diff --git a/docs/testing-and-tooling.md b/docs/testing-and-tooling.md index e7fb742ec..569b05415 100644 --- a/docs/testing-and-tooling.md +++ b/docs/testing-and-tooling.md @@ -10,7 +10,7 @@ status: current 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 commands are the `scripts` in `package.json`; this groups them by what they are -*for*. +_for_. ## 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. -| 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: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 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 translations:check` | missing / malformed translation keys. | -| `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.) | +| 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: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 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 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 translations:check` | missing / malformed translation keys. | +| `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. | ## 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 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.) | 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 / Eidos — there is no silent fallback to direct DOM writes (see [`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 — not bound to the global `document`. - **Sema is ornamental** — `ActiveUix.events` (the perceptual engine) is optional; diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index b48ab8875..46818d621 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -290,6 +290,40 @@ export type DateFieldVariant = ControlVariant; 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 `