You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-blocks-quality.md

224 lines
18 KiB

feat(eidos): `Reveal` + `Backdrop` + cascada por viewport — la capa de acabado Auditoría del tier (docs/process/PLAN-blocks-quality.md): los 7 blocks de F2 componían EXCLUSIVAMENTE la capa de layout (Box/Stack/Grid/Section/Text/Card). Uso de la capa expresiva del framework: cero. Ni un Motion, ni un Cascade, ni un Display, ni un TextGradient, ni un CountUp. La causa es estructural, no de gusto: el contrato B prohíbe que un block traiga `.css`, así que **un block solo puede ser tan expresivo como los componentes del canon que compone** — y faltaban dos primitivas. El arreglo no es maquillar 14 blocks uno a uno, es construir lo que falta en el canon y componerlo. - **`Reveal`** — reproduce la ENTRADA de un preset al entrar en viewport. No posee keyframes ni timing: estampa `data-animation-style` + `data-state='open'` cuando dispara el observador, así que reduced-motion degrada DENTRO del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)`: sin JS la regla no aplica y el contenido se ve siempre — el fallo de una animación de scroll debe ser «sin animación», nunca «página en blanco». - **Escalonado gratis**: el foundation ya escribe `--motion-stagger-index` en cada hijo de `[data-stagger]` (por `:nth-child`, sin JS) y los presets retrasan por `índice × --motion-stagger-each`. El recipe de Reveal le da ritmo por defecto, así que una lista se escalona poniendo `data-stagger` en SU contenedor: cero delays a mano, cero números en el consumidor. - **`Cascade trigger="viewport"`** — la cascada existente esperaba a `open` (disclosure); ahora puede esperar también a ser vista. - **`Backdrop`** — la capa de profundidad que no existía: `glow` · `mesh` · `grid` · `dots`, pintada en un `::before` desde tokens (acento por el forward de paleta, mesh desde `--gradient-aurora`), con `fade` para no cortar en seco. **El hero como prueba del nuevo listón**: `Display` (el primitivo de tipografía de hero que existía y no usaba — el titular pasa de `Heading` a 80px reales), entrada escalonada de eyebrow/título/subtítulo/acciones, media con `scale-fade` y `decor` (glow por defecto) expuesto como control vivo en la demo. Verificado con Playwright: glow pintado desde el token de paleta, 4 Reveal con índices estructurales 0·1·2·3 a 70ms, titular H1 a 80px, y el patrón `grid` con su máscara. `blocks:check` verde (7 blocks) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
# PLAN — Subir el listón de los blocks (análisis comparativo + plan de mejora)
> Encargo del usuario (2026-07-24): *«todos los bloques hechos hasta ahora son
> simplones… no hay animaciones, no hay disposiciones o diseños que los hagan
> sobresalir… la peor versión de todos los que se analizaron»*. Este documento
> audita los 7 blocks hechos contra las referencias del dossier
> (`RESEARCH-blocks-references.md`) **y contra el propio framework**, y fija el
> plan para que cada block gane la comparación en vez de empatarla.
---
## 1. Diagnóstico: qué compone realmente cada block (dato, no opinión)
Auditoría de imports reales en `src/uix/blocks/*/*.svelte`:
| Block | Componentes que compone |
|---|---|
| site-header | box · container · drawer · group · sticky |
| hero | box · container · grid · group · **heading** · section · stack · text |
| feature-grid | auto-grid · box · container · heading · section · stack · surface · text |
| feature-split | box · container · grid · group · heading · icon · section · stack · text |
| pricing | auto-grid · box · card · container · group · heading · icon · section · stack · text · toggle-group |
| testimonials | auto-grid · box · card · container · group · section · stack · text |
| faq | accordion · box · container · section · stack |
**Los 7 componen EXCLUSIVAMENTE la capa de layout** (`Box`/`Stack`/`Grid`/
`Group`/`Section`/`Container`) + tipografía base (`Text`/`Heading`) + `Card`/
`Surface`, más el componente de comportamiento que cada uno necesita
(`Drawer`, `Sticky`, `Accordion`, `ToggleGroup`).
**Uso de la capa expresiva del framework: CERO.** Ni un `Motion`, ni un
`Cascade`, ni un `Display`, ni un `TextGradient`, ni un `CountUp`, ni un `Aura`.
### El framework SÍ tiene esa capa — y no la usamos
| Componente del canon | Qué hace | Dónde debería estar |
|---|---|---|
| **`Display`** | *«eidos single-component **hero typography** primitive»* | **el hero** (usé `Heading`) |
| **`Cascade`** | escalona (stagger) la entrada/salida de sus hijos | feature-grid, testimonials, pricing, stats |
| **`Motion`** | animador de un elemento con presets (`scale-fade`, `slide-fade`, `spring-pop`…) | entradas de cualquier pieza |
| **`TextGradient`** | degradado animado sobre texto real; acepta `colors="aurora"` | titulares de hero/CTA |
| **`CountUp`** | cuenta con spring **al entrar en viewport**, locale-aware, respeta reduced-motion | stats-band, pricing, métricas |
| **`Mockup`** | cromo de navegador/teléfono (lo construí en F2.3b) | hero, feature-split, testimonials |
| **`Aura`** | escena ambiental | fondos de sección |
| `TextBlur` · `TextFocus` · `TextScramble` · `TextCircular` | familia de efectos de texto | acentos puntuales |
| `Carousel` · `Timeline` · `Metrics` · `Image` | — | testimonials, stats, content |
### Por qué salieron planos: la causa es ESTRUCTURAL, no de gusto
El contrato B prohíbe que un block traiga `.css` propio (D-BLK.2). Es una regla
buena — mantiene el tier componiendo en vez de pintando. Pero implica una cosa
que no vi a tiempo:
> **Un block solo puede ser tan expresivo como los componentes del canon que
> compone.**
Con solo primitivas de layout disponibles y **sin primitiva de scroll-reveal ni
de fondo decorativo**, el resultado plano era el único posible. Mi error no fue
"no decorar los blocks": fue **no detectar que faltaban primitivas en el canon y
construirlas** (como sí hice con `Mockup`), y dar por hecho un block que compone
bien pero no gana la comparación.
**Corolario que dirige el plan**: la mejora NO es maquillar 14 blocks uno a uno.
Es **construir 2–3 primitivas que faltan y aplicarlas sistemáticamente** — sube
todo el tier a la vez y queda arquitectónicamente limpio.
---
## 2. Los dos huecos del canon que bloquean el acabado
### Hueco 1 — No hay scroll-reveal
`Cascade` escalona, pero se dispara por `open` (disclosure), no por viewport.
Toda la maquinaria existe ya a nivel de art:
- `eidos.dom.observeIntersection(el, cb)` (adom)
- `src/arts/adom/is-in-viewport.svelte.ts` (primitiva reactiva)
- `eidos.dom.prefersReducedMotion`
`CountUp` la usa **en privado**. Nada la expone como composable. Sin esto, cada
sección aparece de golpe: el rasgo nº1 que separa a Linear/Vercel/Stripe de un
dump estático.
fix(blocks): lo que la auditoría encontró DENTRO del tier Una auditoría multi-agente (7 dimensiones doctrinales + refutación adversarial) confirmó 32 hallazgos sobre la pasada de calidad. Esto corrige los que caen dentro de `blocks/`; la deuda de fuera queda REGISTRADA, no tocada (`PLAN-blocks-quality.md` §6). - **B-7 roto en `pricing`**: el block traía tres strings castellanas propias como defaults del toggle — dos visibles y un nombre accesible. Hardcodeaba un idioma DENTRO del framework y esquivaba `langs`, en el mismo block cuyo README predica «cero strings propias». Ahora los tres son props REQUERIDAS y las pone la app. - **El block pintaba**: `pricing.Plan` metía `box-shadow` por `style=` porque `Card` no expone elevación. Retirado; el hueco queda registrado como candidato a canon en vez de falseado. - **El escalonado estaba roto y el comentario mentía**: puse `spring-pop` (driver JS) en los planes, y un driver JS no lee el `animation-delay` donde vive el stagger — entraban TODOS a la vez mientras el comentario afirmaba lo contrario. Vuelto al preset CSS: verificado, índices 0,1,2 a 70ms. - **Comentario falso en la demo del hero**: atribuía al `intent` visual una consecuencia sonora/háptica que ningún camino produce (el morfo del Button dice explícitamente que `contact-activate` NO lleva intent). Reescrito. - **Composition maps mentían**: los 6 blocks que ahora componen `Motion` no lo decían; el hero seguía diciendo `Heading` (es `Display`), sin `Backdrop` ni `decor`, y declaraba DIFERIDO un preset de cromo de media que ya está shipeado (`Mockup`). Al día, sin filas duplicadas ni datos rancios (el chip de feature-grid es `solid`, no `soft`). - **Handoff y plan**: el plan seguía prescribiendo un `<Reveal>` que esta misma sesión creó y retiró; el handoff daba `feature-split` por «sin decidir» estando hecho, y los gates con números falsos. Verificado: `blocks:check` verde (7) · `svelte-check` sin errores propios · en el navegador, pricing escalona de nuevo, las etiquetas vienen de la app y ninguna tarjeta pinta sombra. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
→ **Resuelto**: `Motion` gana `trigger="viewport"` (NO un componente nuevo — `Reveal` se creó y se retiró por ser un fork) y `Cascade` gana el mismo disparo.
feat(eidos): `Reveal` + `Backdrop` + cascada por viewport — la capa de acabado Auditoría del tier (docs/process/PLAN-blocks-quality.md): los 7 blocks de F2 componían EXCLUSIVAMENTE la capa de layout (Box/Stack/Grid/Section/Text/Card). Uso de la capa expresiva del framework: cero. Ni un Motion, ni un Cascade, ni un Display, ni un TextGradient, ni un CountUp. La causa es estructural, no de gusto: el contrato B prohíbe que un block traiga `.css`, así que **un block solo puede ser tan expresivo como los componentes del canon que compone** — y faltaban dos primitivas. El arreglo no es maquillar 14 blocks uno a uno, es construir lo que falta en el canon y componerlo. - **`Reveal`** — reproduce la ENTRADA de un preset al entrar en viewport. No posee keyframes ni timing: estampa `data-animation-style` + `data-state='open'` cuando dispara el observador, así que reduced-motion degrada DENTRO del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)`: sin JS la regla no aplica y el contenido se ve siempre — el fallo de una animación de scroll debe ser «sin animación», nunca «página en blanco». - **Escalonado gratis**: el foundation ya escribe `--motion-stagger-index` en cada hijo de `[data-stagger]` (por `:nth-child`, sin JS) y los presets retrasan por `índice × --motion-stagger-each`. El recipe de Reveal le da ritmo por defecto, así que una lista se escalona poniendo `data-stagger` en SU contenedor: cero delays a mano, cero números en el consumidor. - **`Cascade trigger="viewport"`** — la cascada existente esperaba a `open` (disclosure); ahora puede esperar también a ser vista. - **`Backdrop`** — la capa de profundidad que no existía: `glow` · `mesh` · `grid` · `dots`, pintada en un `::before` desde tokens (acento por el forward de paleta, mesh desde `--gradient-aurora`), con `fade` para no cortar en seco. **El hero como prueba del nuevo listón**: `Display` (el primitivo de tipografía de hero que existía y no usaba — el titular pasa de `Heading` a 80px reales), entrada escalonada de eyebrow/título/subtítulo/acciones, media con `scale-fade` y `decor` (glow por defecto) expuesto como control vivo en la demo. Verificado con Playwright: glow pintado desde el token de paleta, 4 Reveal con índices estructurales 0·1·2·3 a 70ms, titular H1 a 80px, y el patrón `grid` con su máscara. `blocks:check` verde (7 blocks) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
### Hueco 2 — No hay fondo decorativo de sección
No existe `backdrop`/`pattern`/`glow`/`mesh`/`noise`/`spotlight` en el canon.
Solo hay **un** gradiente nombrado (`--gradient-aurora`) y los acabados de
`Surface`. Las referencias viven de esto (glow radial tras el hero, rejilla de
puntos, mesh, viñeta).
→ **Candidato a canon: `<Backdrop>`** (patrones + glow + mesh sobre tokens).
---
## 3. Comparativa por block: anatomía (dossier) + acabado (nuevo)
**A** = brecha de anatomía ya registrada en el dossier §P1.
**B** = brecha de acabado (nueva, la que motivó este plan).
**C** = superación que podemos meter y ninguna referencia puede.
| Block | A · anatomía que falta | B · acabado que falta | C · superación disponible |
|---|---|---|---|
| **hero** | presets de media ✔(Mockup) · **form-in-hero** · logo-strip como slot · video | `Display` en vez de `Heading` · `TextGradient` en el titular · `Backdrop` (glow/mesh) · reveal escalonado · parallax suave del mockup | tipografía fluida por tokens + dark/RTL/density gratis |
| **feature-grid** | variante en `Card` · numerada | `Cascade` al entrar · hover con elevación/acento en el item · iconos con más presencia | stagger con reduced-motion correcto |
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **feature-split** | ~~banda de fondo por fila~~ → **por SECCIÓN** (enmienda firmada 2026-08-18, ver nota abajo) · media pegajosa | reveal por fila (media y copy con desfase) · ~~`Backdrop` alterno~~ → `Background` en la sección | `Mockup` real (ninguna ref lo compone, lo dibujan a mano) |
feat(eidos): `Reveal` + `Backdrop` + cascada por viewport — la capa de acabado Auditoría del tier (docs/process/PLAN-blocks-quality.md): los 7 blocks de F2 componían EXCLUSIVAMENTE la capa de layout (Box/Stack/Grid/Section/Text/Card). Uso de la capa expresiva del framework: cero. Ni un Motion, ni un Cascade, ni un Display, ni un TextGradient, ni un CountUp. La causa es estructural, no de gusto: el contrato B prohíbe que un block traiga `.css`, así que **un block solo puede ser tan expresivo como los componentes del canon que compone** — y faltaban dos primitivas. El arreglo no es maquillar 14 blocks uno a uno, es construir lo que falta en el canon y componerlo. - **`Reveal`** — reproduce la ENTRADA de un preset al entrar en viewport. No posee keyframes ni timing: estampa `data-animation-style` + `data-state='open'` cuando dispara el observador, así que reduced-motion degrada DENTRO del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)`: sin JS la regla no aplica y el contenido se ve siempre — el fallo de una animación de scroll debe ser «sin animación», nunca «página en blanco». - **Escalonado gratis**: el foundation ya escribe `--motion-stagger-index` en cada hijo de `[data-stagger]` (por `:nth-child`, sin JS) y los presets retrasan por `índice × --motion-stagger-each`. El recipe de Reveal le da ritmo por defecto, así que una lista se escalona poniendo `data-stagger` en SU contenedor: cero delays a mano, cero números en el consumidor. - **`Cascade trigger="viewport"`** — la cascada existente esperaba a `open` (disclosure); ahora puede esperar también a ser vista. - **`Backdrop`** — la capa de profundidad que no existía: `glow` · `mesh` · `grid` · `dots`, pintada en un `::before` desde tokens (acento por el forward de paleta, mesh desde `--gradient-aurora`), con `fade` para no cortar en seco. **El hero como prueba del nuevo listón**: `Display` (el primitivo de tipografía de hero que existía y no usaba — el titular pasa de `Heading` a 80px reales), entrada escalonada de eyebrow/título/subtítulo/acciones, media con `scale-fade` y `decor` (glow por defecto) expuesto como control vivo en la demo. Verificado con Playwright: glow pintado desde el token de paleta, 4 Reveal con índices estructurales 0·1·2·3 a 70ms, titular H1 a 80px, y el patrón `grid` con su máscara. `blocks:check` verde (7 blocks) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
| **pricing** | **tabla de comparación** · single-price | transición del precio al cambiar periodo (`Motion`) · destaque del featured (glow/escala) · `CountUp` en el importe | precio locale-aware (`FormatNumber`) + toggle con estado real |
| **testimonials** | **cita en spotlight** (la variante modal de TODAS las refs) | `Cascade` · logos de empresa · comillas decorativas · avatar con anillo | comillas locale-aware (territorio sin reclamar) |
| **faq** | **lista estática 2/3 col** (6 de 7 en TW) · cola de soporte como slot | transición de apertura con carácter · numeración/iconografía | teclado+ARIA del `Accordion` del canon |
| **site-header** | flyout ✔ · banner hermano | fondo con blur al pegarse (`[data-stuck]` ya lo da) · transición de la sombra | `data-stuck` = polyfill del `scroll-state()` futuro |
| **stats-band** *(pendiente)* | split-with-image · timeline | reveal + `Backdrop` | **`CountUp`: ninguna ref puede shippearlo** (markup estático) — la superación literal |
| **cta** *(pendiente)* | arreglo **justified** · split-with-media | `Backdrop` + gradiente de acabado | — |
| **newsletter** *(pendiente)* | nota de privacidad | estados (enviando/éxito) con `Motion` | validación real del `Form` del canon |
| **site-footer** *(pendiente)* | newsletter en footer · **selector de idioma** | — | el selector de idioma encaja con `langs` (ninguna ref lo tiene funcional) |
| **banner · team · contact · content-section** *(pendientes)* | — | al nivel nuevo desde el día 1 | — |
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Enmienda 2026-08-18 · la banda de `feature-split` va por SECCIÓN.** La columna
> B pedía `Backdrop` alterno POR FILA. Ejecutado en F5 del eje Background quedó en
> la sección, y la enmienda confirma esa forma: una banda por fila obliga a cada
> `Row` a poseer el estado de alternancia —saber su índice y el de sus hermanas—, y
> eso es coordinación entre partes. Un block que coordina deja de ser un block que
> no posee nada (doctrina de `blocks.md`). Si la alternancia se quisiera de todos
> modos, el lugar correcto no es el block: es un selector de la receta
> (`:nth-child`), y entonces es una decisión de eidos, no de composición.
feat(eidos): `Reveal` + `Backdrop` + cascada por viewport — la capa de acabado Auditoría del tier (docs/process/PLAN-blocks-quality.md): los 7 blocks de F2 componían EXCLUSIVAMENTE la capa de layout (Box/Stack/Grid/Section/Text/Card). Uso de la capa expresiva del framework: cero. Ni un Motion, ni un Cascade, ni un Display, ni un TextGradient, ni un CountUp. La causa es estructural, no de gusto: el contrato B prohíbe que un block traiga `.css`, así que **un block solo puede ser tan expresivo como los componentes del canon que compone** — y faltaban dos primitivas. El arreglo no es maquillar 14 blocks uno a uno, es construir lo que falta en el canon y componerlo. - **`Reveal`** — reproduce la ENTRADA de un preset al entrar en viewport. No posee keyframes ni timing: estampa `data-animation-style` + `data-state='open'` cuando dispara el observador, así que reduced-motion degrada DENTRO del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)`: sin JS la regla no aplica y el contenido se ve siempre — el fallo de una animación de scroll debe ser «sin animación», nunca «página en blanco». - **Escalonado gratis**: el foundation ya escribe `--motion-stagger-index` en cada hijo de `[data-stagger]` (por `:nth-child`, sin JS) y los presets retrasan por `índice × --motion-stagger-each`. El recipe de Reveal le da ritmo por defecto, así que una lista se escalona poniendo `data-stagger` en SU contenedor: cero delays a mano, cero números en el consumidor. - **`Cascade trigger="viewport"`** — la cascada existente esperaba a `open` (disclosure); ahora puede esperar también a ser vista. - **`Backdrop`** — la capa de profundidad que no existía: `glow` · `mesh` · `grid` · `dots`, pintada en un `::before` desde tokens (acento por el forward de paleta, mesh desde `--gradient-aurora`), con `fade` para no cortar en seco. **El hero como prueba del nuevo listón**: `Display` (el primitivo de tipografía de hero que existía y no usaba — el titular pasa de `Heading` a 80px reales), entrada escalonada de eyebrow/título/subtítulo/acciones, media con `scale-fade` y `decor` (glow por defecto) expuesto como control vivo en la demo. Verificado con Playwright: glow pintado desde el token de paleta, 4 Reveal con índices estructurales 0·1·2·3 a 70ms, titular H1 a 80px, y el patrón `grid` con su máscara. `blocks:check` verde (7 blocks) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
---
## 4. Plan
### F-Q0 · Primitivas que faltan en el canon *(la palanca)*
| # | Pieza | Por qué |
|---|---|---|
fix(blocks): lo que la auditoría encontró DENTRO del tier Una auditoría multi-agente (7 dimensiones doctrinales + refutación adversarial) confirmó 32 hallazgos sobre la pasada de calidad. Esto corrige los que caen dentro de `blocks/`; la deuda de fuera queda REGISTRADA, no tocada (`PLAN-blocks-quality.md` §6). - **B-7 roto en `pricing`**: el block traía tres strings castellanas propias como defaults del toggle — dos visibles y un nombre accesible. Hardcodeaba un idioma DENTRO del framework y esquivaba `langs`, en el mismo block cuyo README predica «cero strings propias». Ahora los tres son props REQUERIDAS y las pone la app. - **El block pintaba**: `pricing.Plan` metía `box-shadow` por `style=` porque `Card` no expone elevación. Retirado; el hueco queda registrado como candidato a canon en vez de falseado. - **El escalonado estaba roto y el comentario mentía**: puse `spring-pop` (driver JS) en los planes, y un driver JS no lee el `animation-delay` donde vive el stagger — entraban TODOS a la vez mientras el comentario afirmaba lo contrario. Vuelto al preset CSS: verificado, índices 0,1,2 a 70ms. - **Comentario falso en la demo del hero**: atribuía al `intent` visual una consecuencia sonora/háptica que ningún camino produce (el morfo del Button dice explícitamente que `contact-activate` NO lleva intent). Reescrito. - **Composition maps mentían**: los 6 blocks que ahora componen `Motion` no lo decían; el hero seguía diciendo `Heading` (es `Display`), sin `Backdrop` ni `decor`, y declaraba DIFERIDO un preset de cromo de media que ya está shipeado (`Mockup`). Al día, sin filas duplicadas ni datos rancios (el chip de feature-grid es `solid`, no `soft`). - **Handoff y plan**: el plan seguía prescribiendo un `<Reveal>` que esta misma sesión creó y retiró; el handoff daba `feature-split` por «sin decidir» estando hecho, y los gates con números falsos. Verificado: `blocks:check` verde (7) · `svelte-check` sin errores propios · en el navegador, pricing escalona de nuevo, las etiquetas vienen de la app y ninguna tarjeta pinta sombra. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
| Q0.1 | ~~`Reveal`~~ → **`Motion trigger="viewport"`** (+ `rootMargin`, `threshold`, `once`, `delay`) | HECHO. Se creó un `Reveal` aparte y se consolidó: era un fork de `Motion`, que ya era el animador de un elemento — solo le faltaba el CUÁNDO |
| Q0.2 | **`Cascade` por viewport** — `trigger="viewport"` además de `open` | HECHO |
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
| Q0.3 | **`Backdrop`** — fondo de sección: `glow` · `mesh` · `grid` · `dots`, todo sobre tokens | HECHO — y **superado el 2026-08-18** por [`Background`](../../src/uix/eidos/components/background/README.md), que lo absorbe: las cuatro tramas viajan valor por valor a `Background.Pattern` (más `lines` · `noise` · `rings` · `vignette`), y donde `Backdrop` envolvía a su contenido y pintaba UNA capa en un `::before`, la pila se cuelga del anfitrión como hija y apila cuantas capas haga falta — imagen, vídeo, gradiente, velo. El plan y las decisiones firmadas: [`PLAN-background.md`](./PLAN-background.md) |
feat(eidos): `Reveal` + `Backdrop` + cascada por viewport — la capa de acabado Auditoría del tier (docs/process/PLAN-blocks-quality.md): los 7 blocks de F2 componían EXCLUSIVAMENTE la capa de layout (Box/Stack/Grid/Section/Text/Card). Uso de la capa expresiva del framework: cero. Ni un Motion, ni un Cascade, ni un Display, ni un TextGradient, ni un CountUp. La causa es estructural, no de gusto: el contrato B prohíbe que un block traiga `.css`, así que **un block solo puede ser tan expresivo como los componentes del canon que compone** — y faltaban dos primitivas. El arreglo no es maquillar 14 blocks uno a uno, es construir lo que falta en el canon y componerlo. - **`Reveal`** — reproduce la ENTRADA de un preset al entrar en viewport. No posee keyframes ni timing: estampa `data-animation-style` + `data-state='open'` cuando dispara el observador, así que reduced-motion degrada DENTRO del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)`: sin JS la regla no aplica y el contenido se ve siempre — el fallo de una animación de scroll debe ser «sin animación», nunca «página en blanco». - **Escalonado gratis**: el foundation ya escribe `--motion-stagger-index` en cada hijo de `[data-stagger]` (por `:nth-child`, sin JS) y los presets retrasan por `índice × --motion-stagger-each`. El recipe de Reveal le da ritmo por defecto, así que una lista se escalona poniendo `data-stagger` en SU contenedor: cero delays a mano, cero números en el consumidor. - **`Cascade trigger="viewport"`** — la cascada existente esperaba a `open` (disclosure); ahora puede esperar también a ser vista. - **`Backdrop`** — la capa de profundidad que no existía: `glow` · `mesh` · `grid` · `dots`, pintada en un `::before` desde tokens (acento por el forward de paleta, mesh desde `--gradient-aurora`), con `fade` para no cortar en seco. **El hero como prueba del nuevo listón**: `Display` (el primitivo de tipografía de hero que existía y no usaba — el titular pasa de `Heading` a 80px reales), entrada escalonada de eyebrow/título/subtítulo/acciones, media con `scale-fade` y `decor` (glow por defecto) expuesto como control vivo en la demo. Verificado con Playwright: glow pintado desde el token de paleta, 4 Reveal con índices estructurales 0·1·2·3 a 70ms, titular H1 a 80px, y el patrón `grid` con su máscara. `blocks:check` verde (7 blocks) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
Los tres siguen el patrón `Mockup`: componente eidos + morfo mínimo, recipe
sobre tokens, cero color a mano.
### F-Q1 · Pasada de acabado a los 7 hechos
Por block: aplicar A (anatomía) + B (acabado) de la tabla §3. Orden sugerido por
impacto: **hero → pricing → feature-split → testimonials → feature-grid → faq →
site-header**. El hero primero como **prueba del nuevo listón**: si no gana
al lado de la referencia, se itera antes de replicar.
### F-Q2 · Los 7 pendientes, ya al nivel nuevo
stats-band (con `CountUp` — la superación literal) → cta → newsletter →
site-footer → banner → team → contact → content-section.
### F-Q3 · Página compuesta como prueba de aceptación
Todas las refs shippean páginas compuestas (TW 10 · Untitled 105). La nuestra
—los 14 blocks en una landing real— es el test de que el conjunto respira.
### Cambio en la definición de "hecho"
Un block **no** está hecho cuando compone bien y pasa `blocks:check`. Está hecho
cuando, **puesto al lado del mejor ejemplo de las referencias, gana**. Añadir al
checklist del contrato B: ¿tiene movimiento? ¿profundidad/fondo que lo
distinga? ¿una disposición que no sea la rejilla obvia? ¿tipografía con
presencia?
---
## 5. Riesgos
- **Sobre-animar**: el movimiento debe respetar `prefers-reduced-motion` (la
maquinaria ya lo hace) y no retrasar la lectura. Presets sobrios, no circo.
- **Romper el contrato B**: nada de `.css` en blocks — todo el acabado entra por
primitivas del canon. Si algo no se puede, es que falta una primitiva.
- **Regresiones**: `Reveal`/`Backdrop` tocan el canon → suite eidos + barrido de
demos, como en los fixes de `Grid` y `Accordion`.
fix(blocks): lo que la auditoría encontró DENTRO del tier Una auditoría multi-agente (7 dimensiones doctrinales + refutación adversarial) confirmó 32 hallazgos sobre la pasada de calidad. Esto corrige los que caen dentro de `blocks/`; la deuda de fuera queda REGISTRADA, no tocada (`PLAN-blocks-quality.md` §6). - **B-7 roto en `pricing`**: el block traía tres strings castellanas propias como defaults del toggle — dos visibles y un nombre accesible. Hardcodeaba un idioma DENTRO del framework y esquivaba `langs`, en el mismo block cuyo README predica «cero strings propias». Ahora los tres son props REQUERIDAS y las pone la app. - **El block pintaba**: `pricing.Plan` metía `box-shadow` por `style=` porque `Card` no expone elevación. Retirado; el hueco queda registrado como candidato a canon en vez de falseado. - **El escalonado estaba roto y el comentario mentía**: puse `spring-pop` (driver JS) en los planes, y un driver JS no lee el `animation-delay` donde vive el stagger — entraban TODOS a la vez mientras el comentario afirmaba lo contrario. Vuelto al preset CSS: verificado, índices 0,1,2 a 70ms. - **Comentario falso en la demo del hero**: atribuía al `intent` visual una consecuencia sonora/háptica que ningún camino produce (el morfo del Button dice explícitamente que `contact-activate` NO lleva intent). Reescrito. - **Composition maps mentían**: los 6 blocks que ahora componen `Motion` no lo decían; el hero seguía diciendo `Heading` (es `Display`), sin `Backdrop` ni `decor`, y declaraba DIFERIDO un preset de cromo de media que ya está shipeado (`Mockup`). Al día, sin filas duplicadas ni datos rancios (el chip de feature-grid es `solid`, no `soft`). - **Handoff y plan**: el plan seguía prescribiendo un `<Reveal>` que esta misma sesión creó y retiró; el handoff daba `feature-split` por «sin decidir» estando hecho, y los gates con números falsos. Verificado: `blocks:check` verde (7) · `svelte-check` sin errores propios · en el navegador, pricing escalona de nuevo, las etiquetas vienen de la app y ninguna tarjeta pinta sombra. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
---
## 6. Auditoría posterior (2026-07-29) — deuda que dejó esta pasada
Una auditoría multi-agente (7 dimensiones doctrinales + refutación adversarial)
confirmó **32 hallazgos** sobre el trabajo de la pasada. Lo corregido dentro del
tier queda en los README de cada block; lo que cae **fuera de `blocks/`** queda
registrado aquí como deuda, sin tocar:
| # | Dónde | Qué |
|---|---|---|
| A1 | `eidos/components/motion/motion.svelte` | con `trigger='viewport'` no estampa `data-state` hasta que dispara el observador, y su morfo lo declara `always`+`required`. `Cascade` —construido en la misma pasada— sí lo hace bien: estampa el estado y deja la espera en un attr aparte |
| A2 | `motion.css` · `cascade.css` | el `opacity: 0` de espera solo se levanta con la media query del SO; la preferencia de app proyectada como `data-motion='reduce'` NO lo levanta |
| A4 | `motion.svelte` (`leave`) | la salida no pasa por el motor: un preset JS no anima al salir y el nodo se retiene 200 ms |
| C7 | `motion.css` + su README | crear el recipe activó la regla de auditoría R-1.1 (sin selector raíz `[data-motion]`) y el README declara la excepción CONTRARIA («ships no CSS») y sigue diciendo «no JS engine» |
| C8 | `eidos/components/{mockup,backdrop}` | sin README (162 de 165 componentes lo tienen) |
| C10 | `docs/theming/motion.md` | no documenta el `trigger` nuevo ni la llamada al motor |
| D12 | `morfo/components/backdrop.ts` · `cascade.css` | citan `reveal.ts`, borrado; y `Cascade` emite `data-reveal-pending` — nombre de un componente que ya no existe — para el mismo concepto que `Motion` llama `data-animation-pending` |
| E14 | `motion.css:42` | `--motion-stagger-each-default` no existe en ninguna parte: el knob es ficción y siempre vale el literal 70ms. Ya existe el canónico `--motion-stagger` |
feat(blocks): `cta` — el panel que pide el siguiente paso, y tres mentiras que destapó al medirlo F2.8. Slots de snippet (eyebrow · title · description · actions · children), misma forma que `hero`: las partes de un CTA no repiten ni coordinan, así que un compound no se gana nada. Dos disposiciones: `center` para cerrar una página y `justified` —copia al inicio, acciones al final— que es el hueco que nombraba el dossier. Un solo `Motion trigger="viewport"`: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Lo que salió al verificarlo en navegador, medido y no a ojo: - `variant='soft'` **fuera de la API**. Su track queda a `oklch(0.9932)` contra un `--color-surface-default` de `oklch(0.9911)`: 0.002 de luminancia, o sea ningún panel en claro. Y `Surface` no tiene borde al que caer. Cortar el prop es más barato que shipear un estado que se esfuma. - La ranura `contrast` de la paleta es blanco en TODO escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: `primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos; `neutral` 3.32 · `teal` 3.07 fallan en claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan. - `Text align` es inerte por defecto: renderiza un `span`, y `text-align` no hace nada sobre una caja inline. `align="center"` dejaba la copia a la izquierda dentro del layout centrado, sin avisar. Rodeado con `as="p"`. Y `Group` no apila: a 420px la etiqueta de la acción secundaria se parte contra el botón primario, así que las acciones van en `Flex direction={{ base: 'column', sm: 'row' }}`. `hero` compone las suyas con `Group` — anotado. Los tres hallazgos de canon quedan en los gaps del README del block y en `PLAN-blocks-quality.md` §6 (F15/F16/F17), sin tocar nada fuera del tier. Verificado: `center` y `justified` en claro/oscuro/RTL y a 420px, tres colores, entrada disparada, descripción en `<p>` centrada, cero errores de página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Añadidos al construir `cta` (2026-07-30) — medidos en navegador
Tres cosas que salieron al componer F2.8, todas verificadas con sonda de
luminancia / `getComputedStyle`, no a ojo. Caen fuera de `blocks/`, así que
tampoco se han tocado:
| # | Dónde | Qué |
|---|---|---|
| F15 | `eidos/components/surface` | `variant='soft'` no puede acotar un panel: el track de `primary` mide `oklch(0.9932 0.0034 325.6)` contra un `--color-surface-default` de `oklch(0.9911 0 0)` — **0.002 L** — y `Surface` no tiene prop de borde. `Card outline` sí acota pero no acepta `gradient`, así que «panel sosegado con borde» no tiene primitivo. Por eso `cta` **eliminó** su prop `variant` |
| F16 | paleta (`contrast` slot) | la ranura de contraste es `#ffffff` en **todo** escalón sólido, así que un lienzo de luminancia media deja el cuerpo de texto por debajo de AA. Medido sobre el panel: `primary` 5.18 · `indigo` 5.21 · `plum` 4.75 (pasan en ambos modos) vs `neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 (fallan en claro). La garantía de emparejamiento solo se cumple en los lienzos oscuros |
| F17 | `eidos/components/text` | `align` es **inerte** por defecto: el componente renderiza un `span` y `text-align` no hace nada sobre una caja inline. `align="center"` dejó la copia alineada a la izquierda dentro de un layout centrado sin avisar de nada. Se rodea con `as="p"`, pero el prop anuncia un efecto que no tiene hasta que el consumidor cambia el elemento |
feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho F2.9. Slots de snippet (eyebrow · title · description · field · submit · note · children), `center` y `justified`, y `panel` como interruptor real: el panel de marca es la cara de las referencias, pero el control, la ayuda y el error están calibrados para la superficie de página, así que apagarlo es un estado de primera y no un fallback. La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la pista libre y la acción abraza su contenido —eso es lo que hace que un alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado de un campo de correo deja inservibles a los dos. El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/ touched, la agregación de errores y el foco al primer error; `Field` posee el cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`. El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí. ## Superación del dossier: la etiqueta El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel` —arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar. ## Tres hallazgos de canon más, medidos - **La fundación de eidos no trae reset de modelo de caja y lo asume del app.** `[data-field-control]` declara `inline-size: 100%` + padding, así que bajo `content-box` el control mide 30px más que su contenedor: el campo se metía por debajo del botón de envío. Campo 480 / control 510 en la galería frente a 502 / 502 en los docs de componentes, que sí resetean (igual que `web/routes/active/styles.css`). Arreglado en app-land con `web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés, anotada en el handoff. - **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya construido: el componente solo lo reenvía al `createForm` que hace él mismo. El envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no expone el prop: el handler va en el `createForm` del app. - **Los mensajes de SIUM son idlangref.** La vía correcta es `uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una dirección de correo válida»—, pero la demo de docs del propio `Form` parte la cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés. Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6 (F18/F19/F20), sin tocar nada fuera del tier. Verificado en navegador el arco completo: correo inválido → error traducido con `role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo válido → confirmación del app (`Callout` afirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Añadidos al construir `newsletter` (2026-07-30) — el tier de formulario
| # | Dónde | Qué |
|---|---|---|
| F18 | fundación de `eidos` (`index.css`) | **No hay reset de modelo de caja.** Recetas como `[data-field-control]` declaran `inline-size: 100%` + `padding-inline`, así que bajo `content-box` el control mide **30px más que su contenedor**: en la fila `1fr auto` del newsletter el campo se metía por debajo del botón de envío. Medido: campo 480 / control 510 en la galería de blocks frente a 502 / 502 en los docs de componentes, cuyo `uix.css` resetea `box-sizing` bajo `[data-uix-docs]` (y `web/routes/active/styles.css` hace lo mismo). O sea: el framework **asume** que el app pone `border-box` y esa asunción solo está documentada porque app-land la cumple dos veces. Arreglado en mi lado con `web/routes/blocks/_lib/reset.css` (A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y NADA más). La pregunta de canon es si la fundación debería poseerlo en vez de asumirlo |
| F19 | `soma/components/form` (`form.svelte` + `types.ts`) | **`onValidSubmit` / `onInvalidSubmit` son no-op silenciosos cuando se pasa un `form` ya construido.** El componente solo los reenvía al `createForm` que hace él mismo (la rama `defaults`); con un handle externo se ignoran sin aviso, aunque el tipo los documenta como «Called when validation passes». El envío validaba, limpiaba el error y no anunciaba nada. El block los quitó de su API: el handler va en `createForm` |
| F20 | `libs/forms` + `web/routes/uix/components/form` | Los mensajes de SIUM llegan como **idlangref** (`#?sium.errors.email|Must be a valid email address`). La vía soportada es `uix.langs.t(issue.message, issue.params)` — verificado, resuelve el catálogo («Debe ser una dirección de correo válida»), no el fallback inglés. Pero la demo de docs del propio `Form` **parte la cadena a mano** tras el `|` con un helper local `fallbackMessage`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés |
feat(blocks): `site-footer` — el pie de la página, con el selector de idioma vivo F2.10. Es el block donde la regla de forma del tier se ve mejor: `Column` SE REPITE (el app mapea sobre N grupos), así que es parte compound; la marca, la banda de alta, lo social, lo legal y el `extra` no se repiten ni coordinan, así que son slots. El landmark es un `<footer>` de verdad: `contentinfo` sin pedir nada. Las columnas van en `AutoGrid minChildWidth`, **sin un solo breakpoint**: el mismo código sirve para 2 grupos o para 5, y en móvil caen a dos. La entrada es un `Motion trigger="viewport"` solo en la región superior y **sin escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie lee un pie columna por columna. ## Los dos huecos que nombraba el dossier, cerrados - **La banda de alta al boletín EN el pie** (3 de 7 en Tailwind Plus) como slot `signup`, con su propia fila a lo ancho: en la columna de la marca (~230px) la fila de correo tendría que apilarse. El formulario es del app; este block NO importa el block `newsletter` (B-10) ni inventa validación. - **El selector de idioma** como `extra` (el slot libre de D-BLK.6), y está VIVO: escribe `uix.prefs.setIntent('language', …)`, el eje real del ecosistema. Verificado — al elegir «English», `document.documentElement.lang` pasa a `en`. ## Tres números que salieron de medir, no de suponer - El `container` bajó de `xl` a **`lg`**: con `xl` el pie no se alineaba con ninguna sección de la página compuesta encima. - La rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`**: con `2fr`, un pie de cuatro grupos se partía en 3+1. - El gap entre columnas es **6, no 8**: con 8, cinco grupos no comparten fila al ancho `lg` (728px justos). Horizontal más apretado que vertical, porque el gap horizontal es el que decide cuántos grupos caben. ## Dos hallazgos de canon (F21/F22 en `PLAN-blocks-quality.md` §6) - **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading` aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`, `Container`, `Section`— renderiza un `<div>` fijo. Consecuencia: una columna de enlaces no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un block solo puede elegir entre divs o escribir markup que luego no puede estilar. - **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.** `getDisplayText()` resuelve contra un registro de etiquetas que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado no hay ninguno montado: `value=['es']` pintaba «es» en vez de «Español». Rodeado con el `child` de `Select.Value`. Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal, separador `aria-hidden`, los tres `IconButton` con nombre obligatorio, y el idioma cambiando de verdad. Cero errores de página. Gates: `blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### Añadidos al construir `site-footer` (2026-07-30)
| # | Dónde | Qué |
|---|---|---|
uix(section): la sección de una página es un componente, no un esqueleto que cada block reescribe Los 19 blocks del tier repetían el mismo chasis: un <section {...rest}> escrito a mano, dentro Section (el aire), dentro Container (la medida) y a veces Background (la decoración). Medido antes de tocar: containerSize recableado en 16 blocks, sectionSize en 15, decor en 9, y OCHO copias del mismo Header — Box maxWidth 48rem + Motion viewport + Stack gap 3—, una de las cuales ya había derivado en silencio: la de article-grid perdió su Motion. La regla del propio tier dice que una parte se gana el compound cuando SE REPITE. Aquí lo que se repite son los blocks, así que la repetición se gana un componente. Y no uno nuevo: ese componente ya existía a medias y se llama Section. Crece en vez de nacer un hermano — un segundo componente obligaría a explicar en cada README en qué se diferencia de Section, y la explicación sería «Section con tres props más». Section gana, todo opcional: containerSize (compone Container DENTRO, y sin él los hijos se renderizan igual que siempre), decor (pinta Background.Pattern como HIJO, para que la banda sangre a todo el ancho por detrás de la medida sin ganar caja) y la parte Section.Header. Y renderiza un <section> real. Renderizaba un <div> a propósito, con la nota «si un consumidor necesita un <section> de verdad para el landmark, que lo envuelva» — y nueve blocks hacían exactamente eso, pagando un nodo extra para poder nombrarse. Un <section> sin nombre NO es landmark (se expone como region sólo al llevar nombre accesible, que es lo que A-109 dejó medido esta misma semana), así que el tag no ensucia el árbol de nadie y la raíz duplicada desaparece. Para eso Box gana `as`, que cierra F21 («los primitivos de layout no pueden cambiar de elemento»): lista CERRADA de elementos contenedores, nunca void — es una escotilla semántica, no un prop de tag libre—, default 'div', y el modelo de caja, las vars y la receta idénticos rinda lo que rinda. Los ≥2 casos reales que la decisión original pedía para reconsiderarlo llegaron hace tiempo: la columna de enlaces del pie que debía ser ul/li, y estos dieciséis blocks. Migrados DOS como prueba, uno por clase: feature-grid (medida) y stats-band (medida + decoración). Cada uno pierde dos niveles de anidamiento y su <section> duplicado — medido en navegador: [data-section] ES el <section>, el patrón 'grid' sigue siendo su hijo, 1280 de ancho, 64px de aire, 1024 de medida, y el documento pasa de 3 elementos <section> a 2. Los otros 17 van en tandas aparte. ⚠️ El paso de tipos que costó los únicos 5 errores de esta rama, y que lo explica una regla ya escrita en el propio types.ts de stats-band: «sub-parts extend the props of the canon component they wrap, never HTMLAttributes: the raw attribute surface clashes with the canon's refined style/class when spread through (the feature-grid lesson)». Ahora la RAÍZ también envuelve un componente del canon, así que sigue la misma regla: FeatureGridProps y StatsBandProps extienden SectionProps. Gates: svelte-check 72/62 antes y después · vitest eidos+blocks 470/471 (el rojo es skin-media-player, ajeno y documentado) · blocks:check 0/19 · docs:check 0/0. Prosa al día: README de Box y de Section, la demo de Section (que anunciaba la limitación en cuatro sitios) y F21 cerrado en PLAN-blocks-quality §6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| ~~F21~~ **CERRADO 2026-08-19** — `Box` gana `as` (lista cerrada de elementos contenedores, default `div`), así que `Stack`/`Flex`/`Grid`/`Group`/`Wrap`/`Container`/`Section` heredan el polimorfismo; `Section` lo usa para renderizar `<section>`. La columna de enlaces del pie ya puede ser `ul`/`li`. | `eidos/components/box` (y todo lo construido sobre él) | **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading` aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`, `Container`, `Section`— renderiza un `<div>` fijo. Consecuencia concreta: una columna de enlaces de pie no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un block solo puede elegir entre divs o escribir markup que luego no puede estilar (contrato B). La asimetría (tipográficos sí, layout no) ES el hallazgo |
feat(blocks): `site-footer` — el pie de la página, con el selector de idioma vivo F2.10. Es el block donde la regla de forma del tier se ve mejor: `Column` SE REPITE (el app mapea sobre N grupos), así que es parte compound; la marca, la banda de alta, lo social, lo legal y el `extra` no se repiten ni coordinan, así que son slots. El landmark es un `<footer>` de verdad: `contentinfo` sin pedir nada. Las columnas van en `AutoGrid minChildWidth`, **sin un solo breakpoint**: el mismo código sirve para 2 grupos o para 5, y en móvil caen a dos. La entrada es un `Motion trigger="viewport"` solo en la región superior y **sin escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie lee un pie columna por columna. ## Los dos huecos que nombraba el dossier, cerrados - **La banda de alta al boletín EN el pie** (3 de 7 en Tailwind Plus) como slot `signup`, con su propia fila a lo ancho: en la columna de la marca (~230px) la fila de correo tendría que apilarse. El formulario es del app; este block NO importa el block `newsletter` (B-10) ni inventa validación. - **El selector de idioma** como `extra` (el slot libre de D-BLK.6), y está VIVO: escribe `uix.prefs.setIntent('language', …)`, el eje real del ecosistema. Verificado — al elegir «English», `document.documentElement.lang` pasa a `en`. ## Tres números que salieron de medir, no de suponer - El `container` bajó de `xl` a **`lg`**: con `xl` el pie no se alineaba con ninguna sección de la página compuesta encima. - La rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`**: con `2fr`, un pie de cuatro grupos se partía en 3+1. - El gap entre columnas es **6, no 8**: con 8, cinco grupos no comparten fila al ancho `lg` (728px justos). Horizontal más apretado que vertical, porque el gap horizontal es el que decide cuántos grupos caben. ## Dos hallazgos de canon (F21/F22 en `PLAN-blocks-quality.md` §6) - **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading` aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`, `Container`, `Section`— renderiza un `<div>` fijo. Consecuencia: una columna de enlaces no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un block solo puede elegir entre divs o escribir markup que luego no puede estilar. - **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.** `getDisplayText()` resuelve contra un registro de etiquetas que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado no hay ninguno montado: `value=['es']` pintaba «es» en vez de «Español». Rodeado con el `child` de `Select.Value`. Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal, separador `aria-hidden`, los tres `IconButton` con nombre obligatorio, y el idioma cambiando de verdad. Cero errores de página. Gates: `blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| F22 | `soma/components/select` | **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.** `getDisplayText()` resuelve contra un registro de etiquetas que llenan los `Select.Item` AL MONTARSE; con el `Content` dentro de `Select.Portal` y cerrado no hay ninguno montado, así que `value=['es']` pinta «es» en vez de «Español». Se rodea con el `child` de `Select.Value`, pero el caso «select controlado que aún no se ha abierto» es el más común de todos |

Powered by TurnKey Linux.