# CONTINUE — `Affix`, la capa de colocación, y el eje que abrió: los knobs visuales > **Kickoff**: _«Lee `docs/process/CONTINUE-affix.md` y sigue por §3.»_ > **Fecha**: 2026-08-15 (cierre de jornada) · Rama `alpha-0.1-dir-prefs`, > **pusheada en `65536ae6c`** — limpia contra el remoto, 0 por delante. > ⚠️ **RAMA COMPARTIDA, y hoy se notó**: otra sesión trabaja en `blocks` y su > commit `948f7cf7e` es la punta actual. En el árbol quedan **23 ficheros suyos > sin commitear** (`src/uix/blocks/*`, `docs/process/*blocks*`, > `scripts/blocks-check.ts`). Stagear **por lista explícita**, nunca `git add` > de un directorio: hoy un `git add docs/` se llevó cinco ficheros ajenos. > **Doctrina viva**: [`architecture/morfo.md`](../architecture/morfo.md) > §_What morfo does NOT contain_ + §Backlog `undeclaredState` · > [`architecture/eidos.md`](../architecture/eidos.md) · > [`theming/reference.md`](../theming/reference.md) §39 · > [`canon/vocabularies.md`](../canon/vocabularies.md) §_Placement grids (2 × 9)_. > El relato completo del eje `Affix` está en > [`PLAN-affix.md`](PLAN-affix.md) §6 y §7 — **este documento no lo repite**. --- ## 0 · Dónde estás Catorce commits publicados en la jornada; **trece son de este eje** (el catorceavo, `948f7cf7e`, es de la sesión de `blocks` — se publicó con el push porque la rama es una sola). | Commit | Qué | | ------------- | ---------------------------------------------------------------------------- | | `92c8f6466` | **`Affix`** — la pieza que `Sticky` no puede ser, y la capa que `Fab` y `MenuDial` ya habían copiado dos veces | | `1fc0a7a96` | `Fab` deja su copia privada de la colocación y lee la capa | | `c945be23b` | **breaking** — un eje de capa = un token público + una ranura; `--fab-offset` / `--fab-z` retirados sin shim | | `acb7f0b41` | la capa deja de no tener guard: uno de texto y uno de valor computado | | `4b39b718c` | el aviso al pie se lee donde se ve | | `c53efc602` | `MenuDial` lee la colocación de la capa — la última copia privada, borrada | | `ee41b691d` | los 20px del `MenuDial` son una decisión, no una deriva | | `7ea913ab1` | **la rejilla lógica se canoniza** — cinco uniones a mano pasan a una; cierra EID-3 | | `a11805d43` | la doctrina alcanza a la jornada | | `de0615caf` | `affix` deja de declarar lo que sólo lee una capa; el gancho pasa a nombre de CAPA | | `b72a56491` | la capa sale de `components/` — tenerla ahí acoplaba `Fab` y `MenuDial` a `Affix` | | `998d72191` | **los 25 knobs visuales salen del contrato** — comentados, no borrados | | `65536ae6c` | el enum que salió del morfo vuelve, leído de la unión de TypeScript (§2), y este handoff | --- ## 1 · El eje `Affix`: CERRADO Está entero en [`PLAN-affix.md`](PLAN-affix.md) §6 (lo construido vs lo planeado) y §7 (lo que vino después: el peldaño `--z-index-affix: 150`, el patrón de dos tokens por eje, la canonización de la rejilla, los dos guards, y las dos correcciones del autor). **Lo único que sigue abierto ahí**: - **Test de geometría de la capa** — diferido al tercer consumidor. El tercero ya existe (`Affix`, `Fab`, `MenuDial`), así que la condición se cumplió. El hueco y por qué ninguna aserción a nivel de propiedad lo cubre está escrito en la cabecera de [`scripts/layer-check.ts`](../../scripts/layer-check.ts): `getComputedStyle` da el valor USADO, y un `inset: auto` se lee como píxeles (medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). Discriminarlo exige **geometría** — el borde contra su bloque contenedor —, y eso exige recorrer los ancestros buscando `transform` / `filter` / `contain` / `will-change`. --- ## 2 · El eje que abrió: los knobs visuales fuera del contrato No estaba previsto. Nació de la corrección de `de0615caf` (§7 del plan): `affix` declaraba dos attrs que **lee una sola capa**. Al aplicarle la misma vara al resto del árbol salieron **25 declaraciones más**, en seis morfos. ### La sentencia del autor, literal > _«la doctrina prevalece, porque no tiene sentido declarar atributos que sólo se > usan en una capa, morfo funciona como una capa declarativa entre capas, eso es > el diseño.»_ Y el criterio de retirada: > _«no los retires, déjalos comentados y con la explicación del porqué.»_ ### Lo aplicado (`998d72191`) 25 entradas comentadas en sitio — avatar 9 · s-text 6 · image 5 · avatar-group 2 · skeleton 2 · qr-code 1 —, cada bloque citando **tres** fuentes de doctrina (`morfo.md` §_What morfo does NOT contain_, `morfo.md` §Backlog `undeclaredState`, `theming/reference.md` §39). Comentadas y no borradas porque son el registro de lo que el morfo prometía **y** de los enums que un guard podría volver a leer. De todo lo que declaraban esos seis morfos, sobrevive sólo `data-status` (avatar ×3, image ×4): lo mueve `ImageProvider` desde `$soma/layers`, así que **sí** cruza la frontera de capa. Ese es el criterio, y se sostiene solo. ### Lo que se perdió al comentarlos, y se ha devuelto hoy Un `values: [...]` en el morfo le daba a `eidos-lint` con qué rechazar `[data-size='huge']`. Al salir, esa validación se quedó sin fuente. **Se ha devuelto leyendo la unión de TypeScript**, que es donde el enum vive de verdad. No en `eidos-lint`: en `recipe-css-contract.test.ts`, donde el mecanismo **ya existía** para `variant` desde antes (línea ~790, _«Variant canon enforcement»_). Lo hecho es extenderlo, no inventarlo: - el colector de valores CSS pasa a tomar el attr por parámetro; - un extractor nuevo entiende `Extract` — donde viven cinco de los ejes (`AvatarSize`, `ImageSize`, `SkeletonSize`, `QrCodeSize`, `AvatarBadgePosition`) — y el miembro `boolean` de `AvatarRing`; - **11 ejes** en la tabla `KNOB_AXES`, inspeccionando **44 valores CSS reales**. Dos cosas que importan más que el conteo: - **`qr-code.cell-shape` quedó FUERA, con motivo escrito.** Estampa `data-cell-shape` en el `` raíz pero **ninguna regla CSS lo lee**: la forma se dibuja en la geometría del ``. Con él dentro, el test pasaba inspeccionando el conjunto vacío. De ahí la aserción `used.size > 0`: un eje ciego es fallo, no verde. Su prop la sigue guardando `tsc` en el call site. - **Probado por mutación, 4/4**: typo en un valor CSS · unión estrechada · unión reescrita a un alias ilegible · eje cegado por renombrado de reglas. Los cuatro detectados; el árbol restaurado byte a byte después. ### El hallazgo, y la decisión que espera Buscando si los ejes con alias estaban cubiertos en otro sitio apareció [`src/uix/eidos/component-visual-attrs.test.ts`](../../src/uix/eidos/component-visual-attrs.test.ts). **Es la otra mitad del mismo problema, y lleva ahí desde antes de esta jornada**: verifica que el wrapper siga estampando el attr, para 60+ componentes, y guarda `data-size` / `data-variant` / `data-color` **enteramente fuera de morfo**. Es la prueba, en el propio árbol, de que la sentencia del autor no inaugura nada: describe cómo estaba construido el sistema ya. Las dos mitades, ahora explícitas: | Mitad | Pregunta | Dónde | | ------------ | ---------------------------------------------- | ---------------------------------- | | **Presencia**| ¿el wrapper sigue estampando el attr? | `component-visual-attrs.test.ts` | | **Enum** | ¿el valor es uno que el tipo permite? | `recipe-css-contract.test.ts` (§2) | Y el hueco en la mitad de presencia, **medido**: `image`, `skeleton`, `qr-code` y `s-text` no tienen **ninguna** fila (0 de 4); `avatar` tiene 3, pero no lista `data-ring`, `data-position` (badge) ni `data-stacking`. Añadir filas ahí es una decisión del autor, no una consecuencia mecánica: está presentada y **parada**. --- ## 3 · Qué queda, en orden 1. **DECISIÓN PENDIENTE — cerrar el hueco de la mitad de presencia.** Cuatro componentes sin fila y tres ejes de `avatar` sin cubrir (arriba). Es añadir filas a `VISUAL_ATTRS`; lo que no es mecánico es decidir si los 7 knobs booleanos (`data-dot`, `data-truncate`, `data-clamp`, `data-canvas`, `data-italic`, `data-underline`, `data-animated`) entran ahí o no se guardan. 2. **Los dos ejes con alias, sin cubrir por el guard de enum**: `data-color` (→ `ComponentColorProp`, en `eidos/lib/types.ts`) y `data-style` de `s-text` (→ `TextStyle`, en `components/text/types.ts`). El parser **no sigue imports** a propósito. Resolverlos es leer el módulo destino; decidir si vale la pena está abierto. 3. **Test de geometría de la capa** (§1) — el tercer consumidor ya existe. 4. **Ajenos a este eje, preexistentes y NO tocados**: `morfo:check` falla en `fab` (`data-fab-size` sin declarar) y `menu-dial` (`data-state` ausente en el trigger). ⚠️ Sobre el primero: `data-fab-size` **no está en ningún morfo** — `grep -rn "data-fab-size" src/uix/morfo/` no devuelve nada, y `git log -S` confirma que nunca lo estuvo. Lo mismo `data-picker-size` (×7) y `data-marker-size`. **No son deuda: son el modelo.** Lo que chirría es `morfo:check`, que clasifica por PREFIJO mientras la doctrina clasifica por NATURALEZA. La salida limpia sería renombrar a `data-_*` (prefijo reservado, _«intentionally outside morfo»_) — sin firmar. --- ## 4 · Cómo medirlo (estado al cerrar, verificado hoy) ```bash npx vitest run src/uix/eidos ``` | Guard | Estado | | ---------------------------------- | ------------------------------------------------------------------------------ | | `npx vitest run src/uix/eidos` | **421/421** en 32 ficheros (`recipe-css-contract` aporta 42) | | suite completa (`npm run test`) | **4960/4969**; los 9 fallos = 6 reales + 3 falsos (abajo) | | `contracts.test.ts` | **6 fallos PREEXISTENTES**: `waveform`, `media-player`, `audio-player`, `menubar`, `aura`, `radio-group`, `tabs` — ninguno tocado por este eje | | `npm run morfo:check` | 6/159 fallan: `color-field`, `combobox`, `fab`, `gradient-builder`, `menu-dial`, `palabras` (preexistentes) | | `npm run docs:check` | **0 errores** / 627 docs | | `npm run rtl:check` | **0 errores** / 178 ficheros | | `npm run layer:check` | requiere dev server; 0 violaciones sobre 3 consumidores en la última medida | | `npm run check` | **69 errores / 6899 ficheros** — la línea base heredada, sin mover en toda la jornada | --- ## 5 · Tres trampas de la jornada, para no repetirlas - **Un test en verde no prueba nada hasta que falla sobre lo que dice atrapar.** `layer-check` pasó en su primera ejecución habiendo inspeccionado **cero** elementos (la demo de `fab` venía con `placement="static"`, que no estampa gancho). El guard de enum estuvo a punto de repetirlo con `qr-code`. De ahí que **ambos** traten el conjunto vacío como violación, y que los dos estén probados por mutación (8 defectos inyectados en los de la capa, 4 en el de enum; 12/12 detectados). - **`prettier --write` sobre un fichero preexistente sucio reformatea el fichero entero.** Pasó tres veces hoy (66 líneas por 9 mías; 227 por 1). **7 de los 14 tests de `src/uix/eidos/` están sucios en HEAD** — todos sin punto y coma. La regla de CLAUDE.md manda: _match existing style_. No formatear al pasar. - **Correr guards pesados en paralelo con la suite produce fallos fantasma.** `npm run test` dio 9 fallos con cuatro scripts `tsx` corriendo a la vez; los 3 de `soma-attr-audit` / `orca` / `storage` eran **timeouts por inanición de CPU** — 161/161 en aislamiento. Medir en serio es medir solo.