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

39 KiB

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: <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
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, 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

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 — ✅ la MITAD del diagnóstico, ejecutada 2026-08-26 (firma opción 1 del autor; commit del coordinador): tenía razón, el nombre no se declaraba en ningún sitio. El knob deja de ser ficción — primitives.motion.staggerViewport: '70ms' → --motion-stagger-viewport, y la regla emigra de motion.css a renderMotionBlocks. La otra mitad —colapsarlo en --motion-stagger— queda APLAZADA como firma de DISEÑO propia: el cableado conserva 70 ms verbatim porque 70 → 20 ms cambia el píxel en 8 blocks (team · stats-band · feature-grid · hero · article-grid · faq · pricing · testimonials — la entrada se comprime a menos de un tercio; deltas de ~67 ms medidos en AUDIT-blocks-ledger.md) y colapsa dos trabajos perceptuales en un knob: un menú ondea a 20 ms, una sección respira a 70. Espera firma del autor. Acta: docs/theming/changelog.md §57

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 propioForm**parte la cadena a mano** tras el con un helper localfallbackMessage`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés
# 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 <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
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.