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 }
1950 Commits (06f3c264006da1ebe0e2d187e6a1c13f01f3e577)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
06f3c26400 |
docs(background): el handoff del eje, con lo que no pude medir escrito arriba
`CONTINUE-background.md`. F0 → F5 cerradas y commiteadas: el componente existe, tiene demo, está documentado y el tier lo usa. El handoff indexa; la fuente viva sigue siendo `PLAN-background.md` (D-BG.1…18 y la ejecución medida fase por fase). Lo que queda no bloquea el uso: cuatro firmas del autor (la escala `strength` del scrim, que no está ordenada por peso; la foto clara sin ningún peso que llegue a AA; borrar `backdrop/`; y si la banda de `feature-split` va por sección o por fila), una verificación que este entorno no puede dar —el travel real, 60 fps y el detector de reflow necesitan un Chrome visible, y está comprobado que la limitación es del entorno y no del código— y dos tareas que este eje abrió en otros: `Ambient` honrando la pausa, y un puerto de estado para `<video>` que ya tiene dos consumidores. Lleva también las cinco lecciones que costaron medición, para no volver a pagarlas: A30 no es estilo sino supervivencia; `revert-layer` sin `@layer` es `revert`; la demo destapa lo que una sonda no; mide antes de afirmar rendimiento; y la decoración que parece mejor puede ser la ilegible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
331912cf23 |
blocks(background): seis fondos, y el que parecía mejor era el que no se podía leer
F5 de PLAN-background.md: los seis blocks de D-BG.10 (2) reciben `Background.*`. El hero cayó en F2 y la página compuesta ya existía — no había que construirla, había que verificarla con la decoración puesta. La forma es uniforme y tiene un solo desvío: cada block gana un prop `decor` que monta `<Background.Pattern>` como HIJA de su `Section`, la misma composición que el hero ya probó. El desvío es `cta`, donde la decoración va dentro del PANEL, que es donde está el ojo. **Dos hallazgos que sólo la medición podía dar.** Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular bajo AA: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a **2,27:1**. Pintan un lavado claro anclado en `50% 0%` — exactamente donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen, y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1 contra el panel. Por eso el default es `rings`, que además irradia del mismo anclaje superior que usaba el glow. Las tintadas se ofrecen igual: el app decide, pero ya con el número delante, en el tipo y en el README. Y una trama tintada dentro del panel era invisible por construcción: `--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que pinta el panel — un halo del color del panel sobre el color del panel. El block pasa ahora `color="var(--color-content-on-solid)"`, el token que ya usa para su propia copia; medido después, el tinte resuelve a `#ffffff`. **Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band` `grid`, porque una banda de cifras se lee como medida y la rejilla es la trama que lo dice sin decorar por decorar · `testimonials` y `feature-split` `glow`. `site-footer` y `banner` reciben la capacidad APAGADA: la columna B del plan de calidad no pide acabado en ninguno de los dos, y encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió. **Un desvío de alcance, declarado.** La columna B pedía «`Backdrop` alterno» POR FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría a cada `Row` a poseer el estado de alternancia, y eso es coordinación — un block que coordina deja de ser un block que no posee nada. Medido en Chrome sobre la página compuesta real: cinco pilas, los cinco anfitriones adoptados (`position: relative` + `isolation: isolate`) sin que ningún block los posicione; la copia mide 15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y 12,37:1 con `grid`/`dots`. Ledger: seis filas nuevas, A-100…A-105, cada una con su mecanismo y su evidencia. Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · 434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
740274a847 |
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no
F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos
auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los
ejes que F3 añade, y separarlas dejaría un estado que nunca se probó.
Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token
de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más
allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la
deriva contra el puntero), `spotlight` y `attach='fixed'`.
El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de
scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca
`ScrollProgress` y escribe `--background-progress`, que la rama `@supports not`
mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez
porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se
paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll
del propio lector, que es justo la clase que provoca síntomas vestibulares.
**La decisión que no estaba prevista.** El scroll y el puntero quieren mover la
MISMA capa, y una animación sobre `translate` gana a cualquier declaración
estática: el puntero habría dejado de existir sin más. Así que el scroll anima
una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único
`translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el
puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el
shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`.
El precio, dicho porque en la primera redacción escribí lo contrario tres veces:
una custom property NO se puede compositar, así que el navegador recalcula
estilo cada frame. Para una decoración es el intercambio correcto —una property
por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era
falso y ahora el código dice lo que ocurre.
**Dos footguns cerrados por forma, no por disciplina:**
- `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes
había que escribirlo también en la pila, y olvidarlo dejaba la capa
`position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin
error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae
de vuelta). La prop de la pila desaparece: no hay nada que olvidar.
- Un `speed` negativo —una capa que se mueve contra el scroll— invertía el
bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora
usa la magnitud.
**Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una
línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand
la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener
de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando
el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado
e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones
—`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio,
`declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde
el init, y seguir el prop sin escribir en la primera pasada.
**La demo** (`/uix/components/background`, v2, nueve pestañas) monta un
ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí
solo y sin padre no se puede enseñar lo único que importa: que el padre se
adopta y el layout no se mueve. Los chips son uniones completas verificadas por
el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación.
Y fue la demo la que destapó que, con A30, encender `animate` en caliente no
hacía aparecer el control de pausa — el registro era un hecho de montaje.
Invisible en una sonda, obvio con un interruptor.
Registro documental (D-BG.11): `next-features.md` §11 · la frase en
`design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) ·
`PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de
autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y
`Aura` corregida (decía «Not built yet» y está construido) · y en
`motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un
preset nombra una transición discreta CON duración, y esto es modulación
continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor.
Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los
valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y
retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el
`translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada
control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres
de las cuatro clases de capa hasta la segunda auditoría).
⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60
fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y
ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado
que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con
`animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada
por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome
visible.
Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 ·
`rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 ·
`morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de
siempre) · `check` 0 errores propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
dc7c4920d6 |
blocks(cookie-consent): la ley europea como tipo, y las dos salidas al mismo peso
Cierra el tramo C de F2b. El único block cuya fase 0 fue jurídica antes que visual: las referencias shippean el aviso como markup, y el markup no puede impedir las tres cosas que lo vuelven ilegal — un rechazo debilitado, categorías premarcadas, y cerrar tratado como consentir. Aquí son el tipo y el constructor. `essential: true` hace inescribible una UI que ofrezca apagarlo; `initialConsent()` no admite semilla, así que una primera pregunta no puede llegar marcada; `onDecision` es obligatoria, así que un aviso que no informa de vuelta no se puede montar. Las cinco reglas están numeradas en consent.ts y afirmadas por 8 tests, porque son justo lo que un refactor rompe en silencio mientras todo sigue pintando bien. Desviación de la ficha, y a favor: UNA llamada requerida `onDecision(consent, via)` en vez de `onAcceptAll` + `onRejectAll`. Con dos callbacks el consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo renderiza el block y el API no expone knob para degradarlo. Medido: las dos acciones tienen idéntico relleno, tinta, borde, tamaño, peso y alto — 5.96:1 en claro, 8.79:1 en oscuro. Tres cosas se arreglaron por mirar, no por deducir. La barra era un `Card` outline y midió 1.00:1 contra la página: se disolvía en ella, así que va al plano `overlay` del sistema, el mismo que estampa cada superficie flotante del canon. A 375 las tres acciones envolvían con «rechazar» en una fila y «aceptar» en la siguiente, leyéndose como dos botones ajenos justo donde la pareja más importa: ahora son un `Group`, y el markup lo dice. Y el panel devolvía el foco a un nodo muerto — se devuelve aquí, un frame después de que la trampa se desarme, con un pestillo para que el asentamiento del montaje no le robe el foco a nadie al cargar. Tres gaps de canon salieron al componerlo, todos en el README: `Button` acepta `ref` y no lo reenvía (forma de A-94), `Switch` no tiene parte de etiqueta, y el `Dialog` restaura el foco a un nodo muerto cuando su disparador se desmontó. El instrumento mintió antes que el código, otra vez: el pane del navegador no compone frames, así que la animación de salida no termina y un diálogo cerrado sigue pintado — el foco medido allí era ficción. Y `fillStyle` no normaliza oklch, así que todo color leído del estilo computado se volvió negro y dio 1.11:1 sobre texto perfectamente legible. Las cifras de esta ficha se miden por píxel, en Chrome real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
313e4d52fa |
blocks(article-grid): la banda editorial, y el elemento que el canon no podía dar
Lo que las referencias llaman blog. Sus dos preguntas de fase cero quedan resueltas, y ninguna necesitó tocar el canon. El nombre. Blog nombra una sección entera de un sitio, y esto es la fila de artículos recientes dentro de otra página; el tier nombra por función de página y el elemento que renderiza es un article, así que article-grid hace coincidir nombre, función y markup. La palabra blog sobrevive donde un consumidor la busca: la primera línea del readme y la etiqueta del catálogo. El artículo semántico. Un teaser tiene que ser un article o se lee como una caja de texto suelto, pero la tarjeta del canon es un div fijo y eso parecía obligar a elegir entre añadirle un eje de elemento o declarar un hueco. Construirlo enseñó la tercera vía, que no cuesta nada: el bloque renderiza el article y compone la tarjeta dentro. El tier lleva desde su primer bloque renderizando sus propios elementos de seccionado, así que el elemento es del bloque y la superficie sigue siendo del canon. La regla amuralla al primitivo, no a la composición. Y tres decisiones más que sí son de diseño. La tarjeta entera no es un enlace, porque el teaser lleva además un chip y una firma y anidar interactivos dentro de un enlace es marcado inválido: es justo donde acaban las referencias que hacen clicable toda la tarjeta, así que el enlace vive en el título y hay exactamente uno por teaser. El realce al puntero viene encendido, al revés que en precios, porque un teaser sí es algo a lo que se va. Y el bloque no formatea fechas: las compone la app con el componente del canon. Dos errores míos en la tanda, los dos cazados mirando la captura y no la sonda. El ancho mínimo por defecto sólo cabía dos veces en la medida, cuando las referencias lideran con tres columnas; medido y corregido, ahora da tres, dos y una según el ancho. Y el segundo intento de arreglarlo no llegó a aplicarse sin que me diera cuenta, porque lancé el reemplazo sin aserción y el formateador había juntado las props en una línea: culpé a la caché del servidor antes de mirar el fichero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
33031c2ad9 |
uix(background): el media entra en la pila, y el fondo aprende a pararse
F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b7474e499d |
blocks(logo-cloud): la banda de marcas vuelve, y su parte difícil no era la disposición
Se difirió en F2 sobre una premisa que las referencias desmienten —que su versión honesta pedía un marquee o quedaba en un wrap trivial—: Tailwind Plus, Untitled y Flowbite lo shippean estático. Una decisión cuya premisa cae se corrige. Pero el argumento para construirlo no es la paridad. Lo difícil de un muro de logos es la tinta: veinte marcas en veinte paletas gritan sobre la página y entre ellas. Ese tratamiento acaba de entrar en la fundación como contexto, así que el bloque queda fino y es exactamente la forma correcta — cuando lo difícil es un tratamiento, el tratamiento va a la fundación y el bloque sólo lo estampa. Aquí no hay una línea de CSS, que es además lo que el contrato exige. Dos decisiones medidas, no elegidas. Las marcas van en un wrap y no en una rejilla, porque llegan con proporciones dispares y unas pistas iguales dejan huecos irregulares alrededor de las estrechas. Y la parte del ítem se gana su sitio defaulteando el eje de bloque, que es lo único que un logo nunca trae consigo: un vectorial carga su viewBox, un raster sus píxeles, y uno al lado del otro aterrizan a cinco alturas distintas. Verificado con ocho marcas de anchos y colores deliberadamente dispares: las ocho a una sola altura conservando sesenta y cuatro píxeles de diferencia de ancho, que es cada una guardando su proporción. El marquee se queda como hueco con disparador y con su forma decidida: si algún día entra será un preset de movimiento sobre la fila, no un bloque nuevo, y con la preferencia de movimiento reducido parándolo. Y el hueco de la utilidad para nombres sólo audibles sale aquí por tercera vez en la ola. Tres consumidores reales lo piden ya. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ad26922d29 |
uix(foundation): data-ink, el contexto de tinta para marcas ajenas
El hermano de data-on, para un problema distinto: uno re-entinta un subárbol porque cambió su lienzo, este porque el contenido son marcas de otros y se pelean entre sí. Un muro de logos de clientes es el caso que todas las referencias shippean y ninguna resuelve a nivel de sistema: veinte marcas en veinte paletas gritan sobre la página, así que los catálogos retocan cada asset a mano — y por eso venden el modo oscuro como un segundo artefacto, porque un asset retocado no puede seguir a un tema. Va a la fundación y no a un componente, y eso se midió antes de elegir. Image no tiene eje de tinta, ni prop ni regla, y tampoco era su casa: es un componente con máquina de estados de carga para fotos, mientras que un logo suele ser un svg en línea. Y el mecanismo es doble — un vectorial toma la tinta por currentColor y un raster por filtro —, así que un solo eje tiene que cubrir los dos. Un primitivo Logo nuevo se descartó por lo mismo: no habría cargado más que el contexto. Sin tokens nuevos: el reposo y el realce son los que el tier semántico de la escala de opacidad ya tenía. Y el opt-out por marca vale anidado, porque hay marcas registradas que no se pueden alterar y el bloque no puede saber cuáles. Dos medias querys que importan: la transición se anula con la preferencia de movimiento reducido, y bajo colores forzados se retira el desaturado entero, porque ahí la paleta es del sistema y desaturar pelearía contra el contraste que ese modo existe para garantizar. Verificado sobre ocho marcas de anchos y colores dispares: en mono, gris al sesenta y cinco por ciento con la tinta del tema, y otra distinta en oscuro; en brand, el color propio de cada una; al pasar el puntero, color entero. Queda anotada en el generador la trampa que me costó una vuelta: renderBlock une declaraciones con un salto de línea y no añade punto y coma, así que cada entrada trae el suyo salvo la última. Escritas sin ellos, las reglas llegan a la hoja de estilo y no pintan nada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0cee0fa403 |
blocks(tramo B): las quince matrices, y el guard que ya las exige
El entregable central de la ola queda cerrado. Cada block declara ahora, en su propio README, qué composición nuestra alcanza cada variante que shippean las referencias, con la receta exacta, dónde se ha visto y en qué estado queda. Y la regla que gobierna la tabla no admite promesas: una receta que no se ha visto en el navegador no entra. El saldo es lo que la ola buscaba desde que se abrió la pregunta. Unas noventa y cinco filas cubiertas con receta y prueba. Cuatro superaciones con nombre, que ninguna referencia puede expresar porque shippean markup estático: el plan actual que no se puede elegir, las dos secciones que explican por qué un envío está bloqueado, y las cifras que cuentan respetando la preferencia de movimiento. Una veintena de gaps, cada uno con su nombre: la mitad son recetas que existen y no se han enseñado, que es trabajo de demo, y la otra mitad candidatos de canon que ya conocíamos. Y ocho «no se ofrece» con su motivo escrito, que es una postura del sistema y no un agujero — un carrusel en la apertura es movimiento automático sin control de pausa, un mapa es una dependencia externa, filtrar preguntas es estado de datos del app. La primera tabla justificó el tramo entero. El hero declaraba desde julio que el mockup de móvil estaba hecho, y la demo sólo había enseñado el cromo de navegador. El primitivo ofrece tres. Se añadió el control, se midió, y sólo entonces se escribió la fila: una receta declarada no es una receta demostrada, y separarlas es exactamente para lo que este tramo existía. El guard exige ya la sección, encendido al final como mandaba el plan —hacerlo antes habría dejado quince readmes en rojo toda la ola— y con su fixture negativo. Cambiar la regla hizo caer tres fixtures antiguos, que es el self-test haciendo su trabajo, y se actualizaron. Probado en rojo sobre el árbol real: quitándole la sección a team sale su línea exacta, y restaurada vuelve a verde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
00489be263 |
blocks(tramo B): las cinco primeras matrices de equivalencia
El entregable central de la ola, y el que responde a la pregunta que la abrió: no competir en número de dumps, sino demostrar variante a variante qué composición nuestra logra cada una de las suyas — y sacar a la luz, con nombre, lo que no alcanzamos. Cada block gana una sección de equivalencias con cinco columnas: la variante de la referencia, quién la shippea, la receta exacta, dónde se ha visto, y su estado. La regla que gobierna la tabla es que una receta que no se ha visto en el navegador no entra: la columna de demostración es la prueba, no la promesa. Y la primera tabla ya justificó el tramo. El hero declaraba desde julio, en sus gaps, que el mockup de móvil estaba HECHO — y la demo sólo había enseñado nunca el cromo de navegador. El primitivo ofrece tres, así que se añadió el control, se midió el cromo de teléfono a trescientos veinte y sólo entonces se escribió la fila. Una receta declarada no es una receta demostrada, que es exactamente lo que este tramo existe para separar. Van cinco de quince: hero, pricing, cta, testimonials y faq, que son los que esta sesión ha trabajado y cuyas recetas están medidas. Entre las filas quedan nueve gaps con nombre —desde la elevación estática que Card no tiene hasta el carrusel de citas— y dos «no se ofrece» con su motivo: un carrusel en la apertura es movimiento automático sin control de pausa, y filtrar preguntas es estado de datos del app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e7677d6d87 |
blocks(pricing): la tabla de comparación, y las palabras que una marca no dice
La brecha recurrente del dossier en pricing, y la última fila del primer tramo de la ola. Es una PARTE y no un hermano porque tiene que obedecer al periodo de la sección, y sólo una parte puede leer ese contexto: B-10 le prohíbe a un hermano importar el block que lo posee. Vivir dentro de la sección es además lo que permite que la app meta el precio en una celda y cambie con el conmutador, sin que esta página cablee nada. La app construye la tabla y el block la compone. B-5 admite datos justo donde el canon compuesto ya es data-driven, y la tabla del canon lo es: exige la instancia que devuelve createTable. Así que la parte no inventa un formato de matriz propio ni traduce nada de ida y vuelta, que es el acoplamiento que esa regla existe para evitar. Las tarjetas y la tabla conviven, como en las referencias, alimentadas por el mismo array de planes. Dos cosas que el plan había esbozado y que construirlas demostró equivocadas, y quedan escritas en vez de calladas. No hay un Record de ejes visuales por plan: acentuar una columna es componer canon, que es justo lo que ya permite el snippet de cabecera, y ese Record habría sido una segunda fuente de lo que la app declara en su plan. Y el block no fija la columna: el motor es del app, y meter mano ahí sería el block decidiendo cómo se comporta la tabla de otro. Lo que sí posee es lo que la app no puede: el nombre accesible de la tabla y las palabras que una marca de visto o de guion no dice en voz alta. Ese glifo lleva todo el significado de la celda y es silencio para un lector de pantalla, así que va oculto y la frase viaja en su etiqueta, desde un mapa exhaustivo con respaldo en inglés y su test en las dos direcciones. Y absorbe el cast del canon siendo genérico: la tabla tipa su ranura sin parámetro mientras el motor sí lo devuelve, así que todo consumidor castea, empezando por la demo del propio componente. Aquí se castea una vez a la entrada y se deshace a la salida, para que el snippet de celda devuelva la fila con la forma del app. Medido a 1280 y 375: ocho filas por cuatro columnas, tabla nombrada, marcas que se anuncian, y en móvil la tabla desplaza con la columna de características quieta y la última columna alcanzable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
93d18c90df |
uix(table): una tabla ancha vuelve a tener por dónde salir, y expone los tipos que exige
Dos cosas que salieron al componer la tabla de comparación de pricing, y que no son del block. La primera se ve y se mide: a 375 la tabla ocupaba 420 dentro de un contenedor de 327, y esos 93 píxeles eran inalcanzables. Sin barra, sin indicio, columnas desaparecidas. Fijar la primera columna —que funciona— no sirve de nada si no hay nada que desplazar. El envoltorio recortaba en los dos ejes: el recorte existe por el radio de las esquinas y el eje de bloque lo necesita, pero el eje en línea no, y ahí se comía contenido. Ahora el eje en línea desplaza y el de bloque sigue recortando, así que el radio se conserva y nada scrollea en vertical salvo que se pida con maxHeight, que es otra cosa. Medido después: cuatrocientos veinte sobre trescientos veinticinco, desplazable; al mover noventa y cinco la columna fijada se queda donde estaba y la última columna entra en pantalla. La demo del propio componente no cambia. La segunda es de tipos: el componente exige una instancia del motor y no la exportaba, así que cualquiera fuera de libs tenía que cruzar una frontera para tipar su propia envoltura. El guard del tier blocks lo rechazó por nombre y tenía razón: lo que se corrige es que el componente exponga lo que demanda, no que la frontera se ensanche. El guard afirma la forma con su porqué escrito y está probado por mutación: devolviendo el recorte de siempre, falla. Que sea guard y no nota importa porque el fallo era silencioso — nada peta, el contenido desaparece por el borde. Quedan registradas dos filas abiertas que no toco: la columna fijada se ancla con una propiedad física, así que en dirección derecha-izquierda se mueve con el contenido en vez de quedarse —medido, su borde se sale de la pantalla—, y eso es del eje de dirección; y el prop de la tabla no lleva parámetro de tipo, así que todo consumidor tipado castea, empezando por la demo de este mismo componente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b36247944b |
docs(blocks): el plan de ejecución de V1, con la fase 0 corregida
La ficha de Pricing.Compare reservaba dos decisiones y ahora las tiene, resueltas por doctrina y no por gusto: los planes los declara el createTable del app y la parte lo compone —B-5 admite datos sólo donde el canon compuesto ya es data-driven, y Table lo es—, y en móvil la tabla scrollea con la columna de características fijada, porque el canon ya da el pinning. Eso último es una corrección de mi propia fase 0: el README de Table lo lista como gap diferido y es falso a fecha de hoy — el engine, soma y el recipe lo implementan los tres. Me apoyé en el README y di un dato equivocado; el primer paso del plan lo corrige donde vive. Seis pasos con su verificación cada uno, y los riesgos del árbol compartido escritos para el agente que lo ejecute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
70f45033fd |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera
El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA,
no un envoltorio — se renderiza como HIJO de la superficie que viste y se
ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de
foundation, `:where(:has(> [data-background]))`, así que ninguna receta se
parchea y ningún consumidor reestructura su layout. Con la pila entran sus
capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines,
noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el
`on` que reata la tinta del contenido de arriba.
Dos cosas medidas en Chrome real que el plan no preveía:
- La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css`
declara `position: var(--box-position, revert-layer)` en `[data-box]`, que
es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer`
devuelve la propiedad al valor del UA. Una Section o una columna de Grid
computaban `static` y la pila se escapaba al ancestro posicionado más
cercano. La regla escribe ahora también `--box-position: relative`: se
resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que
un `position` por prop —que llega inline— sigue ganando y `Dialog.Content`
conserva su `fixed`. 5/5 anfitriones cubiertos.
- El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README
del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`:
`box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el
shorthand borraba aquel de los tres cuya var estuviera sin poner.
`<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand
con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el
shorthand desaparece de la receta; medido en una fila de 600px:
`flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando.
Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que
un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en
vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el
scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que
la capa lleve su propio `color`.
El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y
gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta
F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es
de F5 por D-BG.13.
Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 ·
recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 ·
504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) ·
`check` 0 errores propios.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f4ce006423 |
blocks(faq): la lista estática, y un tipo que deja de prometer lo que no da
La brecha del dossier para este block, y la mayoría del formato: seis de siete en Tailwind Plus no son acordeón, sino una rejilla de preguntas y respuestas siempre abiertas. El eje vive en la LISTA, no en la raíz. Primero porque la lista es lo único que cambia —la cabecera se lee igual—, y segundo porque esa colocación es la que permite que el tipo diga la verdad: pasa a ser una unión discriminada. Bajo acordeón atraviesa toda la superficie del Accordion del canon; bajo lista no se ofrece ninguna de esas props, porque una rejilla estática no tiene estado abierto que enlazar, nada que colapsar ni modo simple o múltiple. Ofrecer una superficie y descartarla en silencio es justo lo que A-94 dejó dicho que no se hace, y aquí es donde iba a morder después. El ítem lee la disposición del contexto: no puede discriminar sobre la unión de la lista sin obligar a la app a repetir el arreglo en cada pregunta. Su value y su disabled quedan documentados como del acordeón, la misma convención que el fondo del hero y la media del cta. Y bajo lista la pregunta es un encabezado de verdad, porque ahí ES el encabezado de su respuesta; bajo acordeón el canon la mete dentro del disparador y esa semántica es suya, así que el block no inventa una segunda. Medido a 1280: en acordeón, cinco disparadores, ningún encabezado, medida de setecientos sesenta y ocho y respuestas ocultas hasta pulsar; en lista, ningún disparador, cinco encabezados, tres pistas de trescientos nueve, medida de mil veinticuatro y todas las respuestas visibles sin tocar nada. A 375 cae a una columna conservando ambas cosas. El acordeón queda idéntico. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
50a7f47147 |
uix(background): el contrato del fondo existe antes que su primer píxel
F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
f3bfcf2dd9 |
blocks(testimonials): una cita sola no va en una tarjeta
La brecha que el dossier le apuntaba a este block: la cita única es la variante modal de todas las referencias y el grid la minoría, dos de ocho en Tailwind Plus. Entra como disposición, no como hermano. La anatomía no cambia —cita, autor, avatar—, sólo cómo se colocan, y el precedente es el fondo del hero, que también redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide rotación o carrusel, que es otro componente. Viaja por contexto de configuración, el patrón del align de feature-grid y team, no la coordinación de pricing. Y viaja porque el spotlight no es una piel sobre las mismas cajas: la rejilla deja de serlo y de estampar el ritmo estructural —que es para una FILA, no para una cita—, el ítem pierde la tarjeta, la cita sube de tamaño y se centra, y el autor pasa debajo, donde ya no hay una hilera de caras que alinear. Pedirle a la app que repita esa decisión en cinco partes es el agujero que A-84 cerró en feature-grid. Enmarcar una cita sola la hace parecer un ítem de una lista que no está ahí, así que ahí no hay tarjeta y el tipo lo dice, en vez de aceptar una superficie que luego descarta. La cita se centra con un párrafo de verdad: sobre una caja en línea el centrado no aplica, y sería mentira. Slot nuevo para la marca de quien firma, en las dos disposiciones: es lo que hace que una cita se lea como evidencia y no como una opinión, y es la pieza que ponen todas las referencias. Medido: en grid, cinco tarjetas, veinte píxeles, medida de mil veinticuatro y ritmo escalonado; en spotlight, cero tarjetas, una cita de veintiocho centrada en párrafo, medida de setecientos sesenta y ocho, autor centrado y sin escalonado; a 375 la cita cae a veinticuatro. El grid queda idéntico a antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b92dc3bdb2 |
uix(card): el estado deshabilitado deja de pelearse con la animación por la opacidad
Salió midiendo el plan actual de pricing: la tarjeta no se atenuaba. La regla estaba escrita, el token resolvía a 0.4 sobre la propia tarjeta y el cursor de esa misma regla sí aplicaba — pero la opacidad computada seguía en uno. La culpable es su propia animación de entrada. Corre con relleno en los dos extremos y su último fotograma es opacidad uno; una animación gana a una declaración normal en la cascada, así que fijaba la propiedad para siempre. Aislado en el mismo nodo desactivando la entrada, atenúa correctamente. Llevaba ahí desde que el componente existe, invisible porque el cursor hacía parecer que el estado estaba cableado, y alcanzaba a toda Card deshabilitada del ecosistema. El arreglo es dejar de compartir la propiedad: el estado atenúa por filter, que la animación no posee. Mismo token, misma escala, ninguno nuevo, y el bloqueo del puntero en la tarjeta interactiva no se toca. El guard afirma la FORMA con su porqué escrito —que la regla usa filter y no opacity, más el bloqueo del puntero y la presencia de la animación que obliga a esa forma— y está probado por mutación: devolviendo la regla a opacity, falla; restaurada, verde. Verificado con la animación presente: la deshabilitada computa filter opacity a 0.4 y sus hermanas ninguno; en píxeles, su tinta más oscura sube a 164 frente a 139 de la normal sobre el mismo lienzo, que es justo lo que da un 0.4 sobre el fondo de página. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0646d5ebde |
blocks(pricing): el plan dice dónde estás, y el que no puedes elegir dice por qué
Firma del usuario: faltaba la decoración del elegido, y faltaba poder marcar un plan como no disponible — si estoy cambiando de plan, el que ya tengo no debería poder seleccionarse. Lo segundo cambia la posición doctrinal del block. Su README decía desde julio que nada en una tabla de precios puede bloquearse y que por tanto no hay nada que explicar. Eso vale mientras la sección es sólo una comparación; se cae en cuanto el visitante ya está en un plan. Y ahí manda lo que costó contact: una acción bloqueada debe su frase, o es un agujero — algo que no se puede hacer y nada en pantalla diciendo por qué. El plan gana un estado: disponible, actual o elegido. Un enum y no tres booleanos, porque tres booleanos admiten lo que no existe, como elegido y no disponible a la vez. Y ortogonal a featured, que es la recomendación de quien vende: el plan recomendado puede ser justamente el que ya tienes. Se deriva una sola vez en el plan y viaja por contexto. La acción se lo entrega al botón del app por parámetro de su snippet, así que nadie vuelve a deducir en el punto de uso si se puede pulsar, y las palabras del botón siguen siendo del app. La frase, en cambio, es del block: idlangref con respaldo en inglés dentro de un Record exhaustivo, el mismo mapa que bloquea la acción es el que escribe el motivo, así que un estado que bloquea en silencio no se puede ni expresar. Su test lo afirma en las dos direcciones. Y una parte nueva la dice y posee el id al que apunta el aria-describedby del botón deshabilitado, sin renderizar nada cuando no hay nada que decir. La decoración la pone el canon: el elegido reenvía el anillo de Card, el actual su tratamiento deshabilitado. Nada de un data-pricing-selected propio: sería un atributo que nadie lee, porque un block no pinta CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale con cursor de no permitido, botón deshabilitado y aria-describedby que casa con el id de la frase, traducida por el arnés; al elegir otro, su tarjeta gana el anillo de dos píxeles con el primary de su paleta y las demás no. Queda anotado un gap del canon que salió al medir esto y que no toco: Card disabled nunca atenúa. La regla existe y su cursor sí aplica, pero la animación de montaje fija la opacidad en uno con prioridad de animación y se come la del estado; aislado con no-emerge, atenúa. Es el patrón que la doctrina de motion llama frágil-conocido —dos dueños peleándose por una propiedad en un nodo— y alcanza a toda Card deshabilitada del ecosistema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db026e48f5 |
blocks(pricing): la tarjeta responde al puntero, y el guard deja de leer prosa
El plan reenvía el eje nuevo de Card. No es el `interactive` del canon: eso convertiría la tarjeta en un botón y `.PlanAction` ya tiene uno dentro. Apagado por defecto, porque una fila de planes es una comparación y una tarjeta que responde al puntero sugiere que la tarjeta misma es el control; se enciende donde la tarjeta entera ES la elección. Control vivo en la demo y en la URL de la vista previa. Y al escribir el comentario que explica por qué NO se usa un nativo aquí, el guard del tier se puso rojo sobre mi propia prosa: la regla B-2 leía comentarios como código. Su barrido quitaba sólo los de UNA línea, mientras el que entiende todas las formas y conserva los saltos ya existía tres líneas más abajo, usándose sólo para los imports. Es la lección de la fase cinco aplicada a una regla y no a la otra. Arreglado con dos fixtures nuevos —un nativo nombrado dentro de un comentario multilínea es prosa; un nativo de verdad debajo de ese mismo comentario sigue siendo error— y probado en rojo sobre el árbol real: un button inyectado en el hero sale señalado con su línea exacta, y el árbol queda restaurado después. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d61515027a |
docs(theming): el changelog vuelve a su formato — mi entrada no traía 400 líneas
Al escribir la §49 pasé prettier por el fichero y reformateó los ejemplos de código de entradas ajenas: 237 inserciones y 185 borrados de los que apenas veinte eran míos. Es justo lo que el handoff del tier advierte de los ficheros que ya venían con otro formato — formatearlos produce un diff del documento entero y sepulta el cambio real. El fichero vuelve al formato que tenía y la entrada se re-aplica sin pasar prettier. Efecto neto contra el estado anterior al commit: 43 líneas, todas de la §49. No reescribo el commit porque la rama es compartida. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d38c54fdf8 |
uix(card): el realce al puntero deja de ser rehén de `interactive`
El usuario preguntó por qué una tarjeta de plan de precios no responde al puntero. Le dije que Card no exponía estado de puntero y era falso: el realce existe desde siempre —los dos tokens están en la fundación, con reglas para ghost, outline y solid, composición con el anillo de selección para que no desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo colgaba de un solo attr. Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que ya contiene su propia llamada a la acción no puede tomar ese paquete: un button dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de precio, teasers de artículo, tarjetas de testimonio. La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le debe al visitante. Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho. El eidos-lint lo clasifica como eidos-only, 0 inválidos. Medido en navegador sobre estilos computados y posición real: apagado, el hover no mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad; y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button, con cursor de puntero y los mismos dos píxeles de elevación. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
44e4c9328e |
blocks(hero): un vídeo en un hero es paisaje, y que se repita lo decide quien lo pone
Corrección del usuario, y tenía razón: la demo componía el reproductor completo con su barra de controles, y nadie enseña un botón de play en una sección de apertura. Un vídeo de hero es decorativo — mudo, en línea, arrancando solo y sin controles. Y si se repite o no es una decisión, no algo horneado: en bucle el movimiento no termina nunca, así que sale como control vivo propio. Medido: cero botones, sin nodo de controles, y el loop siguiendo al conmutador. De paso queda registrado lo que aquello escondía. La barra que había se pintaba directamente sobre el fotograma sin fondo propio, así que en un clip claro eran siete botones a opacidad uno que nadie podía ver; es del canon y media-player está en la lista de intocables de este plan, así que se documenta por si algún día vuelve. Y un hallazgo más gordo, medido al cablear lo anterior: la preferencia de movimiento no llega a prefs en este arranque. Un vídeo que se repite sin barra de controles no se puede pausar, así que su autoplay tiene que ceder ante la preferencia declarada del visitante, igual que ya hacen las entradas de Motion del propio block. La puerta está escrita en app-land y hoy es inerte: con prefers-reduced-motion emulado en reduce, el valor efectivo sigue resolviendo allow, la raíz sigue estampando data-motion allow y el clip sigue corriendo. Queda puesta —empezará a funcionar el día que el eje lo haga— y anotada como lo que es: un problema de WCAG 2.2.2 a nivel de framework, no un detalle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
fd9df269ef |
blocks(hero): un slot apagado se pasa vacío, no se renderiza a cero
El usuario dijo que en la demo hay propiedades que no hacen nada. Medí los diez
controles uno a uno: nueve tienen efecto comprobable y uno funcionaba a medias.
Apagar el eyebrow quitaba el contenido y dejaba el hueco: el stack de copia
seguía con seis hijos y el primero medía cero de alto, comiéndose los veinte
píxeles del gap.
La causa era de la demo, no del block, y contra una regla que el propio handoff
del tier tiene escrita: el snippet se declaraba con un {#if} DENTRO, así que
siempre llegaba, el {#if eyebrow} del block seguía siendo cierto y sobrevivían el
envoltorio de Motion y su parte del ritmo con nada dentro. Ahora el contenido se
declara fuera y se pasa como undefined cuando no toca — lo mismo para la media,
el formulario y el aviso de confirmación.
Medido: cuatro hijos con el chip puesto, tres sin él, y ningún hijo de altura
cero en ninguna combinación.
Queda registrado además el defecto que el usuario vio en el vídeo del hero: los
controles del MediaPlayer están —siete botones, todos con caja, opacidad uno—
pero se pintan sin fondo propio sobre el fotograma, así que en un clip claro son
ilegibles y se leen como ausentes. Es del canon, y media-player está en la lista
de intocables de este plan, así que se documenta sin tocarlo. Con la nota de
método: contar nodos y leer la opacidad no dice nada de la legibilidad sobre
contenido en movimiento.
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 |
|
|
f11b198f56 |
blocks(hero): el alta por correo entra por una ranura, y el block no toca el formulario
Primera fila de la ola F2b. El hueco lo registraba el dossier desde julio y su README lo tenía esperando firma: las referencias ponen un alta por correo en el hero cuando la llamada a la acción ES un correo. La forma se eligió contra el precedente, no de cero. Newsletter recibe el handle del formulario porque lee la validez de la sección y con ella decide qué se puede pulsar; eso es coordinar, y un block que coordina tiene que poseer una máquina y las palabras de sus estados. Un hero no coordina nada, así que toma la otra puerta —la de site-footer.signup—: UNA ranura, la app compone Form + Field + Form.Submit enteros, el block no ve el handle y sigue sin poseer nada. Aterriza donde aterriza la llamada a la acción: dentro del cúmulo de copia, tras la descripción y antes de actions, así que hereda el ritmo estructural del stagger y su propia entrada. Y con medida propia: una fila de correo tan ancha como la prosa es inusable. El cap es sm y por ser un máximo resulta inerte en split, donde la columna ya es más estrecha — 480px en center contra una sección de 1264, 472px en split, que ES la columna. Los dos slots conviven si llegan los dos. En las refs el formulario sustituye al par de botones y así lo enseña la demo, pero hacerlos excluyentes en el tipo prohibiría un arreglo real: un alta junto a un «hablar con ventas». Medido en Playwright headless sobre píxeles pintados: tinta escrita 15,88:1 en claro y 16,28:1 en oscuro; el relleno del campo contra el lienzo de background 9,61:1; la fila colapsa a 375 y comparte a 1280; Enter llega al handler del app y un correo inválido lo para con el error traducido y aria-invalid; RTL espeja la fila. Dos gaps de canon quedan registrados, no tapados: Form.Submit no reenvía al Button ni el eje intent ni ninguno de anchura, así que el botón no llena su celda al colapsar —newsletter mide lo mismo y el justify de Grid es justify-content, no justify-items—, y la demo usa lo único que hay, el style que sí viaja. Y el contexto de inversión mordió por segunda vez: un Text sin color explícito bajo background mide 1,62:1 contra 10:1 con on-solid. Queda anotado también cómo se mide esto, porque dos sondas dieron cifras seguras y falsas antes de la buena: un parser rgb sobre un sistema que emite oklch, y un canvas cuyo fillStyle tampoco lo normaliza aquí, midiendo NEGRO contra todo. La señal de que la sonda es honesta es que la etiqueta del submit sale a 5,18:1, la cifra que este repositorio ya documenta para primary. 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 |
|
|
68a48a4401 |
refactor(skin-media-player): un cableado y cuatro pinturas, con el movimiento del sistema
Reescritura completa de la versión A (`bf12b8a57`), que el autor valoró como
duplicación. La medición le daba la razón: `fit()` idéntico en dos ficheros,
`hold()` y `stop()` en otros dos, y `MediaPlayerProvider.require()` en los
cuatro. Eran cuatro componentes compartiendo carpeta, no un skin.
## La forma
Un componente cablea, los aparatos sólo pintan.
- `skin-media-player.svelte` — EL punto de cableado: un `require()`, una
derivación de todo, y las teclas generadas recorriendo un mapa.
- `controls.ts` — datos por aparato: lienzo, teclas `[{id,x,y,w,h,action}]` y la
ranura del volumen.
- `actions.ts` — el vocabulario único (`play·pause·toggle·stop·seek-*·rpm·none`),
con `stop()` y el veto por `preventDefault()` en un solo sitio.
- `type-fit.svelte.ts` — el ajuste de `<text>`, una vez.
- Los cuatro cuerpos: props entran, SVG sale. Cero provider, cero `$effect`,
cero contexto. Añadir un quinto aparato es un SVG y una fila de datos.
`<SkinMediaPlayer>` deja de montar player: es una PIEZA dentro de uno. No monta
`Media`, no monta barra y no decide la forma — `inline/bar/row/card` es el eje
`variant` de `AudioPlayer`, y la piel viaja dentro como hija.
## Movimiento: el del sistema, nada a mano
La versión A traía tres `@keyframes` locales y siete transiciones con números
crudos, ignorando `docs/theming/motion.md` (leída ahora, entera).
- Todo giro continuo es el preset **`spin`**; el respirar de la cinta, **`pulse`**.
El periodo va en la ranura del propio loop (`--motion-loop-spin`: 1.333s a 45
rpm, 1.8s a 33).
- Las transiciones consumen `--duration-*` / `--ease-*` por una ranura `--_smp-*`
por eje.
- DEFECTO FUNCIONAL corregido: el generador apaga los loops por
`[data-motion='reduce']` **y** por la media query; el bloque a mano sólo tenía
la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba
el plato. Medido en navegador: ahora `animation-name: none`.
- Queda un `@keyframes` local, la aguja del vúmetro, anotado `functional:` y con
el periodo en token: ningún preset representa un instrumento analógico.
## Tres defectos que sólo se ven en movimiento
Los tres pasaban toda comprobación de atributos y sólo aparecieron mirando la
imagen moverse:
1. **La sombra rotaba con el disco.** El `feDropShadow` colgaba del grupo que
gira, así que su desplazamiento orbitaba (se leía como la sombra subiendo y
bajando) y la penumbra de la región del filtro barría las marcas del plato,
que parecían parpadear. El filtro pasa a un padre quieto — un disco es un
círculo, su silueta no cambia. Igual en el brazo.
2. **Las marcas del plato salían disparadas en arco.** Al rehacer se perdió la
regla del estrobo; sin `transform-origin` el preset gira alrededor del centro
del viewBox (500, 440) en vez del centro del plato (406, 468.5).
3. **El tamaño no llegaba.** `max-inline-size` sólo encoge, así que `lg` y `xl`
eran inertes en cuanto la columna era más estrecha (medido: md/lg/xl los tres
a 720px). Ahora `inline-size: min(100%, …)`, base por objeto y factor por
tamaño, una sola fuente en el recipe — la base estaba declarada dos veces,
inline y en CSS, y ganaba la inline.
## Volumen
Primero se compuso el `Knob` del catálogo y era inmanejable: mapea el valor al
ángulo ABSOLUTO del puntero alrededor del centro (`Gesture.rotate`), o sea
orbitar el ratón en torno a un dial de 40px. Sustituido por el `VolumeSlider`
del propio player corriendo en una guía pintada por la máquina. El re-tinte va
sobre el MISMO descendiente que retinta el player (`media-player.css:420`) o su
regla gana por estar más cerca del thumb.
La banda inferior de la pletina se reordenó para alojarlo: vúmetro a media
anchura con el dial recentrado (aguja intacta), deslizador a 42 unidades del
bisel (20px a `md`), contador y piloto +20 a la derecha, la etiqueta REPROD del
piloto fuera, y «MODELO CR-77» al mismo ancho que «Levante» por `textLength` +
`lengthAdjust="spacing"`.
## Prop `labels` y la decisión de idioma
`labels` (default `true`) apaga la serigrafía entera de la máquina. Los rótulos
NO se traducen, y es decisión firmada: son parte del OBJETO, como la marca del
badge. Los nombres accesibles sí siguen el idioma, porque salen del morfo de las
partes compuestas.
## Demo canónica
`web/routes/uix/components/skin-media-player/+page.svelte` con la plantilla v2
de 9 pestañas + su entrada en el sidebar. El banco provisional de
`/otros/players` se borra: duplicaba la demo con DOS players independientes, que
es lo que produjo dos lecturas falsas («no está sincronizado», «fondo negro»).
Los cuatro bocetos del autor se quedan en la carpeta, ya sin ruta.
`preload="none"` en la demo a propósito: un preload de metadata abre una
petición de rango que el navegador aborta, y `smoke` cuenta ese aborto como
fallo same-origin.
## Auditoría — el hueco que queda
`component-guide.md` §5 dice que incluso un átomo pasivo es morfo-first, con un
morfo mínimo `scope: ['eidos']` y `## Passive justification`. Este componente no
lo tiene porque el alcance acordado excluía `src/uix/morfo/`, y la consecuencia
no es cosmética: **tres máquinas no lo ven** — `component:audit` enumera por
ficheros de morfo y no le da fila, `eidos-lint` lo salta («no morfo file») y
`morfo:check` no visita su demo. Los guards transversales sí pasan; los
específicos de componente no se han ejecutado sobre él ni una vez.
Pendiente además: `data-perm-step` en la demo (A37), sin el cual `perm:check` la
salta en silencio.
Verificado: `npm run check` sin errores nuevos (base 72/59) · 77 tests de eidos
(recipe-css-contract · component-api-contract · component-visual-attrs · motion)
· `rtl:check` 0/179 · `docs:check` 0/0 · `layer:check` 0 · `smoke` PASS ·
`morfo:check` 159/159. Y en Chrome, con el ratón: transporte en los dos
sentidos, brazo siguiendo el surco, bobinas por conservación de área, contador
de cinta, y el volumen moviendo `audio.volume` de verdad.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
bf12b8a571 |
feat(skin-media-player): cuatro aparatos sobre el contrato del player — versión A, para el histórico
Se commitea PARA CONSERVARLA: esta versión se borra y se rehace. El autor la valoró como duplicación, y la medición le da la razón. Queda aquí para que la segunda no repita sus errores. Qué hay: - `eidos/components/skin-media-player/` — cuatro objetos dibujados (disco, giradiscos, casete, pletina) sobre el morfo `media-player`. El aparato anida su soporte como `<svg>` interior, así que la pintura del soporte se reutiliza. - La afordancia es un `<button>` HTML superpuesto, colocado en % del viewBox, y NO un `role="button"` sobre un `<g>` como en los bocetos. Precedente: `onion-menu` («a real HTML button overlaid at the SVG centre… which an SVG element can't do») y el knob de `natural-time-picker`. - Transporte compuesto sobre las partes reales: MARCHA/REPROD sólo arrancan y PAUSA sólo pausa, anulando el toggle de soma con `preventDefault()` en la costura (patrón de `RateFloat`). PARO/STOP no existe en el contrato y se compone `togglePlay()` + `seek(0)`. - `engaged()` — `started` se enciende con la primera reproducción y no se apaga nunca, así que por sí solo dejaba el brazo apoyado sobre un disco parado. Con `started && !ended && (currentTime > 0 || !paused)` salen los tres estados de una máquina: parada, sonando, en pausa. - Geometría portada del boceto: brazo por cinemática inversa de forma cerrada (r² = K + A·cosθ + B·senθ) y bobinas por conservación de área (R_izq² + R_der² constante), de donde sale sola la velocidad correcta de cada bobina. El contador de cinta lee el medio en vez de un `setInterval` propio. - Los 83 colores del autor se conservan intactos: `skin-media-player` entra en `FIXED_TONE_COMPONENTS` — la excepción que el guard ya reserva para los componentes que DIBUJAN un objeto físico (`natural-time-picker`, `color-picker`, `proof-of-human`). Acotada a esta carpeta a propósito: dentro de `media-player/` habría eximido al recipe del player real. - `web/routes/otros/players/` — los cuatro bocetos originales del autor (estado simulado, sin audio) y el banco de verificación. Por qué se rehace, medido: 1. DUPLICACIÓN. `fit()` idéntico en record y cassette; `hold()` y `stop()` idénticos en turntable y deck; `MediaPlayerProvider.require()` + `engaged()` en los cuatro. Son cuatro componentes que comparten carpeta, no un skin: cada cuerpo se agarra al provider por su cuenta y recablea las mismas costuras. Lo correcto es al revés — un componente que recibe el player y resuelve las acciones, y a cada aparato sólo su pintura y su mapa de mandos. 2. MOTION A MANO. Tres `@keyframes` locales y siete transiciones con números crudos, existiendo el motor: `smp-turn` ES el preset `spin` (`loop-spin`, periodo por `--motion-loop-spin`) y `smp-tape-pulse` ES `pulse`; las transiciones debían salir de `--duration-*` / `--ease-*`. Y un defecto funcional: el generador apaga los loops por `[data-motion='reduce']` Y por la media query; mi bloque a mano sólo tenía la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba el plato. 3. La raíz `skin-media-player.svelte` empaqueta `Media + Skin + barra` por dentro, así que la forma del reproductor (inline/bar/row/card, que es el eje de `AudioPlayer`) no se podía elegir y el MediaPlayer no se veía por ningún lado en la demo. Verificado antes de commitear: `npm run check` sin errores nuevos (la base del repo, 72, no se mueve) · guards de eidos 49/49 · `rtl:check` 0/179 · en navegador con audio real, transporte, brazo siguiendo el surco, bobinas por área y contador de cinta. 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 |
|
|
998d72191e |
refactor(morfo): los 25 knobs visuales salen del contrato — comentados, no borrados
Morfo es el contrato ENTRE capas, y un knob de pintura lo lee UNA: el CSS del
propio componente. La doctrina lo dice en tres sitios, y ninguno lo escribi yo:
· morfo.md §«What morfo does NOT contain» → «Visual variants / recipes →
src/uix/eidos/ — Layer-specific, not shared»
· morfo.md §Backlog 2026-08-13 → `undeclaredState` es «the escape hatch for
attrs OUTSIDE the contract — a visual wrapper's `data-size`, a presentation
flag like `data-sheet`», y LANZA si el attr resulta estar declarado
· theming/reference.md §39 → la familia `data-variant`/`data-size` es un
wrapper attr eidos-only; regla de pulgar: morfo declara un attr solo cuando
su prop cruza la frontera de SOMA
Ninguno de los 25 cruza: los seis componentes no tienen capa soma.
avatar 9 size · variant x2 · color x2 · radius · position · dot · ring
s-text 6 style · truncate · clamp · canvas · italic · underline
image 5 fit · position · radius · size · placeholder
avatar-group 2 size · stacking
skeleton 2 shape · animated
qr-code 1 cell-shape
COMENTADOS, NO BORRADOS, por orden del autor, y cada bloque lleva su porque con
las tres citas. Dos motivos que valen la pena: son la huella de lo que el morfo
prometia, y son el enum que `eidos-lint` podra volver a leer el dia que sepa
tomarlo de la union TS (`AvatarSize = Extract<Size, …>`), que es donde vive de
verdad. Hasta entonces esos 25 enums pierden la comprobacion del CSS — el codigo
sigue tipado.
LO QUE SE QUEDA, y por que: los `data-status` de `avatar` (x3) e `image` (x4) los
produce `ImageProvider` de `$soma/layers` — `avatar.svelte:24`,
`avatar-image.svelte:3` — asi que su driver SI cruza, que es exactamente la
condicion que nombra la regla de pulgar.
FUERA DEL BARRIDO, y tampoco son knobs: `separator` y `button-group` declaran
`data-orientation` junto a un `aria-orientation` del MISMO prop (es el espejo CSS
de una propiedad ARIA que el morfo ya posee); `chart` usa `data-chart-axis` para
discriminar dos instancias de la misma parte (identidad estructural); y
`barcode` usa `data-symbology` para decir que estandar codifica el simbolo.
Medido: los seis morfos parsean, `eidos-lint` invalid 0 en todos, y el
desplazamiento esperado — avatar pasa de 24 morfo-backed a 7 y de 46 a 63
eidos-only. morfo:check PASS en los seis (nunca los miro: son attrs desnudos,
no `data-{kebab}-*`). component:audit los seis PASS. check 69 = base intacta.
morfo:vocabulary OK. smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b72a564918 |
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix
Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en
`components/affix/affix.css`, sus consumidores la importaban con
`'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que
es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial`
quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es
geometria.
Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los
tres consumidores importan de ahi. Ninguno depende de ningun componente.
LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo
intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que
el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si
puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de
receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas
que ya son temeables.
Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el
propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave
en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa
exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un
tema mueve la escala de espacio y la escalera de z, no un alias por componente.
Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un
componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus
ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo
de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del
`:root` generado.
Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`,
z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles
haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas
exactas con z 150; y su elemento lleva solo `data-affix`, la identidad.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 ·
smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
de0615caf2 |
fix(morfo): affix deja de declarar lo que solo lee una capa, y el gancho pasa a nombre de CAPA
Correccion doctrinal, senalada por el autor: **morfo es la capa declarativa ENTRE
capas**. Un atributo que consume una sola no es contrato.
`affixMorfo` declaraba `data-affix-placement` y `data-affix-stretch`. Los lee
UNA: el CSS. No debian estar ahi — y la doctrina nombra la familia exacta
(`morfo.md` §`undeclaredState`: «the escape hatch for attrs OUTSIDE the contract
— a visual wrapper's `data-size`, a presentation flag like `data-sheet`»). Los
declare porque `morfo-check` los exigia, que es la herramienta dictando la
doctrina: el guard clasifica por PREFIJO DE NOMBRE, la doctrina clasifica por
NATURALEZA, y las dos solo chocaban porque el gancho llevaba el nombre del
componente.
Y no era su nombre. Lo estampan tres —`Affix`, `Fab`, `MenuDial`—, asi que no es
de ninguno: pasa a **`data-viewport-placement`** / `data-viewport-stretch`. La
colision con el guard desaparece por construccion, sin excepcion que escribir.
Es el mismo error que `--fab-offset` leido por la capa: nombrar por un
participante algo que es de todos.
`affixMorfo` se queda con lo que si es contrato: la identidad `data-affix`, que
es como el DOM dice «esta caja es un Affix» a quien pregunte.
TAMBIEN INTENTADO Y REVERTIDO, con su motivo, para que nadie lo reintente:
mover el fichero a `eidos/lib/viewport-placement.css` junto a `list-surface.css`.
`recipe-css-contract` lo tumbo y tenia razon — **el sistema de tokens esta
indexado por componente**: toda clave de `recipes/base.ts` exige su
`components/{c}/{c}.css`. `list-surface` puede vivir en `lib/` porque NO tiene
clave de receta (sus `--list-*` viven dentro de su propio CSS, por `data-size`,
no como defaults temeables en `:root`); los nuestros si lo son. La parte portante
—«esto no es de nadie»— la lleva el nombre del gancho, no la carpeta. Queda
escrito en la excepcion E-2.2 del README y en el PLAN.
Sin cambio de comportamiento: 68 sustituciones de nombre, mismas reglas, mismos
valores. Verificado en las tres rutas.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
morfo:check affix PASS · layer:check 0/3 · contrato de capa 13/13 ·
recipe-css-contract + api-contract 50/50 · rtl 0/178 · smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
a11805d434 |
docs(eidos): la doctrina alcanza a la jornada — dos guards nuevos, el patron de capa, y EID-3 deja de estar pendiente
Barrido de lo que la sesion cambio y la documentacion todavia no decia. `testing-and-tooling.md` — los dos guards nuevos entran en la tabla de «que atrapa cada script», con su reparto explicito: `layer:check` mira el valor computado (quien gana la cascada) y declara su hueco (la geometria, porque `getComputedStyle` da el valor USADO y un `inset: auto` se lee como pixeles); `shared-layer-contract.test.ts` mira el texto, y existe por lo que el navegador no puede ver — un `env()` ya sustituido devuelve `"0px"` en escritorio. `component-guide.md` fila RTL — deja de remitir a «EID-3 exception» y enuncia la regla: dos rejillas NOMBRADAS, `Position` fisica y `LogicalPosition` logica, y la pregunta que decide entre ellas («¿tiene que voltearse para un lector de derecha a izquierda?»). Estrechar con `Extract<>`, nunca redeclarar. `canon/recipe-contract.md` — la fila de z-index flotante distinguia mal: la banda `--z-index-overlay-*` es de overlays PORTALED. El cromo fijado al viewport que no portala es otra cosa y tiene su peldano (`affix`, 150). Y el item 9 del checklist de recetas decia «si flota → una rung de overlay», que era incompleto. `eidos/components/README.md` — el patron de CAPA COMPARTIDA, que no estaba escrito en ningun sitio pese a tener dos ejemplares vivos (`list-surface` y `affix`). Sus cuatro reglas, tres de ellas aprendidas rompiendose: enganchar en el attr de capa y no en la identidad (o `morfo-check` suelda al consumidor a un contrato ajeno), un eje = token publico + ranura, el puente reafirma `position` si el primitivo compuesto declara uno, y se guarda con dos redes porque ninguna basta sola. `audit-active-uix.md` — EID-3 pasa a RESUELTO, conservando el hallazgo original debajo. Era un P3 de julio cerrado «como excepcion» pero marcado en su propia tabla resumen como «pendiente de doctrina explicita». Ya no lo esta. `PLAN-affix.md` §7 — lo que vino DESPUES de cerrar el plan, que es casi todo lo interesante: las dos migraciones y sus dos lecciones, el peldano de z, el patron de tokens, la canonizacion de la rejilla y los dos guards. Ninguna estaba prevista en el plan; todas salieron de auditar lo construido. docs:check 0/627. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
7ea913ab11 |
refactor(eidos): la rejilla de colocacion logica se canoniza — cinco declaraciones a mano pasan a una
`lib/types.ts` ya tenia `Position`, la rejilla 3x3 FISICA, con su idioma `Extract<Position, …>` y sus consumidores (Dialog, Drawer, Toast). Lo que no tenia nombre era la otra: la LOGICA, escrita a mano cinco veces — `AffixPlacement`, `FabPlacement`, `MenuDialPlacement`, `OnionPlacement` y las cuatro esquinas de `AvatarBadgePosition`—, coincidiendo por mantenimiento manual y no por contrato. Anadir una zona a una no llegaba a las otras cuatro. Ahora hay `LOGICAL_POSITIONS` / `LogicalPosition` junto a `POSITIONS` / `Position`, y las cinco estrechan desde ella. Ambas pasan a ser consts para que `docs:vocabularies` las genere: eran las unicas props visuales compartidas que faltaban en `docs/canon/vocabularies.md`, que ya listaba tamanos, variantes, paletas, arquetipos y el vocabulario sema entero. NO son dos grafias de lo mismo, y por eso ninguna sustituye a la otra: son dos COMPORTAMIENTOS. `top-left` es la izquierda de la pantalla y no espeja nunca; `top-start` sigue la direccion de lectura — medido hoy, de `left: 0` a `right: 0` en RTL. La regla de eleccion queda escrita en los dos typedoc y en el canon generado: ¿tiene que voltearse para un lector de derecha a izquierda? Una tira fijada en `bottom-end` va en el borde de salida en ambas direcciones; un panel que se abre a la derecha fisica porque ahi hay sitio, no. Con eso se cierra EID-3, que el audit de julio dejo asentado como excepcion pero marcado «placement fisico — pendiente de doctrina explicita». La doctrina ya no esta pendiente: son dos rejillas nombradas con la regla de cuando usar cada una. Sin cambio de comportamiento: las uniones resultantes tienen los mismos miembros (`MenuDialPlacement` = la rejilla + `static`; `FabPlacement` = las cuatro esquinas + `static`; `OnionPlacement` y `AffixPlacement` = la rejilla entera; `AvatarBadgePosition` = las cuatro esquinas). check 69 = base intacta · docs:check 0/627 · component:audit los cinco PASS · layer:check 0/3 · rtl 0/178 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ee41b691d9 |
docs(eidos): los 20px del MenuDial son una decision, no una deriva — corregido el registro
El commit anterior (
|
2 months ago |
|
|
c53efc6025 |
refactor(eidos): MenuDial lee la colocacion de la capa — la ultima copia privada, borrada
Cierra la deuda que dejo `Affix`: las tres copias de la geometria de viewport (air/layout/float la tuvo primero, luego Fab con 4 zonas y MenuDial con 9) son ahora una sola. `menu-dial.css` pierde 31 lineas netas. Tres diferencias con la migracion de Fab, y ninguna es cosmetica: 1. `data-placement` SE QUEDA. No es un resto: tiene un segundo lector que el posicionado nunca tuvo — el arco deriva de la zona su apertura y su span (90° en esquina, 180° en borde, 360° en el centro). Un attr dice «que zona», el otro es el gancho de la capa. Verificado tras migrar: `bottom-end` en `arc` sigue computando `arc-start: 270deg` / `span: 90deg`. 2. El puente NO reafirma `position`, y Fab si. Aqui no hay empate que ganar: `[data-menu-dial]` no declara ninguno, y el `relative` que si declara casa solo con `placement="static"`, que no estampa gancho. El empate de Fab viene del `<Button>` que compone; un dial no compone nada en el marco. 3. Su offset es `--space-5` (20px), no el `--space-4` de la capa, y el puente lo PRESERVA. El comentario retirado decia que «mirrors the FAB» y era falso: el FAB va en 16px. Unificar es un movimiento visible de 4px, o sea una decision, no un efecto colateral de migrar — queda declarado en el README hasta que alguien la tome. Es exactamente para lo que existe la ranura de override. El `z-index: var(--z-index-sticky, 1100)` se fue con la copia: el token siempre resolvia, asi que el fallback de 1100 estaba muerto. El dial se queda en la banda `sticky` via `--_affix-z`, igual que Fab. Medidas las diez colocaciones: nueve zonas en `fixed`, z 100, offset 20px conservado (`calc(20px * 1 * 1)`); esquinas a 20 de sus dos bordes; los tres centrados con l = r = 431 y/o t = b = 34; y `static` sin gancho, `position: static`, z `auto`. `layer:check` pasa de 2 a 3 consumidores SIN tocarlo: la lista se deriva de quien importa la capa, que era el punto. El contrato de texto suma menu-dial (13 tests) con la regex de tokens acuñados ampliada a la forma privada `--_x`, que es la que usaba. check 69 = base intacta · component:audit affix/fab/menu-dial PASS · layer:check 0 violaciones sobre 3 · rtl 0/178 · eidos-lint invalid 0 · smoke 311/311. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4b39b718c6 |
fix(eidos): el aviso al pie se lee donde se ve, y la fuente unica deja de mentir sobre si misma
Cierra los dos hallazgos que quedaban de la auditoria.
ORDEN DE LECTURA (a11y). `position: fixed` saca la tira del flujo VISUAL pero no
del DOM: se lee donde esta en el fuente. Con `affix="bottom"` el aviso se ve al
pie y la mini-pagina lo renderizaba primero, asi que un lector de pantalla lo
anunciaba ANTES que la cabecera — medido en la preview del propio block, era el
primer nodo del documento.
Quien decide eso es el APP, no el block: el block coloca, el documento ordena.
Asi que la correccion no es un prop, es que la demo deje de modelar el desajuste
— el aviso se renderiza donde se ve — mas la advertencia en el typedoc de `affix`
y una fila en los Gaps. Verificado en los dos bordes: con `bottom` el aviso va
DESPUES de la cabecera en el DOM y se ve al pie; con `top` va antes y se ve
arriba. En ambos, z 150 y pegado a su borde.
AFIRMACIONES OBSOLETAS. Cinco sitios seguian diciendo que `Fab` "ships a private
copy today" y que su migracion estaba pendiente — falso desde
|
2 months ago |
|
|
acb7f0b416 |
test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado
La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve
zonas, el remapeo RTL, la regla de stretch y el empate de `position` con
`[data-button]` estaban verificados solo a mano. Con dos consumidores y un
tercero planificado, eso era el hallazgo estructural.
Van dos guards, y la division del trabajo entre ellos es medida, no supuesta.
`shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas,
`stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override,
ninguna regla de geometria enganchada en `data-affix` (la identidad), y el
contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens
propios, y reafirma `position` si el primitivo que compone ya declara uno.
Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de
`--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La
sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura
se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto
en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado
y en RTL. El texto es el unico sitio donde ese emparejamiento se ve.
`scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento
con el gancho: `position` es el que la capa promete, la z resuelve, los tokens
publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma
exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de
quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada.
Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la
geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL:
`getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles
(medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay
forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px
porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que
exige caminar ancestros. Diferido al tercer consumidor.
Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba
nada hasta que falla sobre el defecto que dice atrapar:
texto M1 media mitad del remapeo RTL -> falla
M2 zona borrada -> falla
M3 vuelta a un solo nivel de token -> falla
M4 puente sin `position` -> falla
M5 fab reacuna --fab-z -> falla
navegador M6 la capa pierde el position de veras -> falla
M7 token de z renombrado -> falla
M8 la demo deja de ejercitar la capa -> falla
M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado
CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no
estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que
no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a
`bottom-end` — "floating" es el nombre del componente y su demo deberia
ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa).
Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab
NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa
linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo
de un estado roto; por eso la cubre el guard de texto y no el de navegador.
check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2
consumidores · suite 120/120.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
c945be23b6 |
refactor(eidos)!: un eje de capa = un token publico + una ranura, y Fab deja de acunar los suyos
BREAKING: se retiran `--fab-offset` y `--fab-z`. No hay shim (regla del tier).
Rompe a quien los sobreescriba en CSS de app; el sustituto es `--affix-offset`
y la ranura `--_affix-z`.
Salio de auditar lo de ayer. `Affix` hacia que el prop `offset` escribiera el
MISMO nombre que la capa lee, y eso trajo dos problemas a la vez:
1. Un ciclo. `offset="var(--affix-offset)"` producia
`--affix-offset: var(--affix-offset)` — custom property auto-referencial, que
CSS resuelve a guaranteed-invalid, matando el calc() de los insets y dejandolos
en `auto`. Medido: la caja aterrizaba a 324px del borde que debia tocar. Y la
demo ofrecia ese valor COMO UNO DE LOS TRES CHIPS de `offset`, con la
intencion de decir «el default».
2. Redundancia. Como el prop pisaba el token publico, Fab no podia usarlo y tenia
que acunar `--fab-offset` / `--fab-z`, cuyo UNICO lector era el puente que los
traducia de vuelta a los nombres de la capa. Un vocabulario paralelo para
valores que la capa ya posee — justo la deriva que la capa venia a borrar.
La forma correcta ya la habla el arbol (`code.css` x6, `display.css` x6,
`calendar-select` x4, `button` x2, `dialog`, `kbd`): DOS nombres por eje.
--affix-offset / --affix-z publico, en :root, desde recipes/base.ts.
el default temeable, un retoque para todos.
--_affix-offset / --_affix-z la RANURA de override. Ahi escribe el wrapper
desde el prop, y ahi escribe un consumidor en su
puente cuando su default difiere.
Consumo: `var(--_affix-offset, var(--affix-offset))`. Con la ranura distinta del
token, el ciclo se vuelve imposible POR FORMA, no por aviso: escrito a mano,
`--_affix-offset: var(--affix-offset)` ahora resuelve a 16px en vez de morir.
El puente de Fab baja a dos lineas y pierde toda traduccion de tokens:
[data-fab][data-affix-placement] {
position: fixed; /* gana el empate con [data-button] */
--_affix-z: var(--z-index-sticky); /* el UNICO valor en que difiere */
}
Lo que Fab necesitaba no era un token propio sino un VALOR distinto: se queda en
la banda `sticky` y no en el peldano `affix` (150), para que un menu o un dialogo
sigan abriendose por encima del FAB.
La regla que esto fija para consumidores futuros queda escrita en los dos README:
un eje de capa tiene dos nombres —default publico y ranura— y el consumidor
escribe la ranura. Acunar `--{componente}-{eje}` al lado recrea la deriva.
Verificado midiendo. Affix: `default` sin style inline y 16px, `0px` a 0, `2rem`
a 32. Fab: `--fab-offset` y `--fab-z` resuelven a cadena vacia (retirados), y las
esquinas siguen a 16px, `fixed`, z 100 via la ranura; `static` sigue `relative` /
`auto`. Identico a antes byte a byte en comportamiento.
component:audit affix PASS 0/0 · fab PASS 0/2 · check 69 = base intacta ·
rtl 0/178 · blocks 0 · eidos-lint invalid 0 · suite eidos 151/151 · smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1fc0a7a96b |
refactor(eidos): Fab deja de tener su propia copia de la colocacion — la lee de la capa
Primera de las dos migraciones que dejo abiertas `Affix`. `fab.css` tenia cuatro reglas de esquina que eran una de TRES copias privadas del mismo CSS (menu-dial tiene nueve; `air/layout/float` las tuvo antes que las dos). Se borran: ahora `fab.svelte` importa `../affix/affix.css` y estampa `data-affix-placement`, y en `fab.css` queda un puente de tres lineas. Es la forma con la que las recetas de menu/listbox consumen `lib/list-surface.css`. El puente reafirma `position: fixed`, y no es redundancia: `button.css` declara `position: relative` sobre `[data-button]` a la MISMA especificidad (0,1,0) que la regla base de la capa, y los dos ficheros van code-split — en el orden de carga equivocado la capa pierde el empate y un FAB flotante computaria `relative` en silencio. Los ocho insets siguen viniendo de la capa; solo esa propiedad se pelea en local. Queda escrito en los dos README como lo que le debe al puente cualquier consumidor cuyo primitivo compuesto ya declare `position`. Nada cambia de comportamiento ni de API: - `--fab-offset` y `--fab-z` siguen siendo publicos de Fab; el puente apunta la capa a ellos en vez de sustituirlos. `--fab-z` se queda a proposito en la banda `sticky` y NO en el peldano `affix` (150) — un menu o un dialogo tienen que seguir abriendose por encima del FAB. - `placement="static"` ya no estampa gancho ninguno, asi que no casa nada y el consumidor lo coloca. La salida pasa a ser una ausencia en vez de una quinta regla. - `data-placement` desaparece del DOM del FAB; solo lo leia `fab.css`. Medido en las cinco colocaciones sobre la demo: las cuatro esquinas a 16px de sus dos bordes (`--fab-offset` puenteado), `fixed`, z 100 — identico a antes — y `static` sin gancho, `relative`, z `auto`. `MenuDial` intacto: conserva su `data-placement` propio y su trigger es un `Fab placement="static"`. Y queda probado en vivo el motivo del split de attrs: `morfo-check` NO juzga el `data-affix-placement` del `<button>` contra `affixMorfo`, porque se salta todo attr que no empiece por el kebab del propio morfo. component:audit fab PASS 0/2 (sus excepciones documentadas) · affix PASS 0/0 · rtl 0/178 · eidos-lint fab/affix invalid 0 · smoke 311/311. Los dos fallos previos de morfo:check (fab `data-fab-size`, menu-dial `data-state`) siguen exactamente igual: son ajenos a esto. Queda: migrar `MenuDial` (9 zonas), con su fallback `1100` y su offset interno. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
92c8f6466a |
feat(eidos): Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habian copiado dos veces
Un aviso al principio del documento se va con la pagina: `position: sticky` se despega en cuanto termina su contenedor. Medido el 2026-08-10 sobre el block `banner` — `Sticky edge="bottom"` acaba en `top: -8` sin `data-stuck`. No es un cableado roto: es que un elemento anclado al viewport es `fixed`, y eso es otro componente. Pero no es un componente nuevo: la capacidad YA existia dos veces. `fab.css` fija 4 esquinas, `menu-dial.css` nueve, y las dos ya divergian (`--fab-offset` publico vs `--_menu-dial-offset` interno; `var(--fab-z)` vs `var(--z-index-sticky, 1100)`, un fallback de 1100 sobre un token que vale 100). Y antes de eso existio entera: `air/layout/float`, el primitivo de 9 zonas que el refactor retiro sin sustituto — lo dicen los README de `float` y de `Fab`. Asi que `affix.css` no es un tercer ejemplar: es la CAPA, enganchada en `data-affix-placement` y no en `data-affix`. El split es portante — `morfo-check` selecciona `[data-affix]` en toda la pagina y valida cada match contra `affixMorfo`, asi que si `Fab` estampara la identidad quedaria soldado a este contrato. Estampando solo el gancho de capa, no. Es el patron de `lib/list-surface.css`: una fuente, cero nodos extra. Lo que salio midiendo, no razonando: - `--z-index-affix: 150`, peldano nuevo. Con la tira en `top` empataba con `--sticky-z-index` (100 los dos) y el empate lo rompe el ORDEN DEL DOM: el aviso va antes que la cabecera en el fuente, luego perdia. 30,7px de solape con el cromo encima — el fallo original reproducido por su sustituto. Un peldano toca DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`. - NO compone `<Box>`, aunque sea el patron de los primitivos de layout: `box.css` declara `position: var(--box-position, revert-layer)` a la misma especificidad que el gancho, y eidos no usa `@layer`. En el orden de carga equivocado, `position: static` — un `fixed` que no hace nada. - `stretch` es solo de los bordes de bloque. El rail del eje inline se envio y se retiro el mismo dia: `Affix` no dimensiona a su hijo, asi que era una caja invisible de 380px con el hijo de 47px arriba, identica a `top-start` en pantalla. El marco medido decia otra cosa; la pintura mandaba. - `env(safe-area-inset-*)` es fisico y los anclajes logicos: remapeado bajo `:dir(rtl)`. Emparejar `inset-inline-start` con `safe-area-inset-left` despeja la muesca equivocada en RTL apaisado — que es lo que hacen hoy `fab` y `menu-dial`, registrado. `banner` recupera lo que su README daba por imposible: `affix="top" | "bottom"`. La disposicion anterior decia «app-land, el canon ya trae Sticky» y nombraba un componente que no puede hacerlo. Verificado con recorrido real (342px en la demo, 1247px en la preview del block): desplazamiento 0 en los dos bordes, nueve zonas exactas, RTL espejando, el ancestro transformado derivando -342px como esta documentado, y `elementFromPoint` sobre la tira devolviendo el aviso y no el cromo. component:audit PASS 0/0 · morfo:check PASS · rtl 0/178 · smoke 311/311. Queda: migrar `Fab` y `MenuDial` a la capa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f1b672da79 |
docs: la doctrina alcanza a los cambios de la jornada — cinco backlogs y una llave de mas
Auditoria de deriva doc<->codigo sobre los 33 commits del 2026-08-13/14: cinco sitios seguian describiendo lo que el codigo dejo de hacer ayer. Se corrigen COMO BITACORA, al final de cada fichero (el cuerpo es el acta de lo que se firmo, no el estado del codigo; reescribirlo falsearia la firma). El formato es el que ya existia en `arts/adom` y `soma/textarea`: `## Backlog`. - `decisions/book-deviations.md` — dos entradas. D.7 nombraba `IntentExpectedFamily`, tipo que M6 ( |
2 months ago |