# 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. → **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. ### 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: ``** (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 | | **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) | | **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 | — | > **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. --- ## 4. Plan ### F-Q0 · Primitivas que faltan en el canon *(la palanca)* | # | Pieza | Por qué | |---|---|---| | 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 | | 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) | 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`. --- ## 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` | ### 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 | ### 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 | ### Añadidos al construir `site-footer` (2026-07-30) | # | Dónde | Qué | |---|---|---| | ~~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 `
`. 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 `
` fijo. Consecuencia concreta: una columna de enlaces de pie no puede ser `
    /
  • `, 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 | | 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 |