astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
597 Commits (bf90fc62b14e98b7913f0ffdc8411779937089e7)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
e4bcd505d1 |
uix(code): temable — 0 % → 100 %, y el harness que también le pisa el radio
6 claves fusionadas en el bloque `code` de `recipes/base.ts` (donde ya vivía el
forward de paleta). Censo 0 % → **100 %**: no queda un knob de apariencia fuera
del contrato.
**Seis de sus trece knobs son SISTEMA y no se acuñan.** `code` es el primitivo
tipográfico canónico: sus seis ejes de tipo consumen `--style-code-*` con la
escotilla por instancia (`var(--_code-{eje}, var(--style-code-{eje}))`), la
forma que D-TH.2-b fijó, y un primitivo no re-declara el vocabulario de su
capa. Lo que entra al contrato es el CROMO de las variantes `soft` y `outline`.
- El padding es em-relativo a propósito (la píldora crece con el código que
envuelve) y lo declaran IGUAL las dos variantes con cromo: un knob por eje,
no cuatro.
- `--_code-color` sigue privado: conmutador de dos fuentes (tinta de contenido
en línea / forward de paleta cuando llega `data-color`), mismo patrón que el
borde de foco de textarea.
- El `margin: 0` / `padding: 0` / `background: transparent` / `border: 0` de la
base es RESET: la variante `plain` es texto desnudo por identidad.
EL HALLAZGO — el harness pisa también a `code` (→ next-features §13, ampliado):
Medido con CDP: `[data-uix-docs] code` declara `font-family`, `font-size`,
`padding` y `border-radius` y gana **(0,1,1)** contra la regla BASE de la receta
**(0,1,0)**. En la página de documentación el radio del componente es el del
sitio (3px), no el suyo (4px). Sus reglas de VARIANTE (0,2,0) sí ganan, así que
el efecto es selectivo y por eso sólo `radius` lee muerto. **No es un caso
aislado**: es la misma familia de reglas del harness que tapa el `<pre>` de
code-block con siete propiedades. Cualquier decisión sobre esa regla cubre los
dos componentes.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 174 valores × 7 estados. Determinismo
verificado.
- R-5.4: 0/6, los seis adjudicados con su medición — `radius` por el harness
(CDP), los otros cinco porque la demo monta sólo la variante `plain`, sin
cromo; forzando las variantes, los cinco alcanzan.
- censo --only 100 % · component:audit PASS · rtl:check 0 · docs:check 0 ·
check por fichero limpio · suite eidos sin rojos nuevos.
- README con «Talla y tema» y tab `Tokens` en la demo (6 claves).
Nota de proceso: el fichero de excepciones llegó a quedar con sintaxis TS
inválida por un apóstrofe sin escapar, y la comprobación que hice lo dio por
bueno porque el `$?` capturado era el del `grep` de la tubería, no el del
guard. Corregido, y re-verificados los ONCE componentes con ledger comprobando
el código de salida REAL: cero fallos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
d3089e880c |
uix(link): temable — 0 % → 36 %, y ese techo bajo es la respuesta correcta
5 claves fusionadas en el bloque `link` de `recipes/base.ts` (donde ya vivían los dos forwards de paleta). Censo 0 % → 36 %. **Un enlace en línea hereda casi todo del texto que lo rodea**, así que su superficie de tema es pequeña por diseño. Sus dos escotillas por instancia (`--_link-font-size`, `--_link-line-height`) tienen como fallback `inherit`, y ese `inherit` es SEMÁNTICO: el enlace toma la tipografía del párrafo en el que vive. Convertirlo en token exigiría un público con valor `inherit`, que en `:root` no significa lo mismo — se quedan privadas. Con los dos forwards de paleta, ésos son los cuatro privados que forman el grueso del 64 % restante. Lo tokenizable es la GEOMETRÍA: radio del anillo, hueco del glifo externo, tamaño del glifo (`0.85em` en las dos dimensiones → UN knob) y el grosor y desplazamiento del subrayado, cada uno declarado en las DOS reglas que lo dibujan (`always` y `hover`) con el mismo valor. Las tres opacidades son sistema; el `fill: currentColor` del glifo es identidad, sigue a la tinta del enlace a propósito. **La cascada no se toca.** El CSS lleva dos correcciones de especificidad ya medidas y explicadas en su sitio (el `inherit` condicional, para que un `color` pedido por el consumidor no se descarte en silencio; y el `:active` al final, porque una pulsación es siempre también un hover). El diff de computed confirma que tokenizar no las altera. Artefactos y gates: - Sonda antes/después = 0 diffs en 203 valores × 7 estados. Determinismo verificado. - R-5.4: 2/5, los tres mudos adjudicados con su medición. Ninguno es deuda: el glifo externo no se monta en la demo (0 nodos, contados) y el enlace arranca en `data-underline='hover'` sin puntero encima, así que NO HAY subrayado que mover (`text-decoration-line: none`, `thickness: auto`). Forzando el estado, los tres alcanzan. - En el mismo pase, `text-decoration-thickness` y `text-underline-offset` entran en la lista de propiedades del guard, que no las miraba — verificado que los otros DIECISÉIS componentes con ledger no cambian. - censo --only 36 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio · README con «Talla y tema» y tab `Tokens` (5 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
adb77863cd |
uix(kbd): temable — 0 % → 86 %, y 13/13 en el guard sin una sola excepción
13 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 86 %. 1. **`font-family` NO se acuña**: lee `var(--style-code-font-family)`, la capa tipográfica, y un consumidor no re-declara el vocabulario de su capa — precedente inmediato, `code-block`. El `font-size` SÍ entra, y la diferencia está medida: su fallback era un PRIMITIVO, no la capa. `--_kbd-font-size` es la escotilla por instancia que el wrapper escribe desde la prop `size` (`kbd.svelte:37`), y el fallback pasa a ser el público. 2. **El padding y el min-width son em-relativos a propósito** — una tecla crece con su letra —, así que son knobs con valor `em`, no literales pendientes de corregir. El `line-height: 1` sí es identidad: una tecla no tiene interlineado. 3. **El relieve de tecla física es de la variante `surface`**: borde inferior más grueso + luz interior. Dos knobs propios, no adorno del borde general; outline y ghost son planas por identidad. Artefactos y gates: - Sonda antes/después = 0 diffs en 522 valores × 7 estados. Determinismo verificado. - R-5.4: **13/13**, sin una sola entrada en el ledger — el primero de la tanda que no necesita adjudicar nada. - censo --only 86 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con su «Talla y tema» y tab `Tokens` en la demo (13 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
650d76aa0e |
uix(drag-drop): temable — 0 % → 89 %, casi todo en el fantasma del arrastre
10 claves en el bloque `drag-drop` de `recipes/base.ts` (fusionadas con `preview-z`, que ya estaba, y los dos forwards de paleta). Censo 0 % → 89 %, cero globales. **Siete de sus dieciséis knobs YA eran sistema** y no entran al ratio (las opacidades de fantasma y deshabilitado, los dos anillos de foco y el anillo `inset` de los droppables), así que los puntuables eran nueve — seis de ellos en el PREVIEW, el fantasma que sigue al puntero. 1. Los dos forwards de paleta siguen privados (THM-2): el fondo del `[data-dragover]` los lee a pelo y no admite un público sin duplicar la regla — es el 11 % que falta. En cambio el `border` y el `box-shadow` del preview los COMBINAN con una anchura y una sombra que sí son knobs propios, así que esas dos declaraciones entran al alcance. 2. El `--ring-inset-*` de los droppables no se toca: es vocabulario del anillo del SISTEMA y la receta se limita a alimentarlo con su acento. Acuñar un `droppable-ring-*` sería vocabulario paralelo sobre un transversal. 3. El `padding` del preview es un shorthand de dos valores → dos knobs lógicos. Artefactos y gates: - Sonda antes/después = 0 diffs en 1.827 valores × 7 estados. Determinismo verificado. - R-5.4: 0/10 en vivo, y los DIEZ adjudicados con su medición. No es un instrumento roto ni tokens muertos: el preview se crea sólo mientras hay un arrastre en curso (0 nodos en reposo, contados), y forzando el nodo los diez mueven su propiedad — padding 8→1234px, radio 6→1236px, borde 1→9px, fondo, tinta, familia, tamaño 14→99px, sombra y z 100→4321. - censo --only 89 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con su «Talla y tema» y tab `Tokens` en la demo (10 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
7a0fc2eadb |
uix(link-preview): temable — 0 % → 100 %, y el plano overlay que traía tipografía
33 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → **100 %**: no queda un knob de apariencia fuera del contrato (los tres restantes son sistema — anillo de foco y los dos del plano de profundidad que pintan la flecha). Global 48 %, sin contrato 42 → 41, al 100 % 7 → 8. 1. **Los per-talla llevan `parts: ['content']`**, no el host: el panel está PORTALADO y su `data-size` vive en el content, así que una cascada colgada del host no lo alcanza nunca. La talla mueve sólo dimensiones físicas (padding, min-width, tipo); familia, peso e interlineado son constantes a propósito, como ya decía la receta. 2. **La flecha no se tokeniza**: su fill y su stroke consumen el plano `overlay` para casar siempre con el panel; acuñar `arrow-fg` rompería justo esa garantía. El subrayado del trigger es un `color-mix` sobre `currentColor` — la mezcla ES el aspecto, así que el knob es la declaración entera, no el 35 %. Y el fallback encadenado del color del trigger viaja verbatim: la coma es parte del valor, no un segundo knob. EL HALLAZGO — el plano `overlay` impone TIPOGRAFÍA (→ next-features §12.9): `[data-depth='overlay']` (`generated/base.css:7088`) no declara sólo fondo, borde y sombra: también `font-family` y `line-height`. El panel estampa ese plano para su elevación, así que la declaración del plano empata a (0,1,0) con la de la receta y **gana por orden de carga** (la hoja generada va después del chunk de la receta). Medido con CDP `getMatchedStylesForNode`: el par `font-family` / `line-height` de esta receta **no había pintado nunca**. Retirado con **0 diffs**, que es la prueba. Afecta a los **veinte** componentes que estampan el plano. Lo que hay que decidir —aparte, mueve píxel— es si un plano de profundidad tiene competencia sobre la tipografía o sólo sobre la superficie. Cuarta ocurrencia de la clase «empate resuelto por orden de carga». CÓMO SE MIDIÓ, porque el instrumento estándar tampoco servía aquí: El panel abre en HOVER con retardo y está portalado → la sonda genérica ve **1 nodo**, y una sonda que compara un nodo pasa en falso. Sonda dirigida (abre por hover, captura los cinco nodos reales en las cinco tallas): determinismo verificado y **gate antes/después = 0 diffs sobre 550 valores**, repetido tras la retirada. El guard R-5.4 pasó de **3/35 a 31/33** con dos extensiones: `openBy: 'hover'` para este componente (un hover card no se abre con clic — clicar su `<a>` navega) y `text-decoration-color` en la lista de propiedades, que faltaba pese a estar en `KNOB_PROPS` del censo. Verificado que los otros QUINCE componentes con ledger no se mueven ni una cifra. Los dos tokens que siguen mudos son un conflicto ESTRUCTURAL, no un hueco de demo: para abrir el panel hay que tener el puntero en el trigger, y el hover es justo el estado cuya regla pisa la tinta y el subrayado de reposo — el token de hover lee vivo en la misma corrida. Adjudicados con esa razón. Gates: censo --only 100 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio · README con «Talla y tema» y tab `Tokens` en la demo (33 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
67b4c810c7 |
uix(picker-shell): temable — 0 % → 76 %, y el componente que el instrumento no veía
31 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 76 %. Global
48 %, sin contrato 43 → 42.
La cola avisaba «es CAPA de los pickers: mirar antes qué posee y qué presta».
Mirarlo cambió tres cosas del encargo:
1. **NO es una capa compartida: es un COMPONENTE.** A diferencia de
list-surface o calendar-surface no hay hook de capa ni vocabulario prestado
— color-picker, date-picker y chronos importan sus `.svelte` directamente.
No toca `LAYER_VOCABULARY` y el censo ya lo puntuaba bien.
2. **Su eje de talla no es `data-size`, es `data-picker-size` sobre
`[data-popover-content]`**, un ancestro PORTALADO. El TSC no puede emitir esa
cascada (`scope: 'size:xs'` generaría `[data-picker-shell][data-size='xs']`,
que no casa nunca), así que las coordenadas por talla quedan como públicos
PLANOS y la receta las consume desde sus propios selectores. Es el único
componente del eje donde el patrón de nombres resueltos no aplica; la regla
base es el default `sm` y `md` sólo re-declara lo que cambia. La fila de
tiempo lleva un TERCER eje, `data-size` sobre su propia clase.
3. **El instrumental del eje estaba CIEGO aquí, y la sonda genérica devolvió 0
nodos** — el antipatrón de comparar cero valores y pasar. Dos causas a la
vez: `picker-shell` no tiene demo propia (404, se mide dentro de un picker
anfitrión y detrás de su popover), y sus partes se llaman
`data-picker-header` / `-body` / `-footer` en vez de
`data-picker-shell-{parte}` — genéricas A PROPÓSITO, dice la receta, «so
every picker gets the same visual contract for free» —, así que todo filtro
por prefijo del componente ve sólo la raíz.
CÓMO SE VERIFICÓ, ya que el instrumento estándar no servía:
- Sonda DIRIGIDA (abre el popover del date-picker, captura los seis nodos
reales en las cuatro picker-sizes): determinismo comprobado (576 valores, 0
diffs entre dos corridas del mismo código) y **gate antes/después = 0 diffs**.
- Los knobs que la demo no monta —el header entero y la fila de tiempo— se
midieron FORZANDO el nodo: los quince mueven su propiedad, y el font-size del
header se comprobó talla a talla (12/14/16/16px → 102px, cada uno en su
picker-size).
- El guard R-5.4 pasó de **0/31 a 6/31** al ganar un `COMPONENT_OVERRIDES`
(prefijo de atributo + selector de apertura) acotado a este componente.
Verificado que los otros CATORCE con ledger no se mueven ni una cifra.
Adjudicar 31 excepciones sin ese arreglo habría sido escribir una mentira en
el ledger; los 25 que siguen mudos llevan cada uno su razón medida.
Header y footer comparten la regla que los separa del cuerpo → UN par de knobs
(`border-width` + `border`), no dos.
Sin tab `Tokens`: este componente no tiene página donde ponerlo. Registrado en
next-features §13 junto con la pregunta de fondo — si el precio de los nombres
genéricos (un contrato visual común para todos los pickers) compensa la ceguera
del instrumental — y con el eje portalado como caso de prueba si el TSC llega a
ganar un scope configurable.
Gates: censo --only 76 % · suite eidos sin rojos nuevos (el conocido
skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio ·
`component:audit` no lo cubre (eidos-only, sin morfo) y `eidos-lint` tampoco.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
42a3e74136 |
uix(spinner): temable — 0 % → 70 %, y 71 tokens propuestos que eran 18
18 claves en el bloque `spinner` de `recipes/base.ts` (fusionadas con los dos forwards de paleta). Censo 0 % → 70 %, cero globales. Global 48 %, sin contrato 44 → 43. **La §4 proponía 71 tokens para 23 knobs — la peor inflación que ha dado el generador en todo el eje.** El spinner tiene UNA coordenada de talla (`--spinner-size`) que gobierna el anillo, la pista de puntos y las barras, más el trazo (`--spinner-thickness`). La propuesta multiplicaba esa única coordenada por variante × parte × dimensión × talla y producía `ring-track-width-xs`, `dots-track-gap-xs`, `dots-dot-width-xs`, `bars-track-gap-xs`… todos con el mismo `0.75rem`, porque todos SON el tamaño en xs. Un tema habría necesitado veinte claves para cambiar un número. 1. Las proporciones son CONSTRUCCIÓN, no knobs: `calc(size * 0.3)` es el punto, `* 0.18` la barra, `* 0.2` y `* 0.12` los huecos. Definen la FORMA de cada variante y se quedan en la receta — mismo reparto que los gradientes de las guías de tree-grid o el damero de gradient-builder. 2. `fg` / `dots-dot-bg` / `bars-bar-bg` ⚠ no eran tres knobs con dos valores en pugna: son UNA variable (`--_spinner-color`) leída en tres sitios, con el conmutador `data-color='inherit'` que la cambia de forward de paleta a `currentColor`. Se queda privada, igual que `--_spinner-track`, que además envuelve su fuente en un color-mix distinto por rama (60 % / 25 %). Aplanarlas exigiría duplicar cada regla por color. 3. La escala es PROPIA, no la del bundle de iconos: medido, 12/16/20/28/40 px contra los 14/16/18/20/32 de `--icon-size-*`, y sólo coinciden en sm. Verbatim con la desviación anotada — forzar el bundle habría movido el default en cuatro tallas de cinco. 4. `duration` entra al contrato en vez de seguir en `0.9s` a pelo: el registro ya tenía abierta la incoherencia de que el spinner de feed ganó tokens de duración y el de media-player no (§13). Aquí se hace bien de entrada. El 30 % que falta son los dos privados conmutadores, los dos `border-radius: 50%` (identidad de forma) y los fotogramas de los @keyframes. Artefactos y gates: - Sonda antes/después = 0 diffs en 377 valores × 7 estados. Determinismo verificado. - R-5.4: 14/18. Los cuatro mudos, medidos uno a uno y adjudicados: la demo monta SÓLO la variante ring y sin etiqueta (0 barras, 0 puntos, 0 labels, contados), y `track-opacity` sólo se declara dentro de `prefers-reduced-motion` — forzando variante, etiqueta y contexto reduce, los cuatro alcanzan. - censo --only 70 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con su «Talla y tema» y tab `Tokens` en la demo (18 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
c56d6544e6 |
uix(card-group): temable — 0 % → 90 %, y dos privados abreviados que mueren
20 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 90 %, cero
globales y cero privados. Global 47 %, sin contrato 45 → 44.
Es un CASCO de composición y su propia cabecera ya lo decía: compone
ToggleGroup, Collapsible, Card y Button, y la receta posee «ONLY the genuinely
CardGroup-level visual». Eso acota el contrato antes de nombrar nada — el cromo
de las tarjetas es de Card, el del título de disclosure es de Button.
1. **`--_cg-pad` y `--_cg-card-radius` eran privados ABREVIADOS** (§6 r5 los
prohíbe: `cg` por `card-group`), el mismo defecto que los `--_mp-*` de
media-player. Pasar a públicos con el nombre completo es a la vez el
renombrado que la regla pedía y la tokenización. La §3 de la ficha decía
«la receta no declara privados propios»: los declaraba en su bloque raíz
(`card-group.css:15-16`), y el escáner no los veía por buscarlos con el
prefijo completo del componente.
2. **`--shape-outer-radius` NO se acuña: se CALCULA.** La receta lo compone
como `card-radius + padding` para que las tarjetas aniden concéntricamente
(§30). Un tema mueve las dos coordenadas y el radio exterior se recompone
solo; el censo ya lo clasifica como sistema.
3. **El eje `size` es sm|md|lg, no las cinco tallas**
(`Extract<Size, 'sm' | 'md' | 'lg'>`): seis coordenadas por talla, no diez.
Escribir xs/xl habría sido vocabulario muerto.
4. Los tres `padding` son shorthand con una parte fija y otra variable; lo que
varía y no es el inset del grupo es el hueco inferior →
`title-padding-block-end` y `description-padding-block-end`.
5. El chevron es geometría propia (dos bordes girados 45°): `chevron-size`
cubre las dos dimensiones y `chevron-width` el trazo. Su `currentColor`
hereda del título a propósito y no es knob.
6. Los `font-size` van al BUNDLE (`--size-{k}-font-size`, alias 1:1) y no al
primitivo crudo — misma trampa que cayó en code-block: la propuesta copia el
CSS verbatim y hereda el incumplimiento cuando el valor de partida ya
violaba `recipe-css-contract`. Aquí se aplicó de entrada.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 2.581 valores × 7 estados. Determinismo
verificado: dos corridas, 0 diffs.
- R-5.4: 15/20. Los cinco mudos son los del encabezado ESTÁTICO, que la demo no
monta (0 nodos `[data-static]` contra 1 `[data-button]`, CONTADOS); forzado
`data-static` alcanzan los cinco, y cada uno lleva su medición en el ledger.
El hueco de demo queda registrado en next-features §13, misma clase que el
`kind='month'|'year'` de date-range-picker.
- censo --only 90 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check
0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
354d166df1 |
uix(code-block): temable — 0 % → 81 %, y la demo que no enseña el componente
20 claves en una entrada NUEVA de `recipes/base.ts` (no tenía). Censo 0 % →
81 %. Global 46 % → 47 %, sin contrato 46 → 45.
El handoff lo llamaba «el más mecánico de los que quedan». No lo es: sus 26
knobs se parten en dos grupos con doctrinas distintas.
1. **El nodo del código NO acuña nada, y la §4 pedía que lo hiciera.**
`[data-code-block-code]` consume la capa tipográfica `--style-code-*` en sus
cinco declaraciones, y las dos que pasan por privado usan la forma canónica
de la escotilla por instancia
(`var(--_code-block-font-size, var(--style-code-font-size))`, escrita por el
wrapper desde la prop `size` y sólo si llega). Es el patrón que D-TH.2-b
fijó para heading/text/code. Acuñar `code-font-family` y compañía sería el
vocabulario paralelo que la regla 2 de capas prohíbe. Todo lo que entra al
contrato es el CASCO alrededor.
2. **Los tres `font-size` iban al primitivo crudo, y lo cazó un guard.** La
propuesta copia el valor del CSS verbatim, y ese valor ya violaba
`recipe-css-contract` («recipes consume the size bundle, not the raw
size-coordinate primitives»): `title/lang/copy-font-size` corregidos a
`var(--size-{k}-font-size)`. Los alias son 1:1 → 0 diffs tras aplicarlo.
Aviso general: cuando el CSS de partida ya incumple el contrato, la
propuesta generada hereda el incumplimiento.
3. Shorthand partidos: los dos `border` y el `border-bottom` de la cabecera
comparten anchura y color → `border-width` + `border`; el `padding` del
`<pre>` → `pre-padding`. Los `transparent` de outline/ghost y el `border: 0`
de ghost son identidad de variante.
El 19 % que falta son esas cinco de la capa, que el censo cuenta como `global`
porque `code-block` no está en `TYPOGRAPHIC_PRIMITIVES` — es un compuesto que
CONTIENE un primitivo tipográfico, no uno de ellos. Extender la clase `system`
del censo a las capas sigue pendiente de tu firma; la métrica penaliza hacer lo
correcto y no se fuerza de oficio.
DOS HALLAZGOS QUE NO SE ARREGLAN AQUÍ (→ next-features §13):
- **La demo de `code-block` no enseña el `code-block`.** `[data-uix-docs] pre`
(`web/routes/uix/uix.css:801`) pesa (0,1,1) contra los (0,1,0) de
`[data-code-block-pre]`, y `[data-uix-docs]` es el shell de TODAS las
páginas. Siete propiedades del `<pre>` las pinta el harness y no la receta:
padding (16px 20px contra 12px), border (1px contra ninguno), radius (8px
contra 0), background (gris contra transparente), font-family, font-size
(13 contra 14) y line-height (18.2 contra 20.3). El fondo, el borde y el
radio que se ven ahí son del sitio. Por eso `pre-padding` lee muerto en el
guard, adjudicado con esa medición. Acotar la regla toca el CSS del SITIO,
que sirve a ~162 páginas: decisión aparte.
- **El guard de claves duplicadas del handoff inspeccionaba el VACÍO.** El
comando documentado usa `grep -oE "^\t…"`, y `grep -E` lee `\t` como una `t`
literal: 0 coincidencias sobre 130 bloques reales, así que salía «vacío = OK»
sin mirar nada — el antipatrón que el proyecto tiene registrado, dentro del
propio handoff. Comprobado con un contador fiable: cero duplicados en el
árbol, así que no hay daño en los commits que confiaron en él. CONTINUE
queda corregido con una versión que CUENTA los bloques y avisa de que «0
bloques» significa guard roto, no fichero limpio.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.305 valores × 7 estados (dos veces: tras
la tokenización y tras la corrección del bundle). Determinismo verificado.
- R-5.4: 19/20; `pre-padding` adjudicado con las siete propiedades medidas.
- censo --only 81 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos de vuelta al único rojo conocido (skin-media-player) tras
corregir el bundle · rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
dd188f1255 |
uix(tree-view): temable — 0 % → 92 %, y el velo que tiñe la carpeta entera
46 claves en el bloque `tree-view` de `recipes/base.ts` (fusionadas con el
forward de paleta). Censo 0 % → 92 %, cero globales. Global 46 %, sin contrato
47 → 46. Molde del hermano `tree-grid` allí donde el problema es el mismo.
LA PROPUESTA GENERADA FALLABA EN EL NOMBRE DE CASI TODO LO QUE IMPORTA: el
clasificador toma el PRIMER selector de cada regla como si fuera la parte
pintada, y aquí ese primero es el root o el branch-control cuando el nodo real
es otro.
1. `root-width: 1px` / `root-bg` NO son del root: son la GUÍA de indentación
(el `::before` del branch, una línea vertical). → `guide-width` + `guide-fg`,
literalmente los nombres de tree-grid. Un `root-bg` que tiñe una línea es la
clase de nombre que miente.
2. `branch-control-padding-inline-{k}` ⚠ fundía DOS ejes que se SUMAN en el
mismo calc: el ritmo de la fila y el PASO DE PROFUNDIDAD
(`--tree-depth × indent + padding`). Fundidos, tematizar la sangría habría
movido el padding. → `row-padding-inline-{k}` + `indent-{k}`.
3. El prefijo `branch-control-` es erróneo: la regla reza
`[data-tree-view-branch-control], [data-tree-view-item]` — las DOS filas. El
CSS ya llamaba `row` a sus privados. → `row-*`, como tree-grid y como la
lección de table. Y fuera el prefijo `root-` del chasis: el wrapper ES el
componente.
4. `row-padding-block` ⚠ no eran dos valores en pugna sino cinco por talla: xs
colapsa a `0`, las otras cuatro `--space-1`.
5. `branch-indicator-width` + `-height` son UN knob (`1em`) →
`branch-indicator-size`.
Bundle: la altura de fila casa 1:1; padding e indent llevan ritmo propio; la
tipografía un paso por debajo desde lg — verbatim, como table y tree-grid. Los
resueltos con `parts: ['root']` (el `data-size` va en `[data-tree-view-root]`,
wrapper de eidos, no parte del morfo). Los `transparent` de outline/ghost y el
`padding: 0` de ghost son identidad de variante y siguen literales.
`hover-row-bg` SÍ se acuña, al revés que en grid-list, y por medición: la misma
regla pinta dos nodos con arquetipo distinto, y `branch-control` NO lleva
archetype — ahí el plano de la receta es la única pintura (velo medido ausente)
y el token es toda la superficie de tema del hover. Tener un consumidor
legítimo es lo que lo distingue del caso grid-list.
EL DEFECTO QUE LA MEDICIÓN DESTAPÓ (preexistente, NO se toca aquí →
next-features §12.8): al pasar el ratón por la fila de una carpeta abierta se
tiñe LA CARPETA ENTERA, hijos incluidos. `archetype: 'item'` está declarado en
el `<li>` branch (`morfo/components/tree-view.ts:103`), que contiene el control
Y todo el `branch-content`, mientras la fila que el usuario señala es el
`branch-control` de dentro: el nodo velado mide 336 px frente a los 36 px de la
fila — medido y capturado. Un branch anidado acumula además el velo de sus
ancestros, así que sus filas se ven más oscuras que las hermanas. Mover el
arquetipo es morfo y mueve píxel en toda la demo. Es la TERCERA variante de la
misma familia: table lo tiene en fila Y celda, grid-list en fila Y celda sobre
el mismo nodo, y tree-view en un ANCESTRO del nodo señalado.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 8.062 valores × 7 estados.
- R-5.4: 45/46; `disabled-row-fg` adjudicado en el ledger (la demo monta CERO
filas deshabilitadas, contadas; forzada alcanza oklch(0.61 0 0) → rgb(1,2,3)).
- censo --only 92 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (46 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
893aeaf817 |
uix(textarea): temable — 0 % → 80 %, y el contador que casi pierde su 0.85
37 claves en el bloque `textarea` de `recipes/base.ts` (fusionadas con el
forward de paleta que ya estaba). Censo 0 % → 80 %, con **cero globales y cero
literales de valor**: los 16 privados leen ya su público. Global 46 %, sin
contrato 48 → 47.
LO QUE LA MEDICIÓN CAMBIÓ. La §4 generada proponía 35 tokens; cuatro eran
colisiones y uno habría movido el default:
1. `count-font-size-{k}` × 5 BORRABA el factor 0.85. El CSS calcula
`calc(var(--_textarea-font-size) * 0.85)` y la propuesta ponía el tipo del
input crudo en las cinco tallas: el contador habría crecido un 18 %.
D-TH.5 lo prohíbe. Queda en UN token con el `calc` sobre el público del
input — la escala se escribe una vez y la talla la resuelve el input. Pide
`scope: 'host'`: emitido en `:root` su dependencia no existe, y el contrato
de scope de la receta lo caza (lo cazó).
2. `input-fg` ⚠ eran DOS knobs (texto y placeholder) → `input-fg` +
`input-placeholder-fg`. Igual `count-fg` → `count-fg` + `overflow-count-fg`.
3. `input-border` ⚠ NO eran dos knobs sino UNA variable con su swap de paleta,
y el nombre estaba mal: es el borde de FOCO → `focus-input-border`. El de
reposo es otro token, que la propuesta ni listó por viajar en el atajo.
4. `ghost-input-border` se RETIRA: la regla ghost lee ese mismo conmutador —
sería un valor con dos nombres.
Bundle, medido: la tipografía casa 1:1 en las cinco (lee `--size-{k}-font-size`);
el spacing NO — `padding-inline` casa salvo en xs (8px vs 6px) y `padding-block`
sólo en xs. Verbatim y documentado: un campo multi-línea respira más, y el
spacing queda fuera del guard del bundle. Resueltos sin `parts` (el `data-size`
va en `[data-textarea]` y el input hereda); fuera los cuatro `[data-size]`.
DOS ARREGLOS DEL INSTRUMENTO, los dos por medición y no por sospecha:
- **El guard medía con el PUNTERO encima.** `reopen()` corre antes de CADA
token y hace clic en `[data-{c}-input]` cuando el componente no tiene parte
`content`; el `blur()` de la revisión adversarial quitaba el foco pero no el
puntero, así que `:hover` casaba toda la corrida y toda regla de hover pisaba
a sus vecinas. Costaba TRES falsos negativos aquí. Con `mouse.move(0,0)`
tras cada blur, `input-border` revive solo (31 → 32).
- **`::placeholder` SÍ se lee con `getComputedStyle`.** La nota de §13 que
decía lo contrario nunca se comprobó; medido, el color centinela vuelve tal
cual. Entra en el snapshot (32 → 33) y deja STALE la entrada
`command.input-placeholder-fg`, que el guard señaló y se ha borrado.
Corridos los DIEZ componentes con ledger tras los dos arreglos: ningún otro
cambia, un único STALE, el previsto.
Y un hallazgo de píxel que NO se toca aquí (→ next-features §13): el borde de
foco sólo se ve con Tab. Las tres reglas de `border-color` del input están
ordenadas al revés de lo que significan — hover (0,4,0) > foco (0,3,0) >
invalid (0,2,0) — y hacer clic deja el puntero encima por definición, así que
gana el hover; un campo inválido pierde su borde rojo al enfocarlo o al pasar
el ratón. El anillo de foco del sistema sí se ve siempre, así que no es fallo
de accesibilidad. Arreglarlo mueve píxel.
El 20 % que falta son las dos declaraciones que consumen
`--_textarea-border-focus`, privado a propósito: es el conmutador de una
variable con dos fuentes (el público y el forward THM-2), y aplanarlo exigiría
duplicar la regla por color. Mismo techo que `listbox` paga por consumir bien
su capa. Mi propio veredicto predecía ~100 % y la medición lo corrigió a 80 %.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.247 valores × 8 estados (el 0.85 intacto,
que era el riesgo). Determinismo verificado: dos corridas, 0 diffs.
- R-5.4: 33/37; los 4 mudos adjudicados en el ledger con su medición (foco,
invalid y las dos del contador rebasado — estados que la demo no monta).
- censo --only 80 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (37 claves; el panel
muestra el contador computando `calc(calc(1rem * 1) * 0.85)`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
60b5b98969 |
uix(grid-list): temable — 0 % → 85 %, y una selección que nunca pintó
38 claves nuevas en el bloque `grid-list` de `recipes/base.ts` (que ya existía
con los dos forwards de paleta: se FUSIONA, no se añade un segundo). Alcance
del censo 0 % → 85 % (22 de 26 puntuables); global 45 % → 46 %, sin contrato
49 → 48.
El eje `size` se estampa en el PROVIDER —el wrapper de eidos no tiene parte
`root`—, así que los tres nombres resueltos van SIN `parts`: molde `command`,
no `table`/`tree-grid`. Fuera los cuatro bloques `[data-size]` del CSS; los
emite el TSC. Altura de fila 1:1 con el bundle, `row-padding-inline` con ritmo
propio, y la tipografía un paso por debajo de md en adelante — verbatim, como
table y tree-grid.
LO QUE LA MEDICIÓN CAMBIÓ (la §4 generada tenía un error de doctrina y dos
defectos que sólo aparecieron midiendo; el §5 Veredicto se escribió con la
sonda delante, porque estaba vacío):
1. La selección de fila NUNCA HA PINTADO. La receta seleccionaba
`[data-grid-list-row][data-selected]`, un atributo que ni el morfo declara
ni soma estampa (estampa `data-state='selected'` + `aria-selected`): la
regla no casaba jamás y su acento `--_grid-list-palette-element` no llegó
nunca al píxel — una fila seleccionada muestra el velo NEUTRO del
arquetipo, indistinguible del hover (capturado). Retirada la declaración
muerta y su forward huérfano: 0 diffs, que es la prueba. Repararlo mueve
píxel y choca con §12.5 (el arquetipo fija también `color`) → §13, espera
firma. El acento del checkbox (`_palette-solid`) sí vive y se conserva.
2. NO se acuña `hover-row-bg` aunque la propuesta lo pedía. La fila es
`archetype: 'item'`: el velo del sistema la pinta a especificidad plena y
el plano de la receta pierde — medido, el hover pinta DOS VECES sobre el
MISMO nodo (a diferencia de table/tree-grid, donde velo y plano caen en
nodos distintos). Sería un token que no puede ganar; mismo criterio que
listbox.highlighted → §12.7. Y fila Y celda llevan ambas `item`, así que
bajo el puntero se apilan tres capas.
3. «Consume la capa compartida `menu-indicator`» es FALSO. Sale de
`SHARED_LAYERS` (`theming-census.ts:432`), pero esa capa no tiene un solo
selector de grid-list, la receta no la importa y soma no renderiza
indicator: el checkbox ESPEJA el visual pintándolo por su cuenta. Entrada
desviada del censo → §13 (listbox / menubar / navigation-menu están en la
misma entrada y tampoco aparecen en el fichero).
Nombres, sobre la propuesta: `selection-checkbox-{width,height}` son UN knob →
`selection-checkbox-size`; `on-selection-checkbox-bg` → `checked-selection-checkbox-fg`
(es la tinta del glifo, y `on-` es prefijo reservado); `border` partido en
`border-width` + color (molde `command.input-border*`), que el checkbox
comparte; `padding` uniforme se queda `padding` (precedente
`command.viewport-padding`); `max-block` entra al contrato (molde
`command.viewport-max-block`).
Artefactos y gates:
- Sonda antes/después = 0 diffs en 6.090 valores × 7 estados. Determinismo
verificado: dos corridas del mismo código, 0 diffs.
- R-5.4: 37/38 tokens mueven un computed. `invalid-border` adjudicado EN EL
LEDGER con su razón medida (la demo arranca con su control en off; activado
a mano alcanza: oklch(0.9555 0.0207 13.86) → rgb(1,2,3)).
- censo --only 85 % · component:audit PASS (R-5.3 incluida) · eidos-lint
invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el único es el
conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero
limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (38 claves, leídas en
vivo). El tab `Recipe` documentaba la regla retirada: corregido.
Las fichas de calendar / carousel / command / month-grid / range-calendar /
table / year-grid entran porque `--report` regenera el informe entero: sólo
actualizan NÚMEROS DE LÍNEA de commits ya hechos que no lo regeneraron. Cero
cambios de alcance. Las otras 161, que sólo cambiaban la fecha, se revirtieron.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
8400e5bdd4 |
feat(calendar-surface): la capa que ya existía, con nombre, casa y su agujero tapado
Firma 1 del acta, diseño presentado y firmado. **La medición desmontó el
encargo**: el handoff la vendía como una capa NUEVA de ~220 knobs, y la capa ya
existía de hecho, sin nombre. `--calendar-*` se emite en `:root` (76 claves) y
sus consumidores no acuñan NADA — medido: `range-calendar` 114 referencias
prestadas y 0 propias, `month-grid` 77/0, `year-grid` 77/0, y lo único ajeno
que leen es sistema puro (`--focus-ring-*`, `--state-hover`). Su 0 % era el
artefacto de `listbox` (§13), pero total.
**`lib/calendar-surface.css`** (hook `data-calendar-surface`):
- cuatro coordenadas por talla — `padding`, `control-size`, `day-size`,
`font-size` — xs..lg, porque la familia NO tiene xl, y con la celda DOS pasos
por debajo del bundle de control. Esa desviación estaba escondida en cuatro
bloques `[data-size]` idénticos, uno por receta, cada uno puenteando a un
privado con otro nombre; ahora se lee en un sitio.
- la FORMA del anillo de evento y de la marca de festivo.
**Capa HÍBRIDA, y es lo que la distingue de sus hermanas**: `list-surface` y
`viewport-placement` componen primitivos del sistema, así que declaran sus
públicos en el fichero y no tienen entrada de receta. Ésta no puede: su
vocabulario son 76 claves SEMÁNTICAS que un tema alcanza una a una por config,
así que la entrada `calendar` de `recipes/base.ts` pasa a ser la de la FAMILIA
y la capa posee sólo lo que una entrada de receta no sabe expresar.
**El defecto que la justificaba, medido**: `--calendar-event-shadow` y
`--calendar-day-holiday-shadow` se emitían con ámbito `[data-calendar]`
(audit B.2 los host-scopeó por buenas razones) mientras `range-calendar`,
`month-grid` y `year-grid` los leían desde hosts que nunca llevan ese atributo:
variable VACÍA, `box-shadow` inválido en computed, **el anillo sema de
`commit-select` / `commit-set` no pintaba jamás en tres componentes**. Un token
prestado cuyo ÁMBITO no te cubre no es un préstamo, es un agujero silencioso, y
ningún guard lo veía. Ahora la forma vive en la capa y el acento entra por
`--_calendar-surface-accent`, que cada superficie alimenta con su propio
forward de paleta THM-2.
**Siete wrappers estampan, no cuatro** — y esto casi se me cuela: `DatePicker`
y `DateRangePicker` renderizan la superficie soma por sus PROPIOS wrappers
(`date-picker-calendar`, `-month-view`, `-year-view`,
`date-range-picker-calendar`) y un panel portalado no hereda nada del root del
picker. Con sólo los cuatro standalone sellando, ambos quedaban con
`--calendar-padding` VACÍA y el panel a padding 0 (medido). La tentación era
enganchar la capa a las cuatro identidades de componente: eso viola la regla 1
de capas compartidas, y la respuesta correcta es un sello por wrapper.
computed 0 diffs en range-calendar (19.285 valores) · month-grid (3.451) ·
year-grid (3.451) · date-picker (464) · date-range-picker (406).
`calendar` da 12, y son del INSTRUMENTO: dos corridas del MISMO
código dan 24 en los mismos nodos y las mismas dos propiedades.
Los «missing node» son el propio sello entrando en la clave.
**Una incidencia nueva, medida y NO arreglada aquí** (§13): el font-size de los
selectores month/year es una moneda al aire —
`[data-calendar-month-select][data-button]` (0,2,0) empata con
`[data-popover-trigger]:not([data-archetype='field-trigger'])` (0,2,0), la
MISMA regla de popover que dejó muerto el cromo de `gradient-picker`, y gana la
hoja que cargue después: 16px o 14px según la recarga. Arreglarlo fija el píxel
en un lado ⇒ decisión.
De paso, `calendar-select.css` deja de puentear un privado que sólo
`[data-calendar]` declaraba: en range / date-range corría SIEMPRE por el
fallback, clavado a md fuera cual fuera la talla.
**El censo deja de penalizar hacer lo correcto**: `LAYER_VOCABULARY` en
`theming-census.ts`, mismo precedente que D-TH.2-b con `--style-*`. Global
43 % → **45 %**, `calendar` 75 % → **84 %**. `list-surface` NO se registra: sus
consumidores puentean por privados, otra forma, y mueve diez componentes de
golpe. Y aparece el techo de debajo, anotado: a los tres consumidores sólo les
quedan los forwards de paleta THM-2 —que el censo cuenta como `private` en TODO
el catálogo— así que siguen leyendo 0 %.
`recipe-css-contract` aprende que una CAPA también declara públicos (antes sólo
miraba la receta, y una capa que comparte prefijo con un componente la hacía
fallar). Sin debilitarla: un nombre que no declara nadie sigue en rojo.
eidos-lint 0 invalid (calendar 29/8 · range-calendar 41/12 · los grids 23/5) ·
audit --only calendar PASS · vitest eidos 434/435 (el rojo conocido) ·
rtl 0/181 · docs 0/813 · check 0 errores en tocados · prettier: revertido el
reformateo en masa que se coló en cuatro README, el test y el censo (churn
ajeno, no mío)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
df27351ffc |
fix(table): el striped cuenta filas de DATOS — la banda vuelve a existir
Firmado por el autor tras la revisión adversarial: el hallazgo 4 (
|
2 months ago |
|
|
679dfdb291 |
uix(theming): revisión adversarial de F2-A — 8 hallazgos de 34, y nace R-5.4
Pase escéptico del bloque (§7.7, molde Sidebar) con medición propia: worktree
con SOLO el CSS revertido a la base del bloque (DOM de demo constante — prueba
más dura que la del autor), sonda x2 por componente para fijar el suelo de
ruido. 34 hipótesis · 26 refutadas · 8 reales.
Lo que NO se pudo romper: el «default idéntico» aguanta en los ocho (0 diffs;
command 8.091 · table 13.050 · media-player 3.277 · gradient-picker 609 ·
listbox 2.813 · carousel 2.842; los 3 de tree-grid y los 5 de feed salen
idénticos corriendo DOS veces el mismo código → instrumento); 0 tokens
huérfanos de 336; el bundle ES 1:1 con los primitivos; el «verificado a mano»
de gradient-picker era verdad (15/16 alcanzan al montar); sin doble animación;
eidos-lint 0 invalid.
Los 4 de código, arreglados con diff de computed VACÍO (re-medido: command
8.091 y carousel 2.842 valores, 0 diffs):
1. command: el re-point de `--command-radius` dentro del Dialog era una
declaración muerta con comentario falso — la misma regla pone el radio a 0
y el input lee `--command-input-radius`, nunca este token (medido:
`--dialog-content-radius: 9999px` no movía NADA; `--command-radius: 77px`
movía la paleta inline y no la del dialog). Retirado; comentarios y README
reescritos con lo medido.
2. carousel: la regla vertical del indicador activo llevaba el `2.25` a pelo
mientras la horizontal leía `active-indicator-scale` — el token alcanzaba
media superficie (scale:6 → horizontal 18→48px, vertical 18→18px). Ahora
lo leen las dos.
3. carousel: el trío `item-gap*` no podía ganar NUNCA — soma estampa `gap`
INLINE desde la prop (lo necesita para el flex-basis; reglas casadas por
CDP: la de la receta y encima INLINE gap:0rem). Retirados los 3 tokens y
las 2 declaraciones muertas (la base y el gap:0 de ghost): 0 diffs, que es
la prueba. Canal de valor de soma, como --gp-current-gradient. Censo
carousel 35 knobs · 23 públicos · 77 % · 27 claves; global 43 % intacto
(5.190 · 2.102).
4. table: `striped-row-bg` no alcanza NADA en la demo publicada —
`:nth-of-type(even)` cuenta los <tr> de RowDetail y las filas de datos
quedan impares (medido tr a tr: diez inmunes). Arreglarlo mueve píxel →
PENDIENTE DE FIRMA (candidato `:nth-child(even of [data-table-row])`);
adjudicado en el ledger mientras tanto.
El instrumento cargaba 22 falsos negativos de 26 «no effect», con tres causas
medidas, y la cuarta era de la sonda:
- el paso de abrir hacía CLIC en el input, y un clic de ratón sobre un input
de texto SÍ casa `:focus-visible` — la regla de foco pintaba el borde y el
token de reposo leía muerto (era el «falso negativo sin causa» de §13);
- el override se escribía solo en `[data-{c}]`, y las partes que cuelgan de
`{c}-root` no lo recibían;
- `::before`/`::after` eran invisibles (el aro de buffering, el spinner);
- la no-determinación de feed no era una animación sin localizar: era la demo
aún CARGANDO — el auto load-more mantiene [data-busy] ~3 s y la firma
`commit-settle` anima box-shadow sobre el nodo medido. Con la espera de
asentado: 0 diffs y 0 nodos ausentes sobre 6.264 valores, dos corridas.
Con eso NACE EL GUARD R-5.4 (la deuda que §13 pedía desde gradient-picker, y
que carousel convirtió en patrón — un estilo inline de otra capa es invisible
para todo análisis estático): `npm run theming:sentinel -- <c> <url>`
(scripts/theming-sentinel.ts, promovido de `__`; blur tras abrir, override en
todos los nodos del componente, pseudos, pase de hover, settle) + ledger
scripts/theming-sentinel-exceptions.ts con UNA razón medida por token — muerto
sin adjudicar = exit 1, excepción STALE = aviso. Los ocho componentes en
verde: command 50/57 · table 41/45 · media-player 46/61 · tree-grid 45/47 ·
gradient-picker 9/25 · listbox 20/21 · carousel 26/27 · feed 43/50, con 46
excepciones adjudicadas. El guard ya me corrigió a mí: cinco entradas mías
salieron STALE (los control-height-* de media-player SÍ alcanzan) y las borró;
y dos muertos nuevos los produjeron los propios arreglos (el blur apaga
focus-input-border; el settle desmonta el spinner de feed) — adjudicados con
su porqué. Doctrina en recipe-contract §4.
También: tres cifras de especificidad de next-features §12 recontadas (la
banda de tree-grid es (0,8,0); la tinta seleccionada del arquetipo es (0,4,0);
y el `item` de table vive en la fila Y EN LA CELDA — elementsFromPoint pone el
velo en la celda), el comentario de listbox que mandaba pisar el privado
`--_listbox-max-height` apunta ahora al público, y quedan registrados en §13
los dos `header-z` con literal '2' y el spinner de media-player a 720ms
mientras el gemelo de feed lleva tokens.
Guards: censo global 43 % intacto · audit --only command+carousel PASS ·
eidos-lint 0 invalid (command 16/12, carousel 29/9, listbox 12/7) · vitest
eidos 434/435 (el rojo es el conocido skin-media-player) · rtl 0/180 · docs
0/813 · check 0 errores en tocados (72 globales de otras sesiones) · prettier
limpio en lo nuevo (los desvíos de command.css/carousel.css ya estaban en
HEAD y no se tocan).
Pendiente de firma (CONTINUE §«PENDIENTE DE FIRMA»): el striped inerte, el
arquetipo en fila+celda, y que soma lea el gap de un token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f5f2b5d225 |
docs(theming): el handoff, reescrito para que otra sesión arranque sin arqueología
La sesión llega llena. El handoff había crecido por acumulación —cada bloque añadía su sección— y para saber qué hacer había que leerlo entero y deducirlo. Ahora la AGENDA va arriba y el registro histórico abajo, separado por su propio encabezado. Arriba: qué comprobar en los primeros cinco minutos (las dos cifras que deben cuadrar, el dev server antes de medir, leer el veredicto §5); tres opciones con recomendación —revisión adversarial de F2-A, el diseño de `calendar-surface`, o seguir la cola por alcance, con la cola ya recalculada—; el protocolo en diez pasos; y **lo que este bloque enseñó**, que es lo que evita repetir el día: el veredicto orienta pero la medición decide (tres de ocho tenían un error de lectura) · un token que no mueve nada miente, y lo que procede es retirar la declaración muerta · un alias puro se borra, no se renombra · lo que la capa posee el consumidor no lo acuña · y la sonda miente de tres maneras distintas, así que ante un diff inesperado se corre DOS VECES sobre el mismo código antes de sospechar del cambio. El plan §8 gana la entrada de la sesión con las cifras y los commits, y su cabecera de estado pasa a 43 %. Estado que hereda la sesión siguiente: alcance global 43 % (era 33 % al abrir el eje), 15 componentes con contrato, 49 sin él, 7 al 100 %, gramática con 0 desviadas y guard en `error`. 29 commits, sin pushear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
34a95a550c |
feat(feed): temable — 0 % → 94 %, y cierra el bloque F2-A
Octavo y último del bloque.
censo 0 % → 94 % · global 43 %
computed reposo y las CINCO tallas, idénticas al byte
**El título no escala por talla: escala por NIVEL DE ENCABEZADO.** El informe
leyó sus cuatro valores como una colisión de `size`, y son `aria-level` 3..6 —
un encabezado más profundo es más pequeño porque es más profundo. Sus tokens
son `article-title-font-size-{3,4,5,6}`, no coordenadas de talla. Tercera
corrección de este tipo a un veredicto en el bloque.
**El ⚠ del `sentinel` eran DOS medidas**, como sospechaba el veredicto: la
fila-sonda mantiene un alto mínimo para que el observador tenga algo que
intersecar, y el spinner que va dentro tiene su propio diámetro.
El feed tipa por debajo del 1:1 de md en adelante, como `table` y `tree-grid`;
la coordenada lo nombra en vez de esconderlo.
**Sobre la medición, que aquí hubo que trabajarla.** La sonda dio 7 diffs y
ninguno era un cambio:
- el `box-shadow` del nodo `[data-busy]` está ANIMADO, y la sonda congela
`transition` pero nunca `animation` —a propósito, porque congelarla impide
que Presence monte nada—, así que lo muestrea en una fase distinta cada vez:
dos corridas del MISMO código dan cinco diffs;
- y el estado `hover` se mide después de haber forzado las tallas, así que su
altura arrastra la última.
Lo que decide es que las SEIS medidas estables —reposo y las cinco tallas—
salieron idénticas al byte: 544,5 · 564,5 · 628,5 · 792,5 · 966,5 px. Queda
escrito en el README para que el siguiente no persiga el mismo fantasma.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido
Demo con tab Tokens: 50 filas, 0 sin computar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
4cab776677 |
feat(carousel): temable — 0 % → 77 %
Séptimo del bloque F2-A. censo 0 % → 77 % · global 42 % computed 2.842 valores en 7 estados: 0 diffs **Los botones prev/next no se acuñan aquí** — componen `<IconButton>`, así que su cromo es el del Button y un tema los viste por ahí. Esta receta sólo decide DÓNDE se colocan (`trigger-offset`). El veredicto avisaba de comprobarlo antes de copiar filas de trigger, y estaba en lo cierto. **El ⚠ del informe eran DOS gaps distintos**, no un knob con dos valores: el `gap` en columna de la raíz y el `item-gap` ENTRE DIAPOSITIVAS. Y el segundo sólo se aparta del default en `xs`, así que su cascada declara UN override en vez de cinco coordenadas iguales — el TSC no necesita que se le repita lo que ya hereda. **Los puntos del paginador son geometría propia**: 0.375–0.75rem por talla, ni bundle ni desviación que marcar, porque el bundle no tiene coordenada que signifique «un punto». El punto activo se estira a pastilla con un factor que ahora es token (`active-indicator-scale`): es lo que lo hace leer como «estás aquí» y no como un punto más gordo. **El radio interior es concéntrico**, no un segundo radio: viewport y diapositivas redondean por dentro del marco, así que su esquina es la del marco menos el borde — el `2px` suelto pasa a `inner-radius-inset`. Las tres variantes (surface / outline / ghost) entran enteras: padding del marco, borde y superficie. Quedan 5 literales de layout (`block-size: 100%` ×3 para que el viewport vertical no colapse, `max-content` del raíl de puntos) y los 2 privados de paleta. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido Demo con tab Tokens: 30 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
3b325c4394 |
feat(listbox): temable hasta donde le toca — 0 % → 68 %
Sexto del bloque F2-A, y el primero cuyo techo es DELIBERADO. censo 0 % → 68 % · global 42 % computed 2.813 valores en 7 estados: 0 diffs **Este componente no puede acuñar su ritmo de fila, y esa es la regla de oro de las capas compartidas.** Altura, padding inline y block, gap y tamaño de letra los posee `lib/list-surface.css`, por talla, para TODAS las superficies de lista —select, combobox, command y los menús—. Acuñar `--listbox-item-height` sería el vocabulario paralelo que las reglas de esa capa prohíben, y encima ganaría a la capa para todas las demás. Los cinco privados ADOPTAN la capa y se quedan privados; un tema mueve ese ritmo por `--list-*`, que es donde vive. Consecuencia honesta: el censo cuenta esos cinco como privados y el alcance se queda en 68 % en vez de ~86 %. **La métrica penaliza hacer lo correcto** — el mismo defecto de medición que D-TH.2-b arregló para los primitivos tipográficos (`--style-*` pasó a `system`). Registrado en `next-features.md` §13; extenderlo a las capas compartidas es decisión, no corrección al paso. **Dos cosas medidas que NO se tocaron:** - El `highlighted` pinta DOS veces: la receta pone un `--color-surface-overlay` plano y encima el arquetipo `item` añade su velo del 8 %. El plano es el duplicado que §38 deprecó, así que se dejó SIN token — bendecir con un nombre público algo condenado a morir es el error que este eje ya evitó en el codemod. Retirarlo mueve píxel. - La tinta de la fila seleccionada era código MUERTO y se retiró: el arquetipo la fija a (0,4,0) contra los (0,3,0) de la receta. Mismo hallazgo que en `table`, verificado igual, 0 diffs al quitarlo. Lo que sí es suyo y ahora es público: la superficie que sostiene las filas (fondo, borde, radio, padding, gap, tipografía, alto máximo), la esquina y la tinta de la fila, el glifo de selección y el grupo con su etiqueta. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido Demo con tab Tokens: 21 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d177ee331c |
feat(gradient-picker): temable — y su trigger resultó estar muerto entero
Quinto del bloque F2-A. censo 0 % → 88 % · global 42 % computed 609 valores en 8 estados: 0 diffs **El hallazgo llegó por el centinela: 36 de 45 tokens no movían nada.** Con esa cifra no se sigue adelante. El trigger es un POPOVER TRIGGER, y `popover.css` le pinta el cromo entero —altura, padding inline, tipografía, color, fondo, borde, radio, hover y foco— desde `[data-popover-trigger]:not([data-archetype='field-trigger'])`, que gana a `[data-gradient-picker-trigger]` en base (0,2,0 contra 0,1,0) y en los estados (0,3,0 contra 0,2,0). Esta receta re-declaraba TODO eso y nada pintaba: la altura, el padding, el tamaño de letra y el radio venían de popover, y con valores distintos —12px contra el `space-2-5` de la receta, 14px contra su `font-size-md`—. Llevaba así desde que el trigger se volvió trigger de popover. Así que no se tokeniza: **se retira**. Un token sobre una declaración muerta es un token que miente, y este eje ya retiró uno por lo mismo en `table`. Retirarlo dio **0 diffs**, que es la prueba de que estaba muerto. Un tema viste este trigger por el contrato de POPOVER, que es composición funcionando. Lo que sí es del picker se queda y alcanza (verificado a mano, porque el chip usa un hook de CLASE que el centinela no ve): el `gap` de la fila, el chip del degradado (tamaño por talla, radio, borde), las tres anchuras del panel, la lista de paradas y la rejilla de presets. **Correcciones al veredicto**, ambas medidas: `--gp-current-gradient` NO lo estampa el wrapper sino SOMA (`gradient-picker-provider.svelte.ts:197`), así que renombrarlo toca otra capa y no es de este commit; y no es un knob de tema sino un canal de valor —el degradado que eligió el usuario—, sobre el que un tema no tiene nada que decir. Las tres anchuras del panel se quedan como tokens planos leídos por tres reglas, no como cascada del TSC: el content va por PORTAL y dimensiona con `data-picker-size` (el atributo de picker-shell), y el vocabulario de scopes no tiene palabra para el atributo de otro componente. Al registro (`next-features.md` §13) van cuatro incidencias nuevas: que un componente compuesto pueda tener el cromo entero muerto sin que nada avise —merece guard, y el instrumento ya existe—, el nombre abreviado en soma, los dos hooks por CLASE que `eidos-lint` cuenta, y el foco opaco del trigger contra la mezcla suave del sistema. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido Demo con tab Tokens: 25 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
5596fc8707 |
docs(next-features): el registro de incidencias del eje theme-reach
Encargo del autor: anotar todas las incidencias de esta clase para resolverlas después. Van al registro canónico (`next-features.md`), no a un sitio nuevo, y con su MEDICIÓN — no como impresión. **§12 — El contrato de cascada del velo de estado.** Seis incidencias que resultaron ser la misma: nadie fijó nunca cómo compone el velo de `archetypes.css` con las reglas de una receta. Y la raíz explica por qué se manifiestan de formas tan distintas: el velo se pinta con DOS pesos según el arquetipo — `:where()` (0,0,0) para `trigger`, especificidad plena (0,5,0) para `item`/`option`. 131 declaraciones-atajo en 45 componentes cancelan el velo (66 BASE · 32 ACENTO · 23 HOVER · 10 OTRA; cero valores con gradiente, así que el paso a longhand no tiene riesgo técnico; 32 nodos velados que hoy no reaccionan al ratón pasarían a hacerlo) · la prop `hoverable` de table no suprime nada · en tree-grid el empate (0,5,0) lo decide el ORDEN DE CARGA y varía entre recargas · la banda de tree-grid mata el hover en las filas pares · un token de tinta no puede ganar al arquetipo (`selected-row-fg` se retiró por eso) · el thumb de scroll-area no tiene velo al que migrar. Esto explica de paso la adopción 21/135 que midió la auditoría del 2026-07-01: el velo estaba escrito y estructuralmente derrotado. **§13 — Huecos de instrumento y de demo.** Lo que hizo que una medición mintiera o no existiera, que importa porque el eje entero se apoya en ellas: falsos negativos del centinela sobre propiedades transicionadas, pseudo-elementos y componentes compuestos que no ve, la sonda que no pasa el ratón por un `<tr>`, y la lección general —**una sonda sobre una parte que la demo no monta compara CERO valores y pasa**—, con la lista de partes condicionales afectadas. El handoff gana la regla para las sesiones del eje: si una incidencia mueve píxel o toca morfo, se mide, se anota ahí y se sigue; no se arregla dentro del commit del componente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
9748733adc |
feat(tree-grid): el árbol se vuelve temable — 0 % → 95 %
Cuarto del bloque F2-A. 42 knobs con 29 privados que no derivaban de nada; ahora 47 claves públicas y tres privados, los tres legítimos. censo 0 % → 95 % · global 41 % → 42 % computed 9.744 valores en 7 estados: 0 diffs **Las guías de indentación eran UN color, no un fondo.** Lo que la propuesta llamaba `root-bg-image` son las líneas verticales por `aria-level`: cinco gradientes apilados que dibujan una línea por ancestro. El knob es `guide-fg` (+ `guide-width`); la construcción se queda en la receta — el mismo reparto que el damero de gradient-builder, donde el color es el token y el patrón es de la receta. **Tres privados sobreviven, y por razones distintas**: `--_tree-grid-columns` no es un knob de tema sino un CANAL de layout que el wrapper escribe inline desde la definición de columnas del consumidor; `--_tree-grid-palette-element` es el forward THM-2; `--_tree-grid-stable-rows` es dato del consumidor. **El hover de fila es no determinista, y viene de antes de este eje.** Dos hallazgos medidos: 1. En filas CON BANDA el hover está muerto: la regla de `striped` pesa (0,7,0) contra los (0,5,0) del hover y usa el atajo, así que en las pares no ocurre nada al pasar el ratón. 2. En las demás, quién gana depende del ORDEN DE CARGA: el velo del arquetipo `item` y el hover de la receta empatan EXACTAMENTE a (0,5,0). Seis corridas de la misma configuración dieron cuatro veces «sin velo» y dos «con velo». Eso segundo obligó a parar y caracterizarlo: al ver 3 diffs tras un cambio trivial (un `border-width` a token) lo primero que hice fue sospechar de mi cambio; comparar dos corridas del MISMO código dio los mismos 3 diffs, así que no era el código sino un empate de especificidad resuelto por orden — la misma clase de fragilidad que el canon documenta para las bandas de z-index. No se toca aquí: decidir quién manda mueve píxel y es la decisión pendiente del velo. El guard de huérfanos cazó tres tokens que acuñé suponiendo (`font-family`, `fg`, `surface-bg`): existían en la receta pero leyendo el valor crudo. Ahora los leen, que es justo lo que el eje persigue. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido `skin-media-player` Demo con tab Tokens: 47 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
8324e2cedb |
docs(theming): partir recipes/base.ts, al terminar el eje entero
Pregunta del autor: por qué los tokens de 128 componentes viven en un fichero
de 6.223 líneas y no junto a su componente.
La respuesta medida: NO es un fichero de tokens, es el valor del campo
`recipes` de `EidosConfig` —serializable, que un tema hidrata—, y por eso los
valores no pueden mudarse a `components/{x}/`: ya se intentó y se retiró («The
old `tokens/components/*` were retired»). Lo que sí puede repartirse es la
AUTORÍA, con un fichero por componente que `base.ts` compone.
Verificado antes de proponerlo: el muro de tipos de `defineRecipes` sigue
disparando cuando las claves llegan por spread desde un fichero suelto —
probado con `trigger-color` y `padding-x`, ambas siguen sin compilar. Y el
único consumidor sensible a la forma (`eidos-purge`, que hace `Object.keys()`)
es indiferente.
Va al final de TODO el themeable, no al cerrar el bloque: mientras quede un
componente por tokenizar, ese fichero se toca en cada commit, y mover 6.000
líneas en medio garantiza conflicto con cualquier sesión que esté trabajando.
Los tres costes que justifican hacerlo se midieron hoy: casi entra un SEGUNDO
bloque `table` que el catálogo habría descartado en silencio, una coma perdida
rompió el catálogo entero dos veces, y es el punto de conflicto de todas las
sesiones concurrentes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b7a4cef08f |
feat(media-player): el proyector se vuelve temable — 0 % → 88 %
Tercero del bloque F2-A. 45 knobs sin contrato salvo el acento; ahora 61 claves públicas, **cero privados y cero globales**. censo 0 % → 88 % · global 40 % → 41 % computed 3.277 valores en 7 estados: **0 diffs** **El veredicto pedía renombrar `--_mp-*` → `--_media-player-*`; hacen falta menos pasos que eso.** El prefijo abreviado viola theming §6 r5, cierto, pero al leerlos uno a uno los dieciséis privados eran ALIAS PUROS de su fuente (`--_mp-fg: var(--color-content-primary)`, `--_mp-accent: var(--media-player-accent)`…). Un alias puro no se renombra: se borra. La capa privada entera desaparece y cada knob pasa a público. **El acento firmado NO se tocó**: `accent`/`accent-strong` siguen siendo `--scale-amber-9/10` — acento de proyector theme-stable, que debe leerse sobre cualquier vídeo en claro Y oscuro, con su doctrina escrita en el propio bloque. Verificado en vivo que sigue re-tintando el rango del Slider compuesto. **El scrim: UN color, dos gradientes.** La tira inferior de controles y el degradado del título salen del mismo `--media-player-scrim`; dos tokens dejarían que un tema rompiera la pareja sin querer. **`audio-player` viajaba en el mismo barco y no era obvio**: consume siete de esos tokens sin declarar ninguno. No es fuga entre componentes — es un SKIN sobre las mismas partes (`[data-media-player][data-variant]`), así que están en ámbito. Su receta se migró en el mismo pass. **Lo que no se pudo medir, y se dice**: `audio-player` no tiene página de demo (la ruta da 404), así que la sonda encontró 0 nodos — y una comparación de 0 valores pasa siempre, que es justo la clase de guard que no vale. Se verificó a mano estampando `data-variant` sobre el player vivo: el padding pasó a `6px 12px`, exactamente los tokens nuevos. Hueco de demo anotado en el README. Centinela 36/61. Los 25 restantes, adjudicados por clase: partes no montadas en la demo (captions, buffering, título de audio, paneles portalados), un pseudo-elemento (`::after` del scrim) y los que pintan DENTRO del Slider compuesto — verificado a mano que `--media-player-track` sí llega hasta `--slider-track-bg`. Los 6 knobs que quedan son literales de relleno (`100%`, `max-content`). audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido `skin-media-player` Demo con tab Tokens: 61 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
564133b485 |
feat(table): la rejilla se vuelve temable — 0 % → 86 %
Segundo del bloque F2-A. 46 knobs sin una sola entrada de contrato; ahora 45
claves públicas.
censo 0 % → 86 % · global 39 % → 40 %
computed 13.050 valores en 7 estados: **0 diffs**
**Una trampa esquivada de milagro**: `table` YA tenía bloque en `base.ts` — los
forwards de paleta THM-2. Insertar uno nuevo habría creado un segundo bloque
`table:` y el catálogo lo habría descartado EN SILENCIO (gana el último). Las
claves se fusionan en el existente; el guard de duplicadas lo confirma.
**Dos nombres corregidos, como decía el veredicto**: lo que el privado llamaba
`root-height-{k}` es la altura de la FILA (`row-height-{k}`), y el prefijo
`root-` desaparece del resto — el envoltorio ES el componente y
recipe-contract §1 no repite la parte en ese caso.
**Qué casa con el bundle y qué no**, medido eje a eje: la altura de fila sí
(1:1); el padding NO (la rejilla empaqueta más denso en xs y más suelto de md
en adelante — forzar el bundle cambiaría el default); y la tipografía casa
DESPLAZADA: de md en adelante la tabla tipa un paso por debajo
(`font-size-md` = `var(--size-sm-font-size)`), que es deliberado y ahora se lee
en el propio nombre de la coordenada en vez de esconderse en un primitivo.
**Dos hallazgos del arquetipo `item`, y ninguno es mío** — la fila lo lleva, y
ese velo de `archetypes.css` está a especificidad PLENA (0,5,0), a propósito:
1. **La prop `hoverable` no suprime nada.** Una fila se ilumina al pasar el
ratón aunque el atributo no esté — medido con hover real, y ocurría ANTES de
tocar esta receta. El arquetipo y la prop no se ponen de acuerdo sobre quién
decide: es decisión de morfo, se presenta, no se toca de oficio.
2. **`selected-row-fg` era un token que mentía.** La misma regla fija el
`color` de la fila seleccionada y gana a la receta (0,5,0 contra 0,3,0), así
que el token no podía mover nada nunca. Lo cazó el centinela; retirado con su
declaración muerta. La tinta de la fila seleccionada es del arquetipo.
Y una corrección de rumbo a mitad de camino: al pasar todo a `background-color`
el velo apareció en 3 sort-triggers (su velo sí va en `:where()`, 0,0,0). Eso
es el barrido sistemático del velo, que NO está firmado — así que en los nodos
velados se mantiene el atajo a propósito, con el porqué escrito en la línea.
Primero lo diagnostiqué como regresión mía; la medición lo corrigió.
Los 6 knobs que quedan: 3 privados de paleta (patrón THM-2, capa compartida) y
3 literales de layout/identidad (`inline-size: 100%` ×2, `opacity: 1`).
El test `horizontal-escape` afirmaba el nombre del privado; ahora afirma el
público, que es justo lo que este eje persigue.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido `skin-media-player`
Demo con tab Tokens: 46 filas, 0 sin computar. README con «Talla y tema» y los
dos hallazgos del arquetipo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b43e7e2ec0 |
docs(theming): el tab Tokens entra en el protocolo del bloque
Cada componente que se tokenice engancha su tab en el mismo commit — una línea, porque el panel lee el contrato vivo y no hay tabla que mantener. `command` queda marcado como cerrado en la tabla del bloque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4ddea7fe29 |
feat(command): la paleta se vuelve temable — 0 % → 98 %
Primer componente del bloque F2-A, y el peor del catálogo con veredicto
verificado: 52 knobs y NINGUNA entrada de contrato. Ahora tiene 57 claves y no
le queda un solo privado.
censo 0 % → 98 % · global 38 % → 39 %
computed 8.091 valores en 8 estados (reposo · 5 tallas · abierto · hover):
**0 diffs**. El default no se movió.
**El eje `size`** sigue el patrón firmado en Sidebar: coordenadas por talla en
`root` + nombre RESUELTO con `declarations` (default `host` = md, overrides
`size:sm`/`size:lg`), que es lo único que la receta lee. Los bloques
`[data-size]` del CSS se RETIRAN: dejarlos vivos pisaría los tokens nuevos con
los valores viejos y el diff daría 0 porque la ruta vieja seguiría mandando —
la trampa que este eje ya pagó una vez. Las cuatro coordenadas casan 1:1 con el
bundle, medido (`--size-{k}-padding-inline` / `--size-{k}-font-size`), así que
leen la coordenada y no el primitivo suelto — que es justo lo que
`recipe-css-contract` exige, y lo cazó: `group-heading-font-size` entró como
`var(--font-size-xs)` y salió como `var(--size-xs-font-size)`.
**El radio y el modo Dialog**, que el veredicto marcaba como ⚠ colisión y no lo
era: dentro de `<Command.Dialog>` el panel que el usuario ve ES el del dialog,
así que la regla de contexto reapunta el propio público a
`var(--dialog-content-radius)`. Leer el público de un hermano donde ese hermano
ES la superficie es composición correcta, no un segundo token — y de paso el
componente se queda sin privados (antes el radio vivía en `--_command-radius`).
**El hueco del scrollbar** se resuelve con el patrón de combobox:
`viewport-padding-inline` + `scrollbar-inset`, y la regla del overflow los
compone.
**Centinela: 50/57 tokens mueven un computed.** Los 7 restantes, adjudicados
uno a uno en el navegador, no por conjetura:
5 `loading-*`, `viewport-padding`, `empty-*` — sus partes NO están montadas
en la demo (el estado vacío, la barra de carga y el scroller interno son
condicionales). No se puede medir lo que no existe.
1 `input-placeholder-fg` — `::placeholder` es pseudo-elemento y
`getComputedStyle` sobre el nodo no lo ve. Límite del instrumento.
1 `input-border` — FALSO NEGATIVO del centinela: comprobado a mano tres
veces (con la misma congelación de transición que él usa) el token SÍ
alcanza, `oklch(0.3485)` → `rgb(1, 2, 3)`. No he encontrado por qué el
centinela lo pierde; queda anotado como defecto del instrumento.
`R-5.1 exception: inline-size: 100%` — el panel rellena su contenedor; es
mecánica de layout, no apariencia. Es el 2 % que falta.
audit PASS (R-5.3 incluida) · eidos-lint 0 invalid · rtl 0 · docs 0 ·
suite: el único rojo es el conocido `skin-media-player`
README con la sección «Talla y tema» (molde navigation-menu).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
bd032534be |
feat(census): los primitivos tipográficos se miden contra su capa — pieza 0 de F2-A
D-TH.2-b, la firma que va ANTES del bloque porque cambia el suelo del censo: medirla a mitad de camino falsearía todos los antes/después. `heading` marcaba «36 knobs, 0 % alcanzable» y eso no era deuda: era una lectura equivocada. Su receta resuelve cada eje como `var(--_heading-font-size, var(--style-h2-font-size))` — el privado es el escape POR INSTANCIA que el wrapper escribe desde una prop, y el named style es la superficie de tema, ya pública y viva. Acuñar `--heading-*` para espejarla sería un alias por eje × nivel: la clase que mató la purga del changelog §39. El criterio de qué ES un primitivo tipográfico se midió, no se supuso: o la receta SELECCIONA por `data-style` (`heading`, `text`, `s-text` — el named style es su API) o está atada entera a UN style (`code` → `--style-code-*`, `display` → `--style-hero-*`, `label` → `--style-label-*`). `code-block` queda FUERA a propósito: lee un par de tokens de style para su texto pero posee cromo de caja real, y eso sí es suyo. Detalle que costó una vuelta: la comprobación va ANTES de la de privado. Leer el privado primero puntúa el primitivo entero como inalcanzable cuando es completamente temable — por la capa que lo posee. antes 5.199 knobs · 1.837 públicos (37 %) · 894 privados · 232 sistema después 5.199 knobs · 1.837 públicos (38 %) · 788 privados · 338 sistema **Gate**: 106 knobs pasan de `private` a `system` en los SEIS primitivos y **cero** de los otros 156 se mueve — verificado componente a componente contra la versión en HEAD, no a ojo. Alcance `—` (nada que poseer) para heading, text y display; `s-text` al 100 %; `code` y `label` bajan a 7 y 2 knobs de deuda real. Informe regenerado (171 veredictos a mano intactos), veredicto de heading anotado como ejecutado, y recipe-contract §1 gana el párrafo que fija el criterio — incluida la frontera: un menú que lee `--style-label-font-family` para una etiqueta NO es un primitivo y no entra en el conjunto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
5ee796fa98 |
docs(theming): el bloque F2-A preparado — ocho componentes, sus defectos y sus gates
La cola revisada se eleva a plan de bloque: pieza 0 de instrumento (D-TH.2-b: la familia tipográfica mide por --style-* — va primero porque cambia el suelo del censo), los ocho componentes con sus defectos MEDIDOS (censo) y la corrección que su veredicto §5 ya verificó contra el CSS, los gates de §7 por componente, y la revisión adversarial al cierre. 330 knobs hoy a 0 %; el bloque debe llevar el global de 37 % a ~42 %. Y al cerrarlo, el orden de lo que las firmas desbloquearon: el diseño de calendar-surface (220 knobs más), el mandato Field, y el resto de B1-B8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e20e8b8095 |
docs(theming): el informe habla el idioma nuevo — y el fleco del README, cerrado
Al rendir cuentas del repaso de los 7 salió un fleco: el README de gradient-builder mencionaba `checker-color` por su clave SUELTA, sin el prefijo `--gradient-builder-`, y el codemod busca tokens completos — quedó con el nombre muerto. Dos líneas corregidas, más las dos del §5 de su ficha. Y la causa de fondo: las 170 fichas del informe llevaban desde el codemod citando el contrato con los nombres viejos en sus secciones generadas. Regenerado entero (`--report`): 69 fichas cambian, los 171 veredictos §5 escritos a mano sobreviven (verificado), `docs:check` 0. El censo del informe registra 5.199 knobs · 1.837 públicos — cuatro menos que el suelo del codemod, que son EXACTAMENTE las cuatro declaraciones de hover bespoke que la firma 3 retiró (checkbox, splitter, switch, drp ×2 −1 nueva del checked). El alcance global sigue en 37 %. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d27e2ac9fc |
docs(theming): la firma 3 al día — y el atajo que llevaba dos meses ganando
Cuatro componentes migrados con su medición, y el diagnóstico que cambió al ejecutar: el velo del sistema no llegaba a ninguno, y la culpa no era del hover propio sino de la regla base — el atajo `background:` pesa (0,1,0) contra el `:where()` (0,0,0) de archetypes.css y fija `background-image: none` para siempre. Queda escrito lo que NO he hecho y por qué: scroll-area pide decisión de morfo (su thumb no lleva arquetipo con velo), el barrido de las otras 141 declaraciones no entra en una firma que se dio sobre knobs concretos, y la demo del date-range-picker no monta `kind`, así que su overview no se puede medir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
93017975ca |
fix(eidos): la capa de estado es un VELO — 38 hovers que no lo eran, al idioma
El codemod de ayer apartó 47 knobs «en cola de migración a la capa de estado».
Al empezar esa migración se ve que sólo SEIS lo estaban: el velo de §38 es
`background-image: linear-gradient(var(--state-hover), var(--state-hover))` y
no alcanza nada más. De los otros 41, veintidós mueven un BORDE y diecinueve
mueven la TINTA — ninguno es la capa de estado por muy neutro que sea su valor.
Eran deuda de nombre, y yo los había excluido del renombrado.
El clasificador tenía media prueba: miraba el VALOR (¿neutro o con valencia?) y
no la PROPIEDAD. Ahora exige las dos, siguiendo un salto por privado
(`--_switch-track-bg-hover`) y entre componentes (la familia calendar presta
`--calendar-control-*` a month-grid, range-calendar y year-grid, y por eso
esos cuatro no aparecían consumidos en su propia receta).
renombres 38 en 22 componentes · 113 referencias en 30 ficheros
cola real 6 (checkbox · date-range-picker ×2 · scroll-area · splitter ·
switch) — los únicos que pintan fondo
censo 162 · 5.203 · 1.841 (37 %) · 56 — IDÉNTICO
--names 38 → 0 desviadas
diff generated 38 renombres 1:1 · 0 cambios de valor
computed field · tabs contra la línea base ORIGINAL (anterior a las dos
pasadas): 4.031 valores, 16 estados, 0 diffs
huérfanos 0 · formato sin regresión · audit 163 PASS · rtl 0 · docs 0 ·
suite 434 pasan (1 rojo conocido) · check 0 errores en ficheros tocados
La muta-prueba se reescribió contra el catálogo vivo — asertaba nombres que el
codemod ya había renombrado — y gana los casos que faltaban: un knob de borde
neutro es `modifier`, uno de tinta neutro es `both`, y sólo los de fondo son
`state-layer`.
Queda una pregunta que §38 no contesta y que NO he decidido: si un hover de
borde o de tinta POR COMPONENTE debe existir siquiera. R-4.3 guarda
`background*` en `:hover`, nada más. Ahora al menos se llaman como deben
mientras se decide.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
e154b23fb9 |
docs(theming): el handoff al día — el bloque del vocabulario, cerrado
Los cuatro pasos hechos, con sus commits, y lo único que queda del bloque: el tercer muro (el tipo de `defineRecipes`) espera a que la migración a la capa de estado vacíe las 17 claves de hover neutro que todavía dicen `color`. Y las cuatro cosas que el bloque enseñó a base de costar vueltas: el plan se escribe antes de tocar nada o el verificador mide el vacío; un valor puede cambiar de texto sin cambiar de significado; prettier en masa reformatea deriva ajena (88 de 101 ficheros ya salían sucios por el CRLF del checkout); y un hover se clasifica por su VALOR, nunca por su nombre — cosa que este mismo handoff tenía mal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
7781d6e4c8 |
feat(audit): R-5.3 — la gramática de los nombres deja de ser prosa
El canon de nombres llevaba meses documentado y sin guard, y la medición del
2026-07-01 ya decía qué le pasa a un canon así: deriva entre el 30 y el 85 %.
Había derivado. El codemod de ayer lo normalizó; esto es lo que impide que
vuelva.
R-5.3 valida una FORMA, no una lista — ahí se separa del guard de eventos, que
comprueba pertenencia a un vocabulario cerrado. La forma es: tinta = `fg`,
modificador interactivo DELANTE, y detrás lo dimensional y contextual.
No reimplementa la gramática: la consume de `theming-census --names`, que es
la misma fuente sobre la que corrió el codemod. Dos implementaciones de una
gramática son dos gramáticas que acaban discrepando — y este repo ya pagó esa
factura con `commit-resize`, un hook muerto tres meses en una receta con todos
los tests en verde.
Entra en `error` directo, sin rampa `warn`, porque su deuda murió en el mismo
pass (precedente R-4.4). Las claves en cola de migración a la capa de estado se
reportan APARTE: no son deuda de nombre, son knobs que van a desaparecer, y
renombrar lo condenado es churn.
Muta-prueba de tres caras, que es lo único que distingue un guard de un guard
que pasa sobre el vacío:
`-bg-hover` con valor de acento → ROJO
`trigger-color` → ROJO
`primary-solid-hover` → VERDE (canónica: COLOR_ROLE_SLOTS pone
el modificador detrás por construcción)
La tercera es la que importa: es el fallo que un codemod ingenuo habría
cometido sobre las 47 claves de rol, `button` entero incluido.
Doctrina en el mismo pass: recipe-contract §1 gana las dos filas que le
faltaban (tinta y estado) más la frase que las gobierna y las dos familias con
gramática propia; §4 gana la fila R-5.3; theming §6.7 una nota fechada que
acota el principio de plataforma del px/py a los ejes dimensionales. El
checklist de cierre declara la regla — lo cazó `docs:check` con su propio
guard I5, que exige que toda regla del audit esté declarada allí.
Lo que NO entra, y por qué: el tercer muro (el tipo en `defineRecipes`, molde
`PhysicalAxisKey`) está escrito y probado, y dispara sobre 17 claves — los
hovers neutros que la firma 3 manda migrar. Meterlo hoy rompería `npm run
check` a todo el mundo por una deuda que ya tiene dueño y fecha. Entra cuando
la migración a la capa de estado las vacíe; son dos líneas entonces.
component:audit 163 PASS · 3 NEEDS-WORK (badge, mockup, motion — los tres
sin tocar por esto, R-5.3 pasa en los 166)
docs:check 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
bd916ef3fb |
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado
D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo
se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde
que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y
sus 793 referencias en 101 ficheros; no toca un solo valor.
claves renombradas 269 en 66 componentes
referencias 793 en 101 ficheros
censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56
sin contrato — IDÉNTICO, como debe ser un rename
--names antes/después 269 desviadas → 0
Qué NO entra, y por qué:
47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador
detrás POR CONSTRUCCIÓN (`primary-solid-hover`)
13 exentas firmadas `--focus-ring-*` es familia del sistema; el color
de `aura` es un sustantivo; `stop-color` ES una
parte de gradient-builder
47 hovers neutros por VALOR: migran a la capa de estado
(§38 + R-4.3), no se renombran — firma 3
6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background:
reescribir un registro fechado lo vuelve mentira
El clasificador vive en el censo (`--names`), no en un script suelto, para que
el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba
(`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo
promovía al frente CUALQUIER valor declarado por el morfo, y así
`--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en
`--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma
dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo
promociona el vocabulario interactivo cerrado, y lo contextual —`below`,
`loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de
regresión están en la muta-prueba.
También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`:
es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover
bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una
migración que no le toca; se clasifica por el VALOR.
Verificación (§7.4, artefacto por paso):
diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados
reapuntados a los nombres nuevos (`__names-verify-diff`)
computed tag-group · field · tabs: 6.467 valores en 23 estados,
0 diffs — y comprobado en el navegador que sirve el CSS
nuevo, para que ese 0 no sea el de una copia cacheada
huérfanos 0 en código
suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`).
`recipe-css-contract` verde: es el guard que caza el
`var()` sin fallback a un nombre que ya no se emite,
o sea el fallo exacto de un rename a medias
component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto
check 0 errores en ficheros tocados (los 73 globales son de
otras sesiones de la rama; se atribuye por fichero)
rtl:check 0 · docs:check 0
formato el renombrado no alarga ninguna línea: ningún fichero
tiene más líneas de 100 chars que antes, así que no se
pasa prettier — hacerlo reformateaba 300 ficheros de
deriva ajena
Entra aquí la corrección del repaso de los 7 ya hechos:
`gradient-builder.checker-color` → `checker-fg` (el damero de transparencia;
ahí `color` era slot). Sus `stop-color-*` no se tocan.
Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo
mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran
antes (firma 3, ya firmada), o el guard necesita una exención greppable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f09e04fab1 |
docs(theming): el acta — las catorce decisiones que faltaban, firmadas
El eje llevaba desde el 19 ejecutando sólo lo que no dependía de una firma.
Hoy se presentaron las catorce con recomendación fundada y el autor las firmó
en bloque. Quedan escritas donde se ejecutan: §4 del plan fila a fila, y una
sección de acta en el handoff con el QUÉ ejecutable de cada una.
La que cambió de sentido al presentarla fue D-TH.6. La pregunta no era
«normalizar hacia lo documentado» sino una CONTRADICCIÓN entre dos doctrinas
firmadas: theming §6.7 r7 dice que el slot de tinta es `fg`; el principio de
plataforma del codemod px/py («el token se llama como la propiedad») dice
`color`. Se adjudicó `fg`, y con una razón que acota el principio en vez de
romperlo: gobierna los ejes DIMENSIONALES, no la pareja `bg`/`fg` — si la
gobernara, `bg` tendría que llamarse `background-color`, y nadie lo propone.
De la misma lectura salió que el inventario heredado estaba inflado: de las
370 claves «desviadas», 47 son `{rol}-{slot-de-rol}` (`primary-solid-hover`,
`palette-hover`) donde COLOR_ROLE_SLOTS pone el modificador detrás POR
CONSTRUCCIÓN. Un codemod sobre las 370 las habría roto — `button` entero.
El inventario real es 301, y la gramática es POR FAMILIA, no única.
Los 22 «falsos amigos» resultaron ser tres cosas distintas: los que se quedan
(la familia del sistema `--focus-ring-*`, el color como sustantivo de `aura`,
y `stop-color` que ES una parte de gradient-builder), los que el morfo ya
resuelve (`partial`, `read`, `failed` son valores declarados de `data-state` y
`data-delivery`), y los dos `scrim-color-on-*`, que chocaban con el prefijo
canónico `on-` del acento y pasan a `scrim-fg-over-*`.
Y una que no era limpieza sino decisión: los hovers neutros NO se renombran,
MIGRAN a la capa de estado (§38 + R-4.3). Renombrarlos habría presumido que
sobreviven.
Con las firmas, la revisión de los 7 componentes ya corregidos contra la
gramática nueva: 6 limpios y una corrección (`gradient-builder.checker-color`
→ `checker-fg`), que entra en el codemod. Que salgan limpios no es suerte —
el backfill de este eje ya venía escribiendo `fg`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
51122efa32 |
docs(theming): mañana lo primero — el vocabulario de tokens: D-TH.6 (codemod) y R-5.3 (guard de gramática)
La conversación de hoy destapó que el backfill está escribiendo canon sobre un catálogo que habla al revés: el vocabulario de nombres existe (theming §6.7 + recipe-contract §1) pero no tiene guard, y la medición fundacional del 2026-07-01 ya dijo lo que le pasa a un canon defendido por prosa. Medido ahora con `scripts/__names-inventory.ts` (semilla del `--names` del censo): sobre 3.333 claves públicas hay 370 desviadas en 68 componentes — 125 con el modificador detrás, 215 con `color` como slot o segmento, 30 con ambas. Y dos falsos amigos que NO se renombran de oficio: `*-focus-ring-color` espeja la familia del sistema, y los `orb-color-*` de aura usan color como sustantivo. El handoff pone el bloque como primera tarea, con el orden que evita el error clásico: primero la FIRMA (D-TH.6, una decisión con el inventario delante), luego el codemod value-preserving con el triple muro del precedente px/py (renombres 1:1 en generated, censo idéntico, computed intacto en tres sondas), y sólo entonces el guard — R-5.3 como gramática y no como lista, porque el vocabulario de tokens es compositivo donde el de eventos era cerrado: la talla contra las siete canónicas, el modificador cerrado y delante, el slot contra sus tres fuentes, y la parte contra el morfo compilado como ya hace eidos-lint. En `error` directo: tras el codemod no hay deuda que tolerar, y la rampa warn es como estos guards mueren. Con muta-prueba antes de darlo por bueno. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
6e501f4fdf |
docs(blocks): handoff del día — cinco filas cerradas, y el eje que cambió el método
Deja escrito por dónde entrar mañana, en este orden: §0 EL EJE VIVO — el chasis de sección, que es lo que hay que retomar. Con la frase del autor que lo abrió («lo estás convirtiendo en una mierda a base de parches, ninguna es profesional ni de alcance») y su pregunta, que es la que tenía razón: si no debería haber un componente del que se basen todos los bloques. El censo que la confirmó (16 · 15 · 9 · 8 copias del Header), la firma (que CREZCA Section, no que nazca un hermano), por qué el nombre Block se descartó (ya significa «a todo el ancho» en el canon), los tres commits, las dos trampas que costaron —el Background que se capa dentro de la medida, y la raíz que extiende SectionProps y no HTMLAttributes— y las tres cosas que quedan: los 19 README describen el nido viejo, el contrato del tier no dice todavía que la raíz de un block ES un Section, y nada más. §1 LA COLA, REENCUADRADA — con el aviso de que más de la mitad de lo que sigue archivado bajo un block tiene la causa en el canon (A-48, A-53, A-60, A-62, A-65), así que bajarlas dentro de la frontera del tier fabrica parches. Y el trabajo honesto que sí queda dentro: A-74, A-96 y A-71. A-11 replanteada en su ficha por la misma razón: su arreglo profesional es que el canon publique el CATÁLOGO en runtime de sus escalas —como ya hace EIDOS_VARIANTS— y no siete líneas de exhaustividad repartidas por las demos. Queda medido lo que hoy está derivado: ContainerSize sin xxl, Breakpoint con 3 de 6, Decor con 5 de 8. docs:check 0/0 (813). Los avisos de prettier de los dos ficheros son preexistentes (CRLF), comprobado contra HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
35216b591c |
fix(natural-time-picker): la anotación del literal volvió a su línea — prettier la había separado del valor
Regresión mía de ayer, cazada por el guard al preguntar si los nombres pasan por el canon. Al renombrar los privados abreviados a su nombre completo, tres declaraciones cruzaron el ancho de línea y prettier las partió en varias; la anotación que las justifica quedó en otra línea, y R-2.1 la exige en LA MISMA. Resultado: un componente que estaba en PASS pasó a NEEDS-WORK con tres colores crudos donde en realidad hay tres excepciones anotadas de tono fijo. Se arregla con `prettier-ignore` sobre cada declaración, que es lo único que no toca lo que importa: ni se acorta el comentario que documenta por qué el color es fijo, ni se cambia el valor. El diff de computed sigue vacío (6.438 valores, ocho estados) y el componente vuelve a PASS. La lección va al handoff, porque el error no fue el rename sino el ORDEN de la verificación: correr `component:audit` antes del formateo final deja pasar exactamente esta clase de regresión. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
a9b9711657 |
docs(theming): handoff del eje al día — cola revisada, firmas pendientes y el instrumental que las verifica
El eje deja de ser «plan entregado» y pasa a «en ejecución»: siete componentes corregidos, veinte fichas revisadas y el informe vivo. El handoff y el registro del plan lo dicen con cifras y commits, para que mañana se entre por la ficha y no por la memoria de nadie. `CONTINUE-theming.md` se reescribe con lo que hace falta para continuar: la cola inmediata de ocho componentes EJECUTABLES sin firma —cada uno con la trampa concreta que su veredicto ya destapó—, la lista de lo que ESPERA firma (familia calendar, mandato de Field, el perímetro del knob para la familia tipográfica, el hover a la capa de estado) y la tabla de lo ejecutado con su censo antes → después. El plan gana su §8 con el registro completo y corrige su cabecera, que seguía diciendo que no había nada construido. Y se commitea el instrumental, porque sin él el protocolo §7 es una intención: la sonda de computed antes/después, el diff que es el gate (vacío o se para), el centinela que pregunta si cada token alcanza de verdad, y la captura del stage. Se ejecutan con `node` a secas —el loader de tsx inyecta helpers que no existen dentro de `page.evaluate` y toda sonda revienta—, desde la raíz y contra el dev server. Lo que el instrumento aprendió a fuerza de mentir queda escrito donde se consulta: congelar `transition` pero NUNCA `animation` (Presence necesita el `animationend` para montar el panel, y si lo congelas mides un popup que no existe); un token resuelto no se mueve desde `:root` por diseño; un panel portalado no ve el ámbito del componente; la demo puede tapar el componente que mides; y un valor centinela igual al real se lee como «no efecto». Igual las cuatro trampas que costaron un commit cada una, con su guard al lado: la clave duplicada en `recipes/base.ts` que descarta el bloque en silencio, los bloques `[data-size]` del CSS que sobreviven a la cascada del TSC y hacen que el diff dé cero por la ruta vieja, los backticks en `node -e` desde bash, y el encabezado que un Edit se come al insertar una sección. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
3bfe0d4130 |
docs(theming): revisadas las fichas 11-20 — veredicto verificado y corregido en cada una
Segunda tanda de revisión del eje theme-reach: command, table, media-player, tree-grid, field-langs, gradient-picker, listbox, carousel, heading y feed. Cifras reproducidas en las diez; la corrección va escrita en el bloque de veredicto de cada ficha, que sobrevive a la regeneración. Lo que la verificación cambió del plan mecánico: - Tres «colisiones» no eran colisiones: el radio de command es ADAPTACIÓN al Dialog que lo hospeda (lee un público de dialog, legítimo); el ancho del panel de gradient-picker es POR TALLA (20/18/22rem); y el título de feed son las redeclaraciones por talla de su propio privado. - Dos nombres mecánicos estaban mal: el `root-height` de table es la FILA (row-height, al bundle), y el `root-bg-image` de tree-grid son las GUÍAS de indentación — el knob es un color (`guide-color`), los cinco gradientes son de la receta. - Dos prefijos más de la misma plaga: media-player declara `--_mp-*` (el censo lo cuenta global) y field-langs `--_fls-*` — que además lee un privado DE FIELD con la fórmula del label un paso por debajo, sólo resoluble anidado: eso va al mandato field-composition, no se copia congelado. Y gradient-picker estampa `--gp-current-gradient` inline: mismo renombrado que gb. - Una regla de capa a punto de romperse: la propuesta de listbox acuñaba `font-size = var(--list-font-size)`, que es un público DE LA CAPA list-surface — duplicarlo es el vocabulario paralelo que la propia nota prohíbe. Retirada. - Y una lectura equivocada del 0 %: heading NO tiene deuda — es el primitivo tipográfico consumiendo la capa de named styles, que ES su superficie de tema (applyTypeScale la retunea en vivo). Acuñar --heading-* sería el alias por eje×nivel que la purga del §39 mató. Disposición propuesta: medir --style-* como sistema transversal para los primitivos tipográficos — decisión D-TH.2, y aplica a toda la familia B5. media-player trae además contexto que la propuesta ignoraba: su acento ya está firmado en el propio bloque de recetas como theme-stable (una escala cruda a propósito, para leerse sobre cualquier vídeo) — no se «corrige» a un rol. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
07778b597a |
uix(date-range-picker): la vista de década es suya y pasa a tokens; el calendario y el campo esperan su firma
Séptimo del eje theme-reach. Este componente es un híbrido de tres familias y el contrato lo dice ahora explícitamente: la vista de década/mes es invención suya y pasa a contrato público — separación, ancho, cabecera, navegación, rejilla y celdas, con los estados del rango separados por nombre en vez de amontonados en uno solo. Más el acento. Lo que NO se toca tiene dueño y esperando firma: la mitad de calendario lee `--calendar-*` y su corrección es la decisión de familia calendar que la auditoría del sistema dejó registrada; la entrada segmentada lee `--field-*` y le toca el mandato de composición de Field. Duplicar cualquiera de las dos bajo prefijo propio habría sido justo el alias que la purga de 2026-07 mató. Y el mismo defecto de prefijo que sus hermanos, aquí por partida doble: 44 referencias `--_date-field-*` y 9 `--_calendar-font-size` declaradas en ESTE CSS con el nombre de otro componente. Con un DateField o un Calendar anidados se habrían pisado. Renombradas, con el diff de computed en cero. Verificación: diff de computed vacío sobre 493 valores en ocho estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
68eb8af260 |
uix(time-range-picker): el reloj de dos columnas y la franja AM/PM pasan a tokens
Sexto del eje theme-reach, y gemelo de time-picker: mismo reparto, mismas razones. A contrato lo que el componente inventa — el reloj de dos columnas (su separación, ancho, tinta y tipografía), los colores de las tres manecillas y la franja AM/PM entera — y sin tocar el lado con forma de campo, que lee `--field-*` porque compone Field. Y el mismo defecto de prefijo que su gemelo: 56 referencias declaraban `--_time-field-*` dentro de este CSS (más una mención en el wrapper). Renombradas a `--_time-range-picker-*`, con el diff de computed en cero. Dos cosas que la revisión había marcado como acuñables y NO lo son, confirmadas al ejecutar: el realce del reloj al puntero lee `--toggle-palette-solid`, que es la paleta de otro componente — si procede, es un bloque de composición cruzada, y eso es decisión, no corrección; y el `padding-inline` del raíl consume el tamaño del pulgar del Slider, un eje que publica su dueño o no se publica. Verificación: diff de computed vacío sobre 725 valores en ocho estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e4fcdd62ee |
uix(time-picker): el reloj es suyo y pasa a tokens; el campo sigue siendo del Field
Quinto del eje theme-reach, y el primero donde la mitad del componente NO debe tokenizarse: time-picker compone Field, así que su parte con forma de campo lee `--field-*` por diseño y duplicar esos ejes bajo prefijo propio sería el vocabulario paralelo que el contrato prohíbe. Lo que sí es invención suya —la cara del reloj— pasa a contrato: separación, ancho, tinta y tipografía del reloj, los colores de las tres manecillas, el refuerzo del pulgar sobre la superficie del popover y el radio del trigger en línea. Más el acento, que ahora es público en vez de esconderse tras un privado. Y un defecto que el censo no podía ver: la receta declaraba sus privados con el prefijo de OTRO componente (`--_time-field-*`) dentro de su propio CSS. No era sólo cosmético — un TimeField anidado y este picker se habrían pisado el nombre. Son 44 referencias renombradas, con el diff de computed en cero. El 82 % que el censo sigue contando fuera son esos préstamos de Field: no es deriva, es un préstamo con dueño, y su corrección es el mandato de composición de Field que la auditoría del sistema dejó apuntado (§5.3-3), no este eje. El guard de huérfanos me corrigió por el camino: había declarado doce tokens de más, copiados del range-picker (la franja AM/PM y los ticks), que este componente no tiene. Retirados; sólo `trigger-radius` tenía consumidor real y se cableó. Verificación: diff de computed vacío sobre 580 valores en ocho estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4a27715170 |
uix(section): la sección de una página es un componente, no un esqueleto que cada block reescribe
Los 19 blocks del tier repetían el mismo chasis: un <section {...rest}> escrito a
mano, dentro Section (el aire), dentro Container (la medida) y a veces
Background (la decoración). Medido antes de tocar: containerSize recableado en
16 blocks, sectionSize en 15, decor en 9, y OCHO copias del mismo Header —
Box maxWidth 48rem + Motion viewport + Stack gap 3—, una de las cuales ya había
derivado en silencio: la de article-grid perdió su Motion.
La regla del propio tier dice que una parte se gana el compound cuando SE
REPITE. Aquí lo que se repite son los blocks, así que la repetición se gana un
componente. Y no uno nuevo: ese componente ya existía a medias y se llama
Section. Crece en vez de nacer un hermano — un segundo componente obligaría a
explicar en cada README en qué se diferencia de Section, y la explicación sería
«Section con tres props más».
Section gana, todo opcional: containerSize (compone Container DENTRO, y sin él
los hijos se renderizan igual que siempre), decor (pinta Background.Pattern
como HIJO, para que la banda sangre a todo el ancho por detrás de la medida sin
ganar caja) y la parte Section.Header.
Y renderiza un <section> real. Renderizaba un <div> a propósito, con la nota
«si un consumidor necesita un <section> de verdad para el landmark, que lo
envuelva» — y nueve blocks hacían exactamente eso, pagando un nodo extra para
poder nombrarse. Un <section> sin nombre NO es landmark (se expone como region
sólo al llevar nombre accesible, que es lo que A-109 dejó medido esta misma
semana), así que el tag no ensucia el árbol de nadie y la raíz duplicada
desaparece.
Para eso Box gana `as`, que cierra F21 («los primitivos de layout no pueden
cambiar de elemento»): lista CERRADA de elementos contenedores, nunca void —
es una escotilla semántica, no un prop de tag libre—, default 'div', y el
modelo de caja, las vars y la receta idénticos rinda lo que rinda. Los ≥2 casos
reales que la decisión original pedía para reconsiderarlo llegaron hace tiempo:
la columna de enlaces del pie que debía ser ul/li, y estos dieciséis blocks.
Migrados DOS como prueba, uno por clase: feature-grid (medida) y stats-band
(medida + decoración). Cada uno pierde dos niveles de anidamiento y su
<section> duplicado — medido en navegador: [data-section] ES el <section>, el
patrón 'grid' sigue siendo su hijo, 1280 de ancho, 64px de aire, 1024 de
medida, y el documento pasa de 3 elementos <section> a 2. Los otros 17 van en
tandas aparte.
⚠️ El paso de tipos que costó los únicos 5 errores de esta rama, y que lo
explica una regla ya escrita en el propio types.ts de stats-band: «sub-parts
extend the props of the canon component they wrap, never HTMLAttributes: the
raw attribute surface clashes with the canon's refined style/class when spread
through (the feature-grid lesson)». Ahora la RAÍZ también envuelve un
componente del canon, así que sigue la misma regla: FeatureGridProps y
StatsBandProps extienden SectionProps.
Gates: svelte-check 72/62 antes y después · vitest eidos+blocks 470/471 (el
rojo es skin-media-player, ajeno y documentado) · blocks:check 0/19 ·
docs:check 0/0. Prosa al día: README de Box y de Section, la demo de Section
(que anunciaba la limitación en cuatro sitios) y F21 cerrado en
PLAN-blocks-quality §6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
ae91573c32 |
uix(proof-of-human): el cromo del reto se puede temar; la paloma y el cielo no, y eso ya estaba firmado
Cuarto del eje theme-reach. El reto ataba su escenario a primitivos globales: alto mínimo, radio, fondo, borde y los bordes de veredicto no los alcanzaba ningún tema. Ahora son 20 claves públicas, con el alto por talla en el canon de dos piezas y `parts: ['root']`, más la guía del trazo (corredor, línea, estela, marcador de meta) y el token arrastrable. Lo que NO entra es la escena — la paloma, la carta, los cielos del reloj — y no por falta de tiempo: el guard de recetas ya lista este componente en la excepción de tono fijo, y sus literales ya llevan anotación línea a línea. Un sello no se vuelve azul porque el tema lo haga. La revisión había anotado esto como decisión pendiente del autor; era una firma que ya existía y no había leído. Dos fallos míos que la verificación cazó y que quedan escritos porque son reutilizables. El primero: añadí un bloque `'proof-of-human'` a las recetas sin ver que ya había uno (el forward de paleta), y en un object literal la segunda clave gana — mi bloque se descartaba EN SILENCIO. Lo delató el diff de computed con 28 diferencias, empezando por un `border-radius` que caía a cero. El segundo: al fusionar los bloques perdí la última clave, y el guard de tokens fantasma lo cazó al instante. Verificación: diff de computed vacío sobre 406 valores en siete estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. El centinela no vale aquí y así queda dicho: la demo declara su propio `min-block-size` y un degradado sobre el escenario, de modo que tapa dos de los tokens; los que no pisa responden. El 86 % que el censo sigue contando fuera son los dos ficheros de la escena, que enganchan por CLASE en vez de por data-attr — migrarlos es otro eje, anotado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e774ca7e84 |
uix(natural-time-picker): el cromo de la banda se puede temar, y el cielo sigue siendo el cielo
Tercero del eje theme-reach. La receta ataba todo a primitivos globales y guardaba su geometría en privados con un prefijo abreviado, `--_ntp-*`, que ni el censo veía ni el vocabulario permite: alcance 0 %. Ahora son 60 claves públicas y el 61 % medido. Dos de los tres bloqueos que la revisión había anotado no existían: la doctrina ya estaba escrita y no la había leído. El guard de recetas ya lista este componente en la excepción de TONO FIJO — un cielo no cambia con el tema, y sus literales ya llevaban su válvula línea a línea, así que los cielos, la tinta del sol/luna y la línea de posición se quedan privados y anotados, sin decisión que pedir. Y el panel sigue leyendo `--popover-*` porque su propia cabecera dice que esa superficie ES el flotante canónico: duplicarla con prefijo propio habría sido el vocabulario paralelo que el contrato prohíbe. De ahí que el 39 % restante no sea deuda sino préstamo deliberado. Lo que sí es suyo pasa a contrato: banda y knob por talla con `parts: ['panel']` —conservando su derivación de la altura de control, así que la expresión sigue siendo el knob y mover la talla arrastra ambos—, la línea indicadora, los ticks, el readout (hora, meridiem, periodo), los steppers, los chips y el trigger. Más el `panel-gap`, que sí es del panel y no del popover. Las 37 referencias `--_ntp-*` mueren: el prefijo de un privado es el nombre completo del componente (theming §6 regla 5). El rename alcanza receta, wrapper, README y el propio guard que las citaba. Verificación: diff de computed vacío sobre 6.438 valores en ocho estados. Centinela 40/62 automático, y cada uno de los marcados muertos verificado a mano: banda, knob, gap del panel, línea, radio de la línea e inset de los ticks alcanzan; los `trigger-*` NO, porque `[data-popover-trigger]` gana la cascada y viste el trigger — ya eran inertes antes de tokenizarlos, y ahora está dicho en el README en vez de fingir que el token pinta. Guards: component:audit PASS · eidos-lint 0 invalid · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · el guard del bundle obligó a apuntar `tick-font-size` a la coordenada `--size-xxs-font-size`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
cef2d32f16 |
uix(combobox): el campo y su panel pasan a tokens públicos, con la talla resuelta donde manda
Segundo del eje theme-reach. El combobox tenía UN token propio (`content-z`) y
todo lo demás atado a primitivos globales o enterrado en privados: alcance 0 %.
Ahora son 106 claves públicas y el 76 % medido — lo que queda fuera son tres
hovers que esperan firma, seis literales de layout y doce knobs que pasan por
privados que ya derivan de un público.
La talla sigue el canon de dos piezas (coordenada + nombre resuelto), pero con
una diferencia que el DOM impone: los resueltos del control se emiten sobre
`control` e `input`, no sobre el root, porque el `data-size` que manda vive en el
control — y el input suelto lo necesita fuera de él. Los del panel se emiten
sobre `content`, que viaja por portal y nunca vería un token del root. Es el
precedente de `select` aplicado donde toca.
Altura, tipografía y separación del control consumen la coordenada del bundle
`--size-{k}-*`; el relleno inline SE DESVÍA del bundle en cuatro tallas y
conserva su valor de hoy — la desviación queda visible en el contrato en vez de
redondearse a la coordenada, que habría sido un cambio visual disfrazado de
limpieza. El chip conserva sus expresiones (`control-height − inset`,
`control-font-size − offset`), así que mover el control sigue arrastrándolo.
El centinela cazó un fallo real a mitad del trabajo: la primera pasada dejó
vivos los ocho bloques `[data-…][data-size='…']` de la receta, que pisaban los
tokens nuevos con los valores viejos. El diff de computed daba cero JUSTAMENTE
porque la ruta vieja seguía mandando; sin centinela habría pasado por bueno.
Verificación: diff de computed vacío sobre 1.566 valores en ocho estados —
reposo, las cinco tallas, el panel ABIERTO y hover. Centinela 67/106 automático
y el resto verificado a mano: `content-z` alcanza (80 → 4321) aunque viva en el
wrapper flotante; placeholder, disabled e ink del trigger forzando su estado;
los del panel abriéndolo por teclado. Los siete nombres resueltos no se mueven
por diseño — el tema mueve la coordenada. Los catorce `selected-tag-*`, los tres
`separator-*` y `scrollbar-inset` no son verificables en esta demo (no monta
modo múltiple, ni separadores, ni scroll): queda dicho, con su consumidor
comprobado en la receta, en vez de darlos por buenos.
Un token nace inerte y se anota como tal: `content-font-family` y
`content-line-height` los pisa `[data-depth='overlay']` con la misma
especificidad — es la tipografía de portal del Build contract, y tocarla sería
otro eje.
Guards: component:audit PASS · eidos-lint 0 invalid · suite eidos sin rojos
nuevos · rtl:check 0 · docs:check 0 · el guard del bundle obligó a apuntar
`indicator-size` e `item-indicator-size` a `--size-sm-icon-size`.
El informe `docs/audit/theming/` se regenera entero, así que todas las fichas
actualizan su fecha de medida; las que cambian de contenido son combobox y
gradient-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
18165e74ed |
uix(blocks-demos): la vista previa nace con el estado vivo de ActiveUix, no con los defaults
El iframe de la vista previa es OTRO documento con OTRO runtime (BootUix corre
su propio createActiveUix), así que el estado de la sección no cruza el marco:
la rama de 375/768 se abría SIEMPRE en claro, y en 9 demos siempre en LTR, con
la topbar en oscuro y RTL delante (A-29). Dentro del documento los blocks
siempre atendieron a ActiveUix — el shell conduce el framework por
uix.prefs.setIntent y el modeSource de eidos —; lo que faltaba era la única
serialización que existe entre dos runtimes: la URL.
El arreglo va en el ARNÉS, no demo a demo. BlockDemo lee el estado VIVO por la
superficie sancionada del framework — readActiveUixPrefsSlot(uix.prefs,
'direction'|'language')?.get(), las lecturas por dimensión que son rune-backed
(contador $state por dimensión), y eidos.getThemeContext().mode, que lee el
modeSource del shell — y compone la URL final: los props del BLOCK los pone el
demo, los EJES los pone el arnés. El {#key} y el enlace «abrir ↗» pasan a la
URL resuelta, así que mover un toggle global re-monta el iframe con los params
nuevos y el enlace abre lo que estás viendo.
Retirados los 11 controles «dir (solo la vista previa)» de banner, contact,
cta, feature-grid, feature-split, hero, newsletter, site-footer, site-header,
stats-band y team: eran el eje de sección repetido por demo — la regla del
arnés dice que tema, idioma y dirección los da el shell — y no tocaban prefs,
sólo escribían la URL. El shell queda como único dueño de los ejes; una
precedencia demo-gana habría dejado el toggle global inerte en esas páginas.
Dos correcciones a la ficha: testimonials ya derivaba su URL (el literal
desnudo quedó atrás en F2b), y el alcance real era el sistémico que la propia
ficha apuntaba — mode= no viajaba en NINGUNO de los 19 demos y dir= faltaba en
9. La pata lang sigue siendo inerte salvo en contact (blocks.* registrado),
pero viaja igual: es un eje del shell.
Verificado con la topbar REAL de la sección, no con params a mano: en oscuro +
RTL, el iframe de pricing a 375 arranca base-dark + dir=rtl (tinta
oklch(0.9491 0 0)) y el de hero — que tenía control local — hereda igual; en el
estado por defecto los dos vuelven a base-light + ltr. La URL sale con los
props del block primero y mode/dir/lang detrás.
Gates: svelte-check 72/62 antes y después · blocks:check 0/19 · docs:check
0/0. Huella: BlockDemo +31, cada demo −18 (−192/+39 en total).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3cdb5b5f03 |
uix(gradient-builder): el editor entero pasa a tokens públicos — un tema lo mueve sin tocar el sistema
Primer componente del eje theme-reach, y su piloto: la receta declaraba CERO
tokens propios («self-contained», decía la cabecera) y ataba cada knob a un
primitivo global o a un privado. Alcance medido: 0 %. Un tema no podía cambiar
ni el alto del preview ni el radio de la tarjeta sin mover el ecosistema.
Ahora los 72 knobs son contrato público del componente. Los ejes dimensionales
siguen el canon de dos piezas que firmó el Sidebar: coordenadas por talla
(`preview-height-{sm,md,lg}`…) más UN nombre resuelto (`host` = md, `size:sm`,
`size:lg`) que es lo único que consume el CSS — así los bloques `[data-size]`
de la receta desaparecen y la cascada la emite el TSC. La geometría se queda
PROPIA (px × `--scaling`): un raíl de edición no tiene coordenada de
control-height, así que forzarlo al bundle habría sido rediseño. La tipografía
sí lo consume, que es lo que el guard `recipe-css-contract` exige.
De paso mueren dos abreviaturas que escondían tokens al censo y podían cruzarse
por anidamiento: `--gb-checker*` y `--gb-stop-fill` pasan a
`--_gradient-builder-*` (también en el wrapper). El damero de transparencia se
parametriza por `checker-color` + `checker-cell`, y el patrón de cuatro
gradientes se queda privado — un tema cambia el color de la celda, no reescribe
la rampa. El `padding` shorthand se parte en ejes lógicos.
El default no se mueve: sonda antes/después con 6.612 valores computados
comparados en 7 estados (reposo · cinco tallas · hover) sobre 34 nodos, diff
VACÍO. El único diff que apareció era la transición de hover de un Button
compuesto capturada a mitad — demostrado reproduciendo el valor interpolado en
el árbol ya modificado, bajando la espera de asentamiento.
Centinela: 72/72 tokens alcanzan. 49 desde el ámbito del componente en reposo;
las 12 coordenadas de talla y las 2 de preset seleccionado forzando su estado;
`hover-preset-border` con hover real; y los 4 del editor de color sólo desde
`:root`, porque el panel viaja por portal — anotado en el README para que nadie
lo lea como un token muerto.
Dos hallazgos que quedan anotados, no corregidos (mueven píxel, así que son
decisión): el wrapper pinta el preview con el shorthand `background` inline, que
resetea `background-image` y tapa el damero, de modo que la transparencia no
llega a verse; y el foco de la parada usa el patrón de dos anillos con
box-shadow cuando §32 canonizó `outline`.
Guards: component:audit PASS · eidos-lint 0 invalid · suite eidos sin rojos
nuevos (queda el conocido skin-media-player) · rtl:check 0 · docs:check 0 ·
check sin errores en los ficheros tocados · censo 0 % → 80 % (el resto son
privados que YA derivan de públicos, el anillo de foco y tres literales de
layout) · captura 2× revisada.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
2591ec2f03 |
docs(theming): la auditoría de alcance de tema, componente a componente, con su propuesta
El censo medía el eje y no lo explicaba: 5.205 knobs en dos tablas del plan no dicen QUÉ knob de QUÉ línea no alcanza un tema, ni qué token habría que crear. Ahora `theming-census.ts --report` escribe la auditoría entera bajo `docs/audit/theming/`: un README con la vista de conjunto y 170 fichas — 162 recetas con CSS + 8 componentes sin receta, el árbol completo de `eidos/components/`. Cada ficha de receta: knobs fuera de alcance con fichero:línea Y selector (agrupados por clase), sistema transversal aparte, los privados con la columna que decide («¿deriva de un público?»), y una PROPUESTA de corrección — el token a declarar en `base.ts` con su valor VERBATIM y su scope TSC. La propuesta deriva los nombres de recipe-contract §1 y theming §6.7; donde la doctrina no decide, marca `⚠ decisión` en vez de inventar. Y no propone acuñar lo que la doctrina prohíbe: lo que posee una capa compartida, el foco, la capa de estado, un token prestado de otra receta, un shorthand o un eje físico. Las 8 fichas sin receta dicen —medido, no supuesto— dónde vive su visual: el componente que componen (icon-button → button), la capa que consumen (affix → viewport-placement) o la receta ajena que pinta sus attrs (svg → badge/button). Defectos del instrumento corregidos en el mismo pase, todos encontrados mirando la salida: un `@import` pegado al primer selector se comía 2 knobs de color-picker (el suelo del censo no puede moverse); `:not(:disabled)` se leía como estado y producía `hover-disabled-*`; un knob que lee un privado se proponía a sí mismo en vez de resolverse por talla; `--icon-size-sm` se reportaba como préstamo del componente `icon` (ahora el préstamo se verifica contra las claves del dueño en `base.ts`); y el default de talla salía sin nombrar en vez de `-md`. Verificación: censo global idéntico al suelo publicado (5205 · 1621/33% · 856 · 1880 · 614 · 234) y COMPONENTE A COMPONENTE contra la tabla §9 del plan — 0 diferencias en 162 · regenerar dos veces da el árbol idéntico · el bloque de veredicto escrito a mano sobrevive · docs:check 0 errores sobre 813 docs · prettier limpio (el árbol generado entra en .prettierignore junto a eidos/generated). Las 10 primeras fichas llevan ya su veredicto de revisión: análisis correcto en las 10, propuesta apta en 2 (gradient-builder como piloto, combobox con cinco correcciones — dos de ellas evitaban romper el default), y 5 que no se tokenizan en solitario porque son alias de --calendar-*/--field-* y esperan la decisión de familia. Cuatro destaparon privados con prefijo ajeno o abreviado declarados en su propio CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
48bad6c726 |
uix(feature-split): una sección se nombra por su encabezado, y sus filas cuelgan de él
feature-split era el único block de sección del tier sin parte .Header. Sus
filas emitían h2 por defecto, así que una sección de tres filas aportaba tres
h2 hermanos al outline y la sección misma no tenía nombre (A-110). La página
compuesta lo rodeaba a mano: encabezado escrito como primer hijo y un level={3}
repetido fila a fila.
El h2 de las filas no era un descuido: era una decisión ESCRITA en el README
del block —«.Title emits h2 by default (each row is a section-level statement
of its own)… drop it to 3 when the page already put an h2 above the rows»—. Se
deroga en sitio, y no por gusto sino porque el resto del tier declara lo
contrario: FeatureGrid.ItemTitle, las preguntas de faq y Pricing.PlanName ponen
en level 3 lo que se repite, y el h2 lo lleva el Header.
Nace FeatureSplit.Header, calcado de Testimonials.Header (Box con medida +
Motion + Stack) y sin contexto, porque este block no tiene eje align que
heredar. Y .Title baja su default de 2 a 3. La forma queda coherente en los dos
casos, que es lo que la hace preferible a las alternativas: con .Header el h2 es
suyo y las filas cuelgan; sin .Header la app ya puso el h2 encima y las filas
cuelgan igual. La tercera vía —un contexto donde el Header marcase su presencia
y el Title derivara su nivel— daba el mismo resultado a cambio de un hijo
escribiendo en el contexto del padre, que es el patrón que ya costó un bucle de
$effect en este repo. Por un solo bit, no compensa.
Verificado en navegador, outline leído del DOM:
/blocks/feature-split/preview h2 «Del evento crudo a la decisión»
h3 ×3 (las filas) ← antes: h2 ×3, sin nombre
/blocks/landing h2 + h3 ×2, dentro de un outline de página
que ya no salta de nivel
La demo compone su encabezado y LandingSite deja de hacerlo a mano: fuera el
Stack escrito a pelo y fuera los dos level={3}. Header medido en su sitio (x
144, 768 de ancho: la misma medida que la copia de las filas).
Gates: svelte-check 72/62 antes y después · vitest src/uix/blocks 36/36 ·
blocks:check verde (19 blocks, 147 ficheros) · docs:check 0/0.
Primera fila del ledger arreglada DENTRO del tier, por acotación del eje: las
tres anteriores de la sesión (A-111, A-112, A-109) eran todas de canon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1e3a67ad39 |
uix(banner): la tira de aviso no es la cabecera del sitio, así que deja de reclamar su landmark
Toda página con cabecera exponía DOS landmarks banner: el <header> del
site-header y la tira de aviso, que estampaba role="banner" a mano. ARIA
reserva ese rol para la cabecera DEL SITIO —identidad, búsqueda del sitio— y
dice que un documento debería marcar como mucho uno (A-109).
Las dos salidas que la ficha proponía estaban mal planteadas, y lo destapó una
revisión adversarial de tres refutadores independientes:
- «Estampar el rol ANTES de los rest props, que es la clase naming donde gana
el consumidor» contradice la doctrina que la propia ficha citaba: role es
clase CONTRATO por nombre de atributo (morfo/types.ts:811-832;
ARIA_NAMING_ATTRS contiene sólo aria-label). El runtime siempre gana ahí, y
con razón: un data-state o un role pisables son un componente que miente.
- Y el orden de estampado NUNCA fue el mecanismo. Medido: el segundo landmark
es el <header> del site-header, que no lleva atributo role ninguno — recibe
banner IMPLÍCITO por ser una <header> fuera de un elemento de sección. Un rol
pisable habría dejado quitarlo caso por caso, empujando el arreglo a cada
app, en vez de arreglarlo por defecto.
- El morfo, además, nunca declaró el rol (aria: []): era un literal del wrapper
de eidos que el README daba por contrato de la parte. Drift morfo↔eidos que
se queda sin objeto.
Lo firmado: la tira deja de reclamar el landmark. defaultElement header→section
en el morfo, fuera el role literal. Un <section> es region SÓLO cuando tiene
nombre, así que el aria-label del consumidor la hace encontrable y su ausencia
la deja como contenido plano — que es exactamente lo que el censo de landmarks
de A-111 ya sancionaba por escrito.
⚠️ SIN nombre por defecto, y esta es la corrección que la revisión me obligó a
hacer sobre mi propia propuesta: yo había planteado un default traducido
(«Aviso»), copiando el patrón de skeleton/spinner. Habría convertido TODA tira
en landmark sin salida — /temas/grafito apila cuatro y habrían salido cuatro
region con el mismo nombre, que es la clase de defecto de A-111 con otro traje.
El precedente de <nav> no transfiere: un <nav> es SIEMPRE landmark y por eso
pide nombre; un <section> lo es PORQUE lo tiene. Quien quiera una tira
encontrable, la nombra.
Derogadas por escrito las dos afirmaciones que decían lo contrario, porque
existían y estaban firmadas: el README del canon («Banner is a landmark for
page-level announcements — site header, persistent notice, system status bar»)
y el «✓ ejemplar» que docs/audit/components/banner.md le puso a ese contrato el
2026-07-07. La otra mitad de aquella decisión —no role="alert", el feedback
transitorio es de Toast— sigue en pie y se dice.
Verificado con el árbol AX de Chrome por CDP, servidor limpio:
/blocks/landing banner ['Aviso del producto','Acme'] → banner ['Acme']
+ region ['Aviso del producto']
/blocks/banner/preview banner ['Principal'] · region ['Aviso del producto']
/temas/grafito banner [] · region [] — las cuatro tiras sin nombre
dejan de ser landmarks, que es lo correcto
Píxel idéntico: la receta selecciona [data-banner], no el elemento — tira a
sangre 1280×54 con el mismo fondo, capturada y mirada. Ningún test, guard, CSS
ni consulta del repo seleccionaba por role=banner ni por el tag.
Gates: svelte-check 72/62 antes y después · vitest eidos+blocks+morfo 692/693
(el rojo es skin-media-player, ajeno y documentado) · blocks:check 0/19 ·
docs:check 0/0 (644) · morfo:check PASS en banner. Prosa re-sincronizada en 11
sitios: los README de canon, block y app-shell, la ficha de auditoría, las tres
demos, landing y su README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
07a99679d7 |
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente
El autor, al ver que el radio del trigger del nav vivía en un privado clavado a
--radius-default: «¿los componentes son themables? si no lo son, es un error
como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs
en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo
comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en
var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar
moviendo el sistema entero.
Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205
knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del
componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son
literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen
alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %.
El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla
regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens ·
talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para
component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el
autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar
nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error ·
panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el
PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla
mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda
headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la
forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada
token nuevo, desde el píxel hacia arriba, registro de animationstart/end),
guards por fichero, un componente = un commit con sus artefactos, y revisión
adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho.
El instrumento se probó por mutación antes de creerle, y falló la primera vez:
no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que
meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos
mutaciones. Las cifras del plan son las del instrumento final, y el handoff
(CONTINUE-theming.md) obliga a reproducirlas antes de seguir.
Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del
script limpio · tsc del script sin errores · el plan cita ficheros que existen
(los once, comprobados).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
aa3f0e3511 |
uix(navigation-menu): navegar no fija nada, y el megamenú que se abre solo no debería sonar
T-1 de PLAN-sidebar.md §6. El `Link` emitía `commit-select + affirm` al navegar.
El libro lo nombra: TABLA 9.3, «Success de navegación — cambio de contexto con
logro», corregido como «shift + commit.fulfill si procede»; cap. 27 §2 «shift no
es commit… ir a otra pantalla no implica que la operación haya terminado»; y la
apertura de cap. 10, «una navegación no es exitosa por cambiar de pantalla».
Nada queda fijado al pulsar un enlace del nav: `aria-current="page"` es la huella
que deja el router DESPUÉS, no un estado que la parte fije.
Ahora habla el par de cap. 22 §9 —«Link → contact.press seguido de shift.navigate:
la presión no es la navegación»—, el mismo que la fila del Sidebar declaró ayer:
- `contact-activate` ANCLADO en el enlace pulsado (parte repetida → `partInstance`).
- `shift-navigate` en el `provider`, sin anclar: `data-event-*` es UNA ranura por
elemento, y el sujeto del cruce es el `<nav>` que cruza como unidad, no el
control que lo inicia. Sólo si el `<a>` tiene `href` — aquí no es un opt, viaja
en restProps, así que se lee del elemento que la parte ya tenía atado.
Esto REABRE la decisión del 2026-08-05 (ledger A-54), no la pisa: mover el evento
fuera del marco estaba bien; la familia era el defecto.
El pack estuvo a punto de borrarse, y esa habría sido la equivocación de la
sesión. Al quitarle la regla del commit se quedaba con dos selectores vacíos y
propuse retirarlo como ruido. El autor paró y pidió releer: la pregunta no era
qué escribe el fichero, sino qué debe decir la aparición del megamenú. Se abre
por HOVER, y medido con el default de familia un barrido del puntero por dos
triggers y salir de la barra emitía `open` · `open` · `close` — tres sonidos, 6
osciladores, sin un solo clic. Cap. 17 §3 («emerge — normalmente no necesita
sonido»), cap. 26 §7, cap. 32 §10-§11, cap. 5 §11. Así que `emerge-open` y
`emerge-close` son SILENT, la postura que `tooltip` y el flyout del `sidebar`
(D-SB.3) ya tienen para el mismo gesto. El pack DIFIERE del default y vuelve a
ser un pack.
Enmienda D.6 de book-deviations.md (PROJECT_CANON): `navigation-menu` sale del
alcance del `commit.subtle`, y quedan corregidas tres cláusulas suyas caducadas
— el `emerge-open/close` declarado desde el 2026-08-05, el `commit.subtle` que
murió en
|
2 months ago |
|
|
aced677a15 |
uix(toolbar): la acción principal de un cluster ya puede decir su rango — y el gesto ya suena
`Toolbar.Button` tipaba contra el ButtonProps de SOMA ({id, disabled}): sin
variant, sin color, sin size. Un cluster de acciones donde UNA es la principal
no tenía cómo decirlo desde dentro, y la demo del app-shell dejaba «Nueva»
FUERA del toolbar como workaround documentado (A-112).
La ficha ofrecía dos salidas y mi primera recomendación fue la equivocada —
declarar el toolbar deliberadamente uniforme y consagrar el workaround. El
autor la tumbó, con razón: el eidos toolbar-button.svelte era un PASSTHROUGH
que dejaba el <button> nativo de soma, exactamente la clase de bug que
dialog-trigger (2026-06-19) y card-group-title (2026-06-27) ya pagaron, y el
precedente correcto no es menubar-trigger sino Form.Submit. Dos datos que el
primer análisis no tenía:
- El botón era MUDO. El morfo del toolbar sólo declara commit-toggle (el
group-item), así que pulsar una acción no emitía nada. Medido tras el
cambio: click → contact-activate estampado en el nodo + 8 nodos de audio,
donde antes 0.
- La uniformidad la dan los DEFAULTS, no la ausencia del eje: ghost/neutral y
el size del toolbar por contexto de eidos (toolbar/context.ts, calcado del
precedente ButtonGroup y con su misma razón: la receta del Button lee
data-size EN el nodo, un descendant selector no llega). El tipo estrecha a
SelectionVariant (solid | outline | ghost).
La forma es el Button consumer pattern entero: composición vía child de soma
(con el rename outerChild contra la recursión), y la receta CEDE el nodo —
cero cromo de botón en toolbar.css (regla 5); Link y GroupItem siguen siendo
superficie de la barra y los pinta ella. Antes de ceder, las dos recetas sobre
un mismo nodo producían un híbrido medido en el que solid/primary NO pintaba.
La doctrina que faltaba queda escrita en guides/component-guide.md §4: dos
clases de parte con forma de botón en una barra. ACCIÓN en barra (Toolbar.
Button, Form.Submit, Dialog.Trigger) compone el Button; control de SUPERFICIE
de barra (Menubar.Trigger: File, Edit) lo pinta la barra. El test: ¿significa
lo mismo fuera de la barra? Esa distinción vivía en un comentario de
menubar-trigger; el comentario ahora apunta a la doctrina.
Verificado en navegador (Playwright, servidor limpio): el nodo ES el Button
del canon (data-button + data-variant), solid/primary pinta (fondo primary,
tinta blanca), paridad exacta con el GroupItem por construcción — mismo bundle
--size-*: 36px/36px en md, 30px/30px en sm, con la herencia del size del
toolbar medida en vivo al cambiarlo. La demo del app-shell mueve «Nueva» AL
INTERIOR del cluster (workaround retirado, gap del block cerrado en su README)
y la demo del toolbar gana la acción «New» con controles vivos action variant
/ action color.
Gates: svelte-check 72/62 (base 73/62, sin regresión) · vitest eidos+blocks
470/471 (el rojo es skin-media-player, ajeno y documentado en el handoff) ·
blocks:check 0 errores / 19 blocks · docs:check 0/0. Los avisos de prettier de
los dos README ya fallaban en HEAD.
⚠️ El árbol compartido tiene ya encima trabajo NUEVO de otra sesión
(navigation-menu, PLAN-sidebar, book-deviations): este commit lleva sólo los
14 ficheros del eje A-112, verificados por lista.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
cae88ca15b |
uix(navigation-menu): un <ul> no es una región, y por eso el landmark salía sin nombre
El morfo declaraba el nombre por defecto de la navegación en la parte que pinta
el <ul>, con la razón escrita al lado: «the root <nav> delegates its name to the
list it wraps». La delegación no existe. El gesto que un lector de pantalla
ofrece para saltar entre las áreas de una página llega al <nav>, y llegaba a un
<nav> ANÓNIMO — con el nombre del consumidor puesto una capa más adentro, en un
elemento que ninguna navegación por landmarks visita.
Sobrevivió tres semanas de tier porque con UNA navegación en la página un
landmark anónimo es admisible, y todas las demos del componente y todos los
blocks de sitio montan una. Apareció en cuanto el app-shell montó dos ámbitos,
que es lo que un shell hace por definición (A-111). La APG es explícita: si una
página incluye más de un landmark de navegación, cada uno lleva su nombre.
El arreglo NO es el que la ficha proponía. Su paso 1 quería que la lista
heredase el nombre por aria-labelledby; el barrido de los morfos hermanos con
defaultElement 'nav' dijo otra cosa: breadcrumb (<ol>) y nav-tree (<ul>)
declaran aria: [] en su lista y nombran sólo el <nav>. navigation-menu era el
único fuera de esa forma, así que la pareja de nombre —el aria-label con su
suppression prop-falsy ariaLabelledby, y el aria-labelledby por propRef— sube
entera a la parte Provider y la lista se queda con su aria-orientation.
En soma, la fuente ariaLabelledby sube al nivel del runtime (la forma de
breadcrumb) y el wrapper DEJA de sacar aria-label de restProps: viaja con el
resto y gana el merge por política de naming, que es lo que A-85 dejó escrito y
lo que este componente no aplicaba. Se van con ello el opt ariaLabel y el
reenvío condicional que la List hacía a mano.
Y como la regla no estaba escrita en ninguna doctrina —grep de «landmark» en
morfo.md, canon/ y guides/ daba cero—, se escribe: architecture/morfo.md §Step 4
gana «A landmark is named on ITS OWN element», y nace el censo
morfo/landmark-census.test.ts, que falla si una parte con defaultElement 'nav'
no declara un attr de nombre EN SU PARTE. Probado en rojo por mutación tres
veces: deshaciendo el arreglo (navigation-menu.provider), retirando la excepción
(palabras.breadcrumb) y vaciando el catálogo, que es la mutación que impide que
un censo sobre nada pase en verde. El ámbito es 'nav' a propósito: main, header
y footer son únicos por página, y section y form sólo son landmark cuando ya
tienen nombre.
El guard destapó de paso un segundo caso de la misma clase: la parte Breadcrumb
de palabras declara un <nav> con aria: [] y nadie la nombra, ni en soma ni en
eidos. Queda como A-116 con EXCEPCIÓN FIRMADA por el autor: palabras es el eje
de otra sesión y su rama es compartida. La tercera fila del censo obliga a
retirar la excepción el día que esa parte declare un nombre.
Verificado con el nombre COMPUTADO desde el árbol AX de Chrome por CDP, nunca
leyendo el atributo (un aria-label en el DOM prueba el atributo, no el nombre),
con servidor recién arrancado y esperando la puerta de hidratación honesta:
/blocks/app-shell/preview navigation: ['Navegación principal',
'Menú de la aplicación',
'Migas de pan'] (antes: el 2.º null)
/uix/components/navigation-menu (sin label del consumidor)
el <nav> computa 'Principal' — el default del morfo,
traducido, sobre el landmark; el <ul>, sin nombre
Gates: 420/420 en morfo+sema+soma+eidos tocados · component:audit --only
navigation-menu PASS · morfo:check PASS · svelte-check 72/62 antes y después ·
docs:check 0/0. Los avisos de prettier de los ficheros tocados ya fallaban en
HEAD (comprobado con git show HEAD:… | prettier --check); el fichero nuevo va
formateado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
e2754a2b6f |
docs(sidebar): el handoff del eje, y las seis deudas que abrió en otros
En este eje no queda nada: nueve decisiones firmadas, cinco fases ejecutadas y verificadas, y la revisión adversarial pasada con sus cuatro hallazgos arreglados. Lo que queda es de OTROS ejes, y por eso el handoff los lista con nombre y sitio en vez de dejarlos en la cabeza de quien estuvo aquí: T-1 (`navigation-menu` emite `commit-select` al navegar — el antipatrón «success de navegación»), T-2 (nav-tree · anchor-nav · breadcrumb siguen mudos por el contrato que aquí se retiró), T-3 y T-4 (dos fichas del ledger de blocks que suponen lo contrario de lo que ahora hay), T-5 (el guard del bundle sólo ve tokens, no reglas) y T-6 (la densidad y el zoom no bajan a un subárbol, y eso no está escrito en ninguna doc). El handoff también deja por escrito las dos veces que hoy mintió el INSTRUMENTO y no el código: con la pantalla del navegador oculta rAF se congela y `getBoundingClientRect` daba 256px para un panel de 52; y leer `animationName` dentro del callback de un MutationObserver mide el instante anterior al flush, que es como di por viva una animación que moría 3,5 ms después. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
bae03e55c5 |
uix(sidebar): cuatro cosas que sólo se ven cuando alguien intenta refutarte
Revisión adversarial del eje (seis lentes, un escéptico por hallazgo): 21
hallazgos, 17 refutados, 4 reales. Los cuatro, arreglados.
1. El `MenuBadge` no llegaba nunca a `lg`. Indexaba la tabla de su excepción
por `part`, que ya venía capado en `md`, así que la rama `lg→md` era código
muerto y un sidebar `lg` sacaba el chip un paso pequeño. Ahora indexa por la
talla del sidebar: 26 · 26 · 30 · 36.
2. `emerge-close-sub` se sellaba en un nodo que el mismo tick ocultaba. El
`display:none` de `[data-state='closed']` mataba `dismiss-fade` antes de
pintar un frame y, como el pack silencia ese evento a propósito, la retirada
del flyout se quedaba sin NINGÚN canal: una ocurrencia declarada que no
ocurre. La regla se guarda ahora con `:not([data-event^='emerge-close'])`,
que es justo la ventana que el canal visual mantiene abierta. Medido:
opacidad 0,85 → 0,35 → 0,03 con la animación viva, y el nodo oculto a los
~516 ms, cuando el sello se retira.
⚠️ Mi primera sonda dijo que `dismiss-fade` corría, y era mentira: leía
`animationName` DENTRO del callback del MutationObserver, o sea antes del
flush que ocultaba el elemento. El instrumento midió el instante equivocado.
3. La pestaña sema de la demo clavaba las SEIS previsualizaciones en el panel.
Con cuatro eventos apuntando a otras partes, el ▶ de `contact-activate`
estampaba la familia en el panel de 16rem y `press-squeeze` encogía el panel
ENTERO —lo contrario de lo que el propio párrafo de al lado explica—, y los
dos `emerge-*-sub` no podían encontrarse con la regla que los silencia,
atada a `[data-sidebar-menu-sub]`. `SemaPanel` recibe ahora la acción
(parámetro opcional: las otras 23 páginas siguen con su thunk sin
argumentos) y la página resuelve la parte desde el contrato compilado.
Medido: contacto → una fila, open-sub → el sub, shift → el provider.
4. La fila de Gaps del README de `app-shell` seguía afirmando que «sus morfos
sólo declaran emerge-expand/collapse», que es exactamente lo que este eje
dejó de ser cierto. Reescrita: queda viva sólo la mitad que lo sigue siendo
—`NavigationMenu` emitiendo `commit-select` al navegar, el antipatrón
«success de navegación» del cap. 10 §12—, y se registra por qué se refutó la
lectura «la fila SELECCIONA la sección».
Gates: eidos-lint 49/0 invalid · component:audit PASS · rtl:check 0 ·
docs:check 0/639 · vitest eidos 434/435 (el rojo es ajeno) · suite del sidebar
8/8 · `check` con delta CERO contra la base medida con stash (72/72).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
63b2de03c7 |
uix(sidebar): un token de ESPACIO no es la altura de un control, y el raíl de iconos no es una constante
El componente vivía fuera del canon de talla: la fila clavaba
`min-block-size: var(--space-8)` —un token de la escala de ESPACIO haciendo de
altura de CONTROL, que es otra escala de densidad— y `font-size:
var(--font-size-sm)` escrito en el recipe, donde el guard del bundle no lo ve.
El rótulo clavaba `xs`, el trigger `sm`, el badge `xs`, y la acción no componía
`Button`: se quedaba con los 13,33px del user-agent y sin acuse perceptivo
propio.
Ahora el sidebar tiene eje `size` (xs..lg, default `md`, `data-size` en el
WRAPPER —nunca en el morfo, o la resolución de soma lo pisaría con undefined—)
y cada coordenada de la fila sale del bundle `--size-{k}-*`. Medido: 26/12 ·
30/14 · 36/16 · 44/20, con el glifo siguiendo (14/16/18/20) por `--icon-size`,
que es la costura que ya usa `Button` porque un `<Icon>` pinta su tamaño INLINE
y gana a cualquier regla de hoja.
El raíl de iconos deja de ser `3rem`: se DERIVA (fila + 2×padding), así que da
42 · 46 · 52 · 60 y a `md` ya no recorta la fila que tiene que contener.
Tres composiciones más, por la regla que ya existía (container→part capada en
`md`, precedente `Dialog.Close`): el `Trigger` deriva su talla, la `MenuAction`
pasa a ser `IconButton ghost` —y con ella llegan su cromo, su anillo, su capa
de estado y su `contact-activate`—, y el `MenuBadge` deriva con UNA excepción
firmada: baja un paso, porque con la regla al pie el chip sale a 36/16 junto a
una fila de 36/16 e iguala al control en vez de anotarlo.
El hover deja de ser un `--color-surface-raised` a mano y pasa a la capa
canónica `--state-hover`. Al medirlo salió un defecto que no buscaba: la fila
activa no daba NINGUNA respuesta al hover, porque su acento usaba el shorthand
`background`, que resetea el longhand `background-image` donde vive la capa.
Con `background-color` la capa compone encima, que es justo para lo que existe.
Verificado con Playwright headless (la pantalla del navegador estaba oculta y
ahí rAF se congela: las medidas de layout salían falsas). Gates: eidos-lint 49
morfo-backed / 0 invalid · component:audit PASS 0E/0W · vitest src/uix/eidos
434/435 (el rojo es `skin-media-player`, ajeno, idéntico en base) · rtl:check 0
· docs:check 0/639 · `check` con delta CERO contra la base medida con stash
(72/72).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f6d5fa1595 |
uix(sidebar): la fila que navega decía «te sentí» a nadie, y el libro dice cómo se dice
«Navegar una fila es nativo y no suena» era una decisión de la fase 1 tomada en silencio (el plan la dejó como «decidir en fase 1») sobre una premisa falsa — «item = Link puro»—: la fila es `<button>` cuando no tiene `href`, la acción es un botón pelado y el `MenuItem` posee un flyout. Medido en navegador: pulsar fila, sub-fila o acción estampaba CERO; el Trigger sí acusaba recibo, porque compone `Button`. El libro no deja margen: cap. 22 §9 compone el enlace como «contact.press seguido de shift.navigate: la presión no es la navegación», cap. 27 §8 lo repite y cap. 22 §12 llama antipatrón al contacto mudo. Así que la fila declara las dos ocurrencias, y la aparición del flyout —cap. 26 §5-§6, un menú anclado— deja de ser un cambio de estado sin evento. Dónde se estampa cada una lo decidió la doctrina, no la comodidad: el gesto en la mano (la fila, anclado por instancia: es parte REPETIDA) y el cruce en la superficie que cruza como unidad (el provider). Eso evita la quinta pareja `queue` que yo iba a declarar: calendar ya movió su `shift-navigate` fuera del botón porque las dos ocurrencias se comían la única ranura y el press-squeeze no llegaba a pintarse. Voz: contact y shift toman la de su familia (`touch`, `slide`) sin escribir nada; los dos `emerge-*-sub` van SILENT como el tooltip — el flyout se abre al pasar el puntero, y un barrido por el raíl serían ocho apariciones sonando (cap. 26 §7, Criterio 12). El `trigger` NO declara contacto: lo trae por composición de `Button`. El `rail` sí, porque es una franja de cromo y nadie más lo estampa. Medido en el navegador tras el cambio: fila con href → `contact-activate` (press-squeeze corriendo) en ESA fila + `shift-navigate` en el shell; fila deshabilitada → nada; sub-fila → el mismo par; rail → contacto propio + `emerge-collapse` del panel; en modo icono, hover → `emerge-open-sub` (present-rise), hover repetido → nada, salir → `emerge-close-sub` (dismiss-fade). Suite nueva de 8 contratos, `check` con delta CERO contra la base medida con stash (72/72), morfo:check PASS, contracts sin fallos nuevos. Plan y decisiones firmadas: docs/process/PLAN-sidebar.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
23667277c1 |
docs(blocks): el shell dejó cinco filas, y una explica por qué nadie la había visto
Handoff reescrito para mañana y las cinco filas que abrió el `app-shell` fichadas en el ledger (A-111…A-115). La puerta de entrada de mañana es la primera. **A-111 — `NavigationMenu` deja su landmark ANÓNIMO.** El `aria-label` del consumidor se reenvía al `<ul>` de la parte `List` y el `<nav>` se queda sin nombre; el morfo lo declara así a propósito («the root delegates its name to the list it wraps»). Pero un `<ul>` no es una región: quien navega por landmarks ve el `<nav>`, y lo ve anónimo. Con UNA navegación en la página es admisible —y por eso lleva tres semanas de tier sin salir—; con dos ámbitos incumple la APG, y un shell de aplicación monta dos por definición. Medido en el preview: `navigation: ['Navegación principal', null, 'Migas de pan']`. La ficha lleva la forma propuesta en cuatro pasos: mover el default de naming de la `List` al `Provider` y que la lista apunte al root con `aria-labelledby`; precedencia de clase «naming» (el consumidor gana si dice algo); un guard que falle si un componente con `defaultElement: 'nav'` no puede recibir nombre en su root; y re-medir el censo. Toca morfo + soma, así que va con el aviso de mirar antes si la sesión del `sidebar` sigue viva ahí. Las otras cuatro: **A-112** `Toolbar.Button` tipa contra el `ButtonProps` de soma y no acepta `variant`/`color` · **A-113** la foundation emite los estilos semánticos y no los aplica a ningún elemento —Times New Roman en los 19 previews—, ARREGLADA en el reset del arnés pero con la pregunta de canon abierta · **A-114** `Sidebar.Inset` impone `overflow: auto` y se lleva el `sticky` de dentro · **A-115** no existe `description-list` y el panel de detalle del shell es el «primer detail-view real» que su propia ficha F5 pone como disparador. El handoff recoge además la corrección de fondo del día (la barra y el raíl son dos ÁMBITOS, no la misma navegación en dos tamaños), las dos averías del arnés que este block destapó, y lo que NO se ejecutó: la app de referencia en modo attach, que sigue siendo el único sitio donde el ecosistema entero se demostraría cableado. docs:check 0/0 (639). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db7bcc3a55 |
blocks(hero): la mitad de escritorio no se oculta en el móvil, no existe — y el clip no se pide
F4 del eje de visibilidad por breakpoint: el primer caso real, y la receta
escrita donde un agente la va a buscar — el README del block que la usa.
Medido sobre build de PRODUCCIÓN (`build` + `vite preview`, Chrome), que es el
único sitio donde la afirmación es comprobable: a 375, cero `<video>` en el DOM
y NINGUNA petición de `video.mp4`; a 1280, uno y una (`206 Partial Content`).
En dev es falsa por construcción — el servidor renderiza a ancho 0 y sirve la
variante `base` antes de que la hidratación la pode.
Dos criterios que la receta fija para el tier. La puerta va DENTRO del snippet,
nunca alrededor del `<Hero>`: envolver el block duplicaría título, copy,
formulario y acciones por breakpoint, que es lo que hacen las secciones
por-breakpoint de Framer y la razón de que deriven; la media es lo único que
difiere, así que es lo único escrito dos veces. Y `display="contents"`: una
puerta decide presencia, no añade geometría.
La mitad móvil no es un vídeo más ligero: es NINGÚN vídeo. El Surface con
aurora cuesta cero bytes y sigue la paleta del tema.
⚠️ Hallazgo del canon, medido de paso y NO arreglado aquí: la media dentro de
un `Background.Layer` desborda la capa — el mínimo automático del ítem de
rejilla gana a `block-size: 100%`. Hero a 1280: capa 567px, vídeo 714px. En la
demo del propio canon con `Background.Video`: capa 288px, vídeo 435px.
`min-block-size: 0` lo colapsa exacto en ambos. `overflow: clip` lo escondía,
así que `cover` lleva recortando descentrado sin que se viera. Es del eje
`background`, cerrado el 2026-08-18: queda escrito en §Found while composing.
`launch.json` gana `preview-prod`: la entrada llamada `preview` arranca
`npm run dev`, así que no servía el build y la medida no se podía hacer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
fd02780002 |
uix(box): ocultar por CSS descarga el vídeo igual, y evitarlo pedía no tener SSR
`visibleFrom` / `hiddenFrom` no ocultan: no renderizan. El subárbol no está en
el DOM, sus efectos no corren y su media no se pide — la mitad que
`display={{ base: 'none' }}` no puede dar. Los once contenedores que componen
Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) la heredan sin tocar sus ficheros. La puerta es un
`$derived` sobre `eidos.dom.isAtLeast(bp)` y un `{#if}`: sin atributos nuevos,
sin generador, sobre los breakpoints que configura el app.
Ninguna referencia lo hace así. Mantine (`hiddenFrom`/`visibleFrom`), Chakra y
Panda (`hideFrom`/`hideBelow`), Framer y Webflow ocultan por CSS y dejan el DOM
montado — Webflow lo documenta como coste de ancho de banda y Framer llega a
duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una
puerta JS pinta la variante equivocada en el HTML (MUI devuelve un `matches` por
defecto en el primer montaje; Vuetify #17252 reporta los saltos de layout), y el
precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.
Aquí ese precio no existe: el proyecto se despliega como SPA (`adapter-static` +
`fallback`, ninguna ruta con `prerender = true`), así que el primer render
ocurre en el navegador con el `innerWidth` real — el build es un `200.html` de
1,8 KB, shell puro. El que SÍ se paga está escrito en el tipo: cruzar un
breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,
`display`. Sólo el dev server renderiza en servidor (ancho 0) y empieza a
descargar la media de la variante `base` antes de que la hidratación la pode:
queda como caveat de desarrollo en el README.
`base` sale del tipo en ambas props: `visibleFrom="base"` sería «siempre» y
`hiddenFrom="base"` «nunca renderiza», y ninguna de las dos es una cosa que una
prop deba poder decir.
Card, Banner, ScrollArea y Sticky rinden su propia raíz y NO heredan la puerta:
queda como gap diferido con la regla del `as` (≥2 casos reales). El puente de
SSR queda escrito con su disparador, no construido — hoy sería un atributo que
nadie lee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
9333acd661 |
docs(blocks): A-95 deja de ser «bloqueada por una pieza que no existe»
La pieza existe desde ayer. La fila pasa a ARREGLADO con sus cifras: solape 0px
entre el aviso y la cabecera, hit-test dentro de la cabecera, documento = 720 =
el viewport. Lo que la cierra no es un número mejor sino haber quitado la
pregunta que tumbó todas las propuestas anteriores («¿y si tenemos banners a
diferentes alturas?»): el aviso entra en flujo como una fila, así que su altura
es la que sea.
⚠️ Sigue siendo cierto lo que la ficha decía del `Affix`: fuera de un shell, un
aviso con `affix="top"` sigue tapando una cabecera pegada. Es el apilamiento
firmado, no un defecto.
A-109 gana disposición escrita (es del canon: el `role="banner"` va después de
los rest props; dos salidas, y se decide en una sesión del canon).
El handoff cambia de puerta: F3 está abierta y `app-shell` hecho. Queda escrito
lo que la sesión NO llegó a ejecutar — la app de referencia en modo attach, que
sigue siendo el único sitio donde el ecosistema entero se demuestra cableado, y
hoy no lo hace ninguno de los siete shells del repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f3bf6a0299 |
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema
Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que había que preguntar: si su alcance tenía delante `active-app` y `active-uix` enteros. No los tenía. Las cuatro decisiones que se firman, y lo que las obligó: - **Q1 — el block NO cablea servicios.** La integración del ecosistema se demuestra en una app de referencia en app-land, no dentro del block. Un shell que leyera `App.session` para dibujar el menú de usuario sería una raíz de composición disfrazada: funcionaría, y metería el arranque del ecosistema dentro de una pieza que ha de poder caer en cualquier app. - **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed, sin números) es lo que cierra A-95 por construcción; `body` es lo que `docs-shell` necesitará para anclas y TOC pegada. - **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho exigiría partir ese provider: queda como gap de canon para v2. - **Q4 — canon `skip-link` + uno por región montada** (técnica G124). De paso, tres cosas que estaban mal escritas y ahora lo dicen: - El dossier afirmaba que **ninguna referencia trae skip-link** y que era superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera un menú entero. La superación real es generarlos del mismo contrato que estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí miró está deprecado. - **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni cablea servicios. - **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`, `mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo. - **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta `update-password`, que no es un lujo — es donde aterriza el enlace de recuperación, y sin ella el camino de `recover` no termina. `blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría, landmarks, alturas) y dónde está la línea (la raíz de composición es de la app). Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box` expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell no necesita CSS propio—, y `Sticky` expone `root`. docs:check 0 errores, 0 avisos (635 docs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f7be59cdcf |
uix(background): se va `Backdrop`, que llevaba desde F1 absorbido y esperando tu palabra
D-BG.1 firmó la absorción y el borrado el 2026-08-17, pero el borrado exigía orden explícita del autor y no la tenía; llegó hoy. Se van cinco ficheros: `eidos/components/backdrop/` entero —`backdrop.css`, `backdrop.svelte`, `index.ts`, `types.ts`— y su morfo `morfo/components/backdrop.ts`. Con `git rm`, no con `rm`, para que el índice lo registre. El censo se repitió justo antes de tocar nada, no se dio por bueno el de ayer: ningún import del componente ni del morfo fuera de su propia carpeta, no lo exporta el índice de eidos, no aparece en ningún índice de morfos, sin langs, sin demo, y ningún `import.meta.glob` que lo cargara por convención. Los gates lo confirman por aritmética, que es mejor que por ausencia de ruido: `morfo:check` pasa de 168 morfos a 167 y de 8 saltados sin demo a 7 — exactamente uno, el que se fue. `check` conserva sus 72 errores preexistentes y no menciona backdrop en ninguno (el recuento de ficheros baja de 6985 a 6981). vitest de `eidos` + `morfo` 653/654, con el rojo de siempre (`skin-media-player`). `docs:check` 0/635. `smoke` 320/320. Lo que NO se borra son las citas. La §Baseline del README de `Background` sigue nombrando a `Backdrop` —ahora fechada, con la nota de que sus cuatro tramas (`glow · mesh · grid · dots`), su máscara de desvanecido y su caída bajo `prefers-contrast` se migraron valor por valor, y de que la historia guarda el original— porque esa fila explica algo que el código ya no puede: por qué este componente se llama `Background` y no `Backdrop`. El nombre colisionaba con el velo modal (MUI, Vuetify, y el propio archetype `overlay`), y esa colisión es la razón del nombre que hoy llevan `data-background`, `--background-*` y `Background.Video`. Con esto el eje queda cerrado: las cuatro firmas del autor ejecutadas, la verificación en navegador completa —travel, 60 fps, reflow y la rama `@supports not`— y el paseo A–H registrado. Vivo sólo queda lo que pertenece a otros ejes: `Ambient` honrando la pausa (del pack) y el puerto de estado para `<video>`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4a9315d977 |
uix(background): el velo era el dim del modal, y con on="light" combatía a su propia tinta
D-BG.20. El scrim tomaba prestado `--color-overlay`, que es `surface.backdrop`: el dim de los modales (MD3/Radix/Vaul según el changelog), afinado POR MODO porque una página clara necesita menos atenuación que una oscura. Es una herramienta de ATENUACIÓN, y el scrim la usaba como tinta de LEGIBILIDAD — la misma enfermedad que la escala `--opacity-*` prestada que se arregló esta misma mañana, y que trajo tres defectos. El primero, que la misma fotografía se leía distinto según el modo: 1,48:1 en claro y 2,10:1 en oscuro. Una fotografía no cambia con el modo, y la legibilidad no es un eje que el modo pueda mover. El segundo, un TECHO: el alpha propio de la tinta (0,42 / 0,66) acotaba la escala entera, de modo que ni el peso máximo alcanzaba AA y ningún paso nuevo podría haberlo alcanzado. El tercero es el que no había visto y es el peor: con `on="light"` el velo era OSCURO bajo tinta oscura. El scrim combatía a la tinta que existía para sostener. La tinta del velo pasa a ser el SUELO de la tinta que `on` puso en vigor, que es lo que D12 ya define: `--color-content-on-solid-contrast` bajo `on="dark"`, `--color-content-on-solid` bajo `on="light"`, y `--color-surface-default` sin `on`, porque ahí la copia toma la tinta de la página. Tres roles que ya existían —ninguno inventado, §16.C— consumidos como capa 4 → capa 3 (§3) y sin alias. Con la tinta opaca el peso ES el alpha, así que `strength` significa una sola cosa; antes significaba dos, porque con `color="teal"` el ink ya era `--palette-solid`, opaco, y sólo el default era translúcido. La escala se re-afina por el TRABAJO de cada paso, medida con la matemática del propio framework (`$color`: `apcaLc` + `wcagContrastRatio`) sobre la peor obra de cada contexto: `xs` 0,08 · `sm` 0,13 · `md` 0,19 · `lg` 0,40 · `xl` 0,70. `xl` es el único paso que promete legibilidad sobre CUALQUIER fotografía, y lo promete contra los cuatro suelos que el framework ya usa —el criterio del par on-solid (`lib/on-solid.ts`: APCA |Lc| ≥ 60 ∧ WCAG ≥ 3) y el AA 4,5 de §40— en ambos contextos: `on="dark"` sobre foto blanca 6,45:1 con Lc 85, `on="light"` sobre foto negra 8,29:1 con Lc 61. 0,70 es el mínimo que cierra los cuatro; el más exigente, el APCA de `on="light"`, pedía 0,695. Los pasos por debajo son atmósfera y el README lo dice: no son garantías. Verificado en Chrome, no calculado: sin `on` el velo sale `oklch(0.9911 0 0 / 0.19)`, el suelo de la página en claro; con `on="dark"`, `#1c1917` al 19%; con `on="light"`, BLANCO al 19% bajo tinta oscura, que es el arreglo; `xl` en `on="light"` sobre foto negra mide 8,25:1 por píxel donde `$color` predijo 8,29. Y el consumidor real: el hero en layout `background` pinta `#1c1917 / 0.19` con titular blanco y da 1,48 / 5,17 — exactamente lo que daba antes en modo claro, paridad a la cifra. Los heroes en modo oscuro se aclaran de 0,297 a 0,19, que es el modo soltando una decisión que nunca fue suya. El selector de contexto va envuelto en `:where()`, y no por estética: a pelo llega a (0,4,0) y le gana a `[data-color]`, de modo que un `<Background.Scrim color="teal">` dentro de un stack `on="dark"` pintaba el suelo en vez de teal — el sistema de color abierto de §25 derrotado por un selector de conveniencia. Envuelto, la familia baja a (0,2,0): el contexto gana al bloque base por orden y pierde contra el color explícito. Medido antes y después. Queda anotado que el vignette sigue consumiendo `--color-overlay`, y ahí es correcto: es una trama que atenúa bordes, no un velo de legibilidad. Esta decisión salió de leer la doctrina entera después de que el autor preguntara si mi recomendación era conforme al sistema de color. No lo era: yo proponía una tinta opaca única, que servía sólo a `on="dark"` e ignoraba los otros dos contextos, y medía el contraste con un canvas WCAG-only en vez de con los suelos del framework. Los tres puntos los corrigió la documentación. Gates: audit PASS 0/0 · eidos-lint invalid 0, class-hooks 0 · rtl:check 0/180 · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c691a88c1a |
uix(background): un fondo nunca suena, y no estaba resuelto sino evitado
D-BG.19, planteada por el autor: qué pasa con un `Background.Video` cuyo clip
trae pista de audio, y cómo se relaciona con el interruptor de sonido del
framework o con un «no sound» de la app. La respuesta que había en el código era
que no había respuesta. `muted` era una prop con default `true` y la razón
escrita al lado no era doctrinal sino mecánica —un clip sin mutear tiene el
autoplay rechazado en todos los navegadores—, de modo que el silencio era un
efecto secundario de la política del navegador y no una decisión de nadie.
Lo que eso significaba en cuanto alguien escribiera `muted={false}`: el clip
sonaba FUERA del grafo de audio. `sound.buses.content.setMuted(true)` no lo
silenciaba; no participaba del audio focus, así que hablaba por encima de un
`MediaPlayer` en vez de cederle el paso; no atenuaba el bus `ui` mientras sonaba;
no se proyectaba a MediaSession; y el slot `sound` de `$prefs` —el interruptor
«sin sonido» de la app, el que se proyecta al DOM como `data-sound`— no lo
alcanzaba nunca. Un `AudioContext` por documento es la razón de ser de ese art, y
el componente no tenía ni una referencia a él. De WCAG 1.4.2 se salvaba de
rebote: el control de pausa que el stack ya debía lo para, por carambola.
Así que `muted` deja de ser prop y se escribe `true` incondicionalmente: la API
ya no puede expresar un fondo audible. Tres razones, de más a menos vinculante.
La doctrina propia del componente —toda capa es `aria-hidden` y la decoración no
es contenido, así que un clip de aquí no puede portar significado que el audio
entregue; una capa audible sería contenido vestido de fondo—. WCAG 1.4.2: una
decoración no puede pedir un consentimiento que nadie le dio. Y la fuga de
`arts/sound`, que es la parte que hace de esto un asunto de framework y no de
gusto. Un clip que DEBE oírse es contenido: `MediaPlayer`, que ya es ciudadano de
`sound.media()` y obtiene focus, ducking y MediaSession gratis, o montarlo por
`Background.Layer` y registrarlo uno mismo.
La alternativa —hacerlo ciudadano con `sound.media(el, { focus: 'duck' })` cuando
el consumidor desmutea— queda rechazada por lo mismo que la hacía atractiva: abre
la puerta a que un fondo compita con el contenido real, y ese territorio es de
`Ambient` o de un reproductor.
Verificado en Chrome con la capa en `video`: `el.muted === true`, `paused: false`,
`currentTime` avanzando, `readyState: 4` —sigue reproduciéndose— y el control de
pausa presente. Queda anotado en el código un detalle que invita a un arreglo
equivocado: `hasAttribute('muted')` lee false mientras la propiedad es true,
porque Svelte fija `muted` como PROPIEDAD sin reflejarlo al atributo. No es un
agujero: la propiedad se escribe en el mismo efecto y dos líneas antes del
`play()`, y no hay nada que pueda colarse entre las dos.
Ningún consumidor pasaba `muted` —el `HeroSite` ya lo decía en prosa,
«DECORATIVE — muted»—, así que el cambio de API no rompe a nadie.
Gates: audit PASS 0/0 · vitest eidos 434/435 (el rojo es `skin-media-player`, el
de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio ·
prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
73f06a7882 |
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo
Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d2574e8a7c |
uix(background): la línea de tiempo la capturaba un overflow, y el puntero se iba con el scroll
El travel llevaba dos días escrito y sin medir, porque el panel oculto nunca activa una animación scroll-driven declarada en CSS. Con el Chrome del autor delante se pudo medir, y lo que apareció no fue una confirmación: fueron dos defectos que ninguna sonda había estado en posición de ver. La puerta es `document.visibilityState` — con la pestaña oculta la `ViewTimeline` existe, con `source` y `subject` correctos y `playState: 'running'`, y `currentTime` es `null` para siempre; ahí ni un `await requestAnimationFrame` resuelve. El primero estaba en la demo y el límite es del componente. `[data-uix-stage]` declaraba `overflow: hidden`, y eso es un scroll container aunque no pueda scrollear nunca: `view()` se ancló al stage y el progreso quedó clavado en 52,63% en toda posición de scroll, sin error, sin aviso y con un `translate` de aspecto perfectamente razonable. Con `overflow: clip` —recorta igual, respeta el radio, y no es scroll container— el travel aparece: progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono, y los `4rem` completos de `--background-parallax-travel` en el extremo. Entra en el README como quinto límite porque el caso real no es un harness de demos: es el `overflow-x: hidden` con el que cualquier landing contiene su decoración, que convierte al elemento en scroll container en los DOS ejes. El segundo era del componente. El rect del anfitrión se cacheaba en coordenadas de viewport y sólo lo invalidaban `pointerenter` y un resize; el scroll mueve el anfitrión sin disparar ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión de 288px dejaba `--background-pointer-y` 0,77 fuera de sitio —los 111px de scroll sobre media altura, a la centésima—, o sea fuera del rango −1…1 que la variable promete, con el spotlight despegado del cursor unos 115px y sin recuperarse hasta salir y volver a entrar. Cacheada la caja en coordenadas de DOCUMENTO y normalizando contra `pageX`/`pageY`, que en un evento real ya traen el scroll incorporado: error 0 en los dos movimientos, halo a 1px del píxel pedido, sin lectura de layout en el camino caliente y sin el listener de scroll que este componente existe para no tener. Queda dicho el residuo: un scroller anidado vuelve a desviar, porque `pageY` no lo ve, y se autocura al reentrar. Y lo que era el encargo, medido: 60 fps con mediana de 16,7ms, p95 17,0, máximo 17,1 y 0 de 200 frames por encima de 20ms con el travel vivo —la base con `speed: 0` salió peor, que es ruido de entorno—; y 0ms de reflow forzado en esos mismos 200 frames moviendo los DOS ejes, con el observador de Long Animation Frames que usa `arts/perf`. El cero está validado por mutación: un thrash deliberado de 65ms en la misma página se reporta con 43ms forzados y el script atribuido, porque un instrumento que no ve nada y un cero real se leen igual. Sigue pendiente la rama `@supports not`, que sólo se ejercita en Firefox. Las 174 demos comparten ese stage, así que el cambio se auditó: `affix` idéntico y `sticky` idéntico —cuatro pasadas alternando `clip` y `hidden`, porque la primera miente—, y `anchor-nav` cambia a mejor: su rail `position: sticky` es hermano del scroller interno, con `hidden` no se pegaba nunca y se escapaba por arriba perdiendo media lista, y ahora se mantiene a la vista. Ninguna demo tocada. Dos avisos para quien vuelva. Un `PointerEvent` construido reporta `pageY === clientY`, sin sumar el scroll, así que los eventos sintéticos sirven para DESCUBRIR este defecto pero no para verificarlo: con ellos el arreglo bueno mide −1,389 donde el ratón real da −0,008. Y leer las variables después de un screenshot puede devolver el `write(0, 0)` de un `pointerleave` en vez de la medición; el valor que vale es el del último `pointermove`, trazado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db22de616c |
docs(blocks): el handoff de mañana, y las cinco filas que dejó componer la página
F2b queda cerrada entera, así que el handoff cambia de puerta: lo primero de mañana es el repaso block a block que pediste, y va sin ficha escrita a propósito — su forma se acuerda antes de empezar (qué orden, qué se mira, y si lo que salga se escribe en el ledger o en el README de cada block). Las cinco cosas que la página compuesta y cookie-consent encontraron entran al ledger como A-106…A-110, medidas y sin tocar: Button acepta ref y no lo reenvía —la forma exacta de A-94—, Switch no tiene parte de etiqueta y todos los consumidores del repo repiten el mismo apaño, Dialog devuelve el foco a un nodo muerto cuando su disparador se ha desmontado, Banner deja DOS landmarks banner en cualquier página con cabecera, y feature-split es el único block de sección sin .Header. Casi las meto como A-100…A-104, que ya estaban ocupadas. El rango libre empezaba en A-106 y el ledger tiene 110 filas, no 96: las cifras que el handoff repetía —«96 filas, 64-19-13»— eran una foto vieja de julio. Las de ahora están contadas sobre el fichero: 70 ARREGLADO · 24 CONFIRMADO · 13 REFUTADO · 3 DATO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
c786d0f804 |
blocks(landing): las piezas se prueban juntas, y ahí sale lo que ninguna enseñaba sola
Cierra el tramo D y con él F2b entera. Una landing verosímil con 14 de los 18 blocks, a sangre y sin marco de demo, porque el claim del tier —que sus blocks ensamblan sin fricción— es indemostrable block a block: el outline de encabezados sólo existe cruzando secciones, dos landmarks sólo chocan cuando hay dos, y una costura vertical sólo aparece cuando algo se pone debajo. Lo que dice la medida: un h1 y 22 encabezados sin un solo salto de nivel; ningún landmark repetido sin nombre; 0px de hueco entre secciones vecinas —el aire es siempre el padding de cada Section, costuras de 112 a 192px—; cuatro de las cinco cascadas esperando a que su sección entre en viewport; un solo commit-submit en el formulario; setenta tabulaciones sin una trampa de foco y un drawer que a 375px devuelve el foco a su disparador. A-95 vuelve confirmado, ahora con cifras: con affix="top" la tira y la cabecera solapan 49px —la altura entera de la cabecera— y el hit-test en su centro cae en la tira, así que la navegación es inalcanzable. La pieza que falta sigue siendo app-shell. La página compone la tira en flujo, que es lo correcto, y deja el escenario del defecto detrás de ?banner=affix para poder medirlo en vez de citarlo. Un defecto arreglado, y era de los que importan: Newsletter.Reason medía 2,33:1 sobre el panel mientras la nota a su lado medía 8,5:1. Es la frase que explica por qué la acción está bloqueada —la que la doctrina de coordinación obliga a tener— y no se podía leer: la doctrina cumplida en la estructura y derrotada en la percepción. El block ya tenía el mecanismo y ya lo había escrito para su nota («más callado por TAMAÑO, no por tinta»); su razón no lo seguía. Ahora 8,83:1 sobre el panel, y muted fuera de él. Dos gaps nuevos, ninguno tocado aquí: los DOS landmarks banner —el canon estampa role="banner" DESPUÉS de sus rest props, así que nombrar ambos es todo lo que puede hacer la app, y esta página lo hace— y feature-split sin parte .Header, el único block de sección que no la tiene: sin ella emite tres h2 hermanos y la sección se queda sin nombre. El catálogo aprendió a publicar páginas: kind: 'page' en BlockEntry y el guard lo salta en las dos direcciones, con self-test propio probado por mutación. Y una lección de instrumento que costó un susto: una captura fullPage no hace scroll, así que toda cascada bajo el pliegue se queda en opacidad 0 y la imagen sale EN BLANCO. La primera medida dijo «la landing está vacía». Saltar de una vez al final del documento tampoco vale: las secciones pasan de debajo a encima en un frame y el observador nunca las ve entrar. Se recorre en pasos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
06f3c26400 |
docs(background): el handoff del eje, con lo que no pude medir escrito arriba
`CONTINUE-background.md`. F0 → F5 cerradas y commiteadas: el componente existe, tiene demo, está documentado y el tier lo usa. El handoff indexa; la fuente viva sigue siendo `PLAN-background.md` (D-BG.1…18 y la ejecución medida fase por fase). Lo que queda no bloquea el uso: cuatro firmas del autor (la escala `strength` del scrim, que no está ordenada por peso; la foto clara sin ningún peso que llegue a AA; borrar `backdrop/`; y si la banda de `feature-split` va por sección o por fila), una verificación que este entorno no puede dar —el travel real, 60 fps y el detector de reflow necesitan un Chrome visible, y está comprobado que la limitación es del entorno y no del código— y dos tareas que este eje abrió en otros: `Ambient` honrando la pausa, y un puerto de estado para `<video>` que ya tiene dos consumidores. Lleva también las cinco lecciones que costaron medición, para no volver a pagarlas: A30 no es estilo sino supervivencia; `revert-layer` sin `@layer` es `revert`; la demo destapa lo que una sonda no; mide antes de afirmar rendimiento; y la decoración que parece mejor puede ser la ilegible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
331912cf23 |
blocks(background): seis fondos, y el que parecía mejor era el que no se podía leer
F5 de PLAN-background.md: los seis blocks de D-BG.10 (2) reciben `Background.*`. El hero cayó en F2 y la página compuesta ya existía — no había que construirla, había que verificarla con la decoración puesta. La forma es uniforme y tiene un solo desvío: cada block gana un prop `decor` que monta `<Background.Pattern>` como HIJA de su `Section`, la misma composición que el hero ya probó. El desvío es `cta`, donde la decoración va dentro del PANEL, que es donde está el ojo. **Dos hallazgos que sólo la medición podía dar.** Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular bajo AA: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a **2,27:1**. Pintan un lavado claro anclado en `50% 0%` — exactamente donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen, y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1 contra el panel. Por eso el default es `rings`, que además irradia del mismo anclaje superior que usaba el glow. Las tintadas se ofrecen igual: el app decide, pero ya con el número delante, en el tipo y en el README. Y una trama tintada dentro del panel era invisible por construcción: `--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que pinta el panel — un halo del color del panel sobre el color del panel. El block pasa ahora `color="var(--color-content-on-solid)"`, el token que ya usa para su propia copia; medido después, el tinte resuelve a `#ffffff`. **Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band` `grid`, porque una banda de cifras se lee como medida y la rejilla es la trama que lo dice sin decorar por decorar · `testimonials` y `feature-split` `glow`. `site-footer` y `banner` reciben la capacidad APAGADA: la columna B del plan de calidad no pide acabado en ninguno de los dos, y encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió. **Un desvío de alcance, declarado.** La columna B pedía «`Backdrop` alterno» POR FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría a cada `Row` a poseer el estado de alternancia, y eso es coordinación — un block que coordina deja de ser un block que no posee nada. Medido en Chrome sobre la página compuesta real: cinco pilas, los cinco anfitriones adoptados (`position: relative` + `isolation: isolate`) sin que ningún block los posicione; la copia mide 15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y 12,37:1 con `grid`/`dots`. Ledger: seis filas nuevas, A-100…A-105, cada una con su mecanismo y su evidencia. Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · 434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
740274a847 |
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no
F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos
auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los
ejes que F3 añade, y separarlas dejaría un estado que nunca se probó.
Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token
de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más
allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la
deriva contra el puntero), `spotlight` y `attach='fixed'`.
El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de
scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca
`ScrollProgress` y escribe `--background-progress`, que la rama `@supports not`
mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez
porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se
paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll
del propio lector, que es justo la clase que provoca síntomas vestibulares.
**La decisión que no estaba prevista.** El scroll y el puntero quieren mover la
MISMA capa, y una animación sobre `translate` gana a cualquier declaración
estática: el puntero habría dejado de existir sin más. Así que el scroll anima
una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único
`translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el
puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el
shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`.
El precio, dicho porque en la primera redacción escribí lo contrario tres veces:
una custom property NO se puede compositar, así que el navegador recalcula
estilo cada frame. Para una decoración es el intercambio correcto —una property
por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era
falso y ahora el código dice lo que ocurre.
**Dos footguns cerrados por forma, no por disciplina:**
- `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes
había que escribirlo también en la pila, y olvidarlo dejaba la capa
`position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin
error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae
de vuelta). La prop de la pila desaparece: no hay nada que olvidar.
- Un `speed` negativo —una capa que se mueve contra el scroll— invertía el
bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora
usa la magnitud.
**Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una
línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand
la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener
de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando
el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado
e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones
—`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio,
`declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde
el init, y seguir el prop sin escribir en la primera pasada.
**La demo** (`/uix/components/background`, v2, nueve pestañas) monta un
ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí
solo y sin padre no se puede enseñar lo único que importa: que el padre se
adopta y el layout no se mueve. Los chips son uniones completas verificadas por
el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación.
Y fue la demo la que destapó que, con A30, encender `animate` en caliente no
hacía aparecer el control de pausa — el registro era un hecho de montaje.
Invisible en una sonda, obvio con un interruptor.
Registro documental (D-BG.11): `next-features.md` §11 · la frase en
`design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) ·
`PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de
autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y
`Aura` corregida (decía «Not built yet» y está construido) · y en
`motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un
preset nombra una transición discreta CON duración, y esto es modulación
continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor.
Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los
valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y
retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el
`translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada
control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres
de las cuatro clases de capa hasta la segunda auditoría).
⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60
fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y
ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado
que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con
`animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada
por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome
visible.
Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 ·
`rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 ·
`morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de
siempre) · `check` 0 errores propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
dc7c4920d6 |
blocks(cookie-consent): la ley europea como tipo, y las dos salidas al mismo peso
Cierra el tramo C de F2b. El único block cuya fase 0 fue jurídica antes que visual: las referencias shippean el aviso como markup, y el markup no puede impedir las tres cosas que lo vuelven ilegal — un rechazo debilitado, categorías premarcadas, y cerrar tratado como consentir. Aquí son el tipo y el constructor. `essential: true` hace inescribible una UI que ofrezca apagarlo; `initialConsent()` no admite semilla, así que una primera pregunta no puede llegar marcada; `onDecision` es obligatoria, así que un aviso que no informa de vuelta no se puede montar. Las cinco reglas están numeradas en consent.ts y afirmadas por 8 tests, porque son justo lo que un refactor rompe en silencio mientras todo sigue pintando bien. Desviación de la ficha, y a favor: UNA llamada requerida `onDecision(consent, via)` en vez de `onAcceptAll` + `onRejectAll`. Con dos callbacks el consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo renderiza el block y el API no expone knob para degradarlo. Medido: las dos acciones tienen idéntico relleno, tinta, borde, tamaño, peso y alto — 5.96:1 en claro, 8.79:1 en oscuro. Tres cosas se arreglaron por mirar, no por deducir. La barra era un `Card` outline y midió 1.00:1 contra la página: se disolvía en ella, así que va al plano `overlay` del sistema, el mismo que estampa cada superficie flotante del canon. A 375 las tres acciones envolvían con «rechazar» en una fila y «aceptar» en la siguiente, leyéndose como dos botones ajenos justo donde la pareja más importa: ahora son un `Group`, y el markup lo dice. Y el panel devolvía el foco a un nodo muerto — se devuelve aquí, un frame después de que la trampa se desarme, con un pestillo para que el asentamiento del montaje no le robe el foco a nadie al cargar. Tres gaps de canon salieron al componerlo, todos en el README: `Button` acepta `ref` y no lo reenvía (forma de A-94), `Switch` no tiene parte de etiqueta, y el `Dialog` restaura el foco a un nodo muerto cuando su disparador se desmontó. El instrumento mintió antes que el código, otra vez: el pane del navegador no compone frames, así que la animación de salida no termina y un diálogo cerrado sigue pintado — el foco medido allí era ficción. Y `fillStyle` no normaliza oklch, así que todo color leído del estilo computado se volvió negro y dio 1.11:1 sobre texto perfectamente legible. Las cifras de esta ficha se miden por píxel, en Chrome real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
313e4d52fa |
blocks(article-grid): la banda editorial, y el elemento que el canon no podía dar
Lo que las referencias llaman blog. Sus dos preguntas de fase cero quedan resueltas, y ninguna necesitó tocar el canon. El nombre. Blog nombra una sección entera de un sitio, y esto es la fila de artículos recientes dentro de otra página; el tier nombra por función de página y el elemento que renderiza es un article, así que article-grid hace coincidir nombre, función y markup. La palabra blog sobrevive donde un consumidor la busca: la primera línea del readme y la etiqueta del catálogo. El artículo semántico. Un teaser tiene que ser un article o se lee como una caja de texto suelto, pero la tarjeta del canon es un div fijo y eso parecía obligar a elegir entre añadirle un eje de elemento o declarar un hueco. Construirlo enseñó la tercera vía, que no cuesta nada: el bloque renderiza el article y compone la tarjeta dentro. El tier lleva desde su primer bloque renderizando sus propios elementos de seccionado, así que el elemento es del bloque y la superficie sigue siendo del canon. La regla amuralla al primitivo, no a la composición. Y tres decisiones más que sí son de diseño. La tarjeta entera no es un enlace, porque el teaser lleva además un chip y una firma y anidar interactivos dentro de un enlace es marcado inválido: es justo donde acaban las referencias que hacen clicable toda la tarjeta, así que el enlace vive en el título y hay exactamente uno por teaser. El realce al puntero viene encendido, al revés que en precios, porque un teaser sí es algo a lo que se va. Y el bloque no formatea fechas: las compone la app con el componente del canon. Dos errores míos en la tanda, los dos cazados mirando la captura y no la sonda. El ancho mínimo por defecto sólo cabía dos veces en la medida, cuando las referencias lideran con tres columnas; medido y corregido, ahora da tres, dos y una según el ancho. Y el segundo intento de arreglarlo no llegó a aplicarse sin que me diera cuenta, porque lancé el reemplazo sin aserción y el formateador había juntado las props en una línea: culpé a la caché del servidor antes de mirar el fichero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
33031c2ad9 |
uix(background): el media entra en la pila, y el fondo aprende a pararse
F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b7474e499d |
blocks(logo-cloud): la banda de marcas vuelve, y su parte difícil no era la disposición
Se difirió en F2 sobre una premisa que las referencias desmienten —que su versión honesta pedía un marquee o quedaba en un wrap trivial—: Tailwind Plus, Untitled y Flowbite lo shippean estático. Una decisión cuya premisa cae se corrige. Pero el argumento para construirlo no es la paridad. Lo difícil de un muro de logos es la tinta: veinte marcas en veinte paletas gritan sobre la página y entre ellas. Ese tratamiento acaba de entrar en la fundación como contexto, así que el bloque queda fino y es exactamente la forma correcta — cuando lo difícil es un tratamiento, el tratamiento va a la fundación y el bloque sólo lo estampa. Aquí no hay una línea de CSS, que es además lo que el contrato exige. Dos decisiones medidas, no elegidas. Las marcas van en un wrap y no en una rejilla, porque llegan con proporciones dispares y unas pistas iguales dejan huecos irregulares alrededor de las estrechas. Y la parte del ítem se gana su sitio defaulteando el eje de bloque, que es lo único que un logo nunca trae consigo: un vectorial carga su viewBox, un raster sus píxeles, y uno al lado del otro aterrizan a cinco alturas distintas. Verificado con ocho marcas de anchos y colores deliberadamente dispares: las ocho a una sola altura conservando sesenta y cuatro píxeles de diferencia de ancho, que es cada una guardando su proporción. El marquee se queda como hueco con disparador y con su forma decidida: si algún día entra será un preset de movimiento sobre la fila, no un bloque nuevo, y con la preferencia de movimiento reducido parándolo. Y el hueco de la utilidad para nombres sólo audibles sale aquí por tercera vez en la ola. Tres consumidores reales lo piden ya. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ad26922d29 |
uix(foundation): data-ink, el contexto de tinta para marcas ajenas
El hermano de data-on, para un problema distinto: uno re-entinta un subárbol porque cambió su lienzo, este porque el contenido son marcas de otros y se pelean entre sí. Un muro de logos de clientes es el caso que todas las referencias shippean y ninguna resuelve a nivel de sistema: veinte marcas en veinte paletas gritan sobre la página, así que los catálogos retocan cada asset a mano — y por eso venden el modo oscuro como un segundo artefacto, porque un asset retocado no puede seguir a un tema. Va a la fundación y no a un componente, y eso se midió antes de elegir. Image no tiene eje de tinta, ni prop ni regla, y tampoco era su casa: es un componente con máquina de estados de carga para fotos, mientras que un logo suele ser un svg en línea. Y el mecanismo es doble — un vectorial toma la tinta por currentColor y un raster por filtro —, así que un solo eje tiene que cubrir los dos. Un primitivo Logo nuevo se descartó por lo mismo: no habría cargado más que el contexto. Sin tokens nuevos: el reposo y el realce son los que el tier semántico de la escala de opacidad ya tenía. Y el opt-out por marca vale anidado, porque hay marcas registradas que no se pueden alterar y el bloque no puede saber cuáles. Dos medias querys que importan: la transición se anula con la preferencia de movimiento reducido, y bajo colores forzados se retira el desaturado entero, porque ahí la paleta es del sistema y desaturar pelearía contra el contraste que ese modo existe para garantizar. Verificado sobre ocho marcas de anchos y colores dispares: en mono, gris al sesenta y cinco por ciento con la tinta del tema, y otra distinta en oscuro; en brand, el color propio de cada una; al pasar el puntero, color entero. Queda anotada en el generador la trampa que me costó una vuelta: renderBlock une declaraciones con un salto de línea y no añade punto y coma, así que cada entrada trae el suyo salvo la última. Escritas sin ellos, las reglas llegan a la hoja de estilo y no pintan nada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0cee0fa403 |
blocks(tramo B): las quince matrices, y el guard que ya las exige
El entregable central de la ola queda cerrado. Cada block declara ahora, en su propio README, qué composición nuestra alcanza cada variante que shippean las referencias, con la receta exacta, dónde se ha visto y en qué estado queda. Y la regla que gobierna la tabla no admite promesas: una receta que no se ha visto en el navegador no entra. El saldo es lo que la ola buscaba desde que se abrió la pregunta. Unas noventa y cinco filas cubiertas con receta y prueba. Cuatro superaciones con nombre, que ninguna referencia puede expresar porque shippean markup estático: el plan actual que no se puede elegir, las dos secciones que explican por qué un envío está bloqueado, y las cifras que cuentan respetando la preferencia de movimiento. Una veintena de gaps, cada uno con su nombre: la mitad son recetas que existen y no se han enseñado, que es trabajo de demo, y la otra mitad candidatos de canon que ya conocíamos. Y ocho «no se ofrece» con su motivo escrito, que es una postura del sistema y no un agujero — un carrusel en la apertura es movimiento automático sin control de pausa, un mapa es una dependencia externa, filtrar preguntas es estado de datos del app. La primera tabla justificó el tramo entero. El hero declaraba desde julio que el mockup de móvil estaba hecho, y la demo sólo había enseñado el cromo de navegador. El primitivo ofrece tres. Se añadió el control, se midió, y sólo entonces se escribió la fila: una receta declarada no es una receta demostrada, y separarlas es exactamente para lo que este tramo existía. El guard exige ya la sección, encendido al final como mandaba el plan —hacerlo antes habría dejado quince readmes en rojo toda la ola— y con su fixture negativo. Cambiar la regla hizo caer tres fixtures antiguos, que es el self-test haciendo su trabajo, y se actualizaron. Probado en rojo sobre el árbol real: quitándole la sección a team sale su línea exacta, y restaurada vuelve a verde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e7677d6d87 |
blocks(pricing): la tabla de comparación, y las palabras que una marca no dice
La brecha recurrente del dossier en pricing, y la última fila del primer tramo de la ola. Es una PARTE y no un hermano porque tiene que obedecer al periodo de la sección, y sólo una parte puede leer ese contexto: B-10 le prohíbe a un hermano importar el block que lo posee. Vivir dentro de la sección es además lo que permite que la app meta el precio en una celda y cambie con el conmutador, sin que esta página cablee nada. La app construye la tabla y el block la compone. B-5 admite datos justo donde el canon compuesto ya es data-driven, y la tabla del canon lo es: exige la instancia que devuelve createTable. Así que la parte no inventa un formato de matriz propio ni traduce nada de ida y vuelta, que es el acoplamiento que esa regla existe para evitar. Las tarjetas y la tabla conviven, como en las referencias, alimentadas por el mismo array de planes. Dos cosas que el plan había esbozado y que construirlas demostró equivocadas, y quedan escritas en vez de calladas. No hay un Record de ejes visuales por plan: acentuar una columna es componer canon, que es justo lo que ya permite el snippet de cabecera, y ese Record habría sido una segunda fuente de lo que la app declara en su plan. Y el block no fija la columna: el motor es del app, y meter mano ahí sería el block decidiendo cómo se comporta la tabla de otro. Lo que sí posee es lo que la app no puede: el nombre accesible de la tabla y las palabras que una marca de visto o de guion no dice en voz alta. Ese glifo lleva todo el significado de la celda y es silencio para un lector de pantalla, así que va oculto y la frase viaja en su etiqueta, desde un mapa exhaustivo con respaldo en inglés y su test en las dos direcciones. Y absorbe el cast del canon siendo genérico: la tabla tipa su ranura sin parámetro mientras el motor sí lo devuelve, así que todo consumidor castea, empezando por la demo del propio componente. Aquí se castea una vez a la entrada y se deshace a la salida, para que el snippet de celda devuelva la fila con la forma del app. Medido a 1280 y 375: ocho filas por cuatro columnas, tabla nombrada, marcas que se anuncian, y en móvil la tabla desplaza con la columna de características quieta y la última columna alcanzable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
93d18c90df |
uix(table): una tabla ancha vuelve a tener por dónde salir, y expone los tipos que exige
Dos cosas que salieron al componer la tabla de comparación de pricing, y que no son del block. La primera se ve y se mide: a 375 la tabla ocupaba 420 dentro de un contenedor de 327, y esos 93 píxeles eran inalcanzables. Sin barra, sin indicio, columnas desaparecidas. Fijar la primera columna —que funciona— no sirve de nada si no hay nada que desplazar. El envoltorio recortaba en los dos ejes: el recorte existe por el radio de las esquinas y el eje de bloque lo necesita, pero el eje en línea no, y ahí se comía contenido. Ahora el eje en línea desplaza y el de bloque sigue recortando, así que el radio se conserva y nada scrollea en vertical salvo que se pida con maxHeight, que es otra cosa. Medido después: cuatrocientos veinte sobre trescientos veinticinco, desplazable; al mover noventa y cinco la columna fijada se queda donde estaba y la última columna entra en pantalla. La demo del propio componente no cambia. La segunda es de tipos: el componente exige una instancia del motor y no la exportaba, así que cualquiera fuera de libs tenía que cruzar una frontera para tipar su propia envoltura. El guard del tier blocks lo rechazó por nombre y tenía razón: lo que se corrige es que el componente exponga lo que demanda, no que la frontera se ensanche. El guard afirma la forma con su porqué escrito y está probado por mutación: devolviendo el recorte de siempre, falla. Que sea guard y no nota importa porque el fallo era silencioso — nada peta, el contenido desaparece por el borde. Quedan registradas dos filas abiertas que no toco: la columna fijada se ancla con una propiedad física, así que en dirección derecha-izquierda se mueve con el contenido en vez de quedarse —medido, su borde se sale de la pantalla—, y eso es del eje de dirección; y el prop de la tabla no lleva parámetro de tipo, así que todo consumidor tipado castea, empezando por la demo de este mismo componente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b36247944b |
docs(blocks): el plan de ejecución de V1, con la fase 0 corregida
La ficha de Pricing.Compare reservaba dos decisiones y ahora las tiene, resueltas por doctrina y no por gusto: los planes los declara el createTable del app y la parte lo compone —B-5 admite datos sólo donde el canon compuesto ya es data-driven, y Table lo es—, y en móvil la tabla scrollea con la columna de características fijada, porque el canon ya da el pinning. Eso último es una corrección de mi propia fase 0: el README de Table lo lista como gap diferido y es falso a fecha de hoy — el engine, soma y el recipe lo implementan los tres. Me apoyé en el README y di un dato equivocado; el primer paso del plan lo corrige donde vive. Seis pasos con su verificación cada uno, y los riesgos del árbol compartido escritos para el agente que lo ejecute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
70f45033fd |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera
El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA,
no un envoltorio — se renderiza como HIJO de la superficie que viste y se
ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de
foundation, `:where(:has(> [data-background]))`, así que ninguna receta se
parchea y ningún consumidor reestructura su layout. Con la pila entran sus
capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines,
noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el
`on` que reata la tinta del contenido de arriba.
Dos cosas medidas en Chrome real que el plan no preveía:
- La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css`
declara `position: var(--box-position, revert-layer)` en `[data-box]`, que
es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer`
devuelve la propiedad al valor del UA. Una Section o una columna de Grid
computaban `static` y la pila se escapaba al ancestro posicionado más
cercano. La regla escribe ahora también `--box-position: relative`: se
resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que
un `position` por prop —que llega inline— sigue ganando y `Dialog.Content`
conserva su `fixed`. 5/5 anfitriones cubiertos.
- El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README
del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`:
`box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el
shorthand borraba aquel de los tres cuya var estuviera sin poner.
`<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand
con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el
shorthand desaparece de la receta; medido en una fila de 600px:
`flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando.
Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que
un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en
vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el
scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que
la capa lleve su propio `color`.
El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y
gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta
F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es
de F5 por D-BG.13.
Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 ·
recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 ·
504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) ·
`check` 0 errores propios.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f4ce006423 |
blocks(faq): la lista estática, y un tipo que deja de prometer lo que no da
La brecha del dossier para este block, y la mayoría del formato: seis de siete en Tailwind Plus no son acordeón, sino una rejilla de preguntas y respuestas siempre abiertas. El eje vive en la LISTA, no en la raíz. Primero porque la lista es lo único que cambia —la cabecera se lee igual—, y segundo porque esa colocación es la que permite que el tipo diga la verdad: pasa a ser una unión discriminada. Bajo acordeón atraviesa toda la superficie del Accordion del canon; bajo lista no se ofrece ninguna de esas props, porque una rejilla estática no tiene estado abierto que enlazar, nada que colapsar ni modo simple o múltiple. Ofrecer una superficie y descartarla en silencio es justo lo que A-94 dejó dicho que no se hace, y aquí es donde iba a morder después. El ítem lee la disposición del contexto: no puede discriminar sobre la unión de la lista sin obligar a la app a repetir el arreglo en cada pregunta. Su value y su disabled quedan documentados como del acordeón, la misma convención que el fondo del hero y la media del cta. Y bajo lista la pregunta es un encabezado de verdad, porque ahí ES el encabezado de su respuesta; bajo acordeón el canon la mete dentro del disparador y esa semántica es suya, así que el block no inventa una segunda. Medido a 1280: en acordeón, cinco disparadores, ningún encabezado, medida de setecientos sesenta y ocho y respuestas ocultas hasta pulsar; en lista, ningún disparador, cinco encabezados, tres pistas de trescientos nueve, medida de mil veinticuatro y todas las respuestas visibles sin tocar nada. A 375 cae a una columna conservando ambas cosas. El acordeón queda idéntico. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
50a7f47147 |
uix(background): el contrato del fondo existe antes que su primer píxel
F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
f3bfcf2dd9 |
blocks(testimonials): una cita sola no va en una tarjeta
La brecha que el dossier le apuntaba a este block: la cita única es la variante modal de todas las referencias y el grid la minoría, dos de ocho en Tailwind Plus. Entra como disposición, no como hermano. La anatomía no cambia —cita, autor, avatar—, sólo cómo se colocan, y el precedente es el fondo del hero, que también redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide rotación o carrusel, que es otro componente. Viaja por contexto de configuración, el patrón del align de feature-grid y team, no la coordinación de pricing. Y viaja porque el spotlight no es una piel sobre las mismas cajas: la rejilla deja de serlo y de estampar el ritmo estructural —que es para una FILA, no para una cita—, el ítem pierde la tarjeta, la cita sube de tamaño y se centra, y el autor pasa debajo, donde ya no hay una hilera de caras que alinear. Pedirle a la app que repita esa decisión en cinco partes es el agujero que A-84 cerró en feature-grid. Enmarcar una cita sola la hace parecer un ítem de una lista que no está ahí, así que ahí no hay tarjeta y el tipo lo dice, en vez de aceptar una superficie que luego descarta. La cita se centra con un párrafo de verdad: sobre una caja en línea el centrado no aplica, y sería mentira. Slot nuevo para la marca de quien firma, en las dos disposiciones: es lo que hace que una cita se lea como evidencia y no como una opinión, y es la pieza que ponen todas las referencias. Medido: en grid, cinco tarjetas, veinte píxeles, medida de mil veinticuatro y ritmo escalonado; en spotlight, cero tarjetas, una cita de veintiocho centrada en párrafo, medida de setecientos sesenta y ocho, autor centrado y sin escalonado; a 375 la cita cae a veinticuatro. El grid queda idéntico a antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b92dc3bdb2 |
uix(card): el estado deshabilitado deja de pelearse con la animación por la opacidad
Salió midiendo el plan actual de pricing: la tarjeta no se atenuaba. La regla estaba escrita, el token resolvía a 0.4 sobre la propia tarjeta y el cursor de esa misma regla sí aplicaba — pero la opacidad computada seguía en uno. La culpable es su propia animación de entrada. Corre con relleno en los dos extremos y su último fotograma es opacidad uno; una animación gana a una declaración normal en la cascada, así que fijaba la propiedad para siempre. Aislado en el mismo nodo desactivando la entrada, atenúa correctamente. Llevaba ahí desde que el componente existe, invisible porque el cursor hacía parecer que el estado estaba cableado, y alcanzaba a toda Card deshabilitada del ecosistema. El arreglo es dejar de compartir la propiedad: el estado atenúa por filter, que la animación no posee. Mismo token, misma escala, ninguno nuevo, y el bloqueo del puntero en la tarjeta interactiva no se toca. El guard afirma la FORMA con su porqué escrito —que la regla usa filter y no opacity, más el bloqueo del puntero y la presencia de la animación que obliga a esa forma— y está probado por mutación: devolviendo la regla a opacity, falla; restaurada, verde. Verificado con la animación presente: la deshabilitada computa filter opacity a 0.4 y sus hermanas ninguno; en píxeles, su tinta más oscura sube a 164 frente a 139 de la normal sobre el mismo lienzo, que es justo lo que da un 0.4 sobre el fondo de página. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0646d5ebde |
blocks(pricing): el plan dice dónde estás, y el que no puedes elegir dice por qué
Firma del usuario: faltaba la decoración del elegido, y faltaba poder marcar un plan como no disponible — si estoy cambiando de plan, el que ya tengo no debería poder seleccionarse. Lo segundo cambia la posición doctrinal del block. Su README decía desde julio que nada en una tabla de precios puede bloquearse y que por tanto no hay nada que explicar. Eso vale mientras la sección es sólo una comparación; se cae en cuanto el visitante ya está en un plan. Y ahí manda lo que costó contact: una acción bloqueada debe su frase, o es un agujero — algo que no se puede hacer y nada en pantalla diciendo por qué. El plan gana un estado: disponible, actual o elegido. Un enum y no tres booleanos, porque tres booleanos admiten lo que no existe, como elegido y no disponible a la vez. Y ortogonal a featured, que es la recomendación de quien vende: el plan recomendado puede ser justamente el que ya tienes. Se deriva una sola vez en el plan y viaja por contexto. La acción se lo entrega al botón del app por parámetro de su snippet, así que nadie vuelve a deducir en el punto de uso si se puede pulsar, y las palabras del botón siguen siendo del app. La frase, en cambio, es del block: idlangref con respaldo en inglés dentro de un Record exhaustivo, el mismo mapa que bloquea la acción es el que escribe el motivo, así que un estado que bloquea en silencio no se puede ni expresar. Su test lo afirma en las dos direcciones. Y una parte nueva la dice y posee el id al que apunta el aria-describedby del botón deshabilitado, sin renderizar nada cuando no hay nada que decir. La decoración la pone el canon: el elegido reenvía el anillo de Card, el actual su tratamiento deshabilitado. Nada de un data-pricing-selected propio: sería un atributo que nadie lee, porque un block no pinta CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale con cursor de no permitido, botón deshabilitado y aria-describedby que casa con el id de la frase, traducida por el arnés; al elegir otro, su tarjeta gana el anillo de dos píxeles con el primary de su paleta y las demás no. Queda anotado un gap del canon que salió al medir esto y que no toco: Card disabled nunca atenúa. La regla existe y su cursor sí aplica, pero la animación de montaje fija la opacidad en uno con prioridad de animación y se come la del estado; aislado con no-emerge, atenúa. Es el patrón que la doctrina de motion llama frágil-conocido —dos dueños peleándose por una propiedad en un nodo— y alcanza a toda Card deshabilitada del ecosistema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db026e48f5 |
blocks(pricing): la tarjeta responde al puntero, y el guard deja de leer prosa
El plan reenvía el eje nuevo de Card. No es el `interactive` del canon: eso convertiría la tarjeta en un botón y `.PlanAction` ya tiene uno dentro. Apagado por defecto, porque una fila de planes es una comparación y una tarjeta que responde al puntero sugiere que la tarjeta misma es el control; se enciende donde la tarjeta entera ES la elección. Control vivo en la demo y en la URL de la vista previa. Y al escribir el comentario que explica por qué NO se usa un nativo aquí, el guard del tier se puso rojo sobre mi propia prosa: la regla B-2 leía comentarios como código. Su barrido quitaba sólo los de UNA línea, mientras el que entiende todas las formas y conserva los saltos ya existía tres líneas más abajo, usándose sólo para los imports. Es la lección de la fase cinco aplicada a una regla y no a la otra. Arreglado con dos fixtures nuevos —un nativo nombrado dentro de un comentario multilínea es prosa; un nativo de verdad debajo de ese mismo comentario sigue siendo error— y probado en rojo sobre el árbol real: un button inyectado en el hero sale señalado con su línea exacta, y el árbol queda restaurado después. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d61515027a |
docs(theming): el changelog vuelve a su formato — mi entrada no traía 400 líneas
Al escribir la §49 pasé prettier por el fichero y reformateó los ejemplos de código de entradas ajenas: 237 inserciones y 185 borrados de los que apenas veinte eran míos. Es justo lo que el handoff del tier advierte de los ficheros que ya venían con otro formato — formatearlos produce un diff del documento entero y sepulta el cambio real. El fichero vuelve al formato que tenía y la entrada se re-aplica sin pasar prettier. Efecto neto contra el estado anterior al commit: 43 líneas, todas de la §49. No reescribo el commit porque la rama es compartida. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d38c54fdf8 |
uix(card): el realce al puntero deja de ser rehén de `interactive`
El usuario preguntó por qué una tarjeta de plan de precios no responde al puntero. Le dije que Card no exponía estado de puntero y era falso: el realce existe desde siempre —los dos tokens están en la fundación, con reglas para ghost, outline y solid, composición con el anillo de selección para que no desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo colgaba de un solo attr. Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que ya contiene su propia llamada a la acción no puede tomar ese paquete: un button dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de precio, teasers de artículo, tarjetas de testimonio. La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le debe al visitante. Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho. El eidos-lint lo clasifica como eidos-only, 0 inválidos. Medido en navegador sobre estilos computados y posición real: apagado, el hover no mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad; y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button, con cursor de puntero y los mismos dos píxeles de elevación. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
8ee90367c8 |
docs(process): el handoff dice donde estamos en la ola
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
a514ef929d |
blocks(cta): el panel gana una segunda columna, y las acciones no se sueltan de su frase
Tercera fila de la ola F2b. El README lo tenía diferido con la razón escrita: recurre en las referencias, es un tercer arreglo y el Mockup del canon ya existe para la media. Entra ahora porque la demo lo pide. Dos decisiones que la ficha del plan no traía y que salieron al colocarlo. La copia conserva sus acciones en la MISMA columna: separarlas dejaría el botón debajo de la captura, lejos de la frase que lo pide, y una llamada a la acción que no toca a su argumento pierde la mitad de su fuerza. Y split sin media cae a justified, no a una rejilla con una pista vacía — la misma regla que ya sigue el split del hero, porque un layout con un hueco reservado miente sobre lo que tiene. La media es del app, como en el hero: el block no nombra cromo propio. La demo compone el Mockup del canon con un panel falso dentro. Medido: a 1280, dos pistas de 436px en el panel de 992; a 375 una sola pista con el CTA POR ENCIMA de la media al apilar, que es el orden del argumento; idéntico en claro y oscuro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
fd6c55a13b |
blocks(pricing): un plan solo no se sostenía — la fila necesitaba un tope
Segunda fila de la ola F2b, y la que existía para comprobar una sospecha. El README llevaba desde julio diciendo que el plan único «ya lo cubre el layout», diferido por eso. La ficha exigía medirlo antes de darlo por gratis, y no lo cubría. La causa está en la lista de pistas, no en cuántos planes hay: la fila es repeat(auto-fill, minmax(17rem, 1fr)), y auto-fill RESERVA toda pista que quepa, la ocupe alguien o no. Medido a 1280 sobre 992px de fila, un plan solo daba una tarjeta de 315px pegada al borde inicial con 677px de vacío al lado; fijar columns a 1 solo cambiaba eso por una tarjeta de 992px, que es un cartel. Meterlo en un Container sm tampoco: a 640px vuelven a caber dos pistas. Lo que faltaba era un tope sobre la FILA, y la parte no lo dejaba pasar: sus props están cerradas a propósito desde A-94, porque el envoltorio esparce el resto antes de las props que fija y un width del consumidor tipaba y se descartaba en silencio. Así que se abre nombrando: maxWidth entra en el tipo y viaja explícito al AutoGrid, que es exactamente lo que A-94 dejó dicho — el tipo dice lo que la parte honra. El marginX auto va con él sin condición: sin tope la fila ya llena la medida y auto no resuelve a nada. El número lo pone la app. Un block que horneara uno estaría decidiendo cuánto mide un plan; la demo pasa 28rem para uno y 44rem para dos, que es lo que las referencias le dan a cada arreglo, y gana un control vivo de recuento que también viaja en la URL de la vista previa. Medido después: un plan 448px centrado, dos planes 704px en dos pistas de 340 centradas, y tres planes 992px idénticos a antes — el tope es opt-in y no toca el caso que ya existía. A 375 es inerte, porque allí una sola pista ya toma el ancho. Queda anotada la lección en el README y en el handoff, porque no es de este block: una fila «diferida» puede ser una medición equivocada y no un aplazamiento. Nadie había mirado pricing con menos de tres planes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
bedbf4ee14 |
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos
El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
aa60aba6be |
docs(process): el handoff dice donde estamos y por donde se entra
El documento seguia contando la sesion del 2026-08-10: «fase 7, 7 de 15», «quedan 7 en este orden», el ledger a 51/27/12 y unas puertas «SIN COMMITEAR». Nada de eso era cierto ya, y un handoff rancio es peor que ninguno porque se lee como si fuera el estado. Puesto al dia: - **Fase 7 CERRADA 15/15.** Ninguno de los 15 tiene un defecto de composicion: lo que salio vive una capa mas abajo (canon) o en las FICHAS del ledger. Se conserva escrito COMO se llego ahi, porque me equivoque en el camino — declare «conformes» siete blocks de los que solo habia medido un eje, y se corrigio cuando el pregunto si habia terminado. Un barrido no es la rejilla. - **Ledger a 64 ARREGLADO / 19 CONFIRMADO / 13 REFUTADO** sobre 96 filas. - **§Qué queda reescrita como puerta de entrada**, agrupando las 19 vivas por lo que hace falta para cerrarlas en vez de por severidad: tres bloqueadas por decision suya (A-67 al eje de sema con la puerta ya elegida · A-47, que por doctrina es un VALOR de configuracion y no un cambio de CSS por componente · A-09, cuya disposicion apagaria la deteccion live), una bloqueada por una pieza que no existe (A-95 espera a `app-shell`, F3.1), y quince de trabajo normal — con el aviso de que A-55 no es un defecto visible (1,6% del ancho) y de que A-65/A-75 son el mismo patron. - **Las dos sesiones del 16/17** con sus dos commits (`4caf1100a` el renombrado del eje de tamaño, `d02021aee` los dos arreglos de canon), incluido el residuo del 35,4% de A-68, que es peor que mi estimacion y queda escrito como tal. - **Puertas al parar**, con la advertencia de siempre reforzada: la cifra de `svelte-check` SE MUEVE con otras sesiones vivas en el arbol (72, 74, 75, 76, 77 y 80 en dias distintos), asi que se mide justo antes y justo despues del cambio, nunca contra un numero recordado. Y los 8 rojos vivos se nombran uno a uno como ajenos, con el commit que los introduce. Y tres errores de METODO de estas sesiones, que es lo que un handoff existe para que no se repita: 1. Medir un default en una demo que lo pisa no es medir un default. 2. El panel del navegador oculto suspende el IntersectionObserver, no solo el rAF — una medicion dio «contadores congelados» que era la suspension, no el arreglo. La via fiable es Playwright headless. 3. `Motion` es `once: true`: empujar algo bajo el pliegue DESPUES de cargar no lo hace invisible, su observador ya disparo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d02021aee1 |
fix(eidos): dos animadores que no se hablaban y un cluster que no envolvia
Dos hallazgos del contraste doctrinal del tier, ambos de CANON, ambos medidos antes y despues. El tercero (A-67) queda REGISTRADO y sin ejecutar: cae en el eje de sema. ── A-99 · Group no aplicaba NINGUNO de sus dos defaults ────────────────────── Su README los promete en tres sitios («defaults pensados para action rows — `align: center`, `wrap: wrap`»). No pasaba ninguno a `Flex`, que cae a `nowrap` + `stretch`. De ahi que las acciones de `hero` no apilaran a 375px (A-98): 315 de 327px en una linea, con la segunda etiqueta truncandose. ⚠️ CORRECCION A MI PROPIA FICHA. Habia escrito que «el `align` SI se cumple, lo que descarta que el recipe no cargue». Era FALSO: el `center` que medi salia de la demo, que lo pasa explicitamente en su control. `group.css` no declara ni `align-items` ni `flex-wrap` — no habia nada que cargar. Medir un default en una demo que lo pisa no es medir un default. Y de las tres salidas que la ficha proponia, «que lo declare el recipe» es IMPOSIBLE: `flex.svelte` tiene `wrap = 'nowrap'` como default de JS y siempre escribe `--flex-wrap` inline, asi que el fallback de `var(--flex-wrap, nowrap)` no se evalua nunca. Solo ganaria una declaracion dura, que congela el eje. El arreglo usa el mecanismo que el propio fichero ya tenia para `direction` y `effectiveGap`: `align = 'center'` destructurado (sobreescribible — `align` no esta omitido de `FlexProps`) y un `effectiveWrap` interno, `wrap` salvo `attached`, que lo invierte. Espejo exacto de «`attached` implica `gap=0`», y por la misma razon fisica: el recipe cuadra los radios interiores y tira de un margen negativo asumiendo UNA linea, asi que un segmentado envuelto abriria su segunda linea con una esquina plana y el borde recortado. EL CENSO DECIDIO LA FORMA. 77 de 91 usos de `<Group` ya pasan `align`; de los que no, tres blocks lo reenvian con default propio y **`ButtonGroup` es el unico que voltea `Group` a columna en todo el ecosistema** — donde `center` habria dejado los botones verticales en dientes de sierra. Un solo caso no justifica un default condicional, asi que la excepcion vive en su call site. Medido: `hero` a 375x800 pasa a DOS lineas (tops 416,3 / 472,3) y la accion secundaria de 124px truncados a 156,9px, su etiqueta entera. ButtonGroup horizontal `attached` sigue `nowrap`; el vertical, `stretch` con los tres hijos a 301px. El README se corrige entero: negaba `attached` como prop canonico y listaba `grow` como «gap conocido» cuando ambos llevan tiempo implementados — describia la era del baseline `air`. ── A-68 + A-93 · dos observadores que no se hablaban ───────────────────────── `CountUp` trae su propio IntersectionObserver (threshold 0) y `Motion` usa otro (threshold 0.1, rootMargin −10%). Un IO no mira la opacidad, asi que las cifras contaban su recorrido entero detras de `opacity: 0`: por scroll lento la primera aparecia ya al 97,8% de su valor. ⚠️ EL DILEMA QUE PLANTEE ERA FALSO. Dije que escalonar el arranque y aterrizar juntas eran incompatibles — cierto — y de ahi que exponer el `seen` de `Motion` rompiera A-69. No se sigue: el retardo del stagger vive en `animation-delay` (CSS) y los observadores de los cuatro `Motion` disparan en el MISMO instante, porque los stats estan en una fila, a la misma altura. Gatear en el REVELADO da arranque conjunto, duraciones iguales y aterrizaje conjunto intacto. Presente como decision entre dos males algo que tenia una tercera opcion mejor. Tambien descarto la «alternativa minima» que llegue a recomendar (alinear el observador de `CountUp` con el de `Motion`): es acoplamiento por copia — duplica threshold + rootMargin en otro fichero, la deriva que el framework elimina con builders tipados — y cambia los defaults de `CountUp` para todos sus usos sueltos. `Motion` publica ahora su momento visto por contexto (`motion/context.ts`, calcado de `cascade/context.ts`, que ya exponia `open`), y `stats-band-value` lo pasa a `startWhen` ANTES del spread de `countOptions`, para que el app pueda desactivarlo. Cero cambios en `CountUp`, cero observadores propios en el block, B-6 intacto — la costura era justamente lo que faltaba en canon (A-93). Medido con Playwright headless, 1280x800. ⚠️ La primera corrida fue INVALIDA y se descarta: inserto el espaciador DESPUES de cargar, y como `Motion` es `once: true` su observador ya habia disparado con la banda en pantalla. El espaciador tiene que existir en el primer pintado (`addInitScript`). Segunda corrida, con la banda nacida bajo el pliegue: - fuera de pantalla 2,6 s: pending 4, opacidades [0,0,0,0], contadores 0/0/0 - ventana que discrimina (banda a 6px dentro, donde el IO de CountUp dispara y el de Motion no): 0/0/0 en la llegada Y tras 2,6 s. Antes, ahi mismo, corrian (1144/31/4) y acababan practicamente terminados (12.121/330/47) - al revelarse, la primera cifra al 0% — antes 97,8% - A-69 INTACTA: las tres aterrizan a 1781 ms, dispersion 0 ms RESIDUO, y es peor que mi estimacion: las cifras que el stagger revela despues llegan al 16,8% (+70 ms) y 35,4% (+210 ms). Estime «≤10%» y la medicion dice 35,4% — el muelle es sobreamortiguado y cubre mucho recorrido al principio. Es el precio del aterrizaje conjunto que A-69 firmo y no baja sin reabrirla. Queda documentado, no escondido. ── A-67 · registrada, no ejecutada ────────────────────────────────────────── Contrastadas contra `engine.ts` y el canon, las tres puertas de su ficha fallan: el pack no puede distinguir el `collapse` de un intercambio del de un cierre a secas (mismo evento, mismo tipo de nodo; pediria un `:has()` a mano sobre el hermano); el arbitro ya implementa la mitad del «on a tie, the most recent» que un filtro de LLEGADA permite, y la otra mitad exigiria revocar una nota ya agendada, que `EngineSound` no sabe hacer; y un evento unico rompe los `commits`, porque cada uno estampa `data-state` sobre SU item. La puerta correcta no estaba en la lista: un nuance declarado, `emerge-collapse-swap`. La forma es canonica y el provider lo sabe de forma determinista en `setValue`. Cae en el eje de sema, asi que NO se ejecuta aqui — se deja elegida para su sesion. Puertas: svelte-check 72 errores / 59 avisos = linea base exacta · blocks:check 0 sobre 15 blocks · docs:check 0/0 sobre 629 docs. La suite completa deja 8 rojos en 3 ficheros, todos nombrando waveform / skin-media-player / radio-group mas un timeout de orca — ninguno en lo tocado aqui. Prettier solo se queja de ficheros CRLF preexistentes (el diff seria el fichero entero), asi que no se reformatean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4caf1100a1 |
refactor(blocks)!: el eje de tamano dice de QUIEN es — size deja de significar dos cosas
LA AMBIGUEDAD, medida. En el canon `size` no es ambiguo: es «el eje de tamano de
ESTE componente», y cada uno lo materializa a su manera — padding de bloque en
`Section`, densidad de la tira en `Banner`, escala tipografica en `Text`. La
ambiguedad la creaba el tier: un block no es ninguno de esos componentes, y al
reenviar `size` estaba reenviando el eje de una pieza INTERNA. Desde fuera:
<FeatureGrid size="xl"> cambia el AIRE de la seccion
<SiteBanner size="lg"> cambia la ALTURA de la tira
Misma prop, dos cosas, y una con un valor menos en la escala (`BannerSize` no
tiene `xl`). Un consumidor que aprende el eje en trece blocks se equivoca en el
catorceavo.
EL RENOMBRADO:
- `size` -> `sectionSize` en los 13 que envuelven `Section`
- `container` -> `containerSize` en los 14 que envuelven `Container`
- `size` queda LIBRE y solo lo usa `banner`, donde si es el tamano del block
- `minColumnWidth` -> `minChildWidth` en `site-footer`: el mismo concepto tenia
dos nombres en el tier, y ninguno era el del canon
El patron no se invento: el tier YA nombraba un eje por su pieza (`container`).
`containerSize` + `sectionSize` lo hace explicito y simetrico.
LA REGLA, en `architecture/blocks.md` §Conventions, para que no reaparezca: un
block que reenvia el eje de tamano de una pieza interna lo nombra `{pieza}Size`,
y `size` se reserva para el tamano del block. Corolario: un eje que el canon ya
nombra se reenvia CON SU NOMBRE.
METODO — se renombraron los TIPOS primero y se dejo que `svelte-check` senalara
cada consumidor, en vez de buscar a mano. Cazo los cinco call sites que quedaban
(`CtaSite`, `HeroSite`, `SiteHeaderSite`, `TeamSite` y un uso suelto en
`cta.svelte`). ⚠️ Y cazo tambien un error propio: el primer patron era demasiado
ancho y renombro `size` en SIETE sub-partes donde ese `size` es el del `Card` o
el `Text` que envuelven (`testimonials-item`, `contact-reason`,
`team-member-role`…). Revertidas antes de seguir.
DOCUMENTACION revisada entera: corregidas las tres filas de README que describian
props del block con el nombre viejo (`hero` x2, `site-header`); conservadas las
seis que hablan del `size` de un componente del canon (`Banner`, `Accordion`,
`Heading`), que no cambia. `PLAN-blocks.md` se deja intacto: es bitacora, y
reescribir el registro historico para que cuadre con el presente lo falsearia.
LEDGER — el contraste doctrinal llega a 15/15, y cinco filas nuevas:
- A-95 `banner`: con `affix="top"` el aviso tapa la cabecera pegada (medido:
`elementFromPoint` sobre el header devuelve la tira). NO se arregla aqui: la
pieza que falta es la que POSEE las alturas de pagina — Mantine lo resuelve en
`AppShell`, que declara `header={{ height }}` y desplaza el resto; nuestro
equivalente es el block `app-shell`, sin construir.
- A-96 `banner`: `affixOffset` es prop publica sin control vivo en la demo.
- A-97 `feature-grid`: RETIRADA el mismo dia. Se midio el texto `muted` con el
4,5:1 generico de WCAG y el proyecto tiene OTRA vara — «floors APCA >= 60 ∧
WCAG >= 3», con guard propio, y la tinta secundaria es «marginal sub-4.5:1 by
design». Re-medido con `src/arts/color/apca.ts`: Lc 63,7 y 3,70:1, cumple las
dos. (El primer intento con `apcaLc` devolvio 102666 porque le pase 0..255
donde pide 0..1: un valor fuera de rango no es un hallazgo, es un formato.)
- A-98 `hero` + A-99 `Group`: las acciones no apilan ni envuelven. La causa NO es
del tier — `Group` no aplica el `wrap` que su README promete en TRES sitios:
`group.svelte` nunca se lo pasa a `Flex`, el recipe no lo declara, y
`Omit<FlexProps,'wrap'>` impide compensarlo desde fuera. El `align: center` si
se cumple, lo que descarta que el recipe no cargue.
Y A-25 cae (la demo ya no promete elevacion) y A-47 corrige su disposicion: el
anillo de foco es «a config axis» por doctrina — endurecerlo es una decision de
VALOR en `color.focus.ring`, «never a per-component CSS change», que es
exactamente lo que hacia el intento revertido en `e468e764b`.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 72 errores /
59 avisos = linea base exacta, ninguno en blocks · docs:check 0/0 sobre 629 docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
2447a466b4 |
docs(process): el handoff dice donde quedo publicado
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
65536ae6c8 |
test(eidos): el enum que salio del morfo vuelve, leido de la union de TypeScript
Los 25 knobs visuales salieron del contrato en
|
2 months ago |
|
|
948f7cf7e1 |
feat(blocks): el guard vigila la frontera, y siete filas del ledger caen medidas
FASE 5 DEL SANEAMIENTO — `blocks:check` deja de vigilar solo la forma del tier
y pasa a vigilar su frontera, su documentacion y su catalogo. Tres reglas, cada
una con su fixture negativo en el selfTest():
1. Lista blanca de importaciones (B-4). La frontera dura 1 era prosa; ahora es
codigo: $uix, los arts publicos, $libs/forms, svelte y los relativos del
propio block. Los arts se DERIVAN de src/arts/*, no se listan a mano — una
lista escrita se queda atras el dia que aterriza un art. El tier ya cumplia:
cero violaciones al encenderla.
2. README completo (B-9) + declaracion de landmark (B-8). Salieron 7 de 15 sin
ella; escritas leyendo la fuente de cada block, no la plantilla.
3. Ficha en el catalogo de demos (B-9), en los DOS sentidos: un block que el
catalogo no publica es invisible para el rail, y un slug publicado sin block
detras es un enlace muerto.
De paso el escaner deja de leer los comentarios como codigo: los index.ts
documentan su uso con un `// import { Cta } from '$blocks/cta'`, y una lista
blanca que lee prosa habria empezado a acusar a los ejemplos.
Verificado EN ROJO sobre el arbol real, no solo contra los fixtures: un zod y un
$libs/days metidos en hero/types.ts salen con su linea exacta mientras el mismo
import dentro de un comentario NO salta; el catalogo falla en las dos
direcciones; y al romper isAllowedSpec a proposito el self-test aborta el guard
en vez de dar verde.
ARREGLOS DEL CONTRASTE DOCTRINAL — siete filas, cada una con medicion o gate:
- A-88 team: `height="100%"` en el Stack del Member. El div que pinta Motion es
un bloque pelado, asi que el Stack se quedaba a altura de contenido y el
`margin-top:auto` de los enlaces repartia CERO. Medido: la desviacion entre
filas de iconos pasa de 20px a 0, y el margen reparte 20,297px.
- A-94/A-28 (feature-grid, team, testimonials): los tres declaraban su tipo como
`= AutoGridProps` y ponian {...rest} ANTES de las props que fijan, asi que un
consumidor podia escribir align="start" y no obtener nada. Cerrados con
Pick<AutoGridProps, ...>, que conserva los tipos exactos del canon.
- A-84 feature-grid: el eje `align` llega a las partes por contexto, como en
team, en vez de que el typedoc instruya al app a repetirlo. Medido: con
align=center el align-items computado es center en cabecera Y celdas.
- A-19 faq: `marginX="auto"` en el Header — Box no centra solo. Medido: el Box
de 768px resuelve margin-inline 248px/248px donde antes daba 0.
- A-27 testimonials y A-86 pricing: el typedoc decia `soft` donde el codigo hace
`outline`, y el ejemplo de la puerta de entrada no compilaba.
- A-91 banner (fila nueva): el block hereda el eje intent/color que el canon
declara fuera del sistema abierto en su propio typedoc, sin registrarlo.
LEDGER — dos lecciones de instrumento escritas dentro, porque ambas produjeron
hallazgos falsos publicados:
1. El dev server sirve codigo ANTERIOR a HEAD si reusa cache de Vite. Tres filas
(A-36, A-69, A-85) se dieron por vivas estando arregladas. El delator fue
aritmetico: las duraciones medidas eran EXACTAMENTE las que el comentario del
arreglo cita como estado anterior.
2. Contar nodos WebAudio no es medir sonido: la sintesis crea dos osciladores
por earcon, un sound pack de muestras cambia la aritmetica entera, y crear
nodos no es sonar. El contador responde "hubo actividad" y nada mas.
Y el error de metodo del que ambas son sintoma, tambien registrado: se fue a
re-medir A-67 sin leer su ficha, que ya traia los gains del catalogo, el empate
de applyDominance y la regla de CANON.md incumplida — mejor razonado que la nota
que lo sustituia.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 69 errores /
57 avisos = linea base exacta, ninguno en lo tocado · docs:check 0/0 · prettier
limpio en todo lo del commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b72a564918 |
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix
Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en
`components/affix/affix.css`, sus consumidores la importaban con
`'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que
es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial`
quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es
geometria.
Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los
tres consumidores importan de ahi. Ninguno depende de ningun componente.
LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo
intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que
el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si
puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de
receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas
que ya son temeables.
Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el
propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave
en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa
exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un
tema mueve la escala de espacio y la escalera de z, no un alias por componente.
Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un
componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus
ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo
de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del
`:root` generado.
Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`,
z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles
haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas
exactas con z 150; y su elemento lleva solo `data-affix`, la identidad.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 ·
smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
de0615caf2 |
fix(morfo): affix deja de declarar lo que solo lee una capa, y el gancho pasa a nombre de CAPA
Correccion doctrinal, senalada por el autor: **morfo es la capa declarativa ENTRE
capas**. Un atributo que consume una sola no es contrato.
`affixMorfo` declaraba `data-affix-placement` y `data-affix-stretch`. Los lee
UNA: el CSS. No debian estar ahi — y la doctrina nombra la familia exacta
(`morfo.md` §`undeclaredState`: «the escape hatch for attrs OUTSIDE the contract
— a visual wrapper's `data-size`, a presentation flag like `data-sheet`»). Los
declare porque `morfo-check` los exigia, que es la herramienta dictando la
doctrina: el guard clasifica por PREFIJO DE NOMBRE, la doctrina clasifica por
NATURALEZA, y las dos solo chocaban porque el gancho llevaba el nombre del
componente.
Y no era su nombre. Lo estampan tres —`Affix`, `Fab`, `MenuDial`—, asi que no es
de ninguno: pasa a **`data-viewport-placement`** / `data-viewport-stretch`. La
colision con el guard desaparece por construccion, sin excepcion que escribir.
Es el mismo error que `--fab-offset` leido por la capa: nombrar por un
participante algo que es de todos.
`affixMorfo` se queda con lo que si es contrato: la identidad `data-affix`, que
es como el DOM dice «esta caja es un Affix» a quien pregunte.
TAMBIEN INTENTADO Y REVERTIDO, con su motivo, para que nadie lo reintente:
mover el fichero a `eidos/lib/viewport-placement.css` junto a `list-surface.css`.
`recipe-css-contract` lo tumbo y tenia razon — **el sistema de tokens esta
indexado por componente**: toda clave de `recipes/base.ts` exige su
`components/{c}/{c}.css`. `list-surface` puede vivir en `lib/` porque NO tiene
clave de receta (sus `--list-*` viven dentro de su propio CSS, por `data-size`,
no como defaults temeables en `:root`); los nuestros si lo son. La parte portante
—«esto no es de nadie»— la lleva el nombre del gancho, no la carpeta. Queda
escrito en la excepcion E-2.2 del README y en el PLAN.
Sin cambio de comportamiento: 68 sustituciones de nombre, mismas reglas, mismos
valores. Verificado en las tres rutas.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
morfo:check affix PASS · layer:check 0/3 · contrato de capa 13/13 ·
recipe-css-contract + api-contract 50/50 · rtl 0/178 · smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
a11805d434 |
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> |
2 months ago |
|
|
7ea913ab11 |
refactor(eidos): la rejilla de colocacion logica se canoniza — cinco declaraciones a mano pasan a una
`lib/types.ts` ya tenia `Position`, la rejilla 3x3 FISICA, con su idioma `Extract<Position, …>` y sus consumidores (Dialog, Drawer, Toast). Lo que no tenia nombre era la otra: la LOGICA, escrita a mano cinco veces — `AffixPlacement`, `FabPlacement`, `MenuDialPlacement`, `OnionPlacement` y las cuatro esquinas de `AvatarBadgePosition`—, coincidiendo por mantenimiento manual y no por contrato. Anadir una zona a una no llegaba a las otras cuatro. Ahora hay `LOGICAL_POSITIONS` / `LogicalPosition` junto a `POSITIONS` / `Position`, y las cinco estrechan desde ella. Ambas pasan a ser consts para que `docs:vocabularies` las genere: eran las unicas props visuales compartidas que faltaban en `docs/canon/vocabularies.md`, que ya listaba tamanos, variantes, paletas, arquetipos y el vocabulario sema entero. NO son dos grafias de lo mismo, y por eso ninguna sustituye a la otra: son dos COMPORTAMIENTOS. `top-left` es la izquierda de la pantalla y no espeja nunca; `top-start` sigue la direccion de lectura — medido hoy, de `left: 0` a `right: 0` en RTL. La regla de eleccion queda escrita en los dos typedoc y en el canon generado: ¿tiene que voltearse para un lector de derecha a izquierda? Una tira fijada en `bottom-end` va en el borde de salida en ambas direcciones; un panel que se abre a la derecha fisica porque ahi hay sitio, no. Con eso se cierra EID-3, que el audit de julio dejo asentado como excepcion pero marcado «placement fisico — pendiente de doctrina explicita». La doctrina ya no esta pendiente: son dos rejillas nombradas con la regla de cuando usar cada una. Sin cambio de comportamiento: las uniones resultantes tienen los mismos miembros (`MenuDialPlacement` = la rejilla + `static`; `FabPlacement` = las cuatro esquinas + `static`; `OnionPlacement` y `AffixPlacement` = la rejilla entera; `AvatarBadgePosition` = las cuatro esquinas). check 69 = base intacta · docs:check 0/627 · component:audit los cinco PASS · layer:check 0/3 · rtl 0/178 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
92c8f6466a |
feat(eidos): Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habian copiado dos veces
Un aviso al principio del documento se va con la pagina: `position: sticky` se despega en cuanto termina su contenedor. Medido el 2026-08-10 sobre el block `banner` — `Sticky edge="bottom"` acaba en `top: -8` sin `data-stuck`. No es un cableado roto: es que un elemento anclado al viewport es `fixed`, y eso es otro componente. Pero no es un componente nuevo: la capacidad YA existia dos veces. `fab.css` fija 4 esquinas, `menu-dial.css` nueve, y las dos ya divergian (`--fab-offset` publico vs `--_menu-dial-offset` interno; `var(--fab-z)` vs `var(--z-index-sticky, 1100)`, un fallback de 1100 sobre un token que vale 100). Y antes de eso existio entera: `air/layout/float`, el primitivo de 9 zonas que el refactor retiro sin sustituto — lo dicen los README de `float` y de `Fab`. Asi que `affix.css` no es un tercer ejemplar: es la CAPA, enganchada en `data-affix-placement` y no en `data-affix`. El split es portante — `morfo-check` selecciona `[data-affix]` en toda la pagina y valida cada match contra `affixMorfo`, asi que si `Fab` estampara la identidad quedaria soldado a este contrato. Estampando solo el gancho de capa, no. Es el patron de `lib/list-surface.css`: una fuente, cero nodos extra. Lo que salio midiendo, no razonando: - `--z-index-affix: 150`, peldano nuevo. Con la tira en `top` empataba con `--sticky-z-index` (100 los dos) y el empate lo rompe el ORDEN DEL DOM: el aviso va antes que la cabecera en el fuente, luego perdia. 30,7px de solape con el cromo encima — el fallo original reproducido por su sustituto. Un peldano toca DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`. - NO compone `<Box>`, aunque sea el patron de los primitivos de layout: `box.css` declara `position: var(--box-position, revert-layer)` a la misma especificidad que el gancho, y eidos no usa `@layer`. En el orden de carga equivocado, `position: static` — un `fixed` que no hace nada. - `stretch` es solo de los bordes de bloque. El rail del eje inline se envio y se retiro el mismo dia: `Affix` no dimensiona a su hijo, asi que era una caja invisible de 380px con el hijo de 47px arriba, identica a `top-start` en pantalla. El marco medido decia otra cosa; la pintura mandaba. - `env(safe-area-inset-*)` es fisico y los anclajes logicos: remapeado bajo `:dir(rtl)`. Emparejar `inset-inline-start` con `safe-area-inset-left` despeja la muesca equivocada en RTL apaisado — que es lo que hacen hoy `fab` y `menu-dial`, registrado. `banner` recupera lo que su README daba por imposible: `affix="top" | "bottom"`. La disposicion anterior decia «app-land, el canon ya trae Sticky» y nombraba un componente que no puede hacerlo. Verificado con recorrido real (342px en la demo, 1247px en la preview del block): desplazamiento 0 en los dos bordes, nueve zonas exactas, RTL espejando, el ancestro transformado derivando -342px como esta documentado, y `elementFromPoint` sobre la tira devolviendo el aviso y no el cromo. component:audit PASS 0/0 · morfo:check PASS · rtl 0/178 · smoke 311/311. Queda: migrar `Fab` y `MenuDial` a la capa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f1b672da79 |
docs: la doctrina alcanza a los cambios de la jornada — cinco backlogs y una llave de mas
Auditoria de deriva doc<->codigo sobre los 33 commits del 2026-08-13/14: cinco sitios seguian describiendo lo que el codigo dejo de hacer ayer. Se corrigen COMO BITACORA, al final de cada fichero (el cuerpo es el acta de lo que se firmo, no el estado del codigo; reescribirlo falsearia la firma). El formato es el que ya existia en `arts/adom` y `soma/textarea`: `## Backlog`. - `decisions/book-deviations.md` — dos entradas. D.7 nombraba `IntentExpectedFamily`, tipo que M6 ( |
2 months ago |
|
|
e468e764b2 |
revert(eidos): A-47 — el rescope del anillo de foco se deshace, y queda el registro de los errores
Por orden del autor. Mi arreglo del 2026-08-13 (
|
2 months ago |
|
|
89e42735e6 |
fix(eidos): A-69 — `duration` de count-up dice la verdad, y las cifras aterrizan juntas
`duration` se documentaba como «approximate count duration in seconds» y alimentaba los parametros del muelle con λ FIJA: el asentamiento real crecia con el logaritmo de la magnitud. Medido por el ledger y reproducido: 12.500 tardaba 7,88 s (3,9x lo prometido), 340 → 5,09 s, 48 → 3,57 s, 4,31 s de dispersion — y la razon 2,21 entre cifras era INVARIANTE con duration, asi que ningun valor global las igualaba. `onEnd` disparaba desde un timer ciego a `delay + duration`, 5,9 s antes de que la cifra grande parase. La ejecucion es la disposicion del ledger, tras el re-analisis que pidio el autor — mi primera propuesta sustituia el muelle por un ease-out temporal, que era cambiar el DISEÑO de tapadillo: la caida exponencial (arranque rapido, aterrizaje suave) es la semilla que el componente porta, y mi cita de motion.md §drivers para descartarla estaba fuera de jurisdiccion (gobierna presets de UI sobre propiedades CSS; esto es numero→formatter→textContent). La forma se queda; el RELOJ se recalibra: - λ derivada de (recorrido, umbral, duration): el asentamiento es analitico (|y(t)| ≈ AMP·|d0|·e^(−λt)), asi que λ = ln(AMP·|d0|/umbral)/duration hace aterrizar la ULTIMA cifra mostrable exactamente en `duration`, para cualquier magnitud — y una banda de contadores aterriza JUNTA por construccion. ζ fija en 2√2 (el caracter del default de la semilla). - `onEnd` desde la parada REAL del muelle (el callback de settle), no del timer; el tope de 30 s se queda; recorrido menor que el umbral aterriza instantaneo; reduced-motion intacto (su rama es previa y no se toca). - types.ts y README dejan de mentir: «asienta en ≈ duration sea cual sea la magnitud». MEDIDO con Playwright headless (sonda espejo de probe-A-69b del ledger: muestreo DIRECTO de textContent cada 40 ms — su propia ficha documenta el sesgo del MutationObserver), sobre la demo real de stats-band: ANTES 12.500 → 7867 ms · 340 → 5083 ms · 48 → 3567 ms (4310 de dispersion) DESPUES 12.500 → 1977 ms · 340 → 1977 ms · 48 → 1977 ms (dispersion 0) con los valores finales exactos y su agrupacion de locale. La fila del ledger pasa a ARREGLADO sin commitear el fichero (WIP de la otra sesion, protocolo A-85). NO tocados, declarado: A-68 (arranques vs entrada escalonada — item propio) y el driver `spring()` de $motion (sustrato distinto; la nota queda: si un contador necesita fisica interactiva algun dia, el driver existe). Con esto §3.6 (blocks) queda COMPLETO: A-47 y A-69 cerrados. Verificado: eidos 393 ✓ · check 69 = base aislada, diff VACIO · prettier: types.ts limpio en HEAD queda limpio; count-up.svelte y README ya fallaban. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
acda70f1e4 |
fix(eidos): A-47 — el anillo de foco sostiene el contraste sobre CUALQUIER superficie solida
El anillo global es primario translucido — suave a proposito — y sobre un `Surface variant="solid"` de color primario compone primario-sobre-primario: medido en la demo real del cta, 1,02:1 (peor aun que el 1,43:1 del ledger; misma pareja, panel rgb(142,78,198)). Los DOS unicos elementos interactivos del block quedaban sin indicador, contra el minimo 3:1 de WCAG 1.4.11. Arreglada la CLASE, no la instancia: cualquier hijo enfocable de cualquier superficie solida, en cualquier color y modo. Una declaracion en la regla solida que ya existia en surface.css rescopa `--focus-ring-color` a la misma tinta que el texto de ese lienzo — el slot `contrast(on-solid)`, elegido por APCA con suelo WCAG2 (rfc-color-engine §8). Como §32 canonizo UN solo modelo de foco (outline, todos los consumidores beben del var: button, calendar, breadcrumb, anchor-nav…), la variable rescopada repara el catalogo entero por cascada; forced-colors conserva su camino `Highlight`, ajeno a los tokens. A plena fuerza y sin `color-mix` — aritmetica sobre un token garantizado re-pierde la garantia (la clase «el silencio es un valor»). Medido antes/despues con sonda de composicion (canvas getImageData — el panel pinta en oklch wide-gamut y el ring en color(srgb …/0.52); un parser rgb da null) y sobre el ELEMENTO real: dentro del panel 1,02:1 → 5,18:1 (outlineColor del boton enfocado) fuera del panel 2,17:1 → 2,17:1 (el token global INTACTO) Dos hallazgos anotados sin decidir: `--focus-ring-color-error` sobre solido (volcarlo a la tinta de contraste borraria la semantica de error) y el 2,17:1 del anillo global contra la PAGINA (el indicador puede satisfacer 1.4.11 contra el boton adyacente; abrirlo es decision de diseño). La fila del ledger pasa a ARREGLADO pero el fichero NO viaja en este commit: es WIP de la otra sesion (protocolo A-85). Verificado: eidos 393 ✓ · check 69 = base aislada, diff VACIO · prettier: surface.css estaba limpio en HEAD y queda limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
23b20fff5c |
feat(active-uix,sema)!: S-19(ii) — announce encendido por defecto: un sumidero, materializado por la raiz
¿Debia encenderse? El analisis dijo que era LA RAIZ O NADIE: la opcion
`announce` se pasa al construir el engine dentro de `createActiveUix(options)`,
y en ese momento `uix.announce` no existe todavia — huevo y gallina. El unico
cableado que un app podia escribir (`announce: { dom }`) caia en las regiones
propias del canal: un SEGUNDO par de regiones vivas, exactamente lo que la
doctrina «ONE sink» (AUX-1) prohibe. La unica puerta alcanzable violaba la
doctrina; la raiz es el unico sitio que sostiene las dos puntas.
El impl registra el canal POST-construccion con cierre tardio —
`events.register(new AnnounceChannel({ announce: (m, p) => this.announce(m, p) }))`
— la misma forma para standalone y attach. Default ON, y la asimetria con
sound/haptic (apagados) es doctrina, no inconsistencia: esos son ORNAMENTO
(opt-in); announce es SUSTITUCION (sema.md §channels — «the one that
substitutes»), la categoria que el framework ya enciende solo (las regiones de
uix.announce se crean solas, el camino de soma esta siempre activo). La norma
del campo hace lo mismo: Angular CDK LiveAnnouncer es singleton por defecto,
React Aria usa una region ambiental de modulo. Un framework de referencia no
hace opt-in la accesibilidad.
Opt-outs estandar: `events: { announce: false }` — el engine gana el flag
legible `announceOptedOut` (las opciones de construccion no eran observables) —
y un `announce` explicito del app gana: la raiz jamas pisa un canal existente.
Cero doble anuncio, por diseño ya verificado: el runtime de soma no mete
`message` en la señal perceptual (su a11y viaja por sources.announce), asi que
el canal no-opea para todos los componentes de soma.
Visto en ROJO via stash del impl (canal sin registrar) y en verde con el:
una señal con `message` por `uix.events` aterriza en la region COMPARTIDA
(`uix-announce-assertive`, prioridad derivada del intent threat) y el
documento tiene UN solo [role='alert'] — el par fantasma nunca nace. El
opt-out respetado. `sema.md` §announce reescrito: el ⚠️ de «ninguna raiz lo
cablea» pasa a documentar el default y sus salidas.
Verificado: sema+morfo+eidos+contracts 947 ✓ / 6 ajenos · soma navegador
1278✓/1 (el timeout ajeno — TODOS los tests de navegador arrancan la raiz
tocada) · check 69 = base aislada, diff VACIO · docs 0/624 · prettier:
engine.ts limpio en HEAD queda limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3ee17f333c |
docs(sema): los dos hallazgos abiertos quedan DECIDIDOS y escritos donde viven
El autor resolvio los dos hallazgos que salieron al cerrar §3.4/§3.5, y su
decision se documenta EN EL CODIGO, no en un handoff que nadie relee:
1. La rama de politica de `validateSemaEvent` es inalcanzable — SE QUEDA.
`isSemaEvent` es policy-aware (rechaza required-sin-intent y, desde S-33,
forbidden-con-intent), asi que una violacion sale por el throw generico y
el mensaje especifico nunca dispara. MEDIDO, no deducido:
`validateSemaEvent({family:'commit'})` lanza «is not a valid canonical
semantic event». Se conserva porque los dos guards responden preguntas
distintas —predicado de tipo vs validador de politica— y solo su ORDEN hace
redundante a uno; reordenar para hacerla alcanzable cambiaria el mensaje
lanzado sin que nadie lo pida, y borrarla sacaria el enunciado de la
politica de la funcion que lo posee. Si el predicado deja de imponer
politica, la rama ya esta aqui y correcta.
2. `SemaActionEvent` y sus tres funciones no tienen consumidor en produccion —
SE GANAN EL SITIO. No es falta de consumidor: es AUDIENCIA. Es la
superficie publica que usa una app conduciendo `EngineSemantic` SIN soma
para nombrar una ocurrencia — el mismo publico que sirve el canal announce
(dos emisores, dos publicos, §announce de sema.md). Soma nunca toma ese
camino porque baja la declaracion estructurada del morfo hasta abajo; la
asimetria es el diseño. Y por eso se mantiene ATADA al contrato: S-35
estrecho la clave para que la forma etiqueta no regale lo que la
estructurada rechaza, con los tests de politica vigilandolo.
Verificado ademas, a peticion del autor: el cableado del canal announce en las
raices NO estaba hecho — `define-engine-semantic.ts` no menciona `announce` y
`active-uix` solo tiene su propio sumidero. Lo que S-19 cerro fue el
desajuste de firmas que hacia IMPOSIBLE escribirlo; encenderlo sigue abierto
como decision, y queda anotado en el handoff con esa distincion.
Verificado: sema+morfo+contracts 552 ✓ / 6 ajenos · check 69 = base aislada,
diff VACIO · docs 0/624 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f8dd8ff810 |
fix(morfo,sema)!: M1(ii) gradient-picker — el reset delegado deja de declararse dos veces (S-14)
El morfo declaraba `commit-reset`, el pack le dedicaba una regla (settle + tick) y los dos README lo tableaban — y el provider no lo emitia. Lo unico que suena en el Clear es el `commit-reset` del Picker generico COMPUESTO, con la firma del pack `picker`. La ficha S-14 ofrecia dos salidas y su premisa comparativa era falsa, medido: los hermanos color/date/time-picker SI emiten su propio reset (date-picker cableado el 2026-08-11) — pero NO componen el Picker generico: definen su propio pickerShellHandle y son dueños de su transaccion. Gradient-picker la DELEGA entera (su docblock: «The transaction — open / commit / cancel / clear — lives in the composed generic PickerProvider»), asi que la emision sigue a la propiedad: el reset es del picker. Cablear el gemelo habria dado DOBLE firma por un gesto (dos estampados + dos sonidos casi simultaneos — la clase que file-upload pago), y PickerProvider no ofrece hook para silenciar el suyo. Retirada completa, con la palabra explicita del autor: el evento fuera del morfo (una nota en su lugar dice por que y hacia donde), la regla muerta fuera del pack, las filas fuera de los dos README (el de eidos gana la frase correcta: los eventos propios son los del DOMINIO — presets — y el Clear es del picker), la excepcion fuera del censo y el ultimo id fuera de la deuda D9. Con esto la parte (ii) de M1 esta COMPLETA — las 7 resoluciones de la cola medida el 2026-08-12: tooltip x3 CABLEADOS (receta F4 + silencio visual minimo medido), virtual-list/grid x3 `emission: 'host'` (la decision escrita de sus providers, ahora expresable), gradient-picker RETIRADO (delegacion). INERT_EVENT_DEBT y EMISSION_EXCEPTIONS quedan VACIAS por primera vez, cada una con su lapida narrando las resoluciones. M1 entero cerrado: el contrato dice quien dispara cada evento, y ningun evento declarado miente. Sin cambio de conducta: el Clear sonaba por el picker y sigue sonando igual. Verificado: guards 339 ✓ / 6 ajenos · check 69 = base aislada, diff VACIO · docs 0/624 · prettier: los dos ficheros limpios en HEAD quedan limpios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c73cae0227 |
feat(morfo,soma): M1(ii) virtual-list/grid — el scroll declara su emisor: el host
Los tres `handle-scroll*` de virtual-list/grid estaban en la deuda del D9 como «declarados y nunca sonados». Mal clasificados: la resolucion YA estaba escrita en los dos providers como decision con su porque — el listener de scroll dispara por pixel y la familia handle es ['sound','haptic'] (trinquete `step` + tick), asi que una emision por defecto seria un clic-clic continuo bajo scroll con inercia; «apps that want scroll-driven sema feedback should wire it themselves with their own throttle / debounce policy». Lo que faltaba no era el emisor: era un sitio DECLARABLE para esa decision. Es exactamente el caso de diseño del valor `host` de D.2, estrenado aqui: el contrato declara la superficie, el emisor es la aplicacion anfitriona. - `emission: 'host'` en los tres eventos, con el porque en el docblock. - Los dos comentarios de provider apuntan al flag y corrigen la cita desfasada (`activeChannels=['haptic']` — hoy son dos canales, lo que hace la conclusion MAS cierta, no menos). - La promesa del host es viable, verificado: `provider.runtime` es campo publico — un app puede emitir con su propia politica. - Deuda D9: -3 ids. Queda UNA entrada en INERT_EVENT_DEBT: gradient-picker commit-reset (S-14), la ultima resolucion de la parte (ii). Sin cambio de conducta: cero emisiones nuevas, cero navegador que medir. Verificado: guards 309 ✓ / 6 ajenos · check 69 = base aislada, diff VACIO · prettier: virtual-list.ts limpio en HEAD queda limpio; los otros 3 ya fallaban. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
daa6461d62 |
feat(soma,morfo,eidos): M1(ii) tooltip — los tres eventos existen por fin, y el silencio queda donde estaba firmado
El morfo declaraba emerge-present / emerge-dismiss / emerge-dismiss-escape y
nadie los emitia: el SILENT del pack nunca casaba, la puerta de temas que su
propio docblock documenta no podia funcionar, y el preset present-rise que
declara `event: ['emerge-present',...]` jamas pintaba. La excepcion del censo
lo decia desde agosto: «Today's silence is accidental, not the declared
design».
El re-analisis pedido por el autor cazo lo que la primera propuesta no vio:
la firma present-rise es GLOBAL (base.css:6520), la entrada por data-state es
el workaround de la emision ausente (el comentario del provider lo dice
literal), y la semantica autorizada distingue hover (entrada) de focus
(instant-open, «appears with no entrance — its semantics») — cosa que el
evento no distingue. De ahi la opcion B firmada: el estado conserva la
entrada; la emision añade la superficie que faltaba.
Cableado con la receta F4: `present` pre→post — y SIN su `commits` (fijaba
'delayed-open' y pisaria el 'instant-open' del camino focus; los overlays
binarios conservan el suyo porque su valor es total, desviacion declarada);
dismiss ×2 ganan `targetFallback: [trigger]` (Presence sostiene el content por
la salida; el fallback cubre la carrera). Provider: emision SOLO en
transiciones reales — un open-timer cancelado no presenta nada que retirar, un
re-hover abierto no re-emite — y `handleClose` lleva la causa para distinguir
el Escape.
MEDIDO en navegador, los tres caminos y la fuga exacta:
hover present CONTENT · entrada autorizada corriendo (scale-in+fade-in),
present-rise NO — el preset gana la cascada; miedo al doble
movimiento REFUTADO en el camino delayed
focus present CONTENT con instant-open y present-rise CORRIENDO — la fuga
predicha, confirmada → silencio visual MINIMO en la receta, scoped a
[data-state='instant-open'][data-event-phase='active'];
re-medido: anims [] y la entrada delayed intacta
leave dismiss CONTENT via Presence · salida autorizada (scale-out+
fade-out), dismiss-fade no
Escape dismiss-escape CONTENT · desestampado del hold a ~240ms
Deudas limpiadas en el mismo commit (el guard lo exige): 3 ids de
INERT_EVENT_DEBT + 3 excepciones de EMISSION_EXCEPTIONS. Quedan 4 de la parte
(ii): virtual-list/grid ×3 y gradient-picker commit-reset (S-14).
Verificado: soma navegador 1278✓/1 (el timeout ajeno) · tooltip 4/4 ·
sema+morfo+contracts 552✓/6 ajenos · check 69 = base aislada, diff VACIO ·
prettier: morfo/tooltip.ts limpio en HEAD queda limpio; los 3 avisos ya
fallaban en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1c4303b10f |
feat(morfo,sema,contracts): M1(i) — el contrato dice QUIEN dispara cada evento (D.2)
El morfo no podia decir quien emite un evento declarado: todo se presumia del runtime, y la verdad de las excepciones vivia fuera del contrato — en la lista de deuda del guard D9 y en las excepciones del censo de packs. Las tablas de eventos de los README prometian percepcion que no ocurre (la ficha M1, P1). Ejecuta la decision firmada D.2 (IMPLEMENTATION_CONTRACT): - `emission: 'runtime' | 'host' | 'external' | 'declared-only'` en MorfoEvent + literal en el schema sium. Ausente = 'runtime': las ~250 declaraciones existentes no se tocan y la presuncion sigue siendo la norma. - El guard D9 exime por DECLARACION en vez de por lista: solo los 'runtime' exigen emisor. Y si un evento eximido sigue en INERT_EVENT_DEBT, el guard FALLA con «FIXED — remove it», para que la deuda no sobreviva a su resolucion. - El pack-census gana el chequeo simetrico: una regla cuyo alcance son SOLO eventos 'declared-only' afina una percepcion que jamas estampara — muerta por definicion, sin lista de excepciones. 'host' NO cuenta como muerto: el anfitrion dispara por el runtime y la regla casa normal. Fixture con los dos casos (positivo y negativo con hermano runtime al alcance). - `morfo.md` §Step 5.5 lleva el apendice tecnico en los terminos que D.2 exige: la tabla de los cuatro valores × sus guards, y la nota doctrinal de por que NO es doctrina del libro — marcar un evento inerte como declared-only para callar al guard es ensanchar la deuda con otro nombre. La sonda temporal (flag sobre tooltip.emerge-present → correr → revertir) destapo un agujero real y lo cerro: el flag entre `name` y `semantic` hacia INVISIBLE el evento al regex del guard — ni censado ni exento. El regex tolera ahora la linea opcional, y la sonda termino dando la conducta disenada exacta: exencion + exigencia de limpiar la deuda. NADIE estrena el flag: los 7 de INERT_EVENT_DEBT quedan intactos y son la parte (ii) — siete decisiones perceptuales del autor, una a una (la de tooltip: ¿el componente mas ubicuo merece firma, o silencio declarado?). La instancia original de la ficha (los 5 pickers con close inerte) esta muerta desde la normalizacion de nombres. Verificado: suites 945 ✓ / 6 ajenos · docs 0/624 · check 69 = base aislada, diff VACIO · prettier: mis dos limpios siguen limpios, pack-census formateado (regresion mia), contracts y morfo.md ya fallaban en HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
846d201693 |
fix(eidos): E2+E3 — el modo de sistema pasa por ActiveDom y un re-derive fallido ya no desviste la pagina
E2. `createSystemColorSchemeSource` usaba `globalThis.matchMedia` +
`addEventListener` crudos: la regla de la casa (listeners de window/document
pasan por ActiveDom) y la ventana EQUIVOCADA en iframe/popup — globalThis es la
global, no la del dom. Ahora, con dom inyectado, el media query sale de
`dom.getWindow().matchMedia` y la suscripcion va por `dom.listen` (ciclo de
vida gestionado); sin dom, el camino crudo queda de fallback (serializacion
CSS / SSR). Descartada la opcion preferida de la ficha (tracker en adom,
espejo de prefers-reduced-motion) por la regla de ≥2 consumidores: leidos los
6 boots reales de web/, TODOS pasan su propio modeSource — el camino de
sistema tiene hoy cero consumidores vivos; si algun dia gana un segundo, se
promociona, y queda dicho en el comentario.
E3. `#renderSchemeCss` atrapaba el error del re-derive y devolvia '' — y
`apply()` lee '' como «sin esquema» y BORRA el <style> anterior: un cambio de
modo con semilla que no deriva no solo fallaba sin log, desvestia la pagina
del bloque que ya estaba bien puesto. Ahora `#lastSchemeCss` conserva el
ultimo bloque bueno, el catch avisa por `#uix?.logger.warn('eidos.scheme',…)`
(la superficie que ya usa sema; eidos no tiene logger propio), y retirar el
spec sigue limpiando. `applyColorScheme` no cambia: construye EAGER y una
semilla invalida sigue reventando en la cara del llamador — el silencio era
solo del re-derive.
Dos hallazgos del proceso, anotados: el validador de config es FAIL-CLOSED y
rechaza escalas donantes rotas en la puerta (por eso el rojo de E3 no puede
fabricarse via config: la inyeccion va sobre `buildScheme` mismo, passthrough
real hasta que el flag del test lo revienta — el sitio exacto del throw que la
ficha nombraba); y el stub de matchMedia del test de E2 debe dar un mql POR
QUERY, porque el tracker de reduced-motion del propio dom consulta la misma
ventana.
Ambos tests vistos MORDER el codigo viejo (stash de la implementacion, no del
test): E2 porque el query de color iba a la ventana global (jsdom: sin
matchMedia), E3 porque el bloque desaparecia.
Verificado: eidos 28/28 · sema+morfo+eidos+contracts 943 ✓ / 6 ajenos · check
69 = base aislada, diff VACIO · prettier: active-eidos.svelte.ts estaba limpio
en HEAD y queda limpio; el test ya fallaba en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
58695cd08a |
fix(morfo): M4 — la pareja [true,false] solo se infiere donde es exacta
`compileDataPlan` precalculaba el par [etiqueta-true, etiqueta-false] para todo
stateRef+enum con `falseLabel = values.find(v => v !== trueLabel)`: exacto en un
enum BINARIO (el otro miembro, da igual el orden) y dependiente del ORDEN de
declaracion desde 3 valores.
El diagnostico de la ficha se corrigio dos veces con medida:
1. «Hoy no muerde porque los stateRef booleanos usan enums binarios» — falso de
premisa: hay 12 declaraciones con 3+ (aura x3, checkbox x2, image, meter x2,
progress x2, tooltip x2). Pero tampoco muerden, por OTRA razon: bindean
strings y el runtime solo usa la pareja con `typeof raw === 'boolean'`. En
los 12 la pareja se calculaba y jamas se usaba.
2. El arreglo de la ficha («stateRef+enum exige exactamente 2 valores») habria
PROSCRITO esas 12 declaraciones legitimas — la misma clase de error que M5.
El peligro real es el emparejamiento booleano↔enum-no-binario, hoy inexistente,
y el arreglo lo hace imposible EN SILENCIO: con 3+ valores la pareja no se
calcula, un string pasa intacto (la clase viva), y un booleano LANZA
MorfoInvariantError nombrando componente-attr y la salida declarativa — que no
es un campo nuevo sino `v.mapRef(source, { true, false })`, que ya existia en
el vocabulario. Doctrina S-09: un path malo lanza, no inventa (la alternativa
era estampar `data-state="true"`).
Del re-analisis, dos rectificaciones propias que quedan anotadas: mi primera
propuesta invocaba un «guard de contratos» que NO existe (el mecanismo honesto
es el throw), y mi censo de bindings verifico 2 de 12 — la prueba real es la
suite entera: soma navegador 1278✓/1 (el timeout ajeno) con la pareja ya
retirada de los 12, ninguno lanzo.
Tests nacidos en rojo (2/3; el binario paso porque es la conducta de hoy):
binario exacto con orden invertido · 3+ con string = passthrough · 3+ con
booleano = throw con /mapRef/.
Verificado: compile 44/44 · sema+morfo+contracts 550 ✓ / 6 ajenos · soma
navegador 1278✓/1 · check 69 = base aislada, diff VACIO · prettier: ambos
ficheros ya fallaban en HEAD, no se reformatean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b53c93e422 |
feat(morfo,sema): M5 — el matcher `state` habla el contrato de datos del morfo
El matcher aceptaba pares de strings sueltos con la promesa escrita de que «una
iteracion futura los tipara contra el contrato». La iteracion es esta, y no es
UNA puerta sino DOS — el precedente de dos ejes del framework
(allowedTargets/targetFallback, intentRequirement/intentGuidance):
- `state` se tipa con `DataPairOf<M>`, la union discriminada derivada de
`parts[].data`: attr declarado, y donde hay enum, valor del enum. Un typo en
`data-last-action` deja de compilar — la clase de deriva que semaSelector
existe para matar. La union atraviesa intacta el idioma
`Parameters<typeof semaSelector<M>>[2]` de los 67 packs: cero migraciones.
- `undeclaredState` es la puerta ABIERTA con nombre: los attrs que el morfo no
declara A PROPOSITO (data-size/data-sheet del dialog, eidos-only por
docblock). La regla que la usa dice lo que hace, en vez de colarse por un
string abierto. Un solo slot no podia imponer enum Y quedar abierto — la
rama abierta se traga a la estricta (la clase S-11).
El gate de disyuncion vive en el BUILDER, no en el censo (desviacion declarada
del punto 4 firmado, a mas fuerte): attr declarado por la puerta abierta lanza,
attr no declarado por `state` lanza, valor fuera de enum lanza — y como los
packs son modulo, revienta al IMPORTAR: ninguna suite queda verde encima.
`aria` queda abierto con la razon real escrita: su unico uso en el framework
casa el `role` del dialog, que el morfo deliberadamente no declara
(variant-dependent, lo pone el provider).
Nacidos en rojo: 4 tests runtime (enum, attr no declarado via state, attr
declarado via puerta abierta, puerta abierta funcionando) + probes
`@ts-expect-error`. Migrados los 2 usos de dialog y los 3 tests de escaping que
usaban attrs no declarados. Una arista de implementacion documentada: dentro
del cuerpo generico `DataPairOf<M>` es condicional diferido y TS no deja leer
`.attr` — una lectura estructural local, como ya hace el resto del builder.
⚠️ Nota de proceso: mi primera propuesta fue un guard de censo + corregir el
comentario — el mismo error que S-33 (lint donde el tipo puede hablar), y cai
en el en el mismo dia. El re-analisis contra las decisiones del framework lo
invirtio. La ficha M5 queda anotada como INCOMPLETA, no refutada.
Verificado: selectors 15/15 · sema+morfo+contracts 547 ✓ / 6 ajenos · check
69 = base aislada, diff VACIO · docs 0/624 · prettier: dialog.ts era mio y
queda limpio; selectors.ts/test/index ya fallaban en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
13246a2c23 |
refactor(sema,morfo)!: M6 — los alias deprecados mueren; un nombre por concepto
`IntentExpectedFamily` y `SemaEventLabel` eran alias de compatibilidad de `IntentRequiredFamily` y `SemaEventKey`, y el codigo nuevo seguia importando el nombre viejo — la mitad de los tipos hablaba el vocabulario de antes del renombrado. La regla dura del repo (no backward-compat shims: actualiza los consumidores y borra el camino viejo) decide el destino: 13 usos migrados a los canonicos y los DOS alias borrados del tipo y del barrel. La unica duda doctrinal se resolvio antes de tocar: D.3 (book-deviations:489) nombra `IntentExpectedFamily`, pero DESCRIBE la implementacion con el nombre que existia entonces — registro historico, no prescripcion. El precedente es la casa misma: se ha renombrado vocabulario entero sin reescribir los registros de decisiones. Ademas, y declarado en la exposicion: `semaIntentExpectedFamilySchema` (const local de morfo/schema.ts, mismo vocabulario viejo) pasa a `semaIntentRequiredFamilySchema` — y el guard S-34 de contracts.test.ts, que lo busca por nombre LITERAL, se actualiza en el mismo commit; separarlos habria dejado el guard ciego un commit entero. Los 2 `as never` de compile.ts se retiran: eran vestigio de antes de que existiera `DepSink` (un `() => void` encaja en `(value: string) => void`), y el compilador lo confirma. Y `_resetCompileCache`, tercera pata de la ficha, resulto YA borrado — cero apariciones en src/; se anota para que no vuelva a la cola. Censo previo al borrado: cero consumidores de los alias en src/, web/ y scripts/ fuera de los 13 migrados. Las funciones `is/parse/toSemaEventLabel` y el tipo `ParsedSemaEventLabel` conservan su nombre: son API propia, no el alias, y su renombrado quedo explicitamente FUERA de lo firmado. Verificado: sema+morfo+contracts 541 ✓ / 6 ajenos · check 69 = base aislada, diff VACIO · prettier: types.ts era mio y queda limpio; compile.ts y contracts.test.ts ya fallaban en HEAD y no se reformatean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4e7d0be670 |
fix(sema,docs): S-19 — el puerto de anuncios encaja con su sumidero sin adaptador
`AnnounceFn` pedia la prioridad en una bolsa de opciones y `ActiveUix.announce`
la toma POSICIONAL, asi que el cableado que la doctrina prescribe —una sola
region viva compartida, este canal como uno de sus dos emisores— no se podia
escribir: `{ announce: uix.announce }` no compilaba. Y forzarlo era peor que no
tenerlo: el sumidero recibia un OBJETO donde lee una prioridad,
`liveRegionIds[obj]` es `undefined`, y TODO aterrizaba en la region polite — un
`threat` dejaba de interrumpir, que es justo lo que un aviso critico no puede
hacer.
La firma pasa a posicional, como el sumidero y como la fuente equivalente de
soma. Tres puertos, una sola forma. Cambio de conducta: CERO — el canal es
opt-in y ninguna raiz lo enciende hoy.
Visto en rojo antes de tocar: un test con la forma EXACTA de `uix.announce`
(`(message, priority?, timeout?)`) recibia `{priority:'assertive'}` en el hueco
de la prioridad. Tres aserciones existentes migran de bolsa a posicional — su
contrato cambia, y era el contrato defectuoso.
⚠️ Mi analisis inicial estaba equivocado y el autor lo mando revisar. Habia
concluido que el canal estaba muerto porque el runtime de soma no mete `message`
en la señal. No lo mete, cierto, pero es DISEÑO: son dos emisores para dos
publicos —soma cubre sus componentes, el canal cubre a quien usa
`EngineSemantic` sin soma y escribe su propia señal— y precisamente por eso
nada se anuncia dos veces. La propuesta de fusionarlos habria roto ese diseño;
retirada antes de escribir una linea.
`sema.md` §announce reescrito: fuera el aviso del adaptador (ya no hace falta),
dentro la razon de los dos publicos, y el aviso que SI queda — ninguna raiz
enchufa el canal, y encenderlo sin inyectar el anunciador añadiria un segundo
par de regiones vivas junto al compartido. Eso es decision aparte, sin firmar.
Verificado: announce 6 ✓ · sema+morfo+contracts 541 ✓ / 6 ajenos · check 69 =
base aislada, diff VACIO · docs 0/624 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
455260ca34 |
fix(sema): S-30/S-38 — las cinco reglas hapticas abren la puerta que habian olvidado
Cinco reglas escribian una firma haptica sobre familias cuyo activeChannels es
['sound'] — alertdialog (pulse 0.7/60), sheet movil (tap 0.4/24), editable
shift-enter-mode, stepper shift-step y timeline emerge-reveal. El resolver
instalaba la firma, HapticChannel salia por la puerta de entrada: vibrate() 0
veces desde su nacimiento, con la intencion comentada en los propios packs.
La decision se tomo contrastando con el mundo real, no por limpieza: mi
propuesta inicial era borrar los bloques y el autor la corrigio con las
preguntas correctas — ¿que hacen las plataformas? ¿quien decide? Verificado en
las fuentes: Apple HIG prescribe haptic de warning cuando aparece una alerta
importante y tick de seleccion para cambios de valor discretos; Android pide
moderacion pero con constantes CONFIRM/REJECT. O sea: el alertdialog y el
stepper SON patrones de plataforma, y la intencion escrita en los packs era
diseño, no deriva. El framework ademas ya deja la ultima palabra a quien toca:
la puerta por regla (`channels`), la per-emit, y la haptica entera es opt-in
del producto — ensanchar estas reglas no impone vibracion a nadie.
Arreglo: `channels: ['sound', 'haptic']` en las cinco, cada una con su razon
perceptual escrita. Los 5 waivers de CHANNEL_EXCEPTIONS se retiran: el guard
del censo (punto 2, que estos waivers silenciaban desde 2026-08-06) queda
re-armado y ES el aviso automatico para la proxima vibracion con puerta
cerrada. El aviso del waiver («widening requires completing the firma») estaba
ya pagado por la defensa S-31 de kindToPattern: un {kind:'tick'} parcial
resuelve a valores finitos, nunca vibrate(NaN).
Visto en rojo DOS veces antes de verde: el censo sin waivers listando
exactamente las 5, y el e2e nuevo de resolver.test.ts (resolucion con el pack
real + HapticChannel real: 1 vibracion por regla, patron finito) ejecutado
contra los packs sin ensanchar via stash.
Verificado: censo 67/67 · resolver 30/30 · sema+morfo+contracts 540 ✓ / 6
ajenos · check 69 = base aislada, diff VACIO · prettier limpio · docs 0/624.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
cb1fd54d10 |
docs(process): handoff nuevo para la cola de auditorias, con el protocolo de trabajo delante
El eje perceptual quedo completo hoy (F1-F6), asi que lo que sigue ya no es «su» cola: es la cola de auditorias — sema, fable, blocks, eje B, chronos C5. Este handoff la lleva, y pone PRIMERO lo que cambio hoy de verdad: el protocolo. §0 · las cinco preguntas del autor antes de CADA accion, un item por mensaje, y parar a esperar firma. Sin puerta que permita auto-autorizarse lo mecanico: lo propuse y se corrigio en el acto. Con las dos precondiciones que hoy costaron dos rectificaciones completas — el SPEC del COMPONENTE es doctrina y se busca ANTES de diagnosticar (chronos), y se barre `docs/` antes de proponer retirar nada (S-33 esta en CANON.md y en la D.3 firmada). La regla que las resume: el codigo nunca corrige al canon; se le pone al autor la contradiccion delante. §1 · S-33 FIRMADO y sin ejecutar, con la receta y la evidencia del probe de `tsc`: el defecto es el `Exclude`, no «falta una rama». Derivar los tres cubos positivamente cierra la puerta sola al flipar el const, y la tercera rama es inerte hoy porque se teclea sobre `never` — medido, delta de conducta CERO. §2 los 6 commits de hoy · §3 las tres decisiones abiertas con su evidencia ya medida (S-35 · S-30/S-38 · S-19, esta ultima partida en dos porque (ii) cambia conducta) · §4 la cola en orden · §5 la base de verificacion, con los 6 fallos ajenos de contracts nombrados uno a uno · §6 las trampas medidas hoy, para no re-descubrirlas: la laxitud DELIBERADA del guard D9 (endurecerlo convierte 27 emisiones reales en violaciones), el panel oculto congelando la linea de tiempo, y los ficheros ya sucios en HEAD para prettier. docs:check 0/624. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
a157454913 |
docs(process): la 6a fila de chronos no era un defecto — decision revocada por el SPEC
El item llevaba un mes en la cola como «la 6a fila fantasma» y estaba FIRMADO para arreglarse. Al ir a ejecutarlo aparecio el SPEC del propio componente, que dice lo contrario en TRES sitios coordinados: rejilla de 6 filas constante «estabiliza la altura» (SPEC.md:270), inventario de reuso pidiendo «rejilla de mes 6x7» (:492), y `data-outside-month` como estado declarado de `day-cell` CON su propio token de tema (:311 y :461). Las celdas de fuera de mes son diseno, no relleno. La asimetria con la familia —chronos `true`, los otros cuatro `false`— se leyo como deriva y es al reves: chronos es el unico que escribio su decision, y coincide con el default de sus dos referentes de libreria, verificados en la fuente y no citados de memoria: FullCalendar `fixedWeekCount` viene `true` («the calendar will always be 6 weeks tall») y Toast UI `month.isAlways6Weeks` viene `true`. Google/Apple/Outlook si varian las filas, pero porque son aplicaciones que llenan el viewport; chronos no tiene contrato de altura, asi que la receta propuesta tenia que inventarse la altura con `calc(6 * cell-min-block)` — la altura de 6 filas. Era la misma altura con 5 filas mas gordas, y menos informacion. Medido antes de opinar: hoy son 115 px por fila y 724 px de rejilla en todos los meses; con `fixedWeeks:false`, jul→dic 2026 da 609/724/609/609/724/609, o sea el salto de 115 px que la decision queria evitar. Y crecer las filas no ensenaria ni un evento mas: `maxLanes` es `$state(3)` fijo. Corregidos ademas dos errores de la ficha original: el default vive en DOS sitios (chronos.svelte:20 y engine/state.svelte.ts:64), y chronos.css:278 no es la altura de las filas del mes sino la pista de carriles de chips dentro de una fila. La conducta de Google queda anotada como FUNCIONALIDAD con firma propia (contrato de altura + maxLanes derivado), no como flip de flag. docs:check 0/623. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c00e97b91a |
docs(process): F5 cerrada — los tres sitios de las tres auditorias, medidos DESPUES de la campana
F5 no era «volver a medir cada ola»: es la comprobacion de regresion del eje
entero sobre los tres sitios que fable S1, sema S-17 y blocks A-36/A-65
encontraron por separado — y ahora con la API de emision ya borrada.
site-header 375px contact-activate TRIGGER +8,4ms · emerge-open CONTENT +10,3ms,
press-squeeze corriendo SOBRE el trigger, close en el content
knob handle-drop y commit-set separados por 245,4 ms
dropdown+Button +109 / +112,7 ms, close en content, commit-select en el item
PULSADO («Log out», 3.º de 3)
El numero que cierra S-17 es 245,4: el commit encolado entra justo tras el hold
de 240 del drop — ni aplastado a los 24 ms que S-17 midio, ni 1,6 s tarde como
midio el falso verde de `queue`. Y los tres sitios son la misma geometria: un
elemento con DOS morfos (trigger+button, control para drop+commit).
Anotados los dos artefactos del entorno para que no vuelvan como hallazgos: con
el panel sin componer, rAF esta suspendido (el handle-drag continuo del knob no
estampa nunca) y la linea de tiempo de las animaciones no avanza (el
desestampado cae SIEMPRE en el tope de 1500 ms de la espera de expresion, de ahi
los ~1,7 s de limpieza en las tres trazas). getAnimations() dice que corre sobre
que; para duracion de pintura hace falta Chrome real.
docs:check 0/623.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b8aa333fd1 |
feat(soma,morfo)!: targetOverride BORRADO — el estado ilegal del eje es inexpresable
El endgame que §3.0 firmo al abrir F3: con el censo a 0, la opcion de elemento anonimo sale de TriggerOptions, y con ella assertTargetOverride (existia solo para vigilarla; queda assertAnchor, el guard de la puerta que la sustituye). Las dos resoluciones `opts.targetOverride ?? resolveEmitTarget(...)` (emit y foco a11y) quedan en la resolucion unica, y los Omit<..., 'targetOverride'> de los triggers anclados se vuelven vacuos y desaparecen. El tipo lleva LAPIDA doctrinal: la opcion nacio fallbackTarget, se renombro el 2026-08-10 como precondicion del censo, y compensaba un registro sin identidad de instancia — es lo que dejo a tres auditorias (fable S1, sema S-17, blocks A-36/A-65) encontrar la misma deriva sin poder cerrarla. No volver a añadir una opcion de elemento. Y la tesis se probo sola al ejecutarla: el compilador, ya como censo, cazo 9 usos en TESTS que el grep del scratch nunca escaneo. De ellos, 3 eran redundantes (la parte registraba el mismo elemento), 2 del test de metrics migran al registro + trigger plano, y 4 fijaban la conducta borrada — el describe «targetOverride contract» entero y el test del override preferente, retirados con lapida; el guard A-36 de los overlays (morfo sin redireccion + post) se conserva, des-anidado. El scratch del censo, retirado: el censo es `npm run check`. Docs vivos adjudicados: morfo.md §allowedTargets describe la puerta anclada (EventNameTargeting + assertAnchor) y §repeated-part nombra partInstance donde decia targetOverride; los docblocks de las 3 factorias de vistas de eidos dicen la forma nueva; errors.ts deja de ofrecer la opcion muerta como remedio. Las menciones historicas (A-36, «the retired...») se conservan como historia. Verificado: soma navegador 1278✓/1 (el timeout ajeno preexistente de soma-attr-audit, documentado en la base) · runtime+metrics 53/53 · check 69 = base, 0 propios · docs:check 0/623 · prettier limpio en lo que estaba limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e1cedb81ba |
fix(soma): el dropzone de file-upload deja de robar clicks ajenos — un gesto, una ocurrencia
Lo habia dejado ANOTADO en la ola 6 por no cascadear, y estaba mal clasificado: es la clase que este eje persigue (una ocurrencia por gesto), encontrada por mi propia medicion y en un fichero que ya estaba editando. Y al medirlo de verdad, el mecanismo resulto MAS ANCHO que el diagnostico de la nota. No es solo el Trigger burbujeando: el <input type=file> oculto vive tambien DENTRO del Dropzone, asi que el click() SINTETICO que dispara openPicker vuelve a entrar por el mismo manejador. Medido en navegador: UN click en el dropzone llegaba DOS veces, la segunda con target = INPUT[hidden-input] — solo el guard de reentrada que la plataforma pone en click() lo paraba ahi. O sea que TODO click al dropzone duplicaba, no unicamente el del boton. Arreglo: guard por PROPIEDAD en el onclick del dropzone — `closest` sobre los marcadores de `trigger` y `hidden-input`, construidos con partMarkerAttr y nunca a mano, para que un renombrado de parte rompa aqui en vez de ensanchar el alcance en silencio. Es el idioma que table ya practica. Test visto FALLAR con la anidacion real del DOM (trigger + hidden input dentro del dropzone) y los tres casos: la superficie propia abre, el input oculto no re-abre, el boton no re-abre. Medido despues en navegador: click en el dropzone → UN contact-trigger-picker en DROPZONE; click en el boton → UNO en TRIGGER. file-upload 5/5 · check 69 = base, 0 propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4854df3c47 |
refactor(morfo,soma): ola 6 de F3 — las colecciones, y el censo baja a 11 (solo palabras)
Ocho componentes, cuatro clases, cada sitio con la pregunta A-36 primero:
NO-OP DE SINGLETON, retirados: virtual-list ×2 y virtual-grid (viewportRef lo
escribe la propia registracion del viewport) y css-field signal-warn-invalid
(el elemento ERA runtime.partRef('input'), el destino declarado — se lee para
clearTarget, no para redirigir).
ANCLAJE POR IDENTIDAD en las repetidas: menubar trigger · navigation-menu
trigger ×2 + link · tags-input item · file-upload item. En color-field y
time-field no es adorno: DOS partes registran el kebab `input` (grupo visible
+ input oculto), y fieldNode es el ref de la visible, asi que partInstance la
nombra sin depender de quien monto ultimo.
TERMINALES devueltos al provider declarado: tags-input commit-set-add ×3 y
commit-reset; file-upload commit-set-add, signal-warn-reject y commit-reset.
El dropzone que recibe el drop y el boton que ordena el borrado son la
superficie del GESTO, no el sujeto de la ocurrencia — la fila de la doctrina
del sello. Con ellos caen los parametros `target` muertos y sus llamantes.
allowedTargets NUEVO, y es el eje correcto: file-upload contact-trigger-picker
declara [dropzone] junto a su target `trigger`. Es familia contact — el gesto
se sella donde esta la MANO — y las dos superficies son eleccion del llamante
en operacion normal, no estado de montaje.
Medido en navegador: nav-menu abre y cierra sobre el trigger PULSADO
(«Solutions», el 2º de 2, que es la prueba de la identidad) y commit-select
sobre el link «Pricing»; file-upload sella DROPZONE al pulsar la zona y
TRIGGER al pulsar el boton.
⚠️ Hallazgo anotado, NO tocado (preexistente, verificado byte a byte contra
HEAD): pulsar el Trigger de file-upload emite contact-trigger-picker DOS veces
— vive dentro del Dropzone, cuyo onclick no filtra el click burbujeado. Ficha
en el handoff; el arreglo es un guard de propiedad y no se cascadea aqui.
Verificado: suites 46/46 · censo 29 → 11 (solo palabras) · check 69 = base, 0
propios · docs:check 0/623 · prettier limpio en lo que estaba limpio en HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
aa8a630ed2 |
fix(morfo,soma): ola 5 de F3 — select y combobox dejan de sellar el trigger, y present-rise suelta el boton
El superviviente A-36 mas puro de la campaña: el open YA era post (el content
monta antes del emit) pero el provider seguia forzando targetOverride al
trigger — un vestigio de la era pre LEGALIZADO por el allowedTargets del
2026-08-10, que era el eje equivocado (estado de montaje, no eleccion del
llamante). Con el, la firma de motion present-rise corria SOBRE el boton: la
regresion firmada en §1.2 del handoff de normalizacion, medida hoy.
A/B en navegador (la disposicion firmada mandaba medir primero):
ANTES emerge-open TRIGGER · emerge-close TRIGGER · present-rise en el boton
AHORA emerge-open CONTENT · emerge-close CONTENT · present fuera del boton
commit-select → el ITEM elegido («Pear Soft»), anclado por identidad
Receta (la de F4/ola-1): el open pierde allowedTargets y el override; el close
declara targetFallback ([trigger] en select, [trigger, input] en combobox — un
combobox puede vivir sin boton); la seleccion ancla por partInstance al item
registrado, con el nombre computado de ListSelection (SelectionEvent) por la
puerta tipada.
Verificado: suites 24/24 · censo sin un solo sitio de select/combobox ·
check 69 = base, 0 propios · consola limpia (404s = raiz/favicon del arranque).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
69b17f741c |
refactor(eidos,soma): ola 4 de F3 — las vistas de eidos REGISTRAN sus partes (censo 38 → 29)
El unico grupo que no era retirar-o-anclar: metrics, menu-dial y onion-menu
emitian con elementos locales que ningun registro conocia, asi que
partInstance daba null y el override era la unica via. Ahora registran:
- metrics: `value` se registra (id + ref del registro) y emite su
signal-notify-update ANCLADO el mismo — el contexto gana `live` + `runtime`
y deja de acarrear elementos crudos (notifyUpdate muere).
- menu-dial: `list` registrado en la raiz (open/close resuelven solos) y cada
`action` registrada en su componente — el attachment del part atraviesa la
cadena Fab→Button→<button> compuesta, medido: los mini-FABs llevan ids del
runtime y el commit-select aterriza en LA accion pulsada.
- onion-menu: `surface` registrada sobre surfaceEl (SVG cabalga el ref
HTMLElement con el mismo cast que usaba el asTarget retirado) y cada sector
`item` por attachment CACHEADO por clave — la membresia es el stream del
attachment (F2): el drill poda y re-añade sin crecimiento. asTarget muere.
Medido en navegador los tres: onion open→SURFACE, expand→sector d1 «Create»
(el pulsado), select→hoja d2 «Note»; menu-dial open→LIST + contact→trigger
(dos superficies expresando); metrics update→el Value del bloque live con 4
instancias registradas — no el mas nuevo, que es la prueba de que el anclaje
importa. Consola limpia.
⚠️ Trampa pagada y anotada: importar `state` de $libs/reactive en un .svelte
que usa la runa $state rompe TODOS los $state<T> del fichero (shadowing) —
alias `state as refState`.
Verificado: suites 19/19 + metrics 3/3 · censo 38 → 29 (−9, cae tambien el
FOREIGN) · check 69 = base, 0 propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
14d0dee4f2 |
refactor(soma): ola 3 de F3 — css/number-field pierden el targetOverride publico (censo 44 → 38)
La QUEDA pedia retirar targetOverride de la API publica de commit(), y el
censo de llamantes dio la razon entera: NADIE externo lo pasaba — solo los
propios scrubbers en pointerup/lostpointercapture, redirigiendo el TERMINAL
commit-set (declarado provider: «the terminal is stamped where the value
lives») al elemento del scrubber. Clase A-36: sobra la redireccion. Y no era
gratis: con el sello en el scrubber, las reglas de pack que seleccionan el
provider no casaban jamas un commit de scrub — la clase TextArea de S-12.
Tambien fuera los 4 handle-pick/handle-drag-scrub con e.currentTarget: el
scrubber es singleton y registra, el override re-decia el destino declarado.
commit() queda { clamp?: boolean }; los finales de scrub llaman sin bolsa y
los handlers sueltan el parametro e que ya no leian.
Verificado: suites 17/17 · censo 44 → 38 (−6 exactos) · check 69 = base, 0
propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3fdaa8cbb6 |
refactor(soma): ola 2 de F3 — los grids de fecha anclan por identidad (censo 51 → 44)
Clase B entera (el override elegia la instancia correcta de una parte repetida): la eleccion es legitima y se conserva, expresada por la puerta tipada — partInstance + trigger anclado, que verifica la identidad contra el registro en vez de contrabandear un elemento crudo. - calendar: commit-select/unselect anclados al `day` pulsado (1 sitio, 2 eventos). - range-calendar: 5 sitios → helper privado triggerOnDay (select-start · select-range · reset ×3), `day` registrado en :1206. - month-grid y year-grid: commit-set anclado a la `cell` pulsada. Un elemento no registrado o ausente cae a la resolucion del runtime, como en las olas anteriores. Verificado: suites 22/22 · censo 51 → 44 (−7 exactos) · check 69 = base, 0 propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f6e4f40723 |
refactor(soma): ola 1 de F3 — los gestos handle sueltan targetOverride (censo 64 → 51)
Los cuatro componentes de gesto con «campos de elemento propios», y la pregunta A-36 primero en cada sitio: - path-trace y rotate-align (3+3): tokenEl/trackEl y needleEl/dialEl SON los elementos que sus partes registran (los escribe la registracion) — el override re-decia el destino declarado singleton. No-op puros, retirados. - float-panel (4): la QUEDA preguntaba ¿allowedTargets o retirar? y la respuesta estaba en la fila de la doctrina del sello: el content ES la posicion (grip y terminal, morfo.md §Where the stamp lands). contentRef es el elemento registrado del target declarado y el ?? dragEl/resizeEl era cinturon de la era fallbackTarget, inalcanzable durante el arrastre del propio panel. Retirados. - drag-drop (3): draggable y droppable son partes REPETIDAS con la instancia correcta en la mano — anclados por identidad (partInstance + trigger anclado, el idioma de tag-group). Verificado que source/target son exactamente los refs registrados (opts.ref.current en los dos call sites), asi que la identidad resuelve; un elemento ajeno cae a la resolucion del runtime. Verificado: suites navegador de los 4 → 34/34 · censo 64 → 51 (−13 exactos) · check 69 = base, 0 propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
281840db3e |
refactor(morfo,soma): alert-dialog cierra el eje del nombrado — el default es del CONTRATO, cero cadenas mecanicas
La ultima cadena clase 1 del catalogo, excluida del barrido de los 21 porque otra sesion editaba el fichero. Receta identica (morfo.md Step 4): el morfo declara #?components.alert-dialog.action|Confirm y .cancel|Cancel; fuera la cadena resolvedAriaLabel x2, la fuente del runtime, el prop de opts y el destructure de los dos wrappers — el aria-label del consumidor fluye por restProps y gana por politica de merge. ALERT_DIALOG_LANGS queda huerfano (su langs.ts se suma a la lista declarada, 14 → 15; no se borra). El harness del test se alinea con produccion de paso: translate = ts (un solo traductor para las dos puertas — la trampa del harness de editable, donde la identidad dejaba pasar el ref crudo por la cadena resuelta). Medido: navegador en es → aria-label="Confirmar" / "Cancelar" saliendo del contrato, type="button" intacto · translations:check 159 refs compilados (157+2), 0 errores · alert-dialog 2/2 · check 69 = base, 0 propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
5f024b1733 |
docs(eidos,process): el ancho minimo de bar queda escrito, y el handoff deja de deber tres decisiones
README de eidos: bar tiene ~900px de ancho minimo de diseño (medido
2026-08-04) — valor de la variante, como los tamaños de caratula — con la
disposicion firmada del arreglo real: ORDEN DE DESCARTE (bajo el umbral cae el
slider de volumen y el mute recupera sus 36px) via container query, el primer
caso concreto para reabrir D-AP2.9.
CONTINUE-sound-engine adjudicado en tres puntos: el rojo de audio-player ya
estaba RESUELTO en el codigo (categoria COMPETING_ROOTS, lint.test.ts:70, con
la razon escrita — verificado 18/18); el aria-pressed del caption-button queda
FIRMADO (b) y ejecutado (
|
2 months ago |
|
|
653eb5fa3c |
docs(process): el switch Sound del topbar estaba cableado desde d161cedcc — medido hoy, y los handoffs dejan de decir «no tocado»
El hallazgo del 2026-08-04 quedo rancio dos dias despues: |
2 months ago |
|
|
fa79cc5423 |
fix(sema): con sound 'off', prepare ya no abre el AudioContext — la reduccion alcanza la puerta (S-27/S-41)
El gate de shouldPrime solo miraba signal.channels: con la preferencia en 'off' cada emit seguia creando el contexto y registrando los listeners globales de desbloqueo — 'off' silenciaba la salida pero pagaba igual la adquisicion del recurso, contra la cabecera del propio canal y la promesa del art («an app that never makes a sound pays nothing»). La preferencia se lee POR LLAMADA, asi que volver a encenderla prima en el siguiente gesto sin suscripcion. 'reduce' sigue primando (suena, atenuado). El activeChannels de la familia NO se consulta a proposito: prepare corre antes de resolver la cascada y una regla de pack puede ampliar los canales (el 'step' de los sliders cabalga justo eso) — esa mitad de S-41 queda rechazada con la razon escrita en el comentario. El test del canal nacio en ROJO (el de 'off' existente solo ejercitaba handle, que es como el agujero paso inadvertido) y fija ademas la vuelta: off no prima, flip a full y el mismo gesto siguiente prima una vez. chans+sound 84/84. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4a1a0c3907 |
refactor(sema): los 3 resolvers de gesto se retiran — esta vez desde el eje, y el registro se hace verdad
El rediseno del 2026-08-06 (gestos por REPETICION, sound:'step' por emision)
los dejo sin llamador, y book-deviations D.7 escribio en pasado un borrado que
nunca ocurrio. Hoy ocurre, firmado y desde una sesion del eje que empezo por
su handoff — la condicion que la retirada revertida de
|
2 months ago |
|
|
b7784d34a3 |
docs(process): la ronda de decisiones queda firmada — cinco disposiciones, dos corregidas al re-analizar
D10 SE RETIRA (lo ejecuta el eje de sonido, su handoff PRIMERO), y el re-analisis añade un SEXTO sitio a adjudicar: sema.md §per-emit payload aun describe pitch/gain/contour cuando soma adjunta hoy sound:'step' + curva haptica (dragSignalOverrides del slider y del knob, con el trade-off del 2026-08-06 citado en su comentario). fixedWeeks: alinear a false a secas era la opcion debil — las filas del mes son de altura FIJA (grid-auto-rows, chronos.css:278) y un false pelado cambia la semana fantasma por saltos de altura al navegar. Firmado: crecer las filas (modelo Google), con false incluido; es arreglo de RECETA, no un flip de flag. Chip bloqueado: via (b), y la letra pequeña va INCLUIDA — el morfo de drag-drop YA declara data-disabled (declaracion sin emisor) y el cursor vive en drag-drop.css; pero el chip fusiona tres morfos en un elemento clicable, asi que la regla de archetype item se anula en su receta y aria-disabled NO aterriza en el elemento fusionado. mode/scope: SE RETIRAN, con el barrido de la regla 4 hecho de verdad: 0 hits en docs/, 0 lectores del compilado, 4 ficheros de alcance. SemaCause queda como pregunta hermana (mas muerto aun: ni atraviesa morfo). present-rise: medir primero (lo esperable es la receta F4, pero decide la medida). handle-drag-progress: revisar contra CANON, no renombrar — el matiz es legal y puede ser deliberado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d7b339f774 |
docs(process): cerrar el plan y sacar lo demas a su propia iniciativa
El plan de normalizacion queda CERRADO: su aceptacion se cumplio y esta medida (256/256, guard visto fallar, barrido de 172 morfos). Lo que el barrido destapo NO es este eje. El handoff pasa de listar tareas a listar DUEÑOS: D10 al eje de sonido, D13 a la auditoria de sema y al vocabulario de morfo, D14 a chronos, y las dos decisiones al autor. Nada se ejecuta desde ahi sin abrir su propia iniciativa. Queda escrita la leccion de la jornada: un handoff no es un backlog. El ledger se creo a las 03:55 para una sesion futura y a las 13:59 esta misma sesion lo empezo a consumir como lista de trabajo, hasta acabar borrando una libreria de otro eje (revertida). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4ed53f269e |
docs(process): dos correcciones de registro — mi censo repetia uno que ya existia, y peor
D13 mezclaba tres clases en un saco
------------------------------------
`CONTINUE-sema-audit.md` §3.1 YA habia medido esto — «eventos huerfanos: 14 de
249 en 104 morfos con soma» — y, sobre todo, ya los habia CLASIFICADO. Verificado
componente a componente, su clasificacion es la correcta y la mia era gruesa:
- **contrato muerto** — `tooltip` ×3. El unico defecto de la clase D1/D3/D4, y el
unico accionable. Cero `trigger(` en todo su directorio soma.
- **punto de extension documentado** — `virtual-list` / `virtual-grid`
`handle-scroll*`. Los emite la APP; el README los diseca en prosa con su
doctrina. Lo que falta no es cableado: es que el morfo no tiene forma de
DECLARARLO (la auditoria propone `emission: 'app'`).
- **verbo delegado** — `gradient-picker.commit-reset` (S-14, 1 de 6 identicos).
NO se arregla como D3: `GradientPickerProvider` no tiene `clear()` — su
docblock dice que open/commit/cancel/clear viven en el `PickerProvider`
generico compuesto y que esta clase «solo posee el runtime del morfo». No hay
donde enganchar.
Lo que el guard añade sobre aquel censo no es el hallazgo: es que deja de ser una
medicion de una vez y bloquea la regresion. Eso lo demostro D9 al deshacerlo.
`CONTINUE-perceptual-surface.md` afirmaba algo falso sobre chronos
-------------------------------------------------------------------
Su fila de la migracion de `targetOverride` dice «sus chips tampoco REGISTRAN —
plain trigger fallaria igual». No se sostiene, y lo se porque acabo de medirlo
cableando el acarreo (D9): `partInstance('event-chip', el)` RESUELVE, el sello de
`handle-drag` aterriza en el `event-chip`, y el arrastre del tirador da 6
`handle-resize` sobre `event-resize-handle`. Los chips registran.
La otra mitad de la fila (los botones del editor no son partes) queda, y se le
añade lo que dice la auditoria de sema: la exclusion de escritura de chronos se
levanto el 2026-08-10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
0d10b983e5 |
revert(sema): la libreria de sonido vuelve byte a byte — la retirada no era de esta sesion
Revierte la mitad D10 de `5571f3bf7` por orden del autor. La sesion era normalizar nombres de evento; la retirada de los 3 resolvers de gesto nacio de perseguir un homonimo que cree yo mismo al renombrar `SoundDirection` → `SoundContour` sin mirar el namespace de destino, y se ejecuto sin leer primero el handoff vivo del eje (`CONTINUE-sound-engine.md` — la memoria del proyecto lo marca PRIMERO) ni `PLAN-audio-player-v2.md` (gate firmado), que ni siquiera sabia que existia. La verificacion posterior salio limpia — ninguno de los seis documentos no leidos nombra los resolvers — pero limpia por suerte no es limpia por metodo. Restaurados byte a byte a su estado anterior al borrado (diff contra `5571f3bf7^` = 0 lineas): - `sema/sounds.ts` (160 lineas otra vez: resolvers, `DragSoundParams`, `SoundContour`, `clamp`, `lerp`) · `sounds.test.ts` (los 2 tests de arrastre) · `exports.ts` (los 5 re-exports) - las 4 prosas que se habian reescrito describiendo el borrado: `sema/types.ts`, `sema/components/splitter.ts`, `soma/splitter-provider.svelte.ts`, `arts/motion/types.ts` - `scripts/docs-vocabularies.ts` + `docs/canon/vocabularies.md` regenerado - la nota fechada que se añadio al §4 de `PLAN-sound-engine.md` Lo que NO se revierte, porque no depende de la libreria: D9 (el acarreo de chronos) y el guard de eventos inertes — verificados en verde tras el revert. El ledger deja constancia: D10 pasa a CONFIRMADO (retirada REVERTIDA), con el analisis conservado como evidencia PARA la sesion propia del eje de sonido — `git log -S` (un solo commit en la vida de los resolvers), `book-deviations.md` dandolos por borrados el 2026-08-06, los dos planes cerrado/rechazado — y la regla explicita: sounds.ts no se toca sin una sesion que empiece por su handoff. Verificado: sema 300/300 (con los tests restaurados dentro), guard de eventos inertes en verde, `check` 70 errores (linea base), `docs:check` 0/623. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
5571f3bf7e |
feat(soma,sema): el acarreo de chronos habla, los 3 resolvers se van, y un guard cierra la clase
D10 · no era una decision abierta: era una firmada sin ejecutar
---------------------------------------------------------------
Mi correccion de mi propia propuesta estaba sobre-corregida: lei la linea 95 de
un plan y la 367 de otro, no los documentos.
- `PLAN-sound-engine.md` esta CERRADO (2026-07-30) y su columna «sema conserva»
miente en 4 de 5 celdas: `SOUND_LIBRARY` no existe, `SOUND_TUNINGS` se retiro
el 2026-08-06 (con guard que lo mantiene retirado), el sonido salio de la
cascada, y «que firma para que familia × intent» lo sustituyo la ley del
nombre. Es un inventario del 30 de julio, no un mandato.
- `PLAN-audio-player.md` esta ⛔ GATE RECHAZADO — SUSPENDIDO, y su cabecera dice
«nada de lo de abajo se implementa».
- La fuente que gobierna es `docs/decisions/book-deviations.md`, de DECISIONES,
que el 2026-08-06 escribio en pasado: «un gesto continuo suena por REPETICION …
y los tres resolvers se borraron con el». `git log -S` sobre `sounds.ts`
devuelve UN SOLO COMMIT en toda su vida (mayo): nunca se borraron. El registro
dio por ejecutado lo que no se ejecuto.
Retirados los tres, `DragSoundParams`, el `SoundContour` de sema, sus exports,
los dos tests que solo se probaban a si mismos y los helpers `clamp`/`lerp` que
quedaron huerfanos: `sounds.ts` pasa de 160 a 79 lineas. Con ellos cae el
homonimo `SoundContour` — el nombre queda para el art, sin renombrar nada. El
plan cerrado no se reescribe, pero su §4 lleva ya una nota fechada porque me
engaño a mi.
D9 · el acarreo habla
---------------------
`handle` pregunta «¿estoy manipulando directamente este objeto?», y una familia
que no responde su propia pregunta ha fallado en lo unico para lo que existe
(CANON §2). Chronos es el componente mas manipulable del catalogo y hablaba solo
el suelte: el acarreo entero estaba mudo. El *handle invisible*.
No se autora nada — `handle` ya declara `sounds: { default: 'step' }` y los dos
canales, asi que el RITMO es el sonido. Estrangulado a ~14 Hz como slider y
splitter, y ANCLADO: los destinos son partes repetidas, asi que es aplicacion
directa de la ley de `e5811e422`.
Donde el acarreo NO es de chronos: arrastrar un chip con puntero pasa por el
`DragDrop` compuesto, que declara tres eventos y emite tres, sin acarreo continuo
(CANON regla 6). Dos componentes narrando un gesto seria peor. Asi que
`handle-drag` es el acarreo de TECLADO y `handle-resize` cubre las dos vias.
Medido: el arrastre del tirador con puntero da 6 `handle-resize` en 6 pasos,
todos sobre `event-resize-handle`.
El guard, y dos hallazgos que salieron de medirlo
--------------------------------------------------
`book-deviations.md` tipifica el defecto hermano como «mecanicamente comprobable
y merece guard». Este es la misma forma un piso arriba, y es la clase que produjo
D1, D3, D4 y D9. La prueba NO es `trigger('literal')` — chronos despacha por
variable y eso me dio dos falsos negativos durante la auditoria; se comprueba que
el nombre aparezca como literal donde el provider alcance: su directorio soma mas
las librerias compartidas (sin `$libs/selection` en el ambito, combobox y select
se leen como inertes y no lo son).
D13 — quedan 7 eventos inertes en 4 componentes, enumerados en
`INERT_EVENT_DEBT`, que no son exenciones sino la cola. El peor: `tooltip` 3 de
3, cero `trigger(` en todo su directorio, abriendo y cerrando sin firma alguna.
D14 — midiendo D9 salio un defecto anterior: `restoreChipFocus` reenfoca en un
`queueMicrotask` que corre ANTES de que Svelte reponga el nodo, asi que enfoca la
nada y el foco cae al `<body>`. La segunda flecha ya no llega al manejador y el
Escape tampoco: la interaccion que el propio componente anuncia («Grabbed. Arrow
keys move, Enter drops, Escape cancels») muere al primer paso. No lo toco aqui —
es de foco, no de firma, y merece su propia medicion en navegador real.
Verificado: guard visto fallar deshaciendo D9 (caza los dos eventos); `check` 70
errores en linea base; 2250 tests del uix — los 6 fallos de `contracts.test.ts`
siguen siendo de la otra sesion; `docs:check` 0/623 tras regenerar
`vocabularies.md`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
82d269cd8c |
fix(soma): en `modal`, un cierre sin causa es un descarte — y un descarte no commitea
El contrato que los cinco pickers documentan tiene dos salidas: Save confirma,
Cancel revierte al valor con el que se abrio. Escape no es ninguna de las dos, y
tampoco esta bloqueado — descarta en ambos modos por diseño (APG `combobox`; el
framework bloquea Escape en exactamente un sitio, `alertdialog`).
Medido el 2026-08-12: en `modal` el popover no autocerraba al elegir, pero Escape
cerraba Y SE QUEDABA la edicion (05/20 → 05/10). Un tercer camino de salida que
se comportaba como Save, y que ningun documento describe.
Tercera afirmacion falsa de la fila D5
--------------------------------------
Escribi que «`'dismiss'` no lo produce nadie». Lo produce el POPOVER —
`dismissWith('dismiss')` en su manejador de Escape—, no el picker, y por eso el
picker nunca se enteraba. Grepear solo el directorio del picker lo escondio.
El arreglo: la arista de cierre, no la tecla
---------------------------------------------
Interceptar Escape seria fragil y estaria en la capa equivocada: el `Content` es
el del Popover y su gancho de Escape es prop del consumidor, asi que el contrato
del VALOR acabaria viviendo en eidos.
Se vigila la arista de CIERRE: un cierre que no declara causa es un descarte, y
en `modal` un descarte revierte. Eso cubre ademas a un consumidor que baje `open`
a mano, que tampoco es un Save. En `inline` no revierte nada — alli la seleccion
se aplico segun se hacia y el snapshot solo sirve para `cancel()`.
Vive UNA vez, en `watchPickerDismiss` (`picker-shell-handle.svelte.ts`), y los
cinco lo consumen: eran cinco copias del mismo `watch` sobre `open`, con el mismo
`valueOnOpen`. El provider declara su causa desde `closeWith`, asi que `commit()`
y `cancel()` pasan intactos; los de rango pasan su snapshot compuesto tal y como
su propio `cancel()` lo restaura.
Medido, y dos tropiezos propios
--------------------------------
Navegador, date / time / color-picker en `modal`: se edita, Escape cierra el
popover Y el valor vuelve al de apertura.
⚠️ La primera medicion dijo «Escape BLOQUEADO». Era la sonda leyendo a 400 ms, en
plena animacion de salida; la traza temporal muestra el popover aun presente a
+200 ms y ya cerrado, con el valor revertido, a +600 ms. Si me quedo con la
primera lectura, reporto como exito justo la conducta que la doctrina prohibe.
⚠️ Los tres primeros tests pasaban EN VACIO hasta que les añadi `flushSync`:
`watch` no ve una transicion si las tres escrituras caen en el mismo tick, asi
que no observaba ninguna arista. Los cinco estan vistos fallar desactivando la
reversion.
`check` 70 errores (linea base), 2251 tests del uix — los 6 fallos de
`contracts.test.ts` siguen siendo de la otra sesion. `docs:check` 0/623.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
eda25a4866 |
fix(morfo,soma): las 4 ultimas de D11 — el guard se queda sin excepciones
`DEFAULT_ELEMENT_DEBT` esta vacio: las 28 partes que el censo encontro dicen ya la verdad sobre el elemento que renderizan. Estas cuatro pedian un cambio con consecuencias, y la leccion es que «que lado esta mal» solo se contesta mirando. chronos.event-chip — el morfo tenia razon, faltaba el puente ------------------------------------------------------------ El motivo estaba escrito desde el 2026-08-10: el chip CONTIENE el tirador de redimensionado y el modelo de contenido de un `<button>` no admite un control anidado, asi que es `div` + `role='button'`. Lo que faltaba era el «tabindex propiedad de soma» que ese mismo comentario promete y que nunca existio. El provider emite ahora `tabindex: 0` y un `onkeydown` que convierte Enter/Espacio en un clic — con `preventDefault` sobre el espacio, que si no desplaza la pagina— y el componente pasa a ser el `div` que su contrato declara. tooltip.trigger — probe el morfo, me equivoque, y lo corrigio la captura ------------------------------------------------------------------------ Declaraba `button`, asi que renderice un `<button>`. La captura mostro el cromado nativo alrededor del disparador: un recuadro que antes no estaba. La receta llama a esta parte «a focusable shell» y difiere el anillo de foco «to the wrapped element». Es un ENVOLTORIO: el control real del consumidor va DENTRO, y un `<button>` envolviendo un `<button>` es el mismo anidamiento invalido del chip de chronos. El lado equivocado era el morfo. Declara `div`, y la a11y la dan el `tabindex: 0` del provider y el `aria-describedby` del morfo, que es lo que el patron APG de tooltip pide de verdad. stepper.item — no era declaracion, era patron ---------------------------------------------- Un `div` SIN rol dentro de un `role='tablist'`. El patron real es `list (tablist) > item > trigger (tab)`, y un elemento generico entre un tablist y sus tabs rompe la relacion de posesion. Es `role='presentation'` ahora: el envoltorio sale del arbol, igual que un `<li>` dentro de un `role='menu'`. El idioma ya estaba en 8 componentes del catalogo, no lo invento. color-picker.channel-input — una declaracion que nada podia satisfacer ----------------------------------------------------------------------- Declaraba `input` mientras compone `ColorField.Input`, que es `div` + `role='group'` — el contenedor de segmentos, la misma forma que el `input` de date-field. Las piezas editables son las partes hermanas `channel-segment`. Verificado ---------- Guard en verde CON LA LISTA VACIA. Navegador: el trigger de tooltip vuelve a ser texto plano (sin cromado), enfocable, y abre al foco; consola limpia en tooltip y chronos. 2246 tests del uix (los 6 fallos de `contracts.test.ts` siguen siendo de la otra sesion), `check` 70 errores en linea base — una pasada intermedia dejo 71 por un `SomaKeyboardEvent` sin importar en chronos. `docs:check` 0/623. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ff86971a07 |
fix(morfo,soma,eidos): una flecha, una forma — y `defaultElement` deja de mentir en 24 partes
D12 · las dos flechas sueltas
-----------------------------
`internal/arrow.svelte` dibuja poligono MAS un contorno trazado; context-menu y
link-preview rendian un `<svg>` propio con solo el poligono. Dos de las siete
flechas del sistema no tenian contorno — medido: link-preview no tenia `<path>`
ninguno mientras tooltip si.
Ahora componen la primitiva, asi que las siete son el mismo `<span>` con el mismo
`<svg>` y las siete declaran `span`. Las recetas pasan a direccionar las formas
por dentro (`polygon { fill }`, `path { stroke }` con `non-scaling-stroke`):
un `fill` sobre el envoltorio no puede pisar los atributos de presentacion que
poligono y path llevan encima.
⚠️ El arreglo destapo otro defecto. Al componer la primitiva la flecha
DESAPARECIO: `<span>` de 0x0, sin `<svg>` dentro. Los wrappers de eidos de estos
dos pasaban `{@render children?.()}` INCONDICIONALMENTE, y la primitiva lee
cualquier snippet de children como «el consumidor trae su propio glifo» y se
salta el suyo. popover y tooltip no pasan children en absoluto, por eso nunca les
paso. Los dos son condicionales ahora. Es la regla «eidos wrappers need
conditional children», reaprendida midiendo en vez de leyendo.
Medido despues: los dos dan `<span>` 10x5 con `<svg>` 10x5, poligono relleno y
`<path>` con trazo, identicos a tooltip (el control). Consola limpia.
D11 · 24 de 28
--------------
Eran 28, no 24 — mi cifra anterior estaba mal.
Dieciseis son el morfo yendo detras de un componente que ya renderiza lo mas
correcto (`header`, `p`, `span`, `button`): edicion de declaracion, el DOM no se
mueve.
Ocho parecian del grupo contrario Y NO LO ERAN. `tree-view.item` declara `li` y
rinde `div`, si — pero tambien declara `role='treeitem'`, y lo mismo
`file-upload` con `list`/`listitem`, `feed` con `article`, `stepper.list` con
`tablist`, `tree-view.branch-content` con `group`. La a11y NO esta rota: el rol
explicito carga la semantica que daria el elemento nativo. La declaracion era
aspiracional. Cambiar el DOM a listas nativas es una mejora deliberada aparte,
no algo que colar en un pase de verdad-del-contrato.
Las 4 que quedan piden un cambio con consecuencias, y cada una lo lleva escrito
en `DEFAULT_ELEMENT_DEBT`:
- `chronos.event-chip` — aqui el morfo es el que tiene razon, con el motivo
escrito desde el 2026-08-10: el chip CONTIENE el tirador de redimensionado y
el modelo de contenido de un `<button>` no admite un control anidado. El
componente rinde `<button>` igual. Pero el «tabindex propiedad de soma» que ese
mismo comentario promete NO EXISTE en la parte, asi que cambiar el elemento hoy
canjearia HTML invalido por un control sin foco.
- `tooltip.trigger` — `button` declarado, `div` con `tabindex: 0` y sin rol
rendido. Un boton nativo se trae `type=submit` dentro de formularios.
- `stepper.item` — `div` sin rol dentro de un `role='tablist'`. Problema de
patron, no de declaracion.
- `color-picker.channel-input` — compone `ColorFieldInput`, no renderiza un
elemento.
Verificado: guard de `defaultElement` en verde con las 4 de deuda; 2246 tests del
uix (los 6 fallos de `contracts.test.ts` siguen siendo de la otra sesion);
`check` 70 errores, linea base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
7caf231b0c |
feat(soma,morfo): Home y End en la familia de segmentos, con una sola accion declarada
APG `spinbutton` dice que Home salta a `aria-valuemin` y End a `aria-valuemax`.
Tres auditorias lo pidieron por separado —date-field F-1, time-field F-2,
color-field F-2— y las tres coinciden. El min/max ya existia por segmento; solo
faltaba la tecla.
Tercera afirmacion falsa de la fila D6: color-field YA LO TENIA
---------------------------------------------------------------
Su F-2 estaba desfasada. `ColorFieldChannelProvider.handleHomeEnd` existe desde
que se construyo. Asi que el pase no invento conducta: copio la referencia que ya
vivia en el arbol a date-field y time-field, incluidos sus `dayPeriod` — donde
`aria-valuemin` es AM y `aria-valuemax` es PM, luego Home/End son ABSOLUTOS
donde las flechas alternan (pulsar Home dos veces no vuelve a voltear).
Es un JUMP, no un ciclo: no envuelve y no lee el valor previo, asi que aterriza
tambien sobre un segmento vacio.
Una sola accion para los tres
-----------------------------
F-1 proponia `set-min`/`set-max`; color-field declaraba `first-item`/`last-item`.
El tipo obliga a elegir — «Semantic action identifier. Keep consistent across
components for the same intent» — y el catalogo tiene DOS vocabularios: el de
LISTA (`first-item`, 25 usos, y `menu-dial` lo consume por el mapa de acciones
del runtime) y el de VALOR (`set-min`, que usa `knob`). Un segmento es un control
de valor y no tiene items. Los tres declaran ahora `set-min`/`set-max`, y
color-field se alineo: sin riesgo, porque los segmentos despachan por tecla, no
por el mapa del runtime.
Medido
------
Teclado real en el navegador, y cada valor cae exactamente en el aria que su
segmento publica: date-field mes End→12 / Home→01 (min/max 1/12), time-field
hora End→23 / Home→00 (0/23), color-field hex End→ffffff / Home→000000
(0/16777215). Home dos veces no envuelve.
Seis tests nuevos y LOS SEIS VISTOS FALLAR contra la conducta vieja: cerrando el
gate compartido de `isAcceptableSegmentKey` y retirando cada rama por separado.
Uno de ellos es para color-field, que era la referencia del pase y no tenia
ninguno — el arbol copiaba de algo que nadie vigilaba.
⚠️ Un tropiezo propio: la primera version del test numerico llamaba a
`handleHomeEnd` directamente, asi que habria seguido verde si alguien borraba el
enrutado del `keydown`. Reescrito para entrar por `onkeydown`, que es lo que
prueba puerta + enrutado + conducta.
`check` 70 errores (linea base; una pasada intermedia dejo 73 por un acceso a
`dayPeriod` sobre el tipo union — estrechado con `DateAndTimeSegmentObj`, el
mismo que usa el provider). 217/217 en los tres campos + morfo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |