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/CONTINUE-blocks.md

316 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# CONTINUE — F2 blocks de sitio (handoff, act. 2026-08-01)
**Estado: F1 CERRADA (8/8) · F2 CERRADA (15/15).**
Hechos: site-header · hero · feature-grid · **feature-split** · pricing ·
testimonials · faq · stats-band · cta · newsletter · site-footer · banner · team ·
**contact** (§F2.13, el primero que COORDINA) · **content-section** (§F2.14).
**Siguiente**: F3 (blocks de aplicación, 10) — o antes la página compuesta de
prueba que el plan pide para cerrar F2: header, hero, features, pricing, faq, cta
y footer juntos, mirada en claro/oscuro/375/1280. Es el test de integración del
tier y todavía no existe.
> ⚠️ **CAMBIO DE DOCTRINA (decisión del usuario, 2026-07-31).** Lee la sección
> «La doctrina cambió» antes de tocar nada: un block ya no es solo colocación.
> Ya está ESCRITA en el contrato (`architecture/blocks.md` §«Coordination»).
(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}/`).
---
## La doctrina cambió: un block coordina, tiene estado y trae datos
Decisión del usuario, textual: **«si al final es una lista de componentes… el
bloque es coordinación, estado, y data también»**. El motivo, con el `contact`
delante: cuando el estado vive fuera, cada `disabled` es una expresión distinta
montada en el punto de uso y aparecen **huecos** — un envío que no se puede hacer
y nadie dice por qué.
Lo que cambia respecto de lo firmado en `docs/architecture/blocks.md` (B-5/B-7 y
D-BLK: «todo el contenido por snippets, cero cadenas propias, sin servicios»):
1. **El block posee el estado de su sección.** Una máquina única y EXHAUSTIVA,
derivada, en contexto. Sus partes la leen; ninguna la recalcula ni inventa un
`disabled`.
2. **El block trae la forma de sus datos** (el esquema por defecto). El app lo
sustituye pasando el suyo.
3. **El block habla**, con idlangrefs por el traductor
(`#?blocks.<block>.<clave>|fallback`), la misma puerta que usa el canon para
sus `texts:`. Fallback en inglés; el app traduce registrando `blocks.*`.
**ESCRITO** (2026-07-31): `architecture/blocks.md` gana la sección
«Coordination», B-5 admite que el block traiga la FORMA de sus datos, B-7 queda
enmendado (posee las palabras de SUS estados, como idlangref con fallback
inglés) y la convención de servicios se acota: **el traductor es el único
servicio sancionado**. La sección dice también a quién NO aplica.
**Alcance acordado**: `contact` primero como prueba de la forma — **HECHO**.
**Revisión documental de los 15 HECHA** (2026-08-01): cada README declara ahora
su posición bajo la doctrina, y el mapa real salió así —
- **Coordinan**: `contact` (máquina + esquema + palabras) · `pricing` (el periodo
por contexto, pero nada se puede BLOQUEAR ahí, así que no necesita máquina ni
palabras: coordinar no es siempre una máquina).
- **Comparten configuración, no estado**: `team` (`align`) · `content-section`
(la medida).
- **Costura bindable hacia el canon**: `faq` (`value` → `Accordion`) ·
`site-header` (`mobileOpen` → `Drawer`). Reenviar no es poseer.
- **No poseen nada**: banner · cta · feature-grid · feature-split · hero ·
site-footer · stats-band · testimonials.
- ⚠️ **`newsletter` es el candidato ABIERTO**: coloca un envío que puede quedar
bloqueado sin que nada diga por qué (el `disabled` lo monta el app en el punto
de uso) — exactamente el hueco que cerró `contact`. Es anterior a la doctrina
(block del 2026-07-30, doctrina del 2026-07-31). **Siguiente candidato a
tocar**, con decisión tuya antes de moverlo.
⚠️ Al revisarlos, el listón es el que dejó `contact`: **el estado se deriva de
una fuente y las partes lo leen**; si un estado puede bloquear algo, tiene frase
por `Record` exhaustivo. Y **no inventes estado donde no lo hay** — un block de
layout que no coordina nada se queda como está.
---
## F2.13 `contact` — CERRADO (2026-07-31)
Los seis puntos que quedaban están hechos: arco verificado en navegador · README
rehecho · pestañas de doc al día · namespace `blocks` registrado en el arnés ·
ficha §F2.13 + bitácora en `PLAN-blocks.md` · contrato B actualizado.
**Un defecto real, encontrado al verificar**: la máquina leía `form.isValid`,
que con `progressive` significa «aún no se ha encontrado nada mal» — un
formulario vacío e intacto no tiene errores, así que se declaraba válido y
`incomplete` era INALCANZABLE al cargar: la sección pedía resolver la
verificación con los tres campos vacíos. Justo el hueco que la doctrina existe
para cerrar. Se arregla preguntando al ESQUEMA (`validateSync` de SIUM: síncrono
y sin escribir errores; `form.validate()` habría encendido los tres campos en
rojo al cargar). Fijado con `state.test.ts` — 9 casos, **primer test unitario
del tier**, porque `state.ts` es su primera lógica pura.
Añadido de paso el control vivo que faltaba para un prop público:
`verification` con reto / sin reto. **Omitir el prop NO es pasar `idle`**: sin
él la sección no tiene paso de verificación; con un estado que nunca llega a
`verified` tienes una verificación que no pasa, que es otra cosa.
⚠️ **Ojo con el dev server de otra sesión**: el de `:5173` sirvió un módulo VACÍO
para `blocks/contact/index.ts` (500 «does not provide an export named Contact»)
porque tenía el grafo caducado tras crear ficheros nuevos. Arrancado uno propio en
otro puerto, la página va perfecta. Si mañana ves ese 500, es eso: no persigas el
código.
## Dos hilos de canon abiertos (medidos, sin causa raíz)
1. **`Form`/SIUM**: con `progressive`/`onSubmit`, un envío inválido bloquea y
mueve el foco pero **no expone mensajes** (`form.errors` vacío). Con `onChange`
sí aparecen —y traducidos—, pero saltan en los tres campos al escribir en uno;
con `onBlur` aparecen desde la carga. La máquina de estado del block TAPA el
agujero de cara al usuario (el botón dice qué falta), pero el hueco sigue ahí.
2. **`ProofOfHuman`**: el provider de soma escribe `status` él mismo
(`writableActive`), así que el veredicto que devuelve el app tras consultar a su
servidor no se sostiene, y los slots `stage*` no renderizaron en ningún estado.
El camino «rechazado» no es contable hoy desde el app. **Consecuencia medida en
`contact`**: sus estados `verifying` y `rejected` quedan cubiertos por test
unitario de la máquina, no por el reto en vivo.
---
## 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
```text
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
`Card` desplazaba 21px un header con `offset: 0` (su recipe pinta con
`--card-padding-*`, que `padding={0}` de la capa Box no alcanza), y un
`position: sticky` dentro 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-pending` puesto). 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-size` los pone el componente de eidos al
renderizar, así que están desde el primer frame; `type`, `aria-label` y 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í `type` ausente en TODOS los `Button`
del árbol y ningún `onclick` disparando, y a ~6s los mismos botones tenían
`type="button"`, `aria-label="Descartar"` y el clic funcionando. Reporté tres
«defectos del canon» que no existían.
- **MATIZ CARO (2026-07-31): que lo escriba la runtime NO basta — tiene que no
existir en el SSR.** Esperé `button[type="submit"]` con atributo `type` como
puerta de hidratación en `contact`, y **ese atributo viene ya en el HTML del
servidor**: la espera se cumplía sobre el documento estático y yo tecleaba
antes de que Svelte tomara los inputs. Resultado: los valores entraban en el
DOM, `form.values` seguía vacío, y pasé varias rondas persiguiendo un fallo del
block que no existía (llegué a «arreglar» un `$derived` que estaba bien; lo
revertí al medirlo). **La puerta honesta**: algo que solo pueda haber escrito
el cliente — aquí `<style id="uix-blocks-display">`, que pone un `$effect` de
`BootUix` — más un ida y vuelta reactivo real antes de dar por hidratado.
- **Si instrumentas con `console.log` y no lo ves en el navegador, míralo en el
SERVIDOR.** Un `console.log` dentro de un `$derived` sale por el stdout del dev
server durante el SSR. Ver el log SOLO ahí es la prueba de que el componente no
está corriendo en cliente — fue lo que delató el diagnóstico anterior.
- **Playwright headless SÍ sirve para foco y teclado.** El pane suspendido no
(congela rAF y pierde `activeElement`), pero un `page.keyboard.type()` real
dispara `:placeholder-shown` y `:focus-within`: así se verificó que la etiqueta
flotante del `Field` sube al borde. `page.fill()` usa el setter nativo y NO los
dispara. Un script del scratchpad debe importar
`file:///G:/dev/svelte/vicen/node_modules/playwright/index.mjs` (no resuelve
`playwright` por ruta relativa).
- **Los anchos de dispositivo (375/768) van por la ruta `preview` en iframe**, y
es opt-in: en dev, dos documentos sin empaquetar a la vez agotan las
conexiones del navegador (`ERR_INSUFFICIENT_RESOURCES` mata 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
llamado `signup` dentro 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.** `justify` de `Group` se
hereda a los clusters anidados (pon `justify` explícito) y las de `Box` YA 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.
- **`gap` en un `Grid` separa también las COLUMNAS, y una parte que las cruza se
lleva esos huecos encima** (`content-section`, medido): con `gap={8}` y cinco
pistas, la parte que abarca 2/5 medía 1088 en vez de los 1024 del contenedor
con el que debía alinearse, y la de 1/-1 llegaba a 532 dentro de una rejilla de
404 y hacía scrollear la página a 420px. Si las columnas son un instrumento de
medida y no cosas puestas al lado: `rowGap` + `columnGap={0}`.
- **Y `100%` dentro de una pista no es `100%` de la rejilla.** Una pista
`min(medida, 100%)` se come sus propios canalones (a 420px la central se
quedaba los 404 enteros y la rejilla se iba a 436), y algo anidado dentro de una
parte ve el ancho de SU pista, no el de la rejilla — capar igual en los dos
casos descuadra uno de los dos. Los dos se cazaron midiendo, no leyendo.
- **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 que `reset.css` haya 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):
1. **Tabla de comparación** de features×planes en `pricing` (recurre en las refs;
sería un hermano `pricing-table`).
2. **Cita en spotlight** en `testimonials` (la variante modal de TODAS las refs;
el grid es minoría, 2 de 8 en TW).
3. **Lista estática 2/3 columnas** en `faq` (6 de 7 en TW no son acordeón).
4. **`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:check` **verde (14 blocks)**.
- `npx vitest run src/uix/blocks/` — **9/9** (`contact/state.test.ts`).
- `npm run docs:check` — **0 errores, 0 avisos**.
- `svelte-check`: **sin errores propios** (80 en el repo, ninguno en `blocks/`;
el delta sobre los 73 de ayer sale de `web/routes/alpha/` y del árbol sucio del
hilo de audio, ajenos).
- `prettier`: limpio en mis ficheros del tier. ⚠️ Hay **18 ficheros del arnés de
demos que fallan prettier de antes** (`hero`, `pricing`, `site-header`,
`feature-*`, `testimonials`, `stats-band`…) y `docs/architecture/blocks.md`
también fallaba ya en HEAD — no los formateé para no meter diff ajeno.
- `vitest src/uix/contracts.test.ts`: **3 fallos AJENOS** (35 pasan) — escrituras
DOM directas en soma, data-attrs hardcodeados y las claves camelCase de `aura`
(`components.aura.announce.returnedStopped`, …). Ninguno apunta a `blocks/`.

Powered by TurnKey Linux.