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

1035 lines
65 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 — tier blocks (handoff, act. 2026-08-19 noche · el chasis de sección)
**Estado**: F1 CERRADA (8/8) · F2 CERRADA (15/15) · F2b CERRADA ENTERA ·
**F3 ABIERTA — F3.1 `app-shell` CONSTRUIDO, VERIFICADO Y REHECHO**, con su fase
0 firmada (Q1–Q4) y el canon `SkipLink` nacido delante de él.
**Cinco filas cerradas** (A-111 · A-112 · A-109 · A-110 · A-29), cada una con su
doctrina escrita. Y después, **el eje que cambió el método**: en vez de seguir
bajando filas, `Section` crece hasta ser la sección de página y los 19 blocks
dejan de reescribir su chasis. **EMPIEZA POR AHÍ** — §0, abajo.
**19 blocks vivos**: site-header · hero · feature-grid · feature-split · pricing ·
testimonials · faq · stats-band · cta · newsletter · site-footer · banner · team ·
contact · content-section · logo-cloud · article-grid · cookie-consent ·
**app-shell**. Más la página compuesta `/blocks/landing`.
**A-95 CERRADA** con `app-shell`: solape del aviso sobre la cabecera **0px** y
hit-test dentro de la cabecera, medido (antes: 49px).
## Por dónde entrar mañana
### 0 · EL EJE VIVO: el chasis de sección (abierto, tres tandas hechas)
**Idea del autor**, después de parar una cola de parches con una frase que hay
que conservar: *«todo esto es muy engorroso, lo estás ensuciando y convirtiendo
el framework en una mierda a base de parches, ninguna es profesional ni de
alcance»*. Tenía razón, y la pregunta que vino después —*«¿no sería definir un
componente del que se basen todos los bloques?»*— es la que abrió esto.
El censo le dio la razón con números: `containerSize` recableado en **16**
blocks, `sectionSize` en **15**, `decor` en **9**, y **ocho copias del mismo
`Header`**, una ya derivada — la de `article-grid` había perdido su `Motion`.
**Firmado: A — que CREZCA `Section`**, no que nazca un hermano. El nombre que se
propuso, `Block`, se descartó con dato: `block` ya es vocabulario del canon
(`block?: boolean` = «a todo el ancho», 10+ componentes) y colisionaría con el
nombre del tier. Si algún día naciera aparte sería `PageSection`.
**Tres commits, sin push**:
- `4a2771517` — `Section` gana `containerSize`, `decor` y la parte
`Section.Header`, y renderiza un `<section>` real absorbiendo el `{...rest}`.
Para eso **`Box` gana `as`** (lista CERRADA de contenedores, default `div`),
que **cierra F21**. Dos blocks migrados como prueba.
- `ecb23d6a4` — los ocho `Header` pasan a componer `Section.Header`, que gana
`align` para la única variación real. `article-grid` recupera su `Motion`.
- `cc9844434` — las diecisiete raíces restantes. **−420 líneas.**
**Cuatro conservan algo suyo, cada uno por un motivo medido**: `cta` (su
`paddingX` condicional: `full` significa EDGE TO EDGE) · `content-section` (mide
con una rejilla de cinco pistas, no con `Container`) · `site-footer`
(`as="footer"`) · `hero`, que destapó **la trampa del diseño**.
⚠️ **La trampa, y sólo salió MIDIENDO**: si un block pasa `containerSize` Y un
`<Background>` propio como hijo, el fondo queda **atrapado dentro de la medida**
— el `hero` salió con 128px de blanco a cada lado en vez de a sangre. El `decor`
del chasis se pinta FUERA del container justo por eso; un `Background` que llega
como hijo se capa con la copia. Escrito en el sitio donde volvería a morder.
Verificado después: fondo 1280 a sangre, hijo directo de la sección, medida 1024
dentro, en `hero` y en `stats-band`.
⚠️ **Y el paso de tipos que costó 5 + 14 errores**: la raíz de un block extiende
los props del componente del canon que envuelve (**`SectionProps`**), nunca
`HTMLAttributes` — cuya superficie laxa choca con el `style`/`class` refinado
del canon al esparcirse. La regla ya estaba escrita en el `types.ts` de
`stats-band` para las sub-partes; ahora la raíz también envuelve canon.
**Lo que queda de este eje**:
1. **Los 19 README describen el nido viejo** (`Section · Container` en sus mapas
de composición). Es prosa, no está guardada por nada, y ahora miente.
2. **El contrato del tier** (`architecture/blocks.md`) no dice todavía que la
raíz de un block ES un `Section`. Si eso se escribe, `blocks:check` puede
vigilarlo.
3. **Nada más del chasis**: los tres ejes y la parte están migrados y medidos.
### 1 · La cola del ledger, con el aviso que la reencuadra
⚠️ **Antes de bajar otra fila, léete esto**: al revisar la cola con el autor
salió que **más de la mitad de lo que queda archivado bajo un block tiene la
causa en el canon** — A-48 (el `Banner` declara cero eventos), A-62 y A-65 (el
`Button` de soma emite haya o no handler), A-60 (el modelo `auto-fill` de
`AutoGrid`), A-53 (`Link` mudo frente a `Button`). Bajarlas una a una dentro de
la frontera del tier fabrica parches. **A-11 quedó replanteada por eso** (ver su
ficha): su arreglo profesional es que el canon publique el CATÁLOGO en runtime
de sus escalas, como ya hace `EIDOS_VARIANTS`, no siete líneas de exhaustividad
repartidas por las demos.
**Trabajo honesto que queda dentro de la frontera**: A-74 (anclas muertas en la
demo de feature-split) · A-96 (`affixOffset` sin control vivo) · A-71 (el arnés
pinta el block antes del `h1`, en las 15 demos). Media hora, y la cola queda
limpia sin mentir.
### 2 · ~~A-111~~ — **CERRADA 2026-08-19**. Lo que dejó, y lo que abrió
El nombre del landmark vive ya en el `<nav>`. Medido con el árbol AX de Chrome:
`/blocks/app-shell/preview` → `navigation: ['Navegación principal', 'Menú de la
aplicación', 'Migas de pan']` (el `null`, fuera), y la demo del componente sin
label del consumidor → `'Principal'`, el default del morfo traducido sobre el
landmark. Detalle y método en `AUDIT-blocks-ledger.md` §A-111.
Tres cosas que conviene saber al retomar:
- **La forma ejecutada corrige el paso 1 de la disposición**: la lista NO hereda
el nombre por `aria-labelledby`, se queda sin nombre. Los morfos hermanos con
`defaultElement: 'nav'` lo dijeron — `breadcrumb` y `nav-tree` declaran
`aria: []` en su lista. `navigation-menu` era el único fuera de esa forma.
- **Guard nuevo**: `src/uix/morfo/landmark-census.test.ts` — toda parte con
`defaultElement: 'nav'` declara un attr de nombre EN SU PARTE, probado en rojo
por mutación tres veces. Ámbito `nav` a propósito; **`aside` es el candidato
natural a ensanchar el censo** y no se hizo sin firma.
- **Y destapó A-116**: `palabras.breadcrumb` es la misma clase (un `nav` con
`aria: []`, sin nombre en soma ni en eidos). **Excepción firmada** en el censo
— palabras es el eje de otra sesión y su rama es compartida.
⚠️ La doctrina ganó el párrafo que no existía (`architecture/morfo.md` §Step 4,
«A landmark is named on ITS OWN element»). Antes de esto la regla no estaba
escrita en ningún sitio: grep de `landmark` en `morfo.md`, `canon/` y `guides/`
daba cero.
### 2 · Las filas que dejó el `app-shell` (ledger, todas nuevas)
| Fila | Qué | Estado |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| **A-112** | ~~`Toolbar.Button` no acepta `variant`/`color`~~ — **ARREGLADO 2026-08-19, firmada (a)**: compone el `Button` del canon (defaults `ghost`/`neutral`/size del toolbar), la principal lo dice desde dentro, y gana `contact-activate` (era MUDO) | **ARREGLADO** |
| **A-113** | La foundation emite los estilos semánticos y no los aplica a ningún elemento — Times New Roman en los 19 previews del tier | **ARREGLADO** en el reset del arnés; queda la pregunta de canon |
| **A-114** | `Sidebar.Inset` impone `overflow: auto`, y un ancestro con `overflow` se lleva el `position: sticky` de dentro | CONFIRMADO |
| **A-115** | No existe `description-list` y el panel de detalle del shell es el «primer detail-view real» que su ficha F5 pone como disparador | CONFIRMADO |
| **A-116** | `palabras.breadcrumb` declara `defaultElement: 'nav'` con `aria: []` y nadie la nombra: misma clase que A-111. **Excepción firmada** en el censo de landmarks, no se toca desde aquí | CONFIRMADO |
### 3 · Y sigue pendiente lo que tú dejaste pedido
**«Repasar cada bloque»** — un repaso block a block. **No hay ficha escrita**:
su forma se acuerda antes de empezar (qué orden, qué se mira, y si lo que salga
va al ledger o al README de cada block). Pregunta antes de arrancar.
Y **F3 sigue**: el orden del plan es `app-shell` (HECHO) → `error-page` →
`user-menu` → `notifications` → `settings` → `auth` → `dashboard` → `wizard`
→ `data-table` y `kanban`. ⚠️ Recomendación registrada **sin firmar**: subir
`auth` al segundo puesto. Y ojo — `user-menu` (F3.6) y `notifications` (F3.7)
son justo las piezas que le faltan a la barra del shell, así que hacerlas
seguidas tiene un consumidor real esperando.
### Lo que dejó la sesión de A-110 (2026-08-19)
**Primera fila arreglada DENTRO del tier** — decisión tuya de acotar el eje a
`src/uix/blocks/` + `web/routes/blocks/`, después de tres cierres seguidos que
fueron todos de canon.
`FeatureSplit` gana su `.Header` (era el único block de sección sin ella) y
`.Title` baja de `h2` a `h3`, como declaran los cuatro hermanos. ⚠️ El `h2` de
las filas **era una decisión escrita en el README**, no un descuido: queda
derogada en sitio. Medido: el preview pasa de tres `h2` hermanos sin nombre de
sección a `h2` + tres `h3`, y `/blocks/landing` deja de componer el encabezado
a mano y de bajar cada fila a `level={3}`.
**A-29 CERRADA** (el arnés serializa el estado VIVO de ActiveUix a la URL del
preview — `readActiveUixPrefsSlot` + `getThemeContext().mode` — y los 11
controles `dir` locales se retiran: el shell es el único dueño de los ejes).
⚠️ Costó dos propuestas malas ANTES de leer `ActiveUix`: un contexto paralelo
`_lib/axes.ts` y una precedencia demo-gana que dejaba el toggle global inerte
en 10 páginas. La lección es la pregunta 2 del protocolo, otra vez.
**Lo que queda dentro de la frontera**, por si entras por aquí: A-11
(demos que redeclaran tipos del canon) ·
A-71 (el arnés pinta el block antes del `h1`) · A-96 (`affixOffset` sin control
vivo) · A-74 (anclas muertas en la demo de feature-split) · A-53 · A-62 · A-65 ·
A-75 · A-48 · A-60 · A-79 (SOSP) · A-55 (medida, pero el autor la ve centrada) ·
A-92 (sólo doc de motion). Fuera de la frontera y por tanto CONGELADAS mientras
el eje sea «sólo bloques»: A-106, A-107, A-108, A-114, A-115, A-116 (canon) y
A-09, A-47, A-67, cuya ficha sitúa el arreglo en canon.
### Lo que dejó la sesión de A-109 (2026-08-19)
**El `Banner` deja de reclamar el landmark `banner`**: `<section>` sin rol —
`region` sólo cuando el consumidor la nombra. Y las DOS salidas que la ficha
proponía estaban mal planteadas; la revisión adversarial (tres refutadores) es
lo que lo destapó:
- **`role` es clase CONTRATO** (`morfo/types.ts:811-832`), así que «estampar el
rol antes de los rest props para que el consumidor lo pise» contradice la
doctrina que la propia ficha citaba.
- **El orden de estampado nunca fue el mecanismo**: el segundo landmark es el
`<header>` del `site-header`, que **no lleva `role`** — lo recibe implícito.
- **El morfo nunca declaró el rol** (`aria: []`): era un literal del wrapper que
el README daba por contrato. Drift morfo↔eidos, ahora sin objeto.
- ⚠️ **Y mi primera versión del arreglo llevaba un nombre por defecto traducido,
que era un error**: un default convierte TODA tira en landmark sin salida —
`/temas/grafito` apila cuatro y habrían salido cuatro `region` homónimas (la
clase de A-111). El precedente de `nav` no transfiere: un `<nav>` SIEMPRE es
landmark; un `<section>` lo es **porque** tiene nombre.
- **Derogados por escrito** la decisión del README del canon («Banner is a
landmark… site header, persistent notice, system status bar») y el «✓
ejemplar» de `docs/audit/components/banner.md`. La otra mitad —no
`role="alert"`— sigue.
Medido (AX por CDP): `/blocks/landing` pasa de `banner: ['Aviso del producto',
'Acme']` a `banner: ['Acme']` + `region: ['Aviso del producto']`;
`/temas/grafito` deja de exponer landmarks de tira. Píxel idéntico (la receta va
por `[data-banner]`). 11 sitios de prosa re-sincronizados.
### Lo que dejó la sesión de A-112 (2026-08-19, mismo día que A-111)
**Firmada la salida (a) tras tumbar el autor mi primera recomendación (b)** —
y la corrección es la lección: (b) «declarar el toolbar uniforme» habría
consagrado el bug de passthrough documentado (el eidos `toolbar-button.svelte`
dejaba el `<button>` nativo de soma) y dejado el gesto MUDO. La ficha §A-112
lleva el detalle; lo ejecutado:
- `Toolbar.Button` **compone el `Button` del canon** vía `child` (consumer
pattern): defaults `ghost`/`neutral`/el `size` del toolbar por contexto de
eidos (`toolbar/context.ts`, precedente ButtonGroup); tipo estrechado a
`SelectionVariant`. La receta CEDE el nodo (cero cromo de botón); `Link` y
`GroupItem` siguen siendo superficie de la barra.
- **El gesto suena**: `contact-activate` estampado + 8 nodos de audio al click,
antes 0 (el morfo del toolbar sólo declara `commit-toggle`).
- **Doctrina nueva en `guides/component-guide.md` §4**: dos clases de parte con
forma de botón en una barra — ACCIÓN en barra (compone `Button`:
Toolbar.Button, Form.Submit, Dialog.Trigger) vs control de SUPERFICIE de
barra (la pinta la barra: Menubar.Trigger). El test: ¿significaría lo mismo
fuera de la barra?
- **La demo del app-shell mueve «Nueva» AL INTERIOR del cluster** (el
workaround de la ficha, retirado) y la demo del toolbar gana la acción «New»
con controles vivos `action variant` / `action color`.
- Herencia de tamaño medida en vivo: toolbar `sm` → botones `sm` (30px, paridad
exacta con GroupItem por el bundle `--size-*` compartido).
### Lo que dejó la sesión de A-111 (2026-08-19, un commit)
Siete ficheros del canon y tres de proceso. Nada del tier `blocks` se tocó: el
defecto vivía una capa más abajo, como las 8 de 12 de la fase 7.
- `morfo/components/navigation-menu.ts` — la pareja de nombre sube de `List` a
`Provider`; la `List` conserva sólo `aria-orientation`.
- `soma/components/navigation-menu/` — la fuente `ariaLabelledby` sube al nivel
del runtime; el wrapper deja de sacar `aria-label` de restProps (viaja con el
resto y gana el merge, A-85); fuera el opt `ariaLabel` y el reenvío de la
`List`; test del bag reescrito (nombre en el provider, `not.toHaveProperty`
en la lista, suppression por `aria-labelledby`).
- `morfo/landmark-census.test.ts` (nuevo) + `architecture/morfo.md` §Step 4.
- READMEs de soma y eidos del componente; ledger §A-111 y §A-116 nuevas.
**Gates**: 420/420 en morfo+sema+soma+eidos tocados · `component:audit --only
navigation-menu` PASS · `morfo:check` PASS (las 6 rojas —color-field, combobox,
fab, gradient-builder, menu-dial, palabras— son ajenas: el check valida
`data-*`, que esta sesión no toca) · `svelte-check` **72/62 antes y después** ·
`docs:check` 0/0 (640 docs) · prettier: los 4 avisos de ficheros tocados ya
fallaban en HEAD (verificado con `git show HEAD:… | prettier --check`), el
fichero nuevo va formateado.
⚠️ **Método que valió la pena**: el nombre se midió COMPUTADO, desde el árbol AX
de Chrome por CDP, nunca leyendo el atributo — un `aria-label` en el DOM prueba
el atributo, no el nombre. Y el guard se probó en rojo por mutación tres veces;
una de ellas (vaciar el catálogo) es la que impide que un censo sobre nada pase
en verde.
### Lo que dejó la sesión del `app-shell` (2026-08-19)
Ocho commits, de `9fd54c80f` a `edf0639ee`. **Sin push.**
- **Fase 0 firmada** en `PLAN-blocks.md` §F3.1 — Q1 el block NO cablea servicios
· Q2 dos modelos de scroll por prop · Q3 la cabecera dentro del inset · Q4
canon `SkipLink` + uno por región montada.
- **`SkipLink` (canon nuevo)** — eidos-native, 1 parte, 0 eventos, posee sus
palabras por ROL de landmark, peldaño de z propio (950).
- **LOS DOS ÁMBITOS, y es la corrección de fondo del día**: la barra lleva la
APLICACIÓN (su menú general, buscar, avisos, cuenta) y no se mueve; el RAÍL
lleva el CONTEXTO (este proyecto, esta tabla) y cambia entero al cambiar de
contexto. Yo había escrito que un menú en la barra sería «una segunda
navegación»: es al revés, dos navegaciones de ámbito distinto son lo correcto
(Atlassian `TopNav`+`SideNav`, Polaris `TopBar`+`Navigation`). El rastro va en
la cabecera de la PÁGINA, no en la barra.
- **Cuatro trampas de layout medidas**, escritas en el README del block:
`minHeight` es un suelo y no un techo · un grid que sólo declara filas tiene
una columna implícita `auto` que se encoge · `Box` declara
`flex-grow/shrink/basis` y pisa al `flex` de quien lo envuelve · un ancestro
con `overflow` se lleva el `sticky` (A-114).
- ⚠️ **Dos averías del ARNÉS que este block destapó, las dos arregladas**: los
previews llevaban los 8px de margen del navegador (así que NINGÚN block se
enseñaba a sangre: `x: 8, w: 1264` a 1280, la página compuesta incluida) y no
anclaban la tipografía del tema (A-113). **Las medidas de anchura anteriores
al 2026-08-19 llevan el sesgo de los 8px.**
- **La demo se rehizo dos veces, y la lección vale para todo el tier**: primero
estaba escrita a mano (`<div style>`, `16rem` clavado, `font-size` en un
`<strong>`) y luego, ya sin literales, seguía sin usar los componentes que
SIGNIFICAN lo que mostraba. Ahora: `Metrics` para los KPI (con la valencia
desacoplada), `Feed` para bandeja y actividad (`role="feed"` + `article` +
`aria-posinset`), `Toolbar` para los clusters, `NavigationMenu` para el menú
de la aplicación, y la tipografía por los estilos SEMÁNTICOS del tema
(`Text style="label|caption|body"`, `Heading level` + `style`), nunca por un
tamaño elegido a ojo.
- **Lo que NO se ejecutó y sigue sobre la mesa**: la **app de referencia**
(`web/routes/blocks/app/`, `kind: 'page'`) — la primera raíz de composición en
modo **attach** del repo (`createActiveApp` → `attachActiveUix` → `<Uix>` →
`ActiveEidos` con sus fuentes → `createActivePrefsDomProjection` →
`setActiveApp`/`setBus`/`setPermsContext`). Hoy **ningún** shell del repo hace
nada de eso: los siete arrancan `createActiveUix` standalone.
- **CERRADO (2026-08-19), ya no está en manos de nadie**: las dos navegaciones
del shell dicen lo mismo ante el mismo gesto. `Sidebar.MenuButton` dejó de ser
mudo (eje del `Sidebar`, `f6d5fa159`) y `NavigationMenu.Link` dejó de emitir
`commit-select` al navegar (T-1): los dos declaran `contact-activate` anclado
en el control pulsado + `shift-navigate` en la superficie que cruza.
## ⚠️ LO PRIMERO: la fuente viva es el LEDGER, no el documento del 2026-08-01
**`docs/process/AUDIT-blocks-ledger.md`** — filas con id estable desde `A-01`.
El documento del 2026-08-01 se conserva (sus reclamaciones y evidencias son
válidas y el ledger las indexa) pero **su clasificación NO vale**: tenía los
veredictos desemparejados de sus hallazgos por un join por POSICIÓN, y el journal
del workflow era de sesión. Se reprodujo en vivo al re-verificar — el journal
devuelve los resultados en un orden distinto al de entrada.
Estado del ledger (2026-08-19): **77 ARREGLADO · 23 CONFIRMADO · 13 REFUTADO ·
3 DATO** (116 filas: las 90 originales + A-91…A-116). Cero
pendientes: todas están medidas. La tasa real de refutación fue del **13%**, no
del 28%.
**La regla que hay que respetar al escribir en él: unión por `id`, SIEMPRE.**
Ningún proceso vuelve a casar dos listas por posición. Un verificador que no
devuelve su id no escribe fila.
## El hallazgo grande de la re-verificación
**De las 12 ALTA de percepción, 8 tenían la causa raíz en el CANON, no en los
blocks.** Los blocks componen bien; lo que estaba roto era el arco perceptivo del
canon. Todos arreglados ya (ver abajo), pero la lección se queda: cuando una
auditoría de un tier acusa al tier, comprueba en qué capa vive el defecto.
## Lo hecho en la sesión del 2026-08-06 (commit `866089a40`)
**Canon** — `Link` (la prop `color` era inerte en `subtle`/`plain`, el hover
clavaba primary, el `:active` quedaba tapado) · `Card` (prometía `BoxProps` sin
aplicarlos: `height="100%"` era un atributo inerte; ahora el tipo dice la verdad y
el eje de tamaño va por `buildSizeStyle`, helper compartido) · `Form.Submit` y
`Form.Reset` (componen el `Button` del canon, así que la acción principal de un
formulario recupera el `contact-activate` en el gesto; y el `aria-label` genérico
deja de pisar el texto propio — WCAG 2.5.3 para todo consumidor, verificado con el
AX tree de Chrome) · `Field` (dejaba de duplicar `commit-submit` dentro de un
`Form`) · `NavigationMenu` (el `commit-select` se mueve al `Link`, el despliegue
habla como `emerge-open/close` en vez de fingir una selección por hover, y el pack
casa por fin con la parte que recibe el estampado — antes sonaba a la ganancia
base, 10× lo diseñado) · `Badge` (el ✕ compone `IconButton`; la altura del chip
pasa a ser la del control) · **la fundación**: `[data-on]` arrastra la propiedad
`color`, no solo las variables.
**Blocks** — hero 1.61:1 → 6.61:1 · contact separa `incomplete` de `invalid` (el
camino de error por campo era inalcanzable por construcción) y su frase llega a la
AT · site-header ya no congela la página al cruzar el breakpoint · site-footer
emite su commit · pricing no se vacía · cta llega a sangre de verdad · newsletter
COORDINA.
**Doctrina** — la frontera dura 1 nombra `$libs/forms`; el contrato B admite un
**segundo servicio, el anunciador** (`uix.announce`): un block que posee las
palabras de sus estados tiene que poder decirlas.
## Fase 7 — contraste doctrinal de los 15 — **CERRADA 15/15**
Encargo del usuario: contrastar cada block con la documentación del sistema **a
nivel de cada componente que compone y en conjunto**, siguiendo la guía de
creación de blocks y **con la composición como estrategia válida**, comprobando
las directrices de eidos, sema, morfo y soma — más **motion**, que faltaba en la
rejilla y detectó él preguntando si había leído su doctrina. No la había leído;
el primer veredicto sobre coreografía salió sin ella y hubo que retirarlo.
**Detalle completo, rejilla de siete ejes y fichas: `AUDIT-blocks-ledger.md`
§Fase 7.** Aquí sólo el estado y cómo seguir.
### ⚠️ Lo primero: el instrumento mentía
Las mediciones en navegador del 2026-08-09 se hicieron contra un dev server con
caché de Vite **anterior a arreglos ya commiteados**. Al re-medir con servidor
limpio cayeron **A-36, A-69 y A-85**. El delator fue aritmético: las duraciones
medidas (7867/3567 ms, ratio 2.21) eran EXACTAMENTE las que el comentario del
arreglo cita como estado anterior. **Servidor recién arrancado antes de medir**,
y si un número reproduce el «antes» documentado con demasiada exactitud,
sospechar del instrumento antes que del código.
### Estado: los 15 con la rejilla completa
**Ninguno de los 15 tiene un defecto de composición.** Componen canon y lo
componen bien; todo lo que salió vive una capa más abajo (canon) o en las FICHAS
del propio ledger. Ese es el resultado de la fase, y conviene no perderlo: la
auditoría original acusaba al tier.
⚠️ Cómo se llegó aquí importa, porque me equivoqué en el camino: durante días
declaré «conformes» siete blocks de los que sólo había medido UN eje (dónde cae
el `{...rest}` y quién estampa `data-stagger`). Un barrido no es la rejilla.
Se corrigió en el ledger y en este handoff cuando él preguntó «¿ya has
terminado?». Lo que el contraste produjo:
- **Cinco disposiciones corregidas** — A-55 (mecanismo falso: el desvío lo causa
el ancho que resta el `Close`, no unos márgenes auto que miden `0px`), A-09 (su
arreglo apagaría la detección live), A-68 (no es arreglable desde el block sin
romper B-6), A-60 (correcta, y con el camino que nombra el propio `AutoGrid`),
A-61 (su segunda mitad ya la cerró `uix.announce`).
- **Tres filas nuevas** — A-91 (`banner` hereda el eje `intent`/`color` sin
registrarlo), A-92 (una restricción del modelo de cascada que vive en un
comentario de `pricing` y falta en §D.13 de la doctrina de motion), A-93
(`Motion` no expone su momento «visto», que es lo que desbloquearía A-68).
⚠️ Al retomar: **la composición es estrategia válida** (norma del usuario). Un
block que sólo coloca canon no es defectuoso por serlo; sólo lo es si al componer
se salta un contrato o reinventa un primitivo. Y el eje motion se contrasta con
§D.11 (la cascada ES `[data-stagger]` + preset + regla de fundación — modelo
CERRADO, no un atajo), §D.13(1) hijos directos, §D.13(2) ENTER-ONLY, y §2
KNOWN-FRAGILE (firma y stagger sobre UN mismo nodo se pisan).
## Lo hecho en las sesiones del 2026-08-16/17 (commits `4caf1100a`, `d02021aee`)
### 1 · El eje de tamaño dice de QUIÉN es (`4caf1100a`)
`size` significaba dos cosas en el tier: el aire de la `Section` en trece blocks
y la altura de la tira en `banner` — misma prop, distinta escala (`BannerSize`
no tiene `xl`). Renombrado: `size`→**`sectionSize`** (13), `container`→
**`containerSize`** (14), `minColumnWidth`→**`minChildWidth`** en `site-footer`.
`size` queda libre y sólo lo usa `banner`, donde SÍ es el tamaño del block.
**La regla vive ya en `architecture/blocks.md` §Conventions**: un block que
reenvía el eje de tamaño de una pieza interna lo nombra `{pieza}Size`, y un eje
que el canon ya nombra se reenvía CON SU NOMBRE.
Método que funcionó y conviene repetir: **renombrar los TIPOS primero** y dejar
que `svelte-check` señale cada consumidor. Cazó los cinco call sites que
quedaban. ⚠️ Y cazó un error mío: el primer patrón era demasiado ancho y renombró
`size` en siete sub-partes donde ese `size` es el del `Card` o el `Text` que
envuelven. Revertidas antes de seguir.
### 2 · Dos animadores que no se hablaban y un cluster que no envolvía (`d02021aee`)
**A-99 + A-98 — `Group` no aplicaba NINGUNO de los dos defaults que su README
promete** (`align: center`, `wrap: wrap`): nunca se los pasaba a `Flex`, que cae
a `nowrap`+`stretch`. De ahí que las acciones de `hero` no apilaran a 375px.
Arreglado en `group.svelte` con el mecanismo que ya usaba para `direction`, y con
`attached` invirtiendo a `nowrap` (el recipe cuadra radios y solapa bordes
asumiendo UNA línea). El censo decidió la forma: **`ButtonGroup` es el único
`Group` en columna de todo el ecosistema**, así que su `align="stretch"` vive en
su call site y no como default condicional. Medido: `hero` pasa a 2 líneas y la
acción secundaria de 124px truncados a 156,9px.
**A-68 + A-93 — los contadores corrían detrás de `opacity: 0`.** `CountUp` trae
su propio IO (threshold 0) y `Motion` usa otro (0.1 / −10%); un IO no mira la
opacidad. `Motion` publica ya su momento visto por contexto
(`motion/context.ts`, calcado de `cascade/context.ts`) y `stats-band-value` lo
pasa a `startWhen`. Medido en la ventana que discrimina (banda a 6px dentro del
viewport): contadores a 0/0/0 en la llegada y tras 2,6 s, donde antes acababan
en 12.121/330/47. Al revelarse, la primera cifra al **0%** — antes 97,8%. **A-69
intacta**: aterrizaje a 1781 ms, dispersión 0.
⚠️ **Residuo documentado**: las cifras que el stagger revela después llegan al
16,8% y **35,4%**. Estimé «≤10%» y me equivoqué — el muelle es sobreamortiguado
y cubre mucho recorrido al principio. Es el precio del aterrizaje conjunto que
A-69 firmó; no baja sin reabrirla.
### 3 · Tres errores de método de estas sesiones (no los repitas)
1. **Medir un default en una demo que lo pisa no es medir un default.** Escribí
en la ficha de A-99 que «el `align` sí se cumple, luego el recipe carga». El
`center` salía del control de la propia demo, que lo pasa explícito.
2. **El panel del navegador oculto suspende el IntersectionObserver**, no sólo
el rAF. Una primera medición de A-68 dio «contadores congelados» y era la
suspensión, no el arreglo: con el panel oculto el IO de `CountUp` tampoco
dispara, así que el resultado no distinguía una cosa de la otra. La vía
fiable es **Playwright headless** (como las fichas originales), resolviendo
`playwright` desde el `package.json` del repo si la sonda vive en el scratchpad.
3. **`Motion` es `once: true`.** Empujar la banda bajo el pliegue DESPUÉS de
cargar no sirve: su observador ya disparó. El espaciador tiene que existir en
el primer pintado (`addInitScript` con una hoja de estilo).
## Lo hecho en la sesión del 2026-08-09 — fase 5, el guard endurecido
`blocks:check` deja de vigilar sólo la forma del tier y pasa a vigilar su
frontera, su documentación y su catálogo. Tres reglas, cada una con fixture
negativo en el `selfTest()`:
1. **Lista blanca de importaciones (B-4)** — la frontera dura 1 era prosa; ahora
es código. Permitido: `$uix`, los arts públicos, `$libs/forms`, `svelte` /
`svelte/elements` y los relativos del propio block. **Los arts se DERIVAN de
`src/arts/*`**, no se listan a mano — una lista escrita se queda atrás el día
que aterriza un art (y este proyecto ya pagó dos veces esa lección). El tier
ya cumplía: cero violaciones al encenderla. Cierra la disposición de A-05.
2. **README completo (B-9) + declaración de landmark (B-8)** — las cuatro
secciones de la plantilla y un párrafo `**Landmark + headings**`. Al
encenderla salieron **7 de 15** sin declaración: los tres de A-14 más
`banner`, `content-section`, `cta`, `newsletter` y `site-footer`, que
nombraban «landmark» en una celda de tabla pero no declaraban nada. Escritas
leyendo la fuente de cada uno. **A-14 → ARREGLADO.**
3. **Ficha en `_lib/catalog.ts` (B-9)** — el árbol y el catálogo tienen que
coincidir en los DOS sentidos: un block que el catálogo no publica es
invisible para el raíl, y un slug publicado sin block detrás es un enlace
muerto.
De paso, **el escáner deja de leer los comentarios como código**: los `index.ts`
documentan su uso con un `// import { Cta } from '$blocks/cta'`, y una lista
blanca que lee prosa habría empezado a acusar a los ejemplos. (No era teórico:
esos 15 comentarios ya entraban en el escaneo de B-10 y sólo se salvaban porque
cada ejemplo cita su propio block.)
**Verificado en ROJO sobre el árbol real, no sólo contra los fixtures** — que es
la única prueba de que una regla nueva no es decorativa: un `zod` y un
`$libs/days` metidos en `hero/types.ts` salen con su línea exacta mientras el
mismo import dentro de un comentario NO salta; el catálogo falla en las dos
direcciones (`shipped: false` sobre un block vivo, slug fantasma); y al romper
`isAllowedSpec` a propósito el self-test aborta el guard en vez de dar verde.
## Qué queda, por orden
**Las 19 filas CONFIRMADO vivas**, agrupadas por lo que hace falta para cerrarlas.
Cada una lleva mecanismo y evidencia en su ficha del ledger — **léela antes de
re-medirla** (perdí una sesión re-midiendo A-67 sin leer la suya, que ya tenía el
análisis completo).
### a) Bloqueadas por decisión TUYA — no las toco sin tu firma
| Fila | Qué hay que decidir |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A-67** `faq` | Dos earcons simultáneos al cambiar de pregunta. Puerta ya elegida en la ficha: un nuance `emerge-collapse-swap` (morfo + provider + pack). **Cae en el eje de sema**, así que se ejecuta en una sesión suya, no aquí. |
| **A-47** `cta` (ALTA) | Anillo de foco. Por doctrina es **un eje de configuración**: se endurece con un VALOR en `color.focus.ring`, «never a per-component CSS change» — que es justo lo que hacía el intento revertido en `e468e764b`. |
| **A-09** `stats-band` | Su disposición APAGARÍA la detección live: el contrato exige `value` para detectarla. El defecto real es el anuncio crudo en `metrics-value.svelte:40`, que es canon. |
### b) ~~Bloqueada por una pieza que no existe~~ — CERRADA 2026-08-19
- ~~**A-95** `banner`~~ **ARREGLADA**: la pieza era `app-shell` (F3.1) y ya
existe. Solape 0px y hit-test dentro de la cabecera, medido. Lo que sigue
siendo cierto: un `affix="top"` FUERA de un shell sigue tapando una cabecera
pegada — es el apilamiento firmado, y el README de `banner` es donde se dice.
### c) Las 5 que dejó la página compuesta (F2b-D, 2026-08-18)
Todas medidas, todas CONFIRMADO, ninguna tocada — salen de componer 14 blocks en
un documento y de construir `cookie-consent`:
| Fila | Qué |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A-106** `Button` (canon) | Acepta `ref` en su tipo y NUNCA lo reenvía: `bind:ref` tipa bien y no ata nada. **Forma exacta de A-94.** |
| **A-107** `Switch` (canon) | El morfo no tiene parte de etiqueta: la etiqueta visible es hermana y no es diana de clic. Todos los consumidores del repo repiten el mismo apaño. |
| **A-108** `Dialog` (canon) | Devuelve el foco al nodo que lo tenía al abrir; si el disparador se desmontó, acaba en el `body` — y la trampa sigue armada un frame más. |
| **A-109** `Banner` (canon) | DOS landmarks `banner` en toda página con cabecera; el `role` se estampa DESPUÉS de los rest props. |
| **A-110** `feature-split` | Único block de sección sin `.Header`: sus filas emiten `h2` y la sección se queda sin nombre. |
### d) Las 15 restantes — trabajo normal
`A-11` site-header · `A-29` pricing+testimonials · `A-48` banner · `A-53` hero ·
`A-55` banner · `A-60` site-footer · `A-61` contact · `A-62` team · `A-65`
pricing · `A-71` site-footer · `A-74` feature-split · `A-75` pricing · `A-79`
site-footer (SOSP) · `A-92` pricing · `A-96` banner.
⚠️ Dos avisos sobre este grupo: **A-55 no es un defecto visible** (20,5px = 1,6%
del ancho; él lo ve centrado y tiene razón), y **A-65/A-75 son el mismo patrón**
— re-emitir sobre un control que ya emite.
### e) La ola F2b — CERRADA ENTERA 2026-08-18
**La fuente es `PLAN-blocks.md` §F2b** — no dupliques aquí sus tablas. Cuatro
tramos, en orden: **A** variantes de suelo (~~V4 hero `form`~~ ✅ · ~~V8
single-price~~ ✅ · ~~V5 cta split~~ ✅ · ~~V2 testimonials spotlight~~ ✅ ·
~~V3 faq-lista~~ ✅ · ~~V1 `Pricing.Compare`~~ ✅ — **TRAMO A CERRADO 6/6 el
2026-08-17; entra por el tramo B**) · **B** ✅ **CERRADO 2026-08-17** (15 matrices + guard encendido y probado en rojo) — la matriz de
equivalencia variante-a-variante en el README de cada block, con recetas
demostradas en demo (el entregable central: responde a la brecha de
cardinalidad sin competir en dumps) · **C** ✅ **CERRADO 2026-08-18** (~~`logo-cloud`~~ ✅ con el eje `data-ink` en la fundación · ~~`blog`~~ ✅ como **`article-grid`** — el nombre se decidió en su fase 0 — · ~~`cookie-consent`~~ ✅ con su fase 0 legal: las 5 reglas son el TIPO y 8 tests, y **una sola llamada requerida `onDecision(consent, via)` en vez de las dos de la ficha** — es MÁS fuerte, porque el camino de rechazo lo renderiza el block y no el consumidor, que con dos callbacks podía pasar un no-op) **C** tres blocks nuevos — `logo-cloud`
(reabierto: las refs lo shippean estático), `blog`, `cookie-consent` (fase 0
legal profunda; la ley vive en el TIPO) — y `onboarding` NO entra (receta de
`wizard`) · **D** ✅ **CERRADO 2026-08-18** — `web/routes/blocks/landing/`, 14 de 18 blocks a sangre, medidas en su propio README. **F2b QUEDA CERRADA ENTERA.** Lo que dejó: A-95 re-confirmado con cifras (solape 49px = la cabecera entera; falta `app-shell` F3.1) · `Newsletter.Reason` arreglado (2,33 → 8,83:1 sobre el panel: la frase del bloqueo no se leía) · dos gaps nuevos (los DOS landmarks `banner` del canon · `feature-split` sin `.Header`, el único block de sección sin ella) · el catálogo aprendió `kind: 'page'` con guard probado por mutación. ⚠️ **medir una página con motion de viewport**: `fullPage` no hace scroll y sale EN BLANCO, y saltar al final tampoco dispara el observador — se recorre en pasos.
Las 4 filas que la sección «Deuda a decisión tuya» de este handoff listaba
llevan ahí su firma (V1 parte, no hermano · V2/V3 `layout` · V4 slot). V6/V7
(stats-band split-with-image y timeline) salieron a F5: el dossier declara ese
block «Paridad OK».
⚠️ **Lo que dejó V4 y sirve para las que vienen**:
- **Dos gaps de canon** (README del hero §«Found while composing»):
`Form.Submit` no reenvía al `Button` ni `intent` ni eje de anchura — el botón
no llena su celda cuando la fila colapsa, y `newsletter` mide lo mismo, así
que es del canon, no del block. El `justify` de `Grid` es `justify-content`,
no `justify-items`: no se arregla desde fuera. La demo usa `style`, que sí se
reenvía.
- **El contexto de inversión mordió por segunda vez**: bajo `background`, un
`Text` sin `color` explícito mide **1.62:1**; con `on-solid`, 10:1. Bajo
`background` la tinta se pone explícita SIEMPRE.
- **La sonda de contraste**: mide sobre PÍXELES PINTADOS, nunca parseando
colores. Dos sondas dieron cifras seguras y falsas antes (un parser rgb sobre
un sistema que emite `oklch`, y `canvas.fillStyle`, que tampoco normaliza
oklch aquí → medía NEGRO contra todo: 20.46:1 en claro y 1.11:1 en oscuro,
ficción las dos). Control de sanidad: la etiqueta del submit tiene que salir
a **5.18:1**, la cifra que el repo ya documenta para `primary`. Vive en
`scratchpad/v4-final.mjs`.
- **El preview cambia de tema por `?mode=dark`**, no por `prefers-color-scheme`
ni tocando `data-theme` a mano (el boot es el dueño; forzar el atributo NO
repinta y da dos medidas idénticas que parecen «no hay diferencia»).
⚠️ **Y lo que dejó V8, que vale para TODA la ola**: **una fila «diferida» puede
ser una medición equivocada, no un aplazamiento.** El README de `pricing` decía
desde julio que el plan único «ya lo cubre el layout» y era falso — la rejilla
es `auto-fill` y RESERVA las pistas que caben aunque nadie las ocupe (un plan
solo: card de 315px con 677px de vacío al lado). Nadie había mirado ese block
con menos de tres planes. Antes de dar por buena una disposición vieja de
cualquier README, mídela.
⚠️ **Y dos cosas del árbol COMPARTIDO, medidas el 2026-08-17**: otra sesión está
a medias con un componente **`background`** (toca `morfo/`, `langs/` y
`eidos/generated`), así que `svelte-check` da 73 con cosas que no son del tier —
la base real sin nadie es **60**. Y **no hagas `git stash --include-untracked`
aquí**: arrastra su trabajo sin commitear. Si necesitas una base, mide por
fichero.
### e) Cabos y fases
1. **Tres cabos concretos**:
- Un **desbordamiento de texto en las cards del card-group** que el usuario ve
y que NO se reprodujo a 1280 en claro ni en oscuro. Falta el ancho de ventana.
- Los **hermanos del `Field`** (`password-field`, `search-field`, `textarea`)
declaran su propio `commit-submit` copiando el patrón: pueden duplicar igual
dentro de un `Form`. Se comprueba en un comando con la sonda (abajo).
- **La página compuesta** dejó de ser un cabo suelto: es el tramo D de F2b.
2. **F3 del plan original** (10 blocks de aplicación), ya con orden recomendado
en el plan: **`app-shell` PRIMERO** (desbloquea A-95 y F4.1); `data-table` y
`kanban` al final por su fase 0 obligada.
## ⚠️ El sonido CAMBIÓ DE MODELO el mismo día — léelo antes de tocar un pack
Mientras esta sesión trabajaba, la de sema rehízo el sonido dos veces. Modelo
vigente (`CONTINUE-sema-audit.md` §3.0-bis):
```
nombre = per-emit ?? cascada ?? pack ?? morfo ?? familia[verbo] ?? familia.default
sonido = pack[`${nombre}.${intent}`] ?? pack[nombre] ?? nada
```
- **`soundTuning()`, `SOUND_TUNINGS` y `sound()` están RETIRADOS**, con guard
propio (`sounds-grammar.test.ts`). El sonido ya no se modula: **se elige**. Un
intent selecciona OTRO sonido entero, no doblega el mismo.
- **El sonido lo decide el mapa, por VERBO** (`SEMA_MAP.families[f].sounds`), así
que **un pack escribe una regla sólo cuando DIFIERE del default**: de ~165
reglas a 30 en 71 packs. Un pack migrado es casi sólo selectores.
- **`allowedTargets`** en el morfo declara a qué partes puede viajar el estampado.
- Guard de **distinguibilidad**: dos nombres han de diferir en ≥2 ejes.
**Qué significa para la cola de este handoff**: nada se invalida — lo pendiente es
QUÉ evento se emite y SOBRE QUÉ parte, no cómo suena. Lo que cambia es el método:
si al arreglar un confirmado hiciera falta tocar el sonido, ya no se afina, se
NOMBRA; y si el nombre no existe, se registra en el catálogo antes de usarlo.
El pack de `navigation-menu` de esta sesión ilustra el encaje: la otra sesión lo
migró encima y **las tres reglas sobrevivieron sin su `sound:`** — la aportación
era a qué parte apunta cada una, y eso es ortogonal al modelo de sonido.
## La sonda de sema — `G:/tmp/sonda-sema.mjs`
Herramienta nueva, y la forma de contestar «¿esto suena?» sin discutirlo:
```bash
node G:/tmp/sonda-sema.mjs <url> <enter|click|hover> [selector]
```
Dice, por gesto, cuántos nodos de audio se crean y qué eventos se estampan con su
familia e intent. Con ella se midió el doble `commit-submit` del `Field` (2
eventos a 11 ms y 4 nodos de audio → 1 y 2) y el mudo del `NavigationMenu.Link`.
⚠️ Sesgo conocido: asocia los metadatos por ventana temporal de 30 ms, así que
puede atribuir el `intent` de un evento vecino. El `contact-activate` NO lleva
intent (morfo de Button, cap. 22 §11).
⚠️⚠️ **Y su límite, que el handoff de sema documenta como lección cara**: contar
nodos de audio **no es medir la salida**. Sirve para «¿emite o no emite?», que es
para lo que se usó aquí. Para «¿suena como debe?» hay que renderizar el grafo
offline (`OfflineAudioContext`) y medir el pico de muestra — ahí se descubrió que
una firma «silenciada» salía a −13.9 dBFS.
> ⚠️ **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.)
Commiteado en `alpha-0.1-dir-prefs` (`866089a40` + este handoff). SIN PUSH.
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. La sección dice también a quién NO
aplica.
**Enmendado el 2026-08-06**: los servicios sancionados son DOS — el traductor,
que resuelve las palabras, y el anunciador (`uix.announce`), que las entrega a la
tecnología asistiva. Entró por la misma razón que el primero: un block que posee
las palabras de sus estados tiene que poder decirlas, y montar una región viva
propia re-implementa `Announce`.
**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` COORDINA desde 2026-08-06** — era el candidato abierto de esta
lista y ya está cerrado. Máquina de cinco estados (`incomplete · invalid ·
ready · sending · sent`), dos `Record` exhaustivos de razón y acción, y
`Newsletter.Submit` / `Newsletter.Reason` como PARTES que leen el contexto: la
regla de forma del tier aplicada, no una excepción — compound se gana cuando las
partes repiten o **coordinan**. `state.test.ts` con 8 casos.
⚠️ 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 — SALDADA 2026-08-17
Las cuatro se firmaron en `PLAN-blocks.md` §F2b (tramo A) y ya no bloquean:
1. **Tabla de comparación** de features×planes → **`Pricing.Compare`**, PARTE
del block (el toggle de periodo vive en su contexto y B-10 impide que un
hermano lo lea). NO es el hermano `pricing-table` que esta lista sugería.
2. **Cita en spotlight** → `layout="spotlight"` en `testimonials`.
3. **Lista estática 2/3 columnas** → `layout="list"` en `faq`, con unión
discriminada en los tipos (A-94).
4. **`form-in-hero`** → slot `form` 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 (2026-08-17 — TODO COMMITEADO Y PUSHEADO)
- `npm run blocks:check` — **verde, 0 errores sobre 15 blocks / 115 ficheros**.
- `npm run docs:check` — **0 errores, 0 avisos** (629 docs).
- `svelte-check`: **72 errores / 59 avisos**, medido justo antes y justo después
de cada cambio — misma cifra, cero regresión, ninguno en lo tocado. ⚠️ Esa
cifra SE MUEVE: con otras sesiones vivas en el mismo árbol se midió 72, 74,
75, 76, 77 y 80 en días distintos. Mídela justo antes y justo después de tu
cambio, **nunca contra un número recordado**.
- Suite completa: **8 rojos en 3 ficheros, todos AJENOS** —
`contracts.test.ts` (6), `eidos/lint.test.ts` y `soma-attr-audit.test.ts`
nombran `waveform` / `skin-media-player` / `radio-group`, que entran por
`bf12b8a57` y `68a48a440` (sesión del media-player, componentes sin morfo), más
un timeout de `engine-orca` que huele a flaky. Ninguno cita nada del tier.
- `prettier`: los avisos que quedan son de ficheros **CRLF preexistentes**
(`motion.svelte`, `motion.css`, `motion/types.ts`, `motion/README.md`,
`AUDIT-blocks-ledger.md`) — ya fallaban en HEAD y formatearlos produciría un
diff del fichero entero. Se dejan como están, a propósito.
## Estado de gates al parar (2026-08-09, fase 5 — histórico)
- `npm run blocks:check` verde con las tres reglas nuevas, 15 blocks / 114
ficheros · `vitest src/uix/blocks/` 20/20 · `vitest src/uix/eidos
src/uix/blocks` 410/410 · `docs:check` 0/0 (618 docs) · `svelte-check` 74/54.
## ⚠️ El árbol está COMPARTIDO — cuidado al commitear
El commit `866089a40` incluye ficheros que **ya venían modificados** cuando
empezó la sesión: había trabajo sin commitear de otras sesiones sobre `blocks/` y
`eidos/`, y al editar encima no hay forma de separarlo por fichero. Lo que sí se
respetó: `morfo/{calendar,pagination,rating-group,toolbar,tree-view,schema,types}`,
`sema/components/textarea.ts` y `soma/tree-view` quedaron FUERA por ser de otra
sesión.
Antes de tocar sema o morfo, mira si hay otra sesión viva ahí. En este mismo día,
una arregló el emparejamiento de las reglas (`closest()`) y el renombrado que mató
el prefijo `form.` en las claves de sonido, y otra (ésta) cambió a qué parte
apuntan los eventos. Se complementaron por suerte, no por diseño.
---
## Sesión 2026-08-01 — lo que cambió
- **`content-section` HECHO** → F2 cerrada 15/15. Rejilla de escape de 5 pistas
(`measure` col 3 · `wide` col 2/5 · `full` col 1/-1), los tres ejes RESPONSIVE
por `eidos.resolve()`, y `.Media` con default **`measure`**: la figura sigue al
texto y salirse se PIDE, como en todas las referencias.
- **CANON tocado por orden del usuario**: un componente no es librería de otro. Las
cinco escalas tipográficas (`TextTracking`, `TextLeading`, `TextWrap`,
`TextNumeric`, `TextMeasure`) salieron de `components/text/types.ts` a
`eidos/lib/types.ts`. `Heading` ya las importaba de `Text` desde antes.
- **La galería EMITE**: arrancaba sin packs de sema desde F0 — el motor estampaba
`data-event-*` y nada era audible. Ahora `events: { sound, haptic, components }`
con la lista DERIVADA del barrel (`Object.values`), no escrita a mano: la lista a
mano del otro arnés se quedó atrás dos veces.
- **Los 15 READMEs declaran su posición** bajo la doctrina de coordinación. El mapa
no es uniforme y está en la sección de arriba.
### Lecciones de verificación que costaron esta sesión
1. **La puerta de hidratación no puede ser un atributo que el SSR ya manda.** Usé
el `type` del submit y la espera se cumplía sobre el documento estático: tecleé
antes de hidratar, los valores entraron en el DOM, `form.values` siguió vacío, y
perseguí un fallo inexistente varias rondas. La puerta honesta es algo que solo
escriba el cliente (`style#uix-blocks-display`).
2. **Medir el block contra SÍ MISMO no basta.** El defecto de los 16px de
`content-section` solo apareció al poner una sección normal debajo y comparar
bordes. Los 15 blocks están verificados cada uno contra sí mismo.
3. **Inspeccionar en una pestaña de fondo miente**: `visibilityState: 'hidden'`
suspende el rAF y deja `data-animation-pending` con `opacity: 0`, que parece un
fallo de visibilidad grave y no lo es.
4. **Screenshot Y MIRAR.** Generé la captura en oscuro y nunca la abrí.

Powered by TurnKey Linux.