12 KiB
CONTINUE — F2 blocks de sitio (handoff, act. 2026-07-31)
Estado: F1 CERRADA (8/8) · F2 12 de 15. Hechos: site-header · hero · feature-grid · feature-split · pricing · testimonials · faq · stats-band · cta · newsletter · site-footer · banner. Quedan 3: team (§F2.12) · contact (§F2.13) · content-section (§F2.14).
(El plan fijó 14; feature-split se añadió como block hermano con
scope-approval, así que el tier tiene 15 y el denominador honesto es 15.)
Todo commiteado y pusheado a gita/alpha-0.1-sec-dom.
Dónde vive cada cosa:
- Plan y bitácora por block:
docs/process/PLAN-blocks.md(fichas §F2.x + registro cronológico al final, con el detalle de cada cierre). - Contrato del tier:
docs/architecture/blocks.md(B-1..B-11, D-BLK). - Suelo de paridad:
docs/process/RESEARCH-blocks-references.md(el dossier). - Deuda de canon congelada:
docs/process/PLAN-blocks-quality.md§6. - Decisiones por block: el README de cada uno (
src/uix/blocks/{kebab}/).
Lo siguiente: F2.12 team
Ficha en PLAN-blocks.md §F2.12: Section + AutoGrid + Avatar + Heading +
Text + Group de IconButton sociales por miembro. Del dossier: 9 en Tailwind
Plus. .Member se repite → parte compound (la regla de forma, abajo).
Fase 0 obligatoria: leer el README del Avatar (fallback de iniciales, tamaños,
anillo) antes de componer, mirar el equivalente en ≥2 catálogos y anotar en el
README del block qué se adopta y qué se descarta.
Después: contact → content-section.
F2.11 banner HECHO — el block más fino del tier a propósito: reenvía la
superficie del Banner del canon con Omit<BannerProps, 'children'>, lo mete en
columna, envuelve con Wrap y cablea Banner.Close solo si llega onDismiss
(la visibilidad es del app, decisión del componente). Hallazgo real: Banner
estampa role="banner" después de sus rest props, así que con el site-header
en la misma página quedan dos landmarks banner; solo se puede paliar
nombrándolos.
⚠️ Ahí reporté tres «defectos del canon» que eran FALSOS (aria-label de
Banner.Close, type de Button, onclick de IconButton). Los tres eran la
misma medición prematura — ver la regla de verificación afinada más abajo. Si
vuelves a ver algo así, la primera hipótesis es el reloj, no el canon.
La regla de forma del tier (no la re-decidas)
Compound solo si las partes:
- SE REPITEN — el app mapea sobre N (
feature-grid.Item,testimonials.Item,pricing.Plan,faq.Item,stats-band.Stat,site-footer.Column), o - COORDINAN — se hablan por contexto (
pricing.Switch↔PlanPrice).
Si no hacen ninguna de las dos: slots de snippet en la raíz (hero, cta,
newsletter, y la marca / alta / social / legal / extra de site-footer). El
plan original dibujaba varias de esas como partes; la desviación está registrada
en cada README.
Los 8 hallazgos de canon que dejó el tier (congelados)
Restricción del usuario en vigor: no tocar componentes ni librerías fuera de
src/uix/blocks/ y web/routes/blocks/. Están todos medidos en navegador
y listados en PLAN-blocks-quality.md §6 (F15–F22) — más los de motion (A1, A2,
A4, C7, C8, C10, D12, E14) de la pasada de calidad.
| # | Qué |
|---|---|
| F15 | Surface variant='soft' no acota un panel: su track queda a 0.002 de luminancia del fondo de página en claro, y Surface no tiene borde. Card outline sí acota pero no acepta gradient → «panel sosegado con borde» no tiene primitivo. |
| F16 | La ranura contrast de la paleta es blanca en todo escalón sólido, así que un lienzo de luminancia media deja el cuerpo bajo AA: primary 5.18 · indigo 5.21 · plum 4.75 pasan; neutral 3.32 · secondary/slate 3.30 · teal 3.07 fallan en claro. |
| F17 | Text align es inerte por defecto: renderiza un span y text-align no hace nada sobre caja inline. Hay que pedir as="p". |
| F18 | La fundación de eidos no trae reset de modelo de caja y lo asume del app. Bajo content-box, [data-field-control] (inline-size:100% + padding) mide 30px más que su contenedor. Resuelto en app-land: web/routes/blocks/_lib/reset.css. |
| F19 | onValidSubmit/onInvalidSubmit son no-op silenciosos si se pasa un form ya construido: el componente solo los reenvía al createForm que hace él mismo. El handler va SIEMPRE en createForm. |
| F20 | Los mensajes de SIUM son idlangref (#?sium.errors.email|…): resolver con uix.langs.t(issue.message, issue.params). La demo de docs del Form los parte a mano y por eso siempre salen en inglés. |
| F21 | Los primitivos de layout no pueden cambiar de elemento: Text/Heading aceptan as, pero Box —y Stack/Flex/Grid/Group/Wrap/Container/Section— es un <div> fijo. Una columna de enlaces no puede ser ul/li. |
| F22 | Un Select controlado muestra el VALOR crudo hasta abrirse una vez: las etiquetas las registran los Select.Item al montarse y el Content portaleado está cerrado. Rodeo: el child de Select.Value. |
Y dos huecos anteriores que siguen abiertos: Box/Surface flex/grow no
hacen crecer a un hijo flex (se rodea con tracks 1fr de Grid) y Group no
apila (usa Flex direction={{ base: 'column', sm: 'row' }}; hero todavía
compone sus acciones con Group).
La plantilla ya existe — cópiala, no la reinventes
src/uix/blocks/{kebab}/
├── README.md # Función · Mapa de composición · Decisiones · Gaps
├── index.ts # export del compound + tipos
├── types.ts # props (todo contenido entra por snippets, B-5/B-7)
└── {kebab}.svelte # composición: solo componentes del canon, sin CSS
web/routes/blocks/{kebab}/
├── +page.svelte # BlockDemo + controles vivos + pestañas de doc
├── {Name}Site.svelte # el block dentro de contenido REAL de producto
└── preview/
├── +layout@.svelte # `@` resetea el layout: la vista previa es su página
└── +page.svelte # sirve {Name}Site leyendo la URL
Y luego: marcar shipped: true en web/routes/blocks/_lib/catalog.ts (raíl y
galería leen esa única fuente) y npm run blocks:check.
Reglas que ya costaron sangre (no las re-aprendas)
- El block se enseña A SANGRE en la página. Nada entre el block y el borde:
ni marco con relleno, ni caja con scroll, ni cromo pegajoso encima. Medido: un
Carddesplazaba 21px un header conoffset: 0(su recipe pinta con--card-padding-*, quepadding={0}de la capa Box no alcanza), y unposition: stickydentro de un div con scroll es un comportamiento que nadie vive. Si un block se ancla a algo, se mide contra lo que se anclará en producción. - Verifica esperando la CONDICIÓN, nunca un timeout fijo. Con
Form+Field+ SIUM el dev server tarda ~2,5s en hidratar y un screenshot temprano fotografía el panel todavía invisible (data-animation-pendingpuesto). Esperar a que ese atributo desaparezca es la diferencia entre verificar y reportar un bug que no existe. - Y la condición tiene que ser algo que escriba la RUNTIME del morfo, no el
render. Los
data-variant/data-sizelos pone el componente de eidos al renderizar, así que están desde el primer frame;type,aria-labely los handlers los aplica la runtime en un efecto POSTERIOR. Esperar a que exista el nodo (o a que se vea) no basta: a ~1s medítypeausente en TODOS losButtondel árbol y ningúnonclickdisparando, y a ~6s los mismos botones teníantype="button",aria-label="Descartar"y el clic funcionando. Reporté tres «defectos del canon» que no existían. La espera correcta eswaitForFunction(() => boton.hasAttribute('type'))— un atributo que solo puede haber puesto la runtime. - Playwright headless SÍ sirve para foco y teclado. El pane suspendido no
(congela rAF y pierde
activeElement), pero unpage.keyboard.type()real dispara:placeholder-showny:focus-within: así se verificó que la etiqueta flotante delFieldsube al borde.page.fill()usa el setter nativo y NO los dispara. Un script del scratchpad debe importarfile:///G:/dev/svelte/vicen/node_modules/playwright/index.mjs(no resuelveplaywrightpor ruta relativa). - Los anchos de dispositivo (375/768) van por la ruta
previewen iframe, y es opt-in: en dev, dos documentos sin empaquetar a la vez agotan las conexiones del navegador (ERR_INSUFFICIENT_RESOURCESmata las DOS páginas). - Cada prop público, un control vivo en la demo; los ejes de sección (tema, idioma, dirección, densidad) ya los da el shell, no los repitas.
- Un slot que se apaga se pasa como
undefined, no se renderiza vacío. Declara el snippet arriba y pásalo por prop (signup={activo ? banda : undefined}): así el{#if}del block quita también su hueco. Y cuidado con el sombreado:{#snippet signup()}pisa un prop llamadosignupdentro del componente. - Nada de backticks de markdown dentro de
<Text>: se ven literales. Lo que es código va en<Code>. - Ojo con las variables de layout que heredan.
justifydeGroupse hereda a los clusters anidados (ponjustifyexplícito) y las deBoxYA no heredan desde 2026-07-23 (docs/theming/changelog.md§46). - Un hueco del canon se registra, no se falsea. Si al componer falta algo, va a los Gaps del block como candidato a canon y la demo usa lo que hay.
- Los números de layout se miden. Tres defaults de
site-footer(container, la razón de la rejilla, el gap de columnas) salieron mal a la primera y solo el navegador lo dijo. Si un default decide cuántas cosas caben en una fila, se comprueba con el contenido real.
Deuda del arnés de demos (mía, sin tocar)
- La galería (
web/routes/blocks/+layout@.svelte) arranca UIX en línea en vez de usar_lib/BootUix.svelte, que es lo que usan los previews: la misma cadena escrita dos veces, y el motivo de quereset.csshaya que importarlo en los dos sitios. - En
BlockDemo, a ~1400px, el conmutador de anchos de dispositivo se solapa con el párrafo que lo precede. Idéntico en los 11 blocks → es del arnés, no de un block.
Deuda a decisión tuya
Siguen sin decidir, cada una entra por el contrato B con fase 0 ligera (mirar ≥2 catálogos del dossier y anotar adoptado/descartado en el README):
- Tabla de comparación de features×planes en
pricing(recurre en las refs; sería un hermanopricing-table). - Cita en spotlight en
testimonials(la variante modal de TODAS las refs; el grid es minoría, 2 de 8 en TW). - Lista estática 2/3 columnas en
faq(6 de 7 en TW no son acordeón). form-in-hero(slot de alta al boletín dentro del hero).
Resueltas ya: feature-split/alternante (F2.3b, con Mockup) · hero con fondo
cover (layout background) · CTA que navega y parece botón (el child de
Button entrega un snippet content; Button sigue sin href y Link sigue
poseyendo la navegación — doctrina en el README de eidos Button §«CTA que
navega»).
Estado de gates al parar
npm run blocks:checkverde (11 blocks).svelte-check: sin errores propios (73 en el repo, todos ajenos:file-upload, etc.).prettier: limpio en mis ficheros del tier.vitest src/uix/contracts.test.ts: 3 fallos AJENOS (35 pasan) — escrituras DOM directas en soma, data-attrs hardcodeados y las claves camelCase deaura(components.aura.announce.returnedStopped, …). Ninguno apunta ablocks/.