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 }
349 Commits (6a53c3dc991e53baac17045f72859b42de1e457c)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |