|
|
# 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/`.
|