You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-blocks.md

2609 lines
193 KiB

# PLAN — Tier `blocks`: composición reutilizable (infraestructura + componentes base + catálogo)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
> **Kickoff para sesión nueva**: _"Lee `docs/process/PLAN-blocks.md` y continúa
> la fase que toque."_ Decisión de usuario (2026-07-21): existe un tier nuevo
> **`blocks`** — conjuntos de componentes desempeñando una función (cabecera
> sticky, hero, footer, app-shell…). Este plan es autosuficiente: cada fase
> lista QUÉ leer, QUÉ producir y CON QUÉ guard se verifica. Un agente no debe
> descubrir la doctrina por arqueología — este documento la enlaza toda.
>
> **Antes de escribir código de F0**: presentar al usuario las decisiones
> D-BLK de §2 (AskUserQuestion o tabla en chat) y obtener firma. Las
> propuestas de este plan son eso — propuestas razonadas, no decisiones
> tomadas.
---
## 0. Contexto y estado
- **Origen**: análisis del ecosistema (sesión 2026-07-21). Diagnóstico: el
catálogo de primitivas es excepcional (~140 componentes eidos, ~95 con soma);
la brecha está en (a) piezas de contenido/estado de página y (b) el nivel de
composición. Iniciativa registrada en `docs/next-features.md` §8.
- **Precedentes en el repo**: la familia `chat-*` es un "bloque" construido
como componentes canónicos (siguió la ruta de 9 fases porque cada pieza
tiene contrato real); `picker-shell` es un chasis compartido; el tier
`packs` (`docs/architecture/packs.md`) ya resolvió la pregunta "¿cómo vive
un tier fuera del canon?" — este plan lo usa de espejo.
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Estado**: F0/F1/F2 cerradas. Actualizar esta tabla al cerrar
cada tanda, estilo `PLAN-component-coherence.md`.
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Fase | Contenido | Estado |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **F0** | Infraestructura del tier: doctrina + alias + guard + rutas demo | **HECHA 2026-07-21** (F0.1–F0.7; cross-ref en `comparison.md` omitido a propósito — sin aporte hasta que exista catálogo) |
| **F1** | **8** componentes base del CANON que los blocks necesitan (7 + `nav-tree` por E-1) | **CERRADA — 8/8 HECHAS** (empty-state · result · callout · **sticky** · **prose** · **anchor-nav** · **nav-tree** · **sidebar**, PASS las ocho; ver registro) |
| **F2** | Blocks de sitio (**15**: 10 + banner·team·contact·content-section por E-3 + feature-split) | **CERRADA EN UNIDADES — 15/15** (+ saneamiento, guard endurecido y contraste doctrinal 15/15; ledger `AUDIT-blocks-ledger.md`). La página compuesta que su cierre exigía NO se construyó: la integración vive en F2b |
blocks(landing): las piezas se prueban juntas, y ahí sale lo que ninguna enseñaba sola Cierra el tramo D y con él F2b entera. Una landing verosímil con 14 de los 18 blocks, a sangre y sin marco de demo, porque el claim del tier —que sus blocks ensamblan sin fricción— es indemostrable block a block: el outline de encabezados sólo existe cruzando secciones, dos landmarks sólo chocan cuando hay dos, y una costura vertical sólo aparece cuando algo se pone debajo. Lo que dice la medida: un h1 y 22 encabezados sin un solo salto de nivel; ningún landmark repetido sin nombre; 0px de hueco entre secciones vecinas —el aire es siempre el padding de cada Section, costuras de 112 a 192px—; cuatro de las cinco cascadas esperando a que su sección entre en viewport; un solo commit-submit en el formulario; setenta tabulaciones sin una trampa de foco y un drawer que a 375px devuelve el foco a su disparador. A-95 vuelve confirmado, ahora con cifras: con affix="top" la tira y la cabecera solapan 49px —la altura entera de la cabecera— y el hit-test en su centro cae en la tira, así que la navegación es inalcanzable. La pieza que falta sigue siendo app-shell. La página compone la tira en flujo, que es lo correcto, y deja el escenario del defecto detrás de ?banner=affix para poder medirlo en vez de citarlo. Un defecto arreglado, y era de los que importan: Newsletter.Reason medía 2,33:1 sobre el panel mientras la nota a su lado medía 8,5:1. Es la frase que explica por qué la acción está bloqueada —la que la doctrina de coordinación obliga a tener— y no se podía leer: la doctrina cumplida en la estructura y derrotada en la percepción. El block ya tenía el mecanismo y ya lo había escrito para su nota («más callado por TAMAÑO, no por tinta»); su razón no lo seguía. Ahora 8,83:1 sobre el panel, y muted fuera de él. Dos gaps nuevos, ninguno tocado aquí: los DOS landmarks banner —el canon estampa role="banner" DESPUÉS de sus rest props, así que nombrar ambos es todo lo que puede hacer la app, y esta página lo hace— y feature-split sin parte .Header, el único block de sección que no la tiene: sin ella emite tres h2 hermanos y la sección se queda sin nombre. El catálogo aprendió a publicar páginas: kind: 'page' en BlockEntry y el guard lo salta en las dos direcciones, con self-test propio probado por mutación. Y una lección de instrumento que costó un susto: una captura fullPage no hace scroll, así que toda cascada bajo el pliegue se queda en opacidad 0 y la imagen sale EN BLANCO. La primera medida dijo «la landing está vacía». Saltar de una vez al final del documento tampoco vale: las secciones pasan de debajo a encima en un frame y el observador nunca las ve entrar. Se recorre en pasos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **F2b** | **Ola de paridad de variedad**: variantes de suelo (A) + matriz de equivalencia (B) + `logo-cloud`·`blog`·`cookie-consent` (C) + página compuesta (D) | ✅ **CERRADA 2026-08-18** — los cuatro tramos ejecutados |
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **F3** | Blocks de aplicación (10) | pendiente |
| **F4** | Blocks de docs (3) | pendiente |
| **F5** | Backlog condicionado (componentes media/mobile + blocks diferidos) | pendiente |
---
## 1. Qué es un block (doctrina propuesta — aterriza en `docs/architecture/blocks.md` en F0)
Un **block** es una composición nombrada de componentes del canon que
desempeña una función de página: no aporta primitivas nuevas, aporta
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_ensamblaje correcto_ (layout, landmarks, jerarquía de headings, responsive,
puntos de contenido). Consume el framework; el framework nunca lo referencia.
La tabla de tiers queda:
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Tier | Valor | Contrato | Entra por |
| ------------------------------ | ------------------------------------------------------------------------ | ---------------------------- | ------------------------------------------------ |
| **Canon** (`src/uix/`) | densidad de contrato (eventos, ARIA, teclado, tokens que otros consumen) | morfo + matriz de aceptación | ruta de 9 fases (`docs/building-a-component.md`) |
| **Packs** (`src/packs/`) | decoración parametrizada de hoja | contrato P | `packs-check` |
| **Blocks** (`src/uix/blocks/`) | composición de función de página | contrato B (§3) | `blocks-check` |
**Regla de admisión (espejo de la de packs)**: si al construir un block hace
falta comportamiento nuevo con superficie de contrato — un evento real, una
máquina de estados, un `data-attr` que el CSS necesita seleccionar, una
obligación a11y de widget — esa pieza se construye ANTES como componente
canónico por la ruta de 9 fases, y el block la compone. **Nunca se le crece
el privilegio al block**. (Es exactamente la regla "compose existing
components; flag gaps" de `docs/guides/component-guide.md` §4, elevada a
frontera de tier.) La F1 de este plan existe porque ese triage ya está hecho:
los 7 gaps detectados se construyen primero.
Lo que un block SÍ posee semánticamente: **landmarks y estructura de
documento** (`header/nav/main/aside/footer`, jerarquía h1–h6, skip-link,
`aria-label` de región). Los componentes no pueden saber el contexto de
página; los blocks sí. Es su única superficie a11y propia.
Un block PUEDE tener estado de vista local (pestaña activa, toggle
mensual/anual) usando los props/eventos públicos de los componentes que
compone. Lo que NO puede es materializar ese estado con DOM/CSS propio que
requiera contrato (→ regla de admisión).
---
## 2. Decisiones D-BLK — FIRMADAS 2026-07-21
Repasadas y firmadas por el usuario en el kickoff (F0.1 HECHA). **D-BLK.1
quedó enmendada** respecto a la propuesta original del plan (que proponía
`src/blocks/`); el resto se firmó tal como estaba propuesto. D-BLK.3/4/5
derivan de doctrina ya vigente y se firmaron por no-objeción.
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que había que preguntar: si su alcance tenía delante `active-app` y `active-uix` enteros. No los tenía. Las cuatro decisiones que se firman, y lo que las obligó: - **Q1 — el block NO cablea servicios.** La integración del ecosistema se demuestra en una app de referencia en app-land, no dentro del block. Un shell que leyera `App.session` para dibujar el menú de usuario sería una raíz de composición disfrazada: funcionaría, y metería el arranque del ecosistema dentro de una pieza que ha de poder caer en cualquier app. - **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed, sin números) es lo que cierra A-95 por construcción; `body` es lo que `docs-shell` necesitará para anclas y TOC pegada. - **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho exigiría partir ese provider: queda como gap de canon para v2. - **Q4 — canon `skip-link` + uno por región montada** (técnica G124). De paso, tres cosas que estaban mal escritas y ahora lo dicen: - El dossier afirmaba que **ninguna referencia trae skip-link** y que era superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera un menú entero. La superación real es generarlos del mismo contrato que estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí miró está deprecado. - **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni cablea servicios. - **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`, `mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo. - **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta `update-password`, que no es un lujo — es donde aterriza el enlace de recuperación, y sin ella el camino de `recover` no termina. `blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría, landmarks, alturas) y dónde está la línea (la raíz de composición es de la app). Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box` expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell no necesita CSS propio—, y `Sticky` expone `root`. docs:check 0 errores, 0 avisos (635 docs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| # | Decisión | FIRMADO |
| ----------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **D-BLK.1** | Ubicación y alias | **`src/uix/blocks/`** + alias `$blocks` (decisión de usuario: el tier es UI y vive junto a las capas). La prueba de encapsulación se conserva íntegra: borrar `src/uix/blocks/` deja `npm run check` verde y NADA del canon (`morfo/soma/sema/eidos/active-uix/langs`) lo importa — blocks es un tier bajo `src/uix/`, no una quinta capa. |
| **D-BLK.2** | Estilos | **Layout-components-first**: el layout se hace componiendo `Container/Section/Stack/Flex/Grid/AutoGrid/Wrap/Group/Separator/AspectRatio/Surface` y sus props. Un block NO trae `.css` propio; un `<style>` scoped puntual exige justificación en su README y nunca selecciona internals de componentes compuestos. |
| **D-BLK.3** | API | Componente compuesto con partes anidadas: `<SiteHeader>` / `<SiteHeader.Nav>` / `<SiteHeader.Actions>`; contenido SIEMPRE por children, nunca árboles de datos (`items={...}` solo donde el componente canónico compuesto ya es data-driven). (Derivada de la regla compositional-not-data-driven ya vigente.) |
| **D-BLK.4** | Demos | `web/routes/blocks/{kebab}/+page.svelte` + galería índice en `web/routes/blocks/`. (La ruta `web/routes/alpha/` sigue TERMINADA y prohibida — no tocarla.) |
| **D-BLK.5** | Idiomas/strings | Un block no posee NINGÚN string visible: todo texto llega del app como children/props. Si un string parece inevitable, es superficie de contrato → lo posee el componente canónico subyacente (vía `texts:` del morfo + langs). (Consecuencia mecánica de B-1: sin morfo no hay `texts:`.) |
| **D-BLK.6** | Servicios | **v1 sin servicios**: los blocks NO consumen `uix.prefs`/langs/eidos directamente; el cableado (tema/idioma, submit de auth, transporte) llega como handlers/props del app. Revisable si ≥2 blocks demuestran necesidad real (misma vara que la 2-de-3). **ENMENDADA por la práctica — la doctrina vigente es `architecture/blocks.md` §Services, no esta celda**: el traductor entró el 2026-07-31 (un block posee las palabras de SUS estados) y el anunciador el 2026-08-06 (poseerlas sin poder decirlas es medio trabajo), los dos por `ActiveEidos.require()`; y el breakpoint del sistema (`dom.isAtLeast`) es consumo obligado por B-6, que prohíbe el `matchMedia` propio — `site-header` lo hace desde su construcción. Lo que la celda sigue diciendo bien, y no se ha movido: **ningún block crea servicios ni los cablea por su cuenta** (tema, sesión, permisos, transporte, persistencia). Reafirmado en la fase 0 de F3.1 (2026-08-18) contra la tentación más fuerte que ha tenido el tier: un shell que dibujara el menú de usuario leyendo `App.session`. |
| **D-BLK.7** | Naming F1 | `sticky` · `anchor-nav` · `empty-state` · `result` · `callout` · `prose` · `sidebar` — confirmados. (Los matices de naming siguen revisables en la fase 0 de cada uno, como toda fase 0.) |
Cualquier enmienda futura a una D-BLK se registra aquí con fecha ANTES de
seguir construyendo (regla dura: los desvíos de alcance se declaran, nunca en
silencio).
---
## 3. El contrato B (suelo de calidad de un block — guard: `blocks-check`)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| B | Obligación |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **B-1** | Sin morfo, sin pack sema, sin fila en `component:audit`. Un block es composición; el comportamiento con contrato se promociona al canon ANTES (regla de admisión §1). |
| **B-2** | Todo elemento interactivo es un componente eidos del catálogo (`Button`, `Link`, `Field`, …). Elementos nativos interactivos crudos (`button/input/select/textarea/a`) = **error** de `blocks-check`. (Excepción única: el HTML que `Prose` recibe ya renderizado — ese contenido es del app.) |
| **B-3** | Los componentes compuestos se consumen AS-IS por sus props públicos (`variant/size/color/…`). Prohibido re-estilizar sus internals desde el block (ni CSS ni `style=`). Los colores son siempre roles/tokens vía props — un block no decide color fuera del sistema. |
| **B-4** | Dependencia unidireccional: `src/uix/blocks/*` importa `$uix`, `$adom` y arts públicos; nada del canon (`src/uix/{morfo,soma,sema,eidos,active-uix,langs}`) ni de `src/{arts,libs,packs}` importa de `src/uix/blocks/`. Borrar el tier deja `check` verde — la prueba de encapsulación se mantiene aunque viva bajo `src/uix/`. Entre blocks tampoco se importa (B-10). |
| **B-5** | Contenido por composición (children/snippets). Nunca `root={tree}` ni props-árbol propias. |
| **B-6** | Responsive con los mecanismos del framework (props responsive de los componentes de layout, breakpoints canónicos). Cero `matchMedia`/listeners propios — si hiciera falta observar algo, es señal de componente canónico (→ admisión). |
| **B-7** | Cero strings propios (D-BLK.5). |
| **B-8** | Landmarks correctos: elemento sectioning + `aria-label`/`aria-labelledby` cuando hay más de un landmark del mismo tipo; jerarquía de headings coherente y documentada en el README del block (qué nivel emite y cómo se ajusta). |
| **B-9** | Cada block: `README.md` (secciones: **Función · Mapa de composición** — qué componentes canónicos usa y con qué props — **· Decisiones · Gaps-con-disposición**) + demo con profundidad de testbed (cada prop pública = control vivo; guía: `docs/guides/demo-authoring.md`, adaptada — sin las 9 tabs completas de componente, mínimo: escena realista + panel de props + código copiable). |
| **B-10** | Un block no importa otro block. Si dos blocks comparten estructura, la pieza compartida o es un componente canónico o se duplica conscientemente (anotado en Gaps). Excepción declarada: los shells (`app-shell`, `docs-shell`) SÍ componen blocks/componentes de F1 por diseño — se lista explícitamente en su README. |
| **B-11** | Motion: solo vía los props `motion`/presets de los componentes compuestos o `Cascade` para coreografía de entrada. Cero `@keyframes`/transitions propias (la regla R-4.5 del canon aplica moralmente aunque el audit no corra aquí). |
`blocks-check` (F0.5) verifica mecánicamente: B-2 (AST/regex de elementos
nativos interactivos), B-4 (dirección de imports), B-1 (no hay ficheros bajo
`src/uix/morfo/components/` reclamados por blocks; no imports de
`sema/components`), D-BLK.2 (no `.css` bajo `src/uix/blocks/`; `<style>` solo
con `/* justified: … */`), B-9 (README + ruta demo existen), B-10 (imports
entre blocks solo en la allowlist de shells).
---
## 4. Reglas de trabajo (TODAS las sesiones de este plan)
**Lectura obligatoria antes de tocar nada** (leer los docs directamente —
nunca delegar la lectura a agentes):
- Siempre: `CLAUDE.md` (llega solo) · este plan · `docs/README.md` (mapa).
- F1 (componentes canon): `docs/building-a-component.md` — LA puerta; cada
fase de la ruta nombra su doc y su guard. No saltarse la fase 0 (tabla
comparativa vs ≥3 referencias; cada ❌/⚠️ del scope recibe decisión del
usuario ANTES de construir).
- F2–F4 (blocks): `docs/architecture/blocks.md` (existirá tras F0) + los
README de CADA componente que el block compone (el mapa de composición se
escribe leyendo, no de memoria) + referencias de blocks equivalentes
(shadcn blocks · Tailwind UI/Plus · Flowbite blocks · PrimeBlocks · Relume)
— comparativa ANTES de diseñar y al declarar done.
- **Dossier de referencia (2026-07-21)**: TODA fase 0 (F1–F4) contrasta
contra `docs/process/RESEARCH-blocks-references.md` ANTES de diseñar — 6
pistas de investigación con suelos de paridad a nivel de prop, pitfalls y
ángulos de superación por ítem. La fase 0 verifica contra el dossier (y
solo investiga de cero lo que el dossier no cubra); las brechas de suelo
que el dossier señala para el ítem se resuelven en su scope-approval.
**Proceso por tanda** (una tanda = un componente o un block):
1. `git reset -q` + verificar HEAD (sesiones concurrentes; NUNCA amend).
2. Fase 0 del ítem: comparativa + scope al usuario si hay decisiones.
3. Construir. UN fichero → verificar → resto (no-cascade). Componer, jamás
re-implementar; gap detectado = FLAG al usuario, no workaround inline.
4. Verificar: `npm run check` + scope vitest del ítem + guard del tier
(`component:audit --only {kebab}` para F1 · `blocks-check` para F2+) +
**navegador de verdad**: screenshot y MIRARLO, claro Y oscuro
(`colorScheme:'dark'`), móvil y desktop para blocks (resize 375/1280).
5. Demo con profundidad de testbed (B-9); docs del framework en el MISMO pase
(README del ítem + mapa si procede).
6. Commit (convención viva del repo): `uix({kebab}): …` para F1,
`blocks({kebab}): …` para F2+, `docs(blocks): …` para doctrina. Stage
SOLO los paths propios (nunca `git add -A`; excluir `words/`, `palabras/`,
`web/routes/alpha/`).
7. Actualizar la tabla de estado de este plan (y `next-features.md` §8 al
cerrar cada fase).
**Prohibiciones**: no tocar `palabras/`, `chronos/`, `media-player`
(foráneos/WIP — componerlos solo cuando estén landed; hoy chronos NO lo
está); no crear servicios/mocks falsos en tests (instancias reales vía
`createActiveUix`); no `--no-verify`; no borrar nada sin instrucción
explícita; responder en castellano, código y docs en inglés.
---
## F0 — Infraestructura del tier
**Objetivo**: que exista el tier con doctrina, guard y sitio donde vivir —
vacío pero verde.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Paso | Producir | Verificación |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| F0.1 | **Firma D-BLK** (§2) con el usuario; enmendar el plan si procede | **HECHA 2026-07-21** — firmas en §2 (D-BLK.1 enmendada: `src/uix/blocks/`) |
| F0.2 | `docs/architecture/blocks.md` — la doctrina de §1 + §3 en formato espejo de `packs.md` (frontmatter E1, regla de admisión, hard boundaries, contrato B, "promotion path" = la F1 como ejemplo vivido). Enlazar sin copiar: canon → `CANON.md`, ruta → `building-a-component.md` | `npm run docs:check` (links) |
| F0.3 | Alias `$blocks` → `src/uix/blocks` en el const `aliases` de `vite.config.ts` (fuente de verdad) + `svelte.config.js` en sync + fila en la tabla de aliases de `CLAUDE.md` | `npm run check` |
| F0.4 | `src/uix/blocks/README.md` — mapa del tier (inventario vivo = el árbol, como packs) + template de README de block (B-9) | — |
| F0.5 | `scripts/blocks-check.ts` + npm script `blocks:check` — los checks mecánicos listados en §3. Espejo estructural de `packs-check`. Test negativo: un fixture con `<button>` crudo debe fallar | `npm run blocks:check` verde en tier vacío + test negativo rojo |
| F0.6 | `web/routes/blocks/+page.svelte` — galería índice (dogfooding: componer `Container/Section/Card/…` del propio catálogo para la galería) | navegador claro/oscuro |
| F0.7 | Cablear docs: fila E1 en el mapa de `docs/README.md` (architecture/blocks.md) + nota del tier en el diagrama de arquitectura de `CLAUDE.md` (línea `blocks/ → …`) + cross-ref en `docs/comparison.md` si aporta | `npm run docs:check` |
**Cierre F0**: `check` + `docs:check` + `blocks:check` verdes; galería
renderiza vacía con mensaje de "en construcción" compuesto con el catálogo.
**CERRADA 2026-07-21** — evidencia: `docs:check` 0/0 (511 docs);
`svelte-check` 76E/51W = baseline exacto (los archivos nuevos compilan
limpios; los 76 son deuda foránea preexistente del árbol sin commitear);
`blocks:check` verde (self-test 10 fixtures OK — el `<button>` crudo SE
detecta — y 5 532 archivos de canon/arts/libs/packs escaneados sin
violaciones de dirección). Galería verificada en navegador vía árbol de
accesibilidad + estilos computados en AMBOS modos (light: `base-light`, h1
oklch(0.24…); dark: `base-dark`, h1 oklch(0.95…)); la captura de píxeles
del panel embebido expiró (renderer suspendido en segundo plano — clase
conocida) — pendiente de un vistazo humano o Playwright en la primera
sesión F1. El listener `prefers-color-scheme` del layout funciona al boot;
el evento `change` en vivo no dispara con el panel suspendido (peculiaridad
del entorno, mismo patrón raw-matchMedia que el layout de `/uix`).
---
## F1 — Componentes base del CANON (8)
Estos NO son blocks: entran por `src/uix/{morfo,soma,sema,eidos}` siguiendo
**la ruta completa de 9 fases** de `docs/building-a-component.md` (fase 0
Decide → 8 Acceptance), con `component:audit --only {kebab}` como oráculo.
Las fichas siguientes NO sustituyen la ruta — la parametrizan: dan el gap, la
membresía esperada, el boceto de morfo, las referencias mínimas de la fase 0
y las trampas conocidas del ecosistema que aplican. El vocabulario cerrado
(archetypes, familias, verbos, intents, holds) se toma SIEMPRE de
`docs/canon/vocabularies.md` (generado del código) — las fichas nombran
candidatos, el builder valida contra la lista real.
**Orden recomendado**: F1.2 → F1.3 → F1.4 (display, baratos, establecen
ritmo) → F1.1 (desbloquea F2) → F1.5 → F1.6 → F1.8 → F1.7 (desbloquean
F4/F3).
**E-2 firmada (2026-07-21)**: los v1 de F1–F4 se dimensionan al **suelo de
paridad** del dossier (lo que ≥2 referencias convergen) — cada fase 0 trae
la subida concreta al scope-approval; quedarse bajo suelo exige decisión
registrada, nunca omisión.
### F1.1 `sticky` — afijado con estado
- **Gap**: wrapper `position:sticky` que SABE cuándo está afijado
(`data-stuck`) para que la cabecera cambie elevación/fondo al pegarse.
- **Membresía**: soma + eidos (comportamiento real: observación + estado).
- **Fase 0, comparar**: AntD `Affix` · Mantine `Affix` · la técnica sentinel
con IntersectionObserver (CSS-Tricks/web.dev) · shadcn (no lo tiene —
anotarlo como diferencial).
- **Morfo (boceto)**: parts `provider` (el contenedor sticky) + `sentinel`
(1px observado, `aria-hidden`); data: `data-stuck` (presente/ausente),
`data-edge` (`top | bottom`). **Sin eventos v1** (afijarse es hecho de
layout, no ocurrencia perceptiva del usuario — si algún consumidor pide
señal sema, se revisa con el criterio D.4). Sin keyboard, sin `texts`.
Partes display → **sin `archetype`** (un archetype interactivo arrastra
estilos de item; y el archetype `content` pisa `position` — justo lo que
sticky no puede permitirse).
- **Soma**: provider observa el sentinel vía `dom.observe`
(IntersectionObserver por adom) — **JAMÁS** scroll listener +
`getBoundingClientRect` síncrono (regla de reflow: lecturas de layout solo
post-layout vía `dom.measure`/rAF). `data-stuck` lo escriben los effects
(`dom.apply`), único escritor.
- **Eidos**: wrapper fino; tokens `--sticky-top-offset` /
`--sticky-z-index` (alias de la escala `--z-index-*` semántica, no número
crudo). La receta NO decide sombra/fondo del contenido — eso lo hace el
consumidor seleccionando su propio estado visual con `data-stuck` presente
(documentar el patrón en el README).
- **Demo**: página larga con header/toolbar afijable, chip mostrando el
estado, edge top y bottom.
- **Trampas**: `mergeProps` clobberea stamps — attrs visuales del wrapper
fuera del morfo salvo que crucen a soma (aquí `data-stuck` SÍ es soma).
### F1.2 `empty-state` — estado vacío
- **Gap**: patrón universal icono/título/descripción/acción para listas y
paneles sin datos. Hoy no existe nada.
- **Membresía**: morfo + eidos, **sin provider soma** (display puro; hay
precedente sancionado: `metrics` es `scope: ['sema','eidos']` sin soma).
Morfo-first aplica igual: hasta las hojas llevan morfo.
- **Fase 0, comparar**: Chakra `EmptyState` · AntD `Empty` · HeroUI ·
patrones de empty state de Material.
- **Morfo (boceto)**: parts `provider` + `media` (icon o ilustración) +
`title` + `description` + `actions`. Sin eventos, sin keyboard, sin
archetype en partes display. `texts:` NO — los strings los trae el app
(es contenido, no chrome del componente).
- **Eidos**: receta pequeña espejo de la más cercana ya enviada (estudiar
`banner`/`card` antes de escribir una línea); centrado, spacing tokenizado
`--empty-state-*`, tamaño vía canon `size` si aporta (probablemente solo
`sm/md`). `actions` compone `Button` del catálogo vía children.
- **Demo**: en contexto real — un `table`/`grid-list` sin filas mostrando el
empty-state, más la escena aislada con controles.
### F1.3 `result` — página de resultado
- **Gap**: estado terminal de página/flujo (éxito, error, 403/404/500).
Pareja de `empty-state`, distinto rol: cierra un flujo, no describe
ausencia de datos.
- **Membresía**: como F1.2 (morfo + eidos display).
- **Fase 0, comparar**: AntD `Result` · patrones de error page de
Tailwind UI · HeroUI.
- **Morfo (boceto)**: parts `provider` + `media` + `title` + `description` +
`actions` + `extra`. Prop `status` → `data-status`
(`success | error | info | forbidden | not-found | server-error`).
**Cuidado doctrinal**: `data-status` NO es el intent perceptivo — si en
algún momento gana eventos con evaluación, el intent viaja por
`fromProp:intent` del morfo, no reciclando status (regla data-color ≠
perceptual-intent). v1 sin eventos.
- **Eidos**: color del media por rol canónico según status (roles, no
hex); iconografía por status con override por children.
- **Demo**: los 6 status + composición con `Button` home/back.
### F1.4 `callout` — admonición inline
- **Gap**: aviso DENTRO del contenido (info/tip/aviso/peligro). `Banner` es
el anuncio de página; esto es la nota de documento — imprescindible para
`prose` y el docs-shell.
- **Membresía**: morfo + eidos display (sin soma; variante dismissible se
DIFIERE — si se pidiera, el dismiss es evento real → soma + sema en esa
pasada, no antes).
- **Fase 0, comparar**: Radix Themes `Callout` · shadcn `Alert` ·
admonitions de Docusaurus/Starlight · `banner` propio (leer su README:
qué decisiones ya están tomadas para avisos y cuáles NO trasladan).
- **Morfo (boceto)**: parts `provider` (role `note`) + `icon` + `title` +
`content`. La evaluación ES semántica aquí: prop `intent` restringido a
un subset canónico (candidatos: `neutral | affirm | risk | threat`;
validar contra `vocabularies.md`) estampado vía `fromProp:intent` — así
eidos tiñe con la MISMA mecánica evaluativa del sistema, no con un enum
paralelo inventado.
- **Eidos**: tinte por intent usando roles/escalas (fondo suave + borde +
icono saturado — estudiar cómo tiñe `banner` e imitar la mecánica);
tipografía del canon; `--callout-*` para spacing/radius.
- **Demo**: los 4 intents × con/sin título × con contenido multilínea, e
incrustado en un texto largo (anticipo de `prose`).
### F1.5 `prose` — contenido largo estilizado
- **Gap**: contenedor que estiliza HTML/markdown renderizado (h1–h6, p,
listas, blockquote, tabla, código, img, hr) con los tokens del tema. Sin
esto no hay docs ni blog.
- **Membresía**: morfo mínimo + eidos (sin soma). El morfo declara UNA part
`provider`; el trabajo vive en la receta.
- **Fase 0, comparar**: Tailwind Typography (`prose`) — el patrón de
referencia — · Mantine `TypographyStylesProvider` · Radix Themes.
- **Norma que lo hace legal**: los selectores de elemento descendientes
(`[data-prose] h2`, `[data-prose] ul`…) son **selectores estructurales
bajo una parte del morfo** — sancionados por la norma S1 del eidos-lint
(hook = data-attr del morfo o estructural bajo parte; clases NO).
- **Eidos**: LA receta grande del grupo. Reglas: tipografía SOLO vía
primitivos del canon (`--font-*`, escala tipográfica; cero literales — y
los proporcionales que hagan falta con `/* literal: */` justificado);
medida de lectura `--prose-max-width` (~65ch) tokenizada; `code`/`pre`
alineados con los tokens de `code`/`code-block` (leer sus recetas ANTES;
si hay que duplicar valores, es un gap a flag, no un copy-paste);
imágenes `max-width:100%`; tablas con overflow propio. El HTML interno es
del app — aquí la regla B-2 no aplica (es la excepción documentada).
- **Demo**: documento markdown real renderizado (headings, listas anidadas,
tabla, código, blockquote, callout incrustado), claro/oscuro, densidades.
### F1.6 `anchor-nav` — índice con scrollspy
- **Gap**: TOC lateral con sección activa según scroll (docs, settings
largos, landing largas).
- **Membresía**: soma + eidos (observación + estado activo + navegación).
- **Fase 0, comparar**: AntD `Anchor` · Mantine `TableOfContents` ·
Starlight/Docusaurus TOC. APG: no hay patrón de widget — es un `nav`
landmark con `aria-current`; anotarlo en el README (válvula A-1.4 con
nota, como pagination/stepper).
- **Morfo (boceto)**: parts `provider` (nav, `aria-label` vía `texts:` —
aquí SÍ hay string de chrome: "On this page"/"En esta página" → catálogo
langs) + `list` + `item` + `link` (¿archetype de item interactivo? —
validar contra vocabularies; el link compone `Link` del catálogo).
Data: `data-active` en item; `aria-current`. Eventos: click de link =
navegación (familia/verbo a decidir en fase 1 contra `SEMA_VERBS` —
candidato familia `shift`; validar). `expression:` según criterio D.4.
- **Soma**: registro de secciones objetivo (por id o por `attachPart`),
observación vía `dom.observe` (IntersectionObserver, umbrales al gusto
del provider) — mismas reglas anti-reflow que F1.1. El activo es estado
del provider; los effects estampan `data-active`. Scroll programático al
click vía `dom` (scrollIntoView por ActiveDom), no window crudo.
- **Eidos**: raíl vertical con indicador de activo (¡leer la memoria
NavMenu Indicator: soma posiciona, eidos da forma, nunca transition de
transform!); niveles h2/h3 con indentación tokenizada.
- **Demo**: página larga real con `prose` (F1.5) + anchor-nav vivo.
### F1.7 `sidebar` — navegación vertical de app
> ✅ **E-1 FIRMADA (2026-07-21)**: la colisión sidebar-app vs árbol-docs
> (P5/B-5) se resuelve con el componente canónico **`nav-tree`** (F1.8) —
> este sidebar queda app-céntrico. Además: el dossier (P4) fija el suelo
> shadcn (23 partes, dos ejes estado/modo, costuras open/onOpenChange/
> toggle expuestas desde v1) — la fase 0 dimensiona contra él por E-2.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Membresía**: soma + eidos. Es el componente más pesado de F1 —
reservarle tanda propia.
- **Fase 0, comparar**: shadcn `Sidebar` (el patrón de referencia actual) ·
Mantine `AppShell.Navbar` · Ark/Radix (no lo tienen — anotar). Scope al
usuario ANTES de construir: qué features de shadcn entran en v1
(propuesta v1: grupos + colapsable a raíl con tooltips + activo +
slots header/footer; FUERA v1: keyboard shortcut global, persistencia —
llega del app por D-BLK.6, submenús flotantes en raíl).
- **Morfo (boceto)**: parts `provider` + `header` + `content` + `group` +
`group-label` + `item` + `footer` + `trigger` (botón colapso — compone
`Button` con `asChild`/child pattern). Data: `data-collapsed`,
`data-rail`, `data-active` (item). Eventos: toggle de colapso (verbo
candidato del set real; evaluación neutral), activación de item si el
item es más que un `Link` — decidir en fase 1 (si item = `Link` puro, la
navegación no necesita evento propio del sidebar).
- **Soma**: estado collapsed/rail; grupos colapsables PUEDEN componer el
provider de `collapsible` (precedente compose-compound-in-component:
NumberField dentro de Knob, con aislamiento de eventos); variante móvil
COMPONE `Drawer` (no lo reimplementa) — el provider decide qué montar por
breakpoint responsive del sistema, no matchMedia propio.
- **Eidos**: raíl con `will-change` cuidado (memoria: `will-change:
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
transform` produce jitter en raíles finos con DPR≠1 — override a `auto`);
tooltips de item en modo raíl componen `Tooltip`; tokens `--sidebar-*`
(width, rail-width, paddings).
- **Demo**: shell de app simulada, toggle colapso, grupos, móvil (375px) con
drawer, RTL.
### F1.8 `nav-tree` — árbol de navegación data-driven (E-1, 2026-07-21)
- **Gap**: navegación jerárquica profunda (docs-shell, árboles de páginas)
con active-trail — lo que el dossier (P5) demostró que NO cabe en el
sidebar-app: ≥3 niveles, active-trail auto-expandido, auto-colapso de
hermanos, profundidad default-open, badges, scroll-active-into-view,
presentación móvil. **Data-driven por diseño** (el app pasa el árbol como
datos — precedente sancionado: Menubar; B-5 no se viola porque el
componente canónico ES data-driven).
- **Membresía — fase 0 OBLIGADA contra nuestro propio catálogo**: leer
`tree-view` (ya existe: selección de nodos) ANTES de diseñar y delimitar
la frontera tree-view (selección/widget tree) vs nav-tree (navegación por
links, landmark nav) — o justificar extender tree-view. La decisión es
del scope-approval.
- **Fase 0, comparar**: sidebars de Starlight/Docusaurus/Fumadocs (dossier
P5 — comportamientos y anatomía) + APG Disclosure Navigation (dossier
P4/P3: botones `aria-expanded`/`aria-controls`, Esc devuelve foco,
flechas opcionales) + `tree-view` propio.
- **Morfo (boceto)**: parts `provider` (nav landmark, `aria-label` vía
`texts:`) + `list` + `item` + `trigger` (grupo colapsable) + `link`
(compone `Link`; `aria-current="page"` del MISMO estado que `data-active`)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- slot badge (compone `Badge`). Data: `data-active`, `data-expanded`,
profundidad como CSS var tokenizada (no attr por nivel). Keyboard: patrón
disclosure; roving opcional opt-in.
- **Soma**: árbol como datos; el activo llega por costura del app (matcher
de URL — el componente NO conoce el router); active-trail expande
ancestros; colapso de grupos evalúa componer `collapsible`
(compose-first); scroll-active-into-view vía `dom`.
- **Eidos**: indent por nivel con token (`--nav-tree-depth-offset`, estilo
Mantine pero tokenizado), línea/raíl de nivel, estados active/expanded.
- **Demo**: árbol real de ~40 nodos y 3 niveles (p. ej. el propio mapa de
docs), active-trail vivo, móvil 375px.
**Cierre F1**: los 8 con `component:audit --only` PASS (o NEEDS-WORK
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
únicamente por reglas D-\* de demos v3 si esa fase global sigue abierta —
anotar en la tabla), `morfo:check`, `eidos-lint` por componente, suite
`npx vitest run src/uix/eidos` verde, `npm run check` sin regresión sobre
baseline.
---
## F2 — Blocks de sitio (14)
Todos entran por el contrato B. Ficha = **Función · Compone · API (partes) ·
Layout/landmark · v1 · Demo**. Regla transversal: fase 0 ligera SIEMPRE
(mirar el block equivalente en ≥2 catálogos de referencia de §4 y anotar en
el README qué se adopta/descarta). Variantes: v1 = LA variante (una);
ampliaciones = Gaps con disposición, no código especulativo.
**Depende de**: F1.1 (`sticky`) para F2.1; el resto de F2 no depende de F1.
### F2.1 `site-header`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Función**: cabecera de sitio con afijado y cambio de elevación al pegarse;
colapso a menú móvil.
- **Compone**: `Sticky` (F1.1) + `Container` + `NavigationMenu` + `Button` +
`Drawer` (móvil) + `Link` + `Separator`.
- **API**: `<SiteHeader>` (props: `sticky?`, `container?`) + `.Brand` +
`.Nav` + `.Actions` + `.MobileNav` (children del drawer).
- **Layout/landmark**: `<header>` + `<nav aria-label>`; skip-link como primer
foco (decidir en su fase 0 si el skip-link vive aquí o en los shells — una
sola respuesta, documentada).
- **v1**: brand izquierda · nav centro · actions derecha · drawer móvil;
estilización del estado pegado vía `data-stuck` (elevación/fondo con tokens
de los componentes compuestos, no CSS nuevo).
- **Demo**: página con scroll largo, claro/oscuro, 375/1280, RTL.
### F2.2 `hero`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Función**: sección de apertura con titular, subtítulo, acciones y media.
- **Compone**: `Section` + `Container` + `Stack`/`Grid` + `Heading`
(nivel configurable, default h1) + `Text` + `Group` (acciones con
`Button`) + `Badge` (chip anuncio) + `Image`/`AspectRatio`.
- **API**: `<Hero>` (prop `layout: 'center' | 'split'`) + `.Eyebrow` +
`.Title` + `.Description` + `.Actions` + `.Media`.
- **Layout/landmark**: `<section aria-labelledby={title.id}>`; un solo h1
por página es responsabilidad del app — el README lo dice.
- **v1**: `center` y `split` (la segunda existe porque discrimina el layout,
no por lujo — es el criterio "casos que discriminen").
- **Demo**: ambos layouts, con/sin media, con badge, dark.
### F2.3 `feature-grid`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `Container` + `AutoGrid` + `Stack` + `Icon` +
`Heading` + `Text`.
- **API**: `<FeatureGrid>` + `.Header` (title+description de sección) +
`.Item` (con `.ItemIcon`/`.ItemTitle`/`.ItemText` o children libres).
- **v1**: items planos (sin Card — la variante card es Gap).
- **Demo**: 3/6 items, columnas responsive del AutoGrid.
### F2.4 `pricing`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `CardGroup`/`Card` + `Heading` + `Text` + `Badge`
(plan destacado) + `Button` + `ToggleGroup` (mensual/anual) + filas de
features (`Stack` + `Group` + `Icon` check + `Text`).
- **API**: `<Pricing>` + `.Switch` (billing toggle; estado de vista local
permitido §1) + `.Plan` (prop `featured?`) + `.PlanPrice` + `.PlanFeatures`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `.PlanAction`.
- **v1**: 2–4 planes en fila responsive; el precio mostrado por periodo lo
resuelve el app con el valor del toggle (block emite el cambio vía prop
callback del ToggleGroup — sin formatear moneda: eso es `FormatNumber`
del app).
- **Demo**: 3 planes, featured al centro, toggle vivo.
### F2.5 `testimonials`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `AutoGrid` + `Card` + `Avatar` + `Text` +
`Group`.
- **API**: `<Testimonials>` + `.Header` + `.Item` (+`.ItemAuthor` con
Avatar/nombre/cargo).
- **v1**: grid; variante `Carousel` = Gap (el componente existe; entra
cuando una demo real la pida).
### F2.6 `faq`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `Container` (medida estrecha) + `Heading` +
`Accordion`.
- **API**: `<Faq>` + `.Header` + `.Item` (proxy fino de Accordion.Item con
children pregunta/respuesta — respetando el API real del Accordion, que
se lee antes).
- **v1**: una columna; `type` del accordion expuesto tal cual.
### F2.7 `stats-band`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `Group`/`AutoGrid` + `Metrics` + `CountUp` +
`Text`.
- **API**: `<StatsBand>` + `.Stat` (valor + etiqueta; `CountUp` opt-in por
prop).
- **v1**: banda horizontal 2–4 stats; los formatos numéricos llegan ya
formateados o vía `FormatNumber` compuesto por el app.
feat(blocks): `cta` — el panel que pide el siguiente paso, y tres mentiras que destapó al medirlo F2.8. Slots de snippet (eyebrow · title · description · actions · children), misma forma que `hero`: las partes de un CTA no repiten ni coordinan, así que un compound no se gana nada. Dos disposiciones: `center` para cerrar una página y `justified` —copia al inicio, acciones al final— que es el hueco que nombraba el dossier. Un solo `Motion trigger="viewport"`: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Lo que salió al verificarlo en navegador, medido y no a ojo: - `variant='soft'` **fuera de la API**. Su track queda a `oklch(0.9932)` contra un `--color-surface-default` de `oklch(0.9911)`: 0.002 de luminancia, o sea ningún panel en claro. Y `Surface` no tiene borde al que caer. Cortar el prop es más barato que shipear un estado que se esfuma. - La ranura `contrast` de la paleta es blanco en TODO escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: `primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos; `neutral` 3.32 · `teal` 3.07 fallan en claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan. - `Text align` es inerte por defecto: renderiza un `span`, y `text-align` no hace nada sobre una caja inline. `align="center"` dejaba la copia a la izquierda dentro del layout centrado, sin avisar. Rodeado con `as="p"`. Y `Group` no apila: a 420px la etiqueta de la acción secundaria se parte contra el botón primario, así que las acciones van en `Flex direction={{ base: 'column', sm: 'row' }}`. `hero` compone las suyas con `Group` — anotado. Los tres hallazgos de canon quedan en los gaps del README del block y en `PLAN-blocks-quality.md` §6 (F15/F16/F17), sin tocar nada fuera del tier. Verificado: `center` y `justified` en claro/oscuro/RTL y a 420px, tres colores, entrada disparada, descripción en `<p>` centrada, cero errores de página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.8 `cta` — HECHO (2026-07-30)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(blocks): `cta` — el panel que pide el siguiente paso, y tres mentiras que destapó al medirlo F2.8. Slots de snippet (eyebrow · title · description · actions · children), misma forma que `hero`: las partes de un CTA no repiten ni coordinan, así que un compound no se gana nada. Dos disposiciones: `center` para cerrar una página y `justified` —copia al inicio, acciones al final— que es el hueco que nombraba el dossier. Un solo `Motion trigger="viewport"`: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Lo que salió al verificarlo en navegador, medido y no a ojo: - `variant='soft'` **fuera de la API**. Su track queda a `oklch(0.9932)` contra un `--color-surface-default` de `oklch(0.9911)`: 0.002 de luminancia, o sea ningún panel en claro. Y `Surface` no tiene borde al que caer. Cortar el prop es más barato que shipear un estado que se esfuma. - La ranura `contrast` de la paleta es blanco en TODO escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: `primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos; `neutral` 3.32 · `teal` 3.07 fallan en claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan. - `Text align` es inerte por defecto: renderiza un `span`, y `text-align` no hace nada sobre una caja inline. `align="center"` dejaba la copia a la izquierda dentro del layout centrado, sin avisar. Rodeado con `as="p"`. Y `Group` no apila: a 420px la etiqueta de la acción secundaria se parte contra el botón primario, así que las acciones van en `Flex direction={{ base: 'column', sm: 'row' }}`. `hero` compone las suyas con `Group` — anotado. Los tres hallazgos de canon quedan en los gaps del README del block y en `PLAN-blocks-quality.md` §6 (F15/F16/F17), sin tocar nada fuera del tier. Verificado: `center` y `justified` en claro/oscuro/RTL y a 420px, tres colores, entrada disparada, descripción en `<p>` centrada, cero errores de página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `Container` + `Motion` + `Surface` (tratamiento de
fondo del sistema — un CTA se distingue por acabado, y eso ya es vocabulario
del framework) + `Heading` + `Text` + `Flex` (`Button`s).
- **API**: `<Cta layout color gradient level container size>` + slots de snippet
`eyebrow` · `title` · `description` · `actions` · `children`. **Desviación del
plan**: no `.Title`/`.Description`/`.Actions` — las partes no repiten ni
coordinan, así que son slots posicionales (misma forma que `hero`).
`variant` NO existe: medido, el canvas `soft` no acota panel (bitácora).
feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho F2.9. Slots de snippet (eyebrow · title · description · field · submit · note · children), `center` y `justified`, y `panel` como interruptor real: el panel de marca es la cara de las referencias, pero el control, la ayuda y el error están calibrados para la superficie de página, así que apagarlo es un estado de primera y no un fallback. La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la pista libre y la acción abraza su contenido —eso es lo que hace que un alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado de un campo de correo deja inservibles a los dos. El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/ touched, la agregación de errores y el foco al primer error; `Field` posee el cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`. El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí. ## Superación del dossier: la etiqueta El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel` —arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar. ## Tres hallazgos de canon más, medidos - **La fundación de eidos no trae reset de modelo de caja y lo asume del app.** `[data-field-control]` declara `inline-size: 100%` + padding, así que bajo `content-box` el control mide 30px más que su contenedor: el campo se metía por debajo del botón de envío. Campo 480 / control 510 en la galería frente a 502 / 502 en los docs de componentes, que sí resetean (igual que `web/routes/active/styles.css`). Arreglado en app-land con `web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés, anotada en el handoff. - **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya construido: el componente solo lo reenvía al `createForm` que hace él mismo. El envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no expone el prop: el handler va en el `createForm` del app. - **Los mensajes de SIUM son idlangref.** La vía correcta es `uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una dirección de correo válida»—, pero la demo de docs del propio `Form` parte la cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés. Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6 (F18/F19/F20), sin tocar nada fuera del tier. Verificado en navegador el arco completo: correo inválido → error traducido con `role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo válido → confirmación del app (`Callout` afirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.9 `newsletter` — HECHO (2026-07-30)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho F2.9. Slots de snippet (eyebrow · title · description · field · submit · note · children), `center` y `justified`, y `panel` como interruptor real: el panel de marca es la cara de las referencias, pero el control, la ayuda y el error están calibrados para la superficie de página, así que apagarlo es un estado de primera y no un fallback. La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la pista libre y la acción abraza su contenido —eso es lo que hace que un alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado de un campo de correo deja inservibles a los dos. El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/ touched, la agregación de errores y el foco al primer error; `Field` posee el cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`. El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí. ## Superación del dossier: la etiqueta El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel` —arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar. ## Tres hallazgos de canon más, medidos - **La fundación de eidos no trae reset de modelo de caja y lo asume del app.** `[data-field-control]` declara `inline-size: 100%` + padding, así que bajo `content-box` el control mide 30px más que su contenedor: el campo se metía por debajo del botón de envío. Campo 480 / control 510 en la galería frente a 502 / 502 en los docs de componentes, que sí resetean (igual que `web/routes/active/styles.css`). Arreglado en app-land con `web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés, anotada en el handoff. - **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya construido: el componente solo lo reenvía al `createForm` que hace él mismo. El envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no expone el prop: el handler va en el `createForm` del app. - **Los mensajes de SIUM son idlangref.** La vía correcta es `uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una dirección de correo válida»—, pero la demo de docs del propio `Form` parte la cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés. Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6 (F18/F19/F20), sin tocar nada fuera del tier. Verificado en navegador el arco completo: correo inválido → error traducido con `role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo válido → confirmación del app (`Callout` afirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: como F2.8 + `Form` (`variant="plain"`) + `Grid` de fila
`1fr auto`; el `Field` de correo y el `Form.Submit` los compone el app en
sus slots.
- **API**: `<Newsletter layout panel color gradient level container size>` +
`form` (OBLIGATORIO, de `createForm`) + `schema` (opcional) + slots
`eyebrow` · `title` · `description` · `field` · `submit` · `note` ·
`children`. **Desviación del plan**: slots, no `.Title`/`.Description`/
`.Form` (las partes no repiten ni coordinan). **No expone
`onValidSubmit`**: `Form.Provider` lo ignora cuando recibe un `form` ya
construido, así que el handler va en el `createForm` del app.
- **Nota**: separado de `cta` porque el form introduce a11y y estados
(invalid/submitting) que el CTA puro no tiene.
feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho F2.9. Slots de snippet (eyebrow · title · description · field · submit · note · children), `center` y `justified`, y `panel` como interruptor real: el panel de marca es la cara de las referencias, pero el control, la ayuda y el error están calibrados para la superficie de página, así que apagarlo es un estado de primera y no un fallback. La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la pista libre y la acción abraza su contenido —eso es lo que hace que un alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado de un campo de correo deja inservibles a los dos. El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/ touched, la agregación de errores y el foco al primer error; `Field` posee el cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`. El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí. ## Superación del dossier: la etiqueta El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel` —arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar. ## Tres hallazgos de canon más, medidos - **La fundación de eidos no trae reset de modelo de caja y lo asume del app.** `[data-field-control]` declara `inline-size: 100%` + padding, así que bajo `content-box` el control mide 30px más que su contenedor: el campo se metía por debajo del botón de envío. Campo 480 / control 510 en la galería frente a 502 / 502 en los docs de componentes, que sí resetean (igual que `web/routes/active/styles.css`). Arreglado en app-land con `web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés, anotada en el handoff. - **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya construido: el componente solo lo reenvía al `createForm` que hace él mismo. El envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no expone el prop: el handler va en el `createForm` del app. - **Los mensajes de SIUM son idlangref.** La vía correcta es `uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una dirección de correo válida»—, pero la demo de docs del propio `Form` parte la cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés. Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6 (F18/F19/F20), sin tocar nada fuera del tier. Verificado en navegador el arco completo: correo inválido → error traducido con `role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo válido → confirmación del app (`Callout` afirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- El block **no inventa validación**: `Form` posee runtime, esquema, dirty/
touched, agregación de errores y foco al primer error; `Field` posee el
cableado ARIA. Tampoco emite sema: `commit-submit` / `signal-invalid` ya
los declara el morfo del `Form`.
feat(blocks): `site-footer` — el pie de la página, con el selector de idioma vivo F2.10. Es el block donde la regla de forma del tier se ve mejor: `Column` SE REPITE (el app mapea sobre N grupos), así que es parte compound; la marca, la banda de alta, lo social, lo legal y el `extra` no se repiten ni coordinan, así que son slots. El landmark es un `<footer>` de verdad: `contentinfo` sin pedir nada. Las columnas van en `AutoGrid minChildWidth`, **sin un solo breakpoint**: el mismo código sirve para 2 grupos o para 5, y en móvil caen a dos. La entrada es un `Motion trigger="viewport"` solo en la región superior y **sin escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie lee un pie columna por columna. ## Los dos huecos que nombraba el dossier, cerrados - **La banda de alta al boletín EN el pie** (3 de 7 en Tailwind Plus) como slot `signup`, con su propia fila a lo ancho: en la columna de la marca (~230px) la fila de correo tendría que apilarse. El formulario es del app; este block NO importa el block `newsletter` (B-10) ni inventa validación. - **El selector de idioma** como `extra` (el slot libre de D-BLK.6), y está VIVO: escribe `uix.prefs.setIntent('language', …)`, el eje real del ecosistema. Verificado — al elegir «English», `document.documentElement.lang` pasa a `en`. ## Tres números que salieron de medir, no de suponer - El `container` bajó de `xl` a **`lg`**: con `xl` el pie no se alineaba con ninguna sección de la página compuesta encima. - La rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`**: con `2fr`, un pie de cuatro grupos se partía en 3+1. - El gap entre columnas es **6, no 8**: con 8, cinco grupos no comparten fila al ancho `lg` (728px justos). Horizontal más apretado que vertical, porque el gap horizontal es el que decide cuántos grupos caben. ## Dos hallazgos de canon (F21/F22 en `PLAN-blocks-quality.md` §6) - **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading` aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`, `Container`, `Section`— renderiza un `<div>` fijo. Consecuencia: una columna de enlaces no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un block solo puede elegir entre divs o escribir markup que luego no puede estilar. - **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.** `getDisplayText()` resuelve contra un registro de etiquetas que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado no hay ninguno montado: `value=['es']` pintaba «es» en vez de «Español». Rodeado con el `child` de `Select.Value`. Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal, separador `aria-hidden`, los tres `IconButton` con nombre obligatorio, y el idioma cambiando de verdad. Cero errores de página. Gates: `blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.10 `site-footer` — HECHO (2026-07-30)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(blocks): `site-footer` — el pie de la página, con el selector de idioma vivo F2.10. Es el block donde la regla de forma del tier se ve mejor: `Column` SE REPITE (el app mapea sobre N grupos), así que es parte compound; la marca, la banda de alta, lo social, lo legal y el `extra` no se repiten ni coordinan, así que son slots. El landmark es un `<footer>` de verdad: `contentinfo` sin pedir nada. Las columnas van en `AutoGrid minChildWidth`, **sin un solo breakpoint**: el mismo código sirve para 2 grupos o para 5, y en móvil caen a dos. La entrada es un `Motion trigger="viewport"` solo en la región superior y **sin escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie lee un pie columna por columna. ## Los dos huecos que nombraba el dossier, cerrados - **La banda de alta al boletín EN el pie** (3 de 7 en Tailwind Plus) como slot `signup`, con su propia fila a lo ancho: en la columna de la marca (~230px) la fila de correo tendría que apilarse. El formulario es del app; este block NO importa el block `newsletter` (B-10) ni inventa validación. - **El selector de idioma** como `extra` (el slot libre de D-BLK.6), y está VIVO: escribe `uix.prefs.setIntent('language', …)`, el eje real del ecosistema. Verificado — al elegir «English», `document.documentElement.lang` pasa a `en`. ## Tres números que salieron de medir, no de suponer - El `container` bajó de `xl` a **`lg`**: con `xl` el pie no se alineaba con ninguna sección de la página compuesta encima. - La rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`**: con `2fr`, un pie de cuatro grupos se partía en 3+1. - El gap entre columnas es **6, no 8**: con 8, cinco grupos no comparten fila al ancho `lg` (728px justos). Horizontal más apretado que vertical, porque el gap horizontal es el que decide cuántos grupos caben. ## Dos hallazgos de canon (F21/F22 en `PLAN-blocks-quality.md` §6) - **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading` aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`, `Container`, `Section`— renderiza un `<div>` fijo. Consecuencia: una columna de enlaces no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un block solo puede elegir entre divs o escribir markup que luego no puede estilar. - **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.** `getDisplayText()` resuelve contra un registro de etiquetas que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado no hay ninguno montado: `value=['es']` pintaba «es» en vez de «Español». Rodeado con el `child` de `Select.Value`. Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal, separador `aria-hidden`, los tres `IconButton` con nombre obligatorio, y el idioma cambiando de verdad. Cero errores de página. Gates: `blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `<footer>` + `Section` + `Container` + `Motion` + `Grid`
(`1fr 3fr`: marca | columnas) + `AutoGrid` (las columnas) + `Separator` +
`Flex` (barra inferior).
- **API**: `<SiteFooter container size minColumnWidth>` + slots `brand` ·
`signup` · `social` · `legal` · `extra` + partes compound `.Column` y
`.ColumnTitle`. **Desviación del plan**: `.Social`/`.Legal`/`.Extra` son
SLOTS, no partes — no se repiten ni coordinan; `.Column` sí se repite, así
que es compound. `extra` es el slot libre de D-BLK.6.
- **v1**: 2–5 columnas fluidas (`minChildWidth`, sin breakpoints) → 2 en móvil.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.11 `banner` — HECHO (2026-07-31)
- **Compone**: el componente `Banner` del canon (reenviando su superficie
entera vía `Omit<BannerProps, 'children'>`) + `Container` + `Wrap` +
`Banner.Close`.
- **API**: `<SiteBanner container onDismiss>` + slots `badge` · `children` ·
`action`. El dismiss lo posee el `Banner` (composición, no booleano), así que
el block solo cablea el clic y **la visibilidad es del app**.
- **Nota dossier (P1)**: 13 TW · 16 Untitled · 5 Flowbite — la categoría
convergente más barata de cubrir (el componente ya existe).
feat(blocks): `team` — las personas, y un contexto que sirve para una sola cosa F2.12. Compound porque `Member` SE REPITE (el app mapea sobre N), como `feature-grid`. Pero es el primero del tier cuyo **contexto existe para una sola cosa**: el `align` de la sección viaja a cada `Member` y a su fila de enlaces, así que la foto, el nombre, el cargo y los enlaces comparten eje sin que el app repita la alineación en cada tarjeta. Un `align` explícito en una parte siempre gana. Esa es la diferencia con `feature-grid`, cuyos ítems solo se repiten y por eso no llevan contexto: **la regla de forma se aplica parte por parte, no block por block**. ## Dos decisiones de forma - **Sin `.MemberAvatar`.** El plan la dibujaba; no tendría nada que añadir sobre `<Avatar size radius>` y escondería su API (`Image`, `Fallback`, los estados de carga). El app compone el `Avatar` del canon directamente dentro del `Member`. Se envuelve lo que el block DEFAULTEA —tipografía, retícula, eje—, no lo que solo reenvía. - **El nombre es un `Heading level={3}`** apagado con `size="sm"`: una persona en una retícula tiene nombre, y las referencias lo marcan igual. El nivel es estructura del documento; el tamaño, tipografía. Así un lector de pantalla puede saltar de persona a persona. Y un detalle que separa cumplir de sobresalir: la fila de enlaces va pegada al fondo de la tarjeta (`marginTop: auto`). Con biografías de distinto largo las filas quedaban a alturas distintas y la retícula se leía descuadrada; sin biografías las tarjetas ya miden lo mismo y la regla no cambia nada. ## Encontrado al verificar **Un `columns` fijo no colapsa.** A 420px, cuatro columnas dejan celdas de ~90px con el nombre partido en tres líneas y el avatar desbordando. El default del block es fluido (`minChildWidth`), así que el fallo era de la demo: ahora pasa `columns={{ base: 2, md: N }}` — para eso están los props responsivos. Anotado en el README. Verificado en navegador, esperando el atributo que pone la runtime del morfo y no el reloj: 2/3/4 columnas, `align` center/start propagándose por contexto al eje de cada miembro, biografía sí/no, claro/oscuro/RTL y 420px, seis avatares, doce botones de icono **con nombre propio por persona** («Escribir a Ada Lovelace», no «correo»), escalonado estructural con índices 0,1,2…, cero desbordamiento horizontal y cero errores de página. Gates: `blocks:check` verde (13 blocks) · `svelte-check` sin errores propios · `docs:check` 0 · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.12 `team` — HECHO (2026-07-31)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
feat(blocks): `team` — las personas, y un contexto que sirve para una sola cosa F2.12. Compound porque `Member` SE REPITE (el app mapea sobre N), como `feature-grid`. Pero es el primero del tier cuyo **contexto existe para una sola cosa**: el `align` de la sección viaja a cada `Member` y a su fila de enlaces, así que la foto, el nombre, el cargo y los enlaces comparten eje sin que el app repita la alineación en cada tarjeta. Un `align` explícito en una parte siempre gana. Esa es la diferencia con `feature-grid`, cuyos ítems solo se repiten y por eso no llevan contexto: **la regla de forma se aplica parte por parte, no block por block**. ## Dos decisiones de forma - **Sin `.MemberAvatar`.** El plan la dibujaba; no tendría nada que añadir sobre `<Avatar size radius>` y escondería su API (`Image`, `Fallback`, los estados de carga). El app compone el `Avatar` del canon directamente dentro del `Member`. Se envuelve lo que el block DEFAULTEA —tipografía, retícula, eje—, no lo que solo reenvía. - **El nombre es un `Heading level={3}`** apagado con `size="sm"`: una persona en una retícula tiene nombre, y las referencias lo marcan igual. El nivel es estructura del documento; el tamaño, tipografía. Así un lector de pantalla puede saltar de persona a persona. Y un detalle que separa cumplir de sobresalir: la fila de enlaces va pegada al fondo de la tarjeta (`marginTop: auto`). Con biografías de distinto largo las filas quedaban a alturas distintas y la retícula se leía descuadrada; sin biografías las tarjetas ya miden lo mismo y la regla no cambia nada. ## Encontrado al verificar **Un `columns` fijo no colapsa.** A 420px, cuatro columnas dejan celdas de ~90px con el nombre partido en tres líneas y el avatar desbordando. El default del block es fluido (`minChildWidth`), así que el fallo era de la demo: ahora pasa `columns={{ base: 2, md: N }}` — para eso están los props responsivos. Anotado en el README. Verificado en navegador, esperando el atributo que pone la runtime del morfo y no el reloj: 2/3/4 columnas, `align` center/start propagándose por contexto al eje de cada miembro, biografía sí/no, claro/oscuro/RTL y 420px, seis avatares, doce botones de icono **con nombre propio por persona** («Escribir a Ada Lovelace», no «correo»), escalonado estructural con índices 0,1,2…, cero desbordamiento horizontal y cero errores de página. Gates: `blocks:check` verde (13 blocks) · `svelte-check` sin errores propios · `docs:check` 0 · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Section` + `Container` + `AutoGrid` + `Stack` + `Heading` +
`Text` + `Group` + `Motion`. El `Avatar` lo compone el APP dentro del
`Member`.
- **API**: `<Team align container size>` + `.Header` + `.Members` + `.Member` +
`.MemberName` + `.MemberRole` + `.MemberLinks`. **Desviación del plan**: NO
hay `.MemberAvatar` — no tendría nada que añadir sobre `<Avatar size radius>`
y escondería su API (`Image`/`Fallback`/estados de carga). Se envuelve lo que
el block DEFAULTEA, no lo que solo reenvía.
feat(blocks): contact CERRADO — la coordinacion entra en el contrato B El estado se le pregunta al ESQUEMA, no al Form. `form.isValid` con `validationBehaviour: 'progressive'` significa «aun no se ha encontrado nada mal»: un formulario vacio e intacto no tiene errores, asi que se declaraba valido y `incomplete` era INALCANZABLE al cargar — la seccion pedia resolver la verificacion con los tres campos vacios, justo el hueco que la doctrina existe para cerrar. Ahora la raiz valida los valores con `validateSync` de SIUM: sincrono y sin escribir errores (`form.validate()` habria encendido los tres campos en rojo en el primer frame). `state.test.ts` fija la maquina con 9 casos — primer test unitario del tier, porque `state.ts` es su primera logica pura: el orden de prioridad, que solo `ready` deja enviar, y que todo estado bloqueado tiene frase. Contrato B (architecture/blocks.md): seccion «Coordination» con las tres cosas que posee un block que coordina (estado, forma de sus datos, palabras), B-5 admite el esquema por defecto, B-7 enmendado (posee las palabras de SUS estados como idlangref con fallback ingles) y la convencion de servicios acotada: el traductor es el UNICO servicio sancionado. Dice tambien a quien NO aplica — los blocks de layout no coordinan nada. i18n por la via real: el arnes registra el namespace `blocks` en las DOS cadenas de arranque (galeria y previews, que duplican el boot) y la demo se lee en castellano en vez de caer al fallback ingles. Ademas: README y pestanas de doc rehechos al reparto nuevo, ficha F2.13 y bitacora en PLAN-blocks.md, y el control vivo que faltaba para un prop publico — `verification` con reto / sin reto, porque omitir el prop no es lo mismo que pasar un estado que nunca verifica. Verificado en navegador, arco completo y las dos ramas: sin reto vacio → un campo → tres campos (se desbloquea) → «Enviando…» → «Enviado»; con reto, con los tres campos llenos el boton sigue bloqueado y la razon pasa a «Resuelve la verificacion de arriba». `verifying` y `rejected` quedan cubiertos por test unitario, no por el reto en vivo: el provider de ProofOfHuman escribe `status` el mismo y el veredicto del app no se sostiene (hilo de canon abierto). Gates: blocks:check verde (14 blocks) · vitest 9/9 · docs:check 0 · svelte-check sin errores propios · prettier limpio en los ficheros propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.13 `contact` — HECHO (2026-07-31) · el primer block que COORDINA
- **Compone**: `Section` + `Container` + `Grid` + `Form` + `Field` +
`TextArea` + `Stack` + `Group` + `Motion`; las filas de contacto las llena
el app (`Icon` + `Text` + `Link`).
- **API**: `<Contact container size form? schema? verification? bind:sent
onSend>` + `.Header` + `.Body` + `.Form` + `.Fields` + `.Submit` +
`.Reason` + `.Details` + `.Detail` + `.DetailLabel` + `.DetailValue`.
- **Desviación del plan**: el plan dibujaba `.Info` + `.Form` y dejaba
«validación y submit = handlers del app». Se cumple lo segundo (el envío
sigue siendo del app por `onSend`), pero el reparto cambió con la doctrina
de coordinación: el block posee el ESTADO de la sección, la FORMA de sus
datos y las PALABRAS de cada estado. `.Info` se partió en
`.Details`/`.Detail` porque la fila se repite, y aparecieron `.Fields`,
`.Submit` y `.Reason` porque coordinan (leen el estado por contexto).
- **La máquina** (`state.ts`): `incomplete · unverified · verifying ·
rejected · ready · sending · sent`, con `CONTACT_REASON` y `CONTACT_ACTION`
como `Record` exhaustivos — un estado bloqueado sin frase no se puede
escribir. Cubierta por `state.test.ts` (9 casos), primera lógica pura del
tier y por tanto su primer test unitario.
- **Defecto 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 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. Arreglado preguntando al esquema (`validateSync` de SIUM: síncrono
y sin escribir errores en el formulario).
- **i18n**: fallbacks ingleses en el block; el arnés registra el namespace
`blocks` (`web/routes/blocks/_lib/blocks-langs.ts`) en las DOS cadenas de
arranque (galería y previews) y la demo se lee en castellano.
- **Contrato**: la doctrina está escrita en
[`architecture/blocks.md`](../architecture/blocks.md) §«Coordination» +
B-5/B-7 enmendados + la convención de servicios (el traductor es el único
servicio sancionado).
feat(blocks): content-section cierra F2 — la rejilla de escape, y las escalas tipograficas salen de Text El ultimo block de sitio, y el que mas cerca estuvo de ser un envoltorio: `Section` + `Container` + `Prose` no habria anadido nada, porque `Prose` ya trae su medida de lectura. Lo que falta en el ecosistema es el ESCAPE — dentro de un `Container` nada puede ser mas ancho que el, asi que una figura a sangre hay que sacarla del articulo y ponerla de hermana, y el orden de lectura se rompe. La seccion es una rejilla de 5 pistas (canalon · flanco · MEDIDA · flanco · canalon) y cada parte dice hasta donde llega: `measure` col 3, `wide` col 2/5, `full` col 1/-1. Todas las longitudes son tokens del sistema (`--measure-*`, `--container-width-*`, `--container-padding-inline`). `.Body` y `.Media` son compound porque SE REPITEN; la cabecera son slots de snippet. `.Body` apaga la medida de `Prose`: dos duenos del mismo ancho se pelean y ganaria el mas estrecho en silencio. Los tres ejes son RESPONSIVE (`measure`, `wide`, y el `width` de cada parte), resueltos con `eidos.resolve()` — el mismo camino que usan los primitivos de eidos, asi que los unicos breakpoints en juego son los canonicos. Es lo que un valor unico no puede decir: `{ base: 'full', md: 'wide' }` = foto a sangre en movil y contenida de `md` arriba. CANON: un componente no es libreria de otro. `Heading` importaba CINCO escalas tipograficas de `Text` (`TextTracking`, `TextLeading`, `TextWrap`, `TextNumeric`, `TextMeasure`) y al tipar este block repeti la violacion. Las cinco se mueven a `eidos/lib/types.ts`, que es donde su propio encabezado dice que van y donde ya esta documentado el precedente del mismo fallo (`ColorRole` re-escrito a mano que derivo a 8 miembros contra 9). Sin shim de compatibilidad: `text` y `heading` las importan de la libreria. Cuatro defectos que solo dijo el navegador: 1. `gap` separa tambien las COLUMNAS y una parte que las cruza se lleva esos huecos encima — `wide` media 1088 en vez de los 1024 del contenedor con el que debe alinearse, y a 420px `full` llegaba a 532 dentro de una rejilla de 404 y hacia scrollear la pagina. Van `rowGap` + `columnGap={0}`. 2. Una pista `min(medida, 100%)` ignora sus propios canalones: a 420px la central se quedaba los 404 enteros y la rejilla se iba a 436. 3. Un token de container es un ancho EXTERIOR: con `--container-width-lg` a pelo la figura `wide` sobresalia 16px por cada lado respecto al texto de la seccion hermana de debajo, a 1280/1440/1920. La pista ancha resta ahora `2 * --container-padding-inline` (992 en `lg`) y el desfase es 0. Lo cazo el usuario mirando la pantalla: yo habia medido el block contra si mismo, no contra la pagina. 4. `100%` significa algo distinto dentro de cada parte — en `measure`/`wide` ya es una pista, en `full` es la rejilla entera con canalones —, asi que capar el pie igual en los dos casos lo dejaba a 340 contra una columna de 372 en uno y a 404 en el otro. Verificado en navegador: barrido 375→1920 sin desbordamiento; alineacion pie↔prosa identica en 3 viewports × 3 medidas × 3 anchuras (18/18); figura `wide` a ras del texto de la seccion hermana en 1280/1440/1920; y el valor responsive cambia exactamente en el `md` canonico (a sangre a 767, contenida a 768). Claro, oscuro, 420px y 1920 mirados, y la geometria confirmada tambien en el Chrome del usuario. Semantica propia `figure`/`figcaption` (no interactivos → estructura de documento, B-8) con el hueco senalado: el canon no tiene primitiva `Figure`. Contexto de CONFIGURACION, no de estado — es un block de layout y no coordina nada. Gates: blocks:check verde (15 blocks) · svelte-check sin errores en lo tocado · eidos 28/29 suites (la que falla es `audio-player` sin morfo, del hilo de audio, ajena) · docs:check 0 · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F2.14 `content-section` — HECHO (2026-07-31) · cierra F2
- **Compone**: `Section` + `Grid` + `Box` + **`Prose` (F1.5)** + `Heading` +
`Text` + `Motion`.
- **API**: `<ContentSection measure wide size level>` + snippets de cabecera
(`eyebrow`/`title`/`lede`/`meta`) + `.Body` + `.Media`.
- **Desviación del plan — SIN `Container`**: el plan decía `Section` +
`Container` + `Prose`, pero **dentro de un `Container` nada puede ser más
ancho que él**, así que la propia meta del plan (media FUERA del flujo del
prose) sería imposible: una figura a sangre habría que sacarla del artículo
y ponerla de hermana, rompiendo el orden de lectura. La sección es una
REJILLA de 5 pistas cuya central es la medida, y cada parte dice hasta dónde
llega (`measure` col 3 · `wide` col 2/5 · `full` col 1/-1). Ese escape es lo
único que este block añade sobre un `<Prose>` en una caja — sin él sería un
envoltorio de tres capas.
- **Todas las longitudes son tokens**: medidas `--measure-*` (54/66/78ch),
pista ancha `--container-width-*`, canalones con suelo en
`--container-padding-inline`.
- **Semántica propia**: renderiza `<figure>`/`<figcaption>` de verdad (no son
interactivos → estructura de documento, B-8, no la regla de admisión). Hueco
señalado: el canon no tiene primitiva `Figure`.
- **Contexto de CONFIGURACIÓN** (no estado): la raíz comparte la medida para
que el pie de una figura desbordada se alinee con la columna de lectura —
mismo uso que el `align` de `team`. Es un block de LAYOUT y no coordina nada,
como dice `architecture/blocks.md` §«Coordination».
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_(`logo-cloud` **REABIERTO el 2026-08-17** y movido a F2b: la premisa con la que
se difirió —«su versión honesta pide `Marquee`»— no casa con las referencias,
que lo shippean como **grid estático** (TW 6 · Untitled · Flowbite). `blog` y
`cookie-consent` entran también por F2b. El resto de la unión del dossier —
bento, gallery, popups, careers, events, comparison, timeline — sigue en F5 con
disparador, E-3.)_
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Cierre F2**: `blocks-check` verde; galería `web/routes/blocks/` con los 15.
⚠️ **Enmendado el 2026-08-17**: este cierre exigía además una **página compuesta
de PRUEBA** (header + hero + features + pricing + faq + cta + footer juntos) y
esa página NO se construyó — quedó fuera de alcance por decisión del usuario.
F2 está cerrada **en unidades**: cada block verificado contra sí mismo. La
integración —lo único que puede probar que los blocks ensamblan sin fricción—
es la puerta de cierre de F2b (tramo D). Mientras esa página no exista, el
claim del tier está sin demostrar, y aquí se dice así en vez de dar por probado
lo que no se probó.
---
## F2b — Ola de paridad de variedad (firmada 2026-08-17)
**De dónde sale**: el saldo de la tabla §P1 del dossier
(`RESEARCH-blocks-references.md`) contra los 15 blocks ya construidos, medido
el 2026-08-17 leyendo el `types.ts` y la tabla de Gaps del README de cada uno.
De las 11 filas del dossier, **6 están hechas** (frames de media vía el canon
`Mockup` · `hero layout="background"` · `feature-split`, que ERA la brecha #1 ·
`cta layout="justified"` · `newsletter note` · `site-footer signup` + `extra`)
y **4 se resolvieron por composición o app-land**, declaradas en su README
(flyout del header por `NavigationMenu`, announcement por el block `banner`,
logo-strip y vídeo por los slots del `hero`, cola de soporte del `faq`).
⚠️ **Ninguna fila de variante de esta ola es un hallazgo nuevo**: estaban ya
registradas con disposición en el README de su block desde julio. Lo que
faltaba no era medir — era decidir.
⚠️ **Esta sección se reescribió entera el mismo día en que se escribió.** La
primera versión atacaba sólo el suelo de §P1 — ocho variantes — y con eso NO se
cierra la brecha que motivó la revisión, que es de **cardinalidad percibida**
(TW: Heroes 12, Features 15; nosotros 1–3 arreglos por block). El propio
dossier fija el método en P6.1 y la primera versión no lo trajo: **no competir
en número de dumps, sino demostrar la equivalencia variante-a-variante**. De
ahí que la ola tenga cuatro tramos y no uno.
### La regla de forma de la ola — se firma ANTES que las filas
No es doctrina nueva: es el precedente que F2 ya vivió. Son **cuatro**
términos, y el primero es el que más trabajo hace:
1. **Variante alcanzable componiendo lo que ya hay → RECETA demostrada, CERO
código.** Se prueba en la demo y se escribe en el README del block. Sin este
término, cada fila de las referencias parece pedir un `layout` nuevo o un
hermano, y el tier engorda por imitación en vez de por función.
2. **Misma anatomía, otro ARREGLO → prop `layout`** en el mismo block —
`hero` (`center|split|background`), `cta` y `newsletter`
(`center|justified`).
3. **Otra anatomía → block hermano** — `feature-split` frente a
`feature-grid`: filas alternantes con lista de features y acciones no son
celdas de icono.
4. **Variante que debe OBEDECER al estado de su sección → PARTE** del block que
lo posee, nunca hermano: un hermano no puede leer ese contexto y B-10 le
prohíbe importarlo. (Gobierna V1.)
### Tramo A — las variantes de suelo
blocks(pricing): un plan solo no se sostenía — la fila necesitaba un tope Segunda fila de la ola F2b, y la que existía para comprobar una sospecha. El README llevaba desde julio diciendo que el plan único «ya lo cubre el layout», diferido por eso. La ficha exigía medirlo antes de darlo por gratis, y no lo cubría. La causa está en la lista de pistas, no en cuántos planes hay: la fila es repeat(auto-fill, minmax(17rem, 1fr)), y auto-fill RESERVA toda pista que quepa, la ocupe alguien o no. Medido a 1280 sobre 992px de fila, un plan solo daba una tarjeta de 315px pegada al borde inicial con 677px de vacío al lado; fijar columns a 1 solo cambiaba eso por una tarjeta de 992px, que es un cartel. Meterlo en un Container sm tampoco: a 640px vuelven a caber dos pistas. Lo que faltaba era un tope sobre la FILA, y la parte no lo dejaba pasar: sus props están cerradas a propósito desde A-94, porque el envoltorio esparce el resto antes de las props que fija y un width del consumidor tipaba y se descartaba en silencio. Así que se abre nombrando: maxWidth entra en el tipo y viaja explícito al AutoGrid, que es exactamente lo que A-94 dejó dicho — el tipo dice lo que la parte honra. El marginX auto va con él sin condición: sin tope la fila ya llena la medida y auto no resuelve a nada. El número lo pone la app. Un block que horneara uno estaría decidiendo cuánto mide un plan; la demo pasa 28rem para uno y 44rem para dos, que es lo que las referencias le dan a cada arreglo, y gana un control vivo de recuento que también viaja en la URL de la vista previa. Medido después: un plan 448px centrado, dos planes 704px en dos pistas de 340 centradas, y tres planes 992px idénticos a antes — el tope es opt-in y no toca el caso que ya existía. A 375 es inerte, porque allí una sola pista ya toma el ancho. Queda anotada la lección en el README y en el handoff, porque no es de este block: una fila «diferida» puede ser una medición equivocada y no un aplazamiento. Nadie había mirado pricing con menos de tres planes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| # | Block | Qué falta | Forma firmada | Coste |
| ------ | -------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| **V1** | `pricing` | tabla comparativa features×planes | **`Pricing.Compare`** (parte, por el término 4). **Tanda propia con fase 0**: dos decisiones de diseño que la ficha no puede prejuzgar — (a) **quién declara las columnas** si `.Plans` y `.Compare` conviven (doble declaración de planes) y (b) **qué pasa en móvil** (las refs colapsan a lista-por-plan o a scroll) | **alto** |
| **V2** | `testimonials` | cita única en spotlight | **`layout="grid" \| "spotlight"`**. ⚠️ No es «misma anatomía» exacta: gana un slot `logo`, pierde la `Card` y `Items` pasa a un solo protagonista. Aun así `layout`, con el precedente de `hero layout="background"`, que también redefine partes. El hermano se reabre SÓLO si un día pide rotación / carousel | medio |
| **V3** | `faq` | lista estática 2/3 columnas | **`layout="accordion" \| "list"`**, con `.List` conmutando entre `Accordion` y una rejilla de Q&A siempre abiertas. **El punto fino es el TIPO**: hoy `FaqListProps = AccordionProps`, y bajo `list` no aplican ni esas props ni `value`/`disabled` de `.Item` → unión discriminada, jamás superficie descartada en silencio (A-94) | medio |
| **V4** | `hero` | `form-in-hero` | ✅ **HECHA 2026-08-17.** Slot `form`, el app compone `Form` + `Field` + `Form.Submit` entero (el hero NO toma el handle: tomarlo lo convertiría en un block que coordina). Aterriza en el sitio del CTA — tras la descripción, antes de `actions` — con medida propia `sm`; los dos conviven si llegan los dos. Detalle y medidas: README del block §«The sign-up slot» | bajo |
| **V5** | `cta` | split-with-media | tercer **`layout="split"`** + slot `media` (el nombre que ya usa `hero`); el `Mockup` existe. Es suelo: el dossier la lista | bajo |
| **V8** | `pricing` | single-price | ✅ **HECHA 2026-08-17 — y NO era gratis.** Medido: la fila es `auto-fill`, así que RESERVA las pistas que caben aunque nadie las ocupe → un plan solo daba 315px pegados al borde con 677px de vacío, y `columns={1}` lo cambiaba por una card de 992px. Resuelto con **`.Plans maxWidth`** (tope de fila, centrado, reenviado nombrado por A-94); el número lo pone el app. Detalle: README del block §«The row cap» | ~0 → bajo |
#### Plan de ejecución de V1 — `Pricing.Compare` (firmado 2026-08-17)
**Fase 0 hecha, y con una corrección propia**: el README de `Table` lista el
pinning de columnas como gap diferido, y **es falso a fecha de hoy** — el engine
tiene `enablePinning` por columna e `initialColumnPinning`, soma escribe
`position: sticky` con su offset (`buildColumnStyle`) y el recipe pinta
`[data-pinned]` con un borde que revela lo que hay detrás. La primera versión de
esta fase 0 se apoyó en el README y dio un dato equivocado; el paso 1 lo corrige.
**Las dos decisiones que la ficha reservaba, resueltas por doctrina:**
- **(a) Quién declara los planes → el `createTable` del app, y `.Compare` lo
COMPONE.** B-5 admite `items={…}` «sólo donde el canon compuesto ya es
data-driven», y `Table` lo es: exige la instancia de `createTable`. Así que
`.Compare` NO inventa un formato de matriz propio ni lo traduce por dentro: el
app crea la tabla como en cualquier otra del ecosistema y `.Compare` la recibe.
`.Plans` y `.Compare` CONVIVEN (las refs muestran ambos), alimentados por el
MISMO array del app — sin registro por contexto (acoplamiento por orden de
montaje) y sin sustitución. Descartadas: matriz propia (viola B-5 y duplica el
engine) · registro de `.Plan` en contexto (acopla dos partes que no se hablan).
- **(b) Móvil → scroll horizontal con la columna de características FIJADA.**
Es lo que hacen las referencias y el canon ya lo da (`enablePinning` +
`initialColumnPinning: { left: [...] }`); cero código nuevo, cero gap.
Descartado el colapso a lista por plan: el mismo contenido dos veces, y una
tabla que deja de ser tabla pierde la navegación por celdas que es la razón de
existir de una comparación.
**Qué es `.Compare` (y qué no):**
- **PARTE** de `pricing` (término 4 de la regla de forma): coordina con el
periodo — la fila de precio lee el contexto y muestra la cifra mensual o anual;
el app entrega ambas por snippet, como en `PlanPrice`.
- **Coloca**: fija la columna de características, y sobre las columnas de plan
aplica `featured` (acento) y `state` (`current` atenuado · `selected` con
anillo) — los mismos ejes que `.Plan`, así que **el estado se declara UNA vez
por plan y viaja a las dos vistas**.
- **Posee la semántica**: la tabla lleva su `label` (nombre accesible; el canon
`Table` lo tiene) y las celdas booleanas (✓ / —) el `aria-label` que sus glifos
no dan — palabras del block por idlangref con fallback inglés, como
`PRICING_PLAN_REASON`.
- **No traduce, no formatea, no inventa**: precios por snippet, palabras del app.
**Pasos, cada uno con su verificación:**
1. **Corregir el README de `Table`** (fila «Column pinning» → HECHO, con las tres
capas nombradas) → `docs:check`.
2. **`compare-langs`**: idlangrefs `#?blocks.pricing.compare.included|Included` ·
`.notIncluded|Not included` (para el `aria-label` de las celdas booleanas) →
fila en `blocks-langs.ts` del arnés y test que afirma prefijo + fallback.
3. **`.Compare`** (`pricing-compare.svelte` + tipos): recibe `table`
(`TableInstance`), `label`, y un `plans` alineado con las columnas — `Record`
por id de columna → `{ featured?, state? }` — para pintar acento/estado; fija
la primera columna si el app no pinneó ninguna (default sensato, opt-out con
`pinFeatures={false}`); la fila de precio compone `PlanPrice`; las celdas
booleanas componen `Icon.Check` + `aria-label` → `svelte-check` 0 en `pricing`
- `blocks:check` verde.
4. **Demo**: `createTable` del app con la matriz real (features × 3 planes) ·
control vivo `comparar` sí/no · `.Plans` y `.Compare` a la vez · el toggle de
periodo mueve la fila de precio de la tabla Y las cards a la vez → medido.
5. **Medir** (Playwright, píxeles y AX): 1280 → la tabla es `<table>` real con
`caption`/`aria-label`, columnas de plan con `data-featured`/estado, precio
cambia con el periodo · **375 → la tabla scrollea en horizontal y la columna
de características queda FIJA** (`position: sticky`, `left: 0`, borde de
`[data-pinned]`) · lectura AT: cada celda anuncia fila + columna, ✓/— con
nombre · claro/oscuro · RTL (la columna fija va a `right`).
6. **README + registro** (§«The comparison table» con las dos decisiones y las
medidas; fila de Gaps → SHIPPED; entrada en §7 del plan; handoff).
**Riesgos que el agente debe saber:**
- La otra sesión está tocando `hero/` y `eidos/generated` (componente
`background`): stage SÓLO `src/uix/blocks/pricing`, `web/routes/blocks/pricing`,
`web/routes/blocks/_lib/blocks-langs.ts`, `src/uix/eidos/components/table/README.md`
y los docs de proceso. Nada de `git add -A`, nada de `stash`.
- `Table` con `maxHeight` envuelve su propio scroll; sin él, pega al ancestro.
Aquí NO hay `maxHeight`: el scroll horizontal lo da el `overflow-x` del
contenedor de la tabla — comprobar qué envuelve el recipe antes de asumir.
- El pinning en RTL: soma escribe `${pinned}:${offset}px` con la posición FÍSICA
(`left`/`right`). El plan pide medir en RTL, no asumir que espeja.
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
**Fuera de la ola, a F5 con disparador** — el dossier declara `stats-band`
**«Paridad OK»**, así que estas dos son _adoptables_, no suelo, y meterlas aquí
diluiría E-2 (que dice suelo = v1, no «todo lo que exista en una referencia»):
- **V6** `stats-band` split-with-image — arreglo de 2 columnas.
- **V7** `stats-band` timeline / stepped — el canon `Timeline` existe y esto es
otra sección, no una banda.
### Tramo B — la matriz de equivalencia (el entregable central)
**Es el tramo que responde a la brecha percibida**, y sale del dossier P6.1:
«cubrir la matriz con 1 block parametrizado + presets, **documentando la
equivalencia variante-a-variante**; no competir en cardinalidad copy-paste».
Por cada block de F2, una tabla en su README: **variante de la referencia →
composición nuestra que la logra** (con la receta real: props + qué se pasa por
qué slot), y lo que NO se alcance sale como **gap con nombre** en vez de
quedarse en la sensación de que «tienen más». Cada receta se demuestra en la
demo del block — una receta no verificada en navegador no entra en la tabla.
Tres cosas que esto produce y las ocho variantes no producían:
1. La prueba de la tesis del tier («variantes como props tipadas vs N dumps»),
que hoy es una afirmación de la doctrina sin evidencia enseñable.
2. La lista **honesta** de lo que de verdad no podemos hacer — que es la
entrada legítima a nuevas filas, en vez de imitar catálogos.
3. Material directo para la galería pública: la matriz ES la respuesta a «¿por
qué 15 y no 133?».
**La plantilla de la matriz** — se fija aquí para no re-decidirla block a
block:
| Columna | Qué lleva |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Variante | el nombre que la referencia le da («Hero with app screenshot», «Split with image») |
| Ref | TW · Flowbite · Untitled · PrimeBlocks · sv-blocks — las que la ofrecen (≥2 = era suelo) |
| Receta | la composición nuestra EXACTA: props + slots (`layout="split"` + `media` con `<Mockup chrome="phone">`), o el término 2/3/4 aplicado |
| Demostrada en | el control vivo de la demo o la página compuesta donde se ve — **una receta sin demostración en navegador NO entra en la tabla** |
| Estado | `cubierta` · `gap: <nombre>` (candidata a variante o a canon, con disposición) · `no se ofrece` (con el motivo — p. ej. dark pattern) |
- **Dónde vive**: sección `## Equivalencias` en el README de cada block. La
plantilla del tier (`src/uix/blocks/README.md`) la gana, y **`blocks:check`
la exige como ÚLTIMA tanda del tramo** — encenderla el primer día pondría 18
READMEs en rojo durante toda la ola; encenderla al final se verifica EN ROJO
quitando la sección de un block (la regla de toda regla nueva del guard: un
guard que inspecciona el conjunto vacío pasa).
- **Regla dura**: una fila `gap` sin disposición no cierra el tramo; las filas
«no se ofrece» llevan el motivo escrito, porque son una postura del sistema,
no un hueco.
- La fuente de variantes por block es la tabla §P1 del dossier + el catálogo
vivo de cada referencia mirado al escribir la tabla (los conteos del dossier
son de 2026-07-21; si una ref añadió una variante convergente nueva, se
anota con fecha).
### Tramo C — el catálogo que se reabre (tres decisiones firmadas)
| # | Block | Decisión |
| ------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **N1** | `logo-cloud` | **ENTRA, v1 estático.** Se difirió sobre una premisa que las referencias desmienten (TW 6 · Untitled · Flowbite lo shippean como grid estático), y una decisión cuya premisa cae se corrige. El argumento positivo es más fuerte que el de paridad: el problema real de un logo-cloud es **la tinta** — logos de marcas dispares que gritan entre sí y desaparecen en dark, que las refs venden como segundo artefacto. Tratamiento monocromo derivado de los tokens del tema = la tesis del sistema donde nadie más puede. `Marquee` queda como Gap con disparador: si entra, es un **preset de motion sobre este block**, no un block nuevo |
| **N2** | `blog` | **ENTRA.** Misma naturaleza que `team` — grid de entidades con metadatos (`Card` + `Image` + `Badge` + fecha + autor) — y `team` entró: negarle la entrada sería arbitrario. Es la categoría de marketing más visible que queda fuera. Fase 0 normal; **el nombre se decide ahí** (`blog` es el término de las referencias pero nombra ambiguo — la función es «grid de artículos») |
| **N3** | `cookie-consent` | **ENTRA, con doctrina propia y tanda propia — NO es una sección**, es un overlay legal con estado. El proyecto ya firmó la regla que le aplica en la familia commerce: **la ley europea vive en el TIPO**. Un cookie-consent de referencia hace lo que ningún dump hace: el tipo OBLIGA a que rechazar exista al mismo nivel que aceptar y a que las categorías no esenciales nazcan apagadas — legalidad por construcción, no por disciplina del consumidor. Fase 0 profunda (ePrivacy/GDPR + refs) |
| — | `onboarding` | **NO entra como block.** Es un `wizard` (F3.8) con contenido de bienvenida; Flowbite lo clasifica en marketing por accidente de catálogo. Crear `onboarding` sería duplicar una función con otro nombre — exactamente la inflación (los 16 sidebars) que nuestro modelo existe para evitar. Se resuelve como **receta** de `wizard` en el tramo B (+ `tour` de F5 si algún día hay spotlight) |
Las fichas siguen el formato de F2 (Función · Compone · API · Landmark · v1 ·
Demo). Como toda ficha del plan, **parametrizan la tanda, no la sustituyen**:
la fase 0 de cada una lee los README reales de lo que compone.
#### Ficha N1 `logo-cloud`
- **Función**: banda de prueba social — logos de clientes/partners bajo un
encabezado corto («Trusted by…» — palabras del app, B-7).
- **Compone**: `Section` + `Container` + `Box`/`Stack` (header) + `Wrap` o
`AutoGrid` (la fila; fase 0 decide midiendo con logos reales) + `Motion`.
Los logos son contenido del app: `Image` del canon o `<svg>` inline.
- **⚠️ La tinta vive en el CANON, no en el block.** Un block no posee CSS
(D-BLK.2), así que el tratamiento monocromo — la razón de reabrir este
block — se promociona ANTES por la regla de admisión del tier. Fase 0
decide el hueco: prop del canon `Image` (candidato `tone`) o primitivo
nuevo; si es primitivo, entra por la ruta de 9 fases antes que el block.
Para `<svg>` inline el camino natural es `currentColor` + la tinta del
tema; para raster, filtro. El opt-out por logo existe (marcas que exigen
su color) y lo decide el consumidor, no el block.
- **API**: `<LogoCloud>` (props `containerSize` · `sectionSize` + el eje de
tinta) + slots `eyebrow`/`title` + `.Items` + `.Item` (compound: el logo SE
REPITE). Alturas normalizadas por token — un logo no trae su tamaño.
- **Landmark**: con título, `<section aria-labelledby>`; sin título es una
banda decorativa y el README declara qué significa eso para la AT (los
logos siguen necesitando nombre accesible — `alt` del app).
- **v1**: grid/wrap estático. `Marquee` = Gap con disparador (preset de
motion sobre este block, no block nuevo).
- **Demo**: 6–8 logos reales de tamaños dispares (la normalización se VE),
control del eje de tinta, claro/oscuro (la razón del block se ve ahí),
RTL, 375/1280.
#### Ficha N2 `blog` (nombre a validar en su fase 0)
- **Función**: rejilla de artículos recientes — la puerta editorial del sitio
(posts, noticias, casos de estudio).
- **Compone**: `Section` + `Container` + `Box` (header) + `AutoGrid` +
`Card` + `AspectRatio`/`Image` (media) + `Badge` (categoría) + `Heading` +
`Text` + `Link` + `Group`/`Avatar` (autor — el patrón de
`testimonials.Author`). **Fechas: el app compone `FormatDate` /
`RelativeTime`** — el block no formatea fechas, como `pricing` no formatea
moneda.
- **API**: `<Blog>` + `.Header` + `.Posts` + `.Post` (compound: SE REPITE) +
`.PostMedia` + `.PostMeta` (categoría + fecha) + `.PostTitle` (el `Link`
del app dentro — la card entera NO es un link: anidaría interactivos) +
`.PostExcerpt` + `.PostAuthor`.
- **Landmark**: `<section aria-labelledby>`. ⚠️ La semántica de `<article>`
choca con F21 (los primitivos de layout no cambian de elemento; `Card`
renderiza `div`): fase 0 decide entre `as` en `Card` (decisión de CANON,
se presenta antes) o gap declarado en el README. No se finge con roles.
- **v1**: UNA variante — el grid de 3. Featured-post y lista horizontal =
Gaps con disposición (candidatos a `layout` si el dossier de su fase 0
los da como convergentes).
- **Demo**: 3–6 posts con contenido real, con/sin media, autor on/off,
claro/oscuro, 375/1280.
blocks(cookie-consent): la ley europea como tipo, y las dos salidas al mismo peso Cierra el tramo C de F2b. El único block cuya fase 0 fue jurídica antes que visual: las referencias shippean el aviso como markup, y el markup no puede impedir las tres cosas que lo vuelven ilegal — un rechazo debilitado, categorías premarcadas, y cerrar tratado como consentir. Aquí son el tipo y el constructor. `essential: true` hace inescribible una UI que ofrezca apagarlo; `initialConsent()` no admite semilla, así que una primera pregunta no puede llegar marcada; `onDecision` es obligatoria, así que un aviso que no informa de vuelta no se puede montar. Las cinco reglas están numeradas en consent.ts y afirmadas por 8 tests, porque son justo lo que un refactor rompe en silencio mientras todo sigue pintando bien. Desviación de la ficha, y a favor: UNA llamada requerida `onDecision(consent, via)` en vez de `onAcceptAll` + `onRejectAll`. Con dos callbacks el consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo renderiza el block y el API no expone knob para degradarlo. Medido: las dos acciones tienen idéntico relleno, tinta, borde, tamaño, peso y alto — 5.96:1 en claro, 8.79:1 en oscuro. Tres cosas se arreglaron por mirar, no por deducir. La barra era un `Card` outline y midió 1.00:1 contra la página: se disolvía en ella, así que va al plano `overlay` del sistema, el mismo que estampa cada superficie flotante del canon. A 375 las tres acciones envolvían con «rechazar» en una fila y «aceptar» en la siguiente, leyéndose como dos botones ajenos justo donde la pareja más importa: ahora son un `Group`, y el markup lo dice. Y el panel devolvía el foco a un nodo muerto — se devuelve aquí, un frame después de que la trampa se desarme, con un pestillo para que el asentamiento del montaje no le robe el foco a nadie al cargar. Tres gaps de canon salieron al componerlo, todos en el README: `Button` acepta `ref` y no lo reenvía (forma de A-94), `Switch` no tiene parte de etiqueta, y el `Dialog` restaura el foco a un nodo muerto cuando su disparador se desmontó. El instrumento mintió antes que el código, otra vez: el pane del navegador no compone frames, así que la animación de salida no termina y un diálogo cerrado sigue pintado — el foco medido allí era ficción. Y `fillStyle` no normaliza oklch, así que todo color leído del estilo computado se volvió negro y dio 1.11:1 sobre texto perfectamente legible. Las cifras de esta ficha se miden por píxel, en Chrome real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
#### Ficha N3 `cookie-consent` — ✅ EJECUTADA 2026-08-18
> **Cerrada.** Lo entregado y las dos desviaciones, antes de la ficha original:
>
> - **Una sola llamada requerida, `onDecision(consent, via)`**, en vez de los
> `onAcceptAll` + `onRejectAll` que la ficha pedía (la propia ficha ya
> contenía las dos formas). Es **más fuerte**, no menos: con dos callbacks el
> consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo
> renderiza el block y el API no expone knob para tocarlo. `via` distingue los
> tres caminos para el log del app.
> - **`Dialog`, no `Drawer`** — la fase 0 lo decidía con la guía delante: el
> panel es una decisión corta con una salida clara, no una superficie que se
> arrastra.
> - **La barra NO es un `Card`**: `data-depth="overlay"`, el plano del sistema.
> El `Card variant="outline"` fue el primer intento y midió 1.00:1 contra la
> página — la barra se disolvía en ella.
> - **Tres gaps de canon** salieron al componerla, todos en el README: `Button`
> acepta `ref` y no lo reenvía (forma de A-94) · `Switch` no tiene parte de
> etiqueta · el `Dialog` restaura el foco a un nodo muerto cuando su
> disparador se ha desmontado.
> - Medido en Chrome real (el pane del navegador no compone frames y daba foco
> ficticio): tres caminos, teclado, anuncio capturado en la región polite,
> claro/oscuro (5.96:1 / 8.79:1 en las dos acciones), 375, RTL.
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Función**: el aviso de consentimiento — overlay legal con estado, no una
sección. Barra anclada + panel de preferencias por categoría.
- **Fase 0 PROFUNDA (obligada, tanda propia)**: ePrivacy/GDPR + guía
CNIL/EDPB ANTES de diseñar el API. Las referencias (Flowbite · Relume) se
miran como **anti-patrón**: sus dumps priman «aceptar» y esconden
«rechazar», que es exactamente lo que el tipo debe hacer inconstruible.
- **La ley vive en el TIPO** (la doctrina que commerce ya firmó):
`onAcceptAll` Y `onRejectAll` REQUERIDOS — el block no se puede montar sin
camino de rechazo; las dos acciones se renderizan con la MISMA prominencia
por construcción y el API no expone ningún knob para degradar «rechazar»;
las categorías no esenciales nacen apagadas (el tipo no admite
pre-marcado); `essential` es fija y no desactivable.
- **Coordina** (doctrina de coordinación): máquina de la sección
(`unset · preferences · decided`), la FORMA de sus datos (B-5: el esquema
de categorías por defecto, sustituible) y las palabras de sus estados por
idlangref `#?blocks.cookie-consent.*` con fallback inglés (B-7). La
decisión se anuncia a la AT (`uix.announce` — el segundo servicio
sancionado).
- **Persistencia y ejecución = del APP** (D-BLK.6): el block emite
`onDecision(consent)`; jamás escribe cookies ni bloquea scripts.
- **Compone**: `Card`/`Surface` (la barra) + la capa de colocación anclada
(el eje affix canonizado 2026-08-15 — su fase 0 lee ese README y NO
re-implementa colocación) + `Dialog` o `Drawer` (preferencias; fase 0
decide con la guía delante) + `Switch` por categoría + `Button` + `Group` +
`Text`/`Link` (política del app).
- **Foco y descarte**: la barra NO roba el foco al aparecer (no es modal);
`Escape` no decide — cerrar sin elegir no es consentir, y eso también es
estructura, no estilo.
- **Landmark**: `role="region"` nombrada; el panel de preferencias hereda la
semántica del `Dialog`/`Drawer` que compone.
- **v1**: barra inferior no bloqueante + panel de preferencias + esquema de
4 categorías por defecto (essential · functional · analytics · marketing).
- **Demo**: los tres caminos (aceptar todo · rechazar todo · granular) con la
decisión emitida VISIBLE, teclado completo, AT (anuncio verificado),
claro/oscuro, 375/1280, RTL.
### Tramo D — la página compuesta (la puerta de cierre)
Es la condición de cierre que F2 escribió y no cumplió, y **entra aquí**. El
claim del tier es «los blocks ensamblan sin fricción», y ese claim sólo se
prueba compuesto: la jerarquía de headings CRUZANDO blocks, los landmarks
únicos, el ritmo vertical entre `sectionSize` consecutivos, la cascada de
motion sección a sección. Todo lo que vendemos como superación es
**indemostrable block a block**.
No es teórico: el defecto de los 16px de `content-section` sólo apareció al
poner otra sección debajo, y **A-95** (el `banner affix="top"` tapando la
cabecera pegada) es un defecto que sólo existe cruzado.
Va **al final** — componer antes de cambiar los blocks sería componer dos
veces — es el vehículo natural de la matriz del tramo B, y se promociona a
demo pública (P1: todas las referencias shippean páginas compuestas como
artefacto de prueba; TW 10 · Untitled 105).
blocks(landing): las piezas se prueban juntas, y ahí sale lo que ninguna enseñaba sola Cierra el tramo D y con él F2b entera. Una landing verosímil con 14 de los 18 blocks, a sangre y sin marco de demo, porque el claim del tier —que sus blocks ensamblan sin fricción— es indemostrable block a block: el outline de encabezados sólo existe cruzando secciones, dos landmarks sólo chocan cuando hay dos, y una costura vertical sólo aparece cuando algo se pone debajo. Lo que dice la medida: un h1 y 22 encabezados sin un solo salto de nivel; ningún landmark repetido sin nombre; 0px de hueco entre secciones vecinas —el aire es siempre el padding de cada Section, costuras de 112 a 192px—; cuatro de las cinco cascadas esperando a que su sección entre en viewport; un solo commit-submit en el formulario; setenta tabulaciones sin una trampa de foco y un drawer que a 375px devuelve el foco a su disparador. A-95 vuelve confirmado, ahora con cifras: con affix="top" la tira y la cabecera solapan 49px —la altura entera de la cabecera— y el hit-test en su centro cae en la tira, así que la navegación es inalcanzable. La pieza que falta sigue siendo app-shell. La página compone la tira en flujo, que es lo correcto, y deja el escenario del defecto detrás de ?banner=affix para poder medirlo en vez de citarlo. Un defecto arreglado, y era de los que importan: Newsletter.Reason medía 2,33:1 sobre el panel mientras la nota a su lado medía 8,5:1. Es la frase que explica por qué la acción está bloqueada —la que la doctrina de coordinación obliga a tener— y no se podía leer: la doctrina cumplida en la estructura y derrotada en la percepción. El block ya tenía el mecanismo y ya lo había escrito para su nota («más callado por TAMAÑO, no por tinta»); su razón no lo seguía. Ahora 8,83:1 sobre el panel, y muted fuera de él. Dos gaps nuevos, ninguno tocado aquí: los DOS landmarks banner —el canon estampa role="banner" DESPUÉS de sus rest props, así que nombrar ambos es todo lo que puede hacer la app, y esta página lo hace— y feature-split sin parte .Header, el único block de sección que no la tiene: sin ella emite tres h2 hermanos y la sección se queda sin nombre. El catálogo aprendió a publicar páginas: kind: 'page' en BlockEntry y el guard lo salta en las dos direcciones, con self-test propio probado por mutación. Y una lección de instrumento que costó un susto: una captura fullPage no hace scroll, así que toda cascada bajo el pliegue se queda en opacidad 0 y la imagen sale EN BLANCO. La primera medida dijo «la landing está vacía». Saltar de una vez al final del documento tampoco vale: las secciones pasan de debajo a encima en un frame y el observador nunca las ve entrar. Se recorre en pasos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
#### Ficha — la página compuesta — ✅ EJECUTADA 2026-08-18
> **Cerrada.** `web/routes/blocks/landing/` con 14 de los 18 blocks, a sangre.
> Las medidas viven en su README con fecha e instrumento (Playwright headless).
> Lo que salió, resumido:
>
> - **Outline** 1 `h1`, 22 encabezados, 0 saltos · **landmarks** sin duplicado
> anónimo · **ritmo** 0px de hueco entre secciones (el aire es el `padding` de
> cada `Section`; costuras 112–192px) · **motion** 4 de 5 cascadas esperan a su
> sección · **sema** un solo `commit-submit` · **teclado** 70 tabulaciones sin
> trampa y el drawer devuelve el foco.
> - **A-95 RE-CONFIRMADO con cifras**: con `affix="top"` el solape es 49px — la
> altura entera de la cabecera — y el hit-test cae en la tira. `app-shell`
> (F3.1) sigue siendo la pieza que falta; la página compone en flujo.
> - **Un defecto arreglado**: `Newsletter.Reason` medía **2,33:1** sobre el panel
> (la nota a su lado, 8,5:1) — la frase que explica por qué la acción está
> bloqueada no se podía leer. Ahora toma la tinta del panel: **8,83:1**.
> - **Dos gaps nuevos**: los DOS landmarks `banner` (el canon estampa el `role`
> después de los rest props) y **`feature-split` sin parte `.Header`**, el único
> block de sección que no la tiene.
> - **El catálogo aprendió a publicar páginas**: `kind: 'page'` en `BlockEntry`, y
> el guard lo salta en las dos direcciones (self-test propio, probado por
> mutación).
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Ruta**: `web/routes/blocks/landing/` con el patrón de las previews
(`+layout@.svelte` que resetea: la página se ve A SANGRE, sin marco de
demo) + ficha en `_lib/catalog.ts` (B-9) para que el raíl la publique.
- **Qué es**: una landing VEROSÍMIL de un producto — contenido real de
principio a fin (el patrón `{Name}Site`), no un desfile de blocks. El
criterio de inclusión es la naturalidad: los blocks que una landing real
lleva (site-header + banner + hero + logo-cloud + feature-grid +
feature-split + stats-band + testimonials + pricing + faq + cta +
newsletter + site-footer + cookie-consent encima) entran; los que no le
caben con naturalidad (team · contact · content-section · blog) quedan
REGISTRADOS en el README de la página como no-compuestos — una segunda
página compuesta (about/blog) es Gap con disparador, no alcance de esta.
- **Qué verifica** (cada punto con su medida, no a ojo):
- **Outline de headings**: UN `h1` (el hero), `h2` por sección — la
política de heading-level que vendemos como superación, medida sobre el
árbol de accesibilidad.
- **Landmarks únicos y nombrados** — aquí se re-mide el hallazgo de los
DOS `role="banner"` (block `banner` + `<header>` del site-header).
- **Ritmo vertical**: los `sectionSize` consecutivos comparados en las
costuras (el método que destapó los 16px de `content-section`: bordes
medidos entre secciones vecinas, no cada block contra sí mismo).
- **Motion sección a sección**: las cascadas disparan al entrar cada una
en viewport, no todas a la vez (§D.13; Playwright headless — el pane
suspendido congela IO y rAF).
- **A-95** (`banner affix="top"` + header pegado): si `app-shell` (F3.1)
existe ya, se arregla y se cierra; si no, se re-confirma y queda.
- **La sonda de sema** sobre el formulario de la página (el cabo de los
hermanos del `Field`: ¿doble `commit-submit`?).
- Claro/oscuro · 375/1280 · RTL · **teclado de punta a punta** (sin
trampas de foco; el drawer del header devuelve el foco).
- **Gates**: `blocks:check` verde · la página en el catálogo · las medidas
de arriba anotadas en el README de la página con fecha e instrumento.
### Orden
**A** V4 → V8 (medir primero) → V5 → V2 → V3 → **V1 en tanda propia** ·
**B** la matriz por tandas, un block por vez, según se tocan ·
**C** N1 `logo-cloud` → N2 `blog` → N3 `cookie-consent` (tanda propia) ·
**D** la página compuesta cierra.
Cada fila entra por el proceso por tanda de §4. La fase 0 ligera de las
variantes ya está hecha y vive en el README de su block; N1/N2/N3 traen la
suya, y la de N3 es profunda.
### Cierre F2b
`blocks:check` verde; **cada variante nueva con su control vivo en la demo**
(B-9, «cada prop público un control»); cada fila de Gaps del README pasada de
«scope-approval pending» a la decisión firmada; **la matriz del tramo B escrita
y demostrada en los 15 + los 3 nuevos**; la página compuesta verificada en
claro/oscuro y 375/1280, con recorrido de teclado completo.
La ola **no reabre ninguna fila CONFIRMADO del ledger** — son ejes distintos y
se miden aparte — con una excepción esperada: la página compuesta es donde
**A-95** deja de ser inarreglable, si para entonces existe `app-shell` (F3.1).
---
## F3 — Blocks de aplicación (10)
Mismas reglas y ficha que F2. **Depende de**: F1.2/F1.3/F1.4 (estados),
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
F1.7 (`sidebar`) para F3.1. Las cuatro dependencias están CERRADAS, así que F3
no está bloqueada por nada del canon.
**Orden recomendado** (2026-08-17): **F3.1 `app-shell` PRIMERO** — es la pieza
que desbloquea dos cosas ajenas a su propia ficha: la fila **A-95** del ledger
(`banner affix="top"` tapa la cabecera pegada; no se arregla en ninguno de los
dos blocks porque falta quien POSEA las alturas de página) y **F4.1
`docs-shell`**, que lo especializa. Después, por coste creciente y con las
fases 0 profundas al final: F3.9 `error-page` (compone `Result`, ya hecho) →
F3.6 `user-menu` → F3.7 `notifications` → F3.5 `settings` → F3.2 `auth` →
F3.4 `dashboard` → F3.8 `wizard` → **F3.3 `data-table`** y **F3.10 `kanban`**,
que son las dos con fase 0 OBLIGADA y más profunda (leer `soma/components/table`
- `$libs/datagrid` y `soma/components/drag-drop` antes de diseñar el API).
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que había que preguntar: si su alcance tenía delante `active-app` y `active-uix` enteros. No los tenía. Las cuatro decisiones que se firman, y lo que las obligó: - **Q1 — el block NO cablea servicios.** La integración del ecosistema se demuestra en una app de referencia en app-land, no dentro del block. Un shell que leyera `App.session` para dibujar el menú de usuario sería una raíz de composición disfrazada: funcionaría, y metería el arranque del ecosistema dentro de una pieza que ha de poder caer en cualquier app. - **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed, sin números) es lo que cierra A-95 por construcción; `body` es lo que `docs-shell` necesitará para anclas y TOC pegada. - **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho exigiría partir ese provider: queda como gap de canon para v2. - **Q4 — canon `skip-link` + uno por región montada** (técnica G124). De paso, tres cosas que estaban mal escritas y ahora lo dicen: - El dossier afirmaba que **ninguna referencia trae skip-link** y que era superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera un menú entero. La superación real es generarlos del mismo contrato que estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí miró está deprecado. - **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni cablea servicios. - **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`, `mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo. - **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta `update-password`, que no es un lujo — es donde aterriza el enlace de recuperación, y sin ella el camino de `recover` no termina. `blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría, landmarks, alturas) y dónde está la línea (la raíz de composición es de la app). Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box` expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell no necesita CSS propio—, y `Sticky` expone `root`. docs:check 0 errores, 0 avisos (635 docs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### F3.1 `app-shell` — fase 0 FIRMADA 2026-08-18
> La ficha original (cinco líneas) se escribió antes de que existieran el canon
> `Sidebar` y la capa `affix`, y no miraba el ecosistema. Ésta la sustituye. Lo
> que la motivó: el autor preguntó si el alcance del shell tenía delante
> `active-app` y `active-uix` enteros. No los tenía.
- **Función**: la geometría de una página de aplicación — el esqueleto que
POSEE las regiones, sus landmarks y sus alturas, y por tanto el único sitio
donde el aviso, la cabecera pegada y el contenido dejan de pelearse.
- **Compone**: `Sidebar` (F1.7, que ya trae `<aside>`+`<nav>`+`<main>`, el
drawer móvil y los tres modos de colapso) + `Box`/`Grid` para la rejilla +
`Sticky` (con `root`, el scroll-host) + el nuevo canon `SkipLink` + el
`Container` de la medida. **NO** `ScrollArea` en v1: el scroll del main es
`overflow` de una caja, y montar barras propias sobre la región que scrollea
toda la app es una decisión visual que nadie ha pedido (Gap).
- **API**:
```svelte
<AppShell scroll="main|body" nav={{ collapsible, side, mobileBreakpoint }}
bind:navOpen aside={{ breakpoint }} bind:asideOpen>
<AppShell.Banner> <!-- aviso EN FLUJO, altura intrínseca -->
<AppShell.Nav> <!-- compone Sidebar; la app rellena las filas -->
<AppShell.Header> <!-- <header> DENTRO del inset (decisión Q3) -->
<AppShell.Main> <!-- <main id> — destino del skip-link -->
<AppShell.Aside> <!-- <aside aria-label>, plegable por breakpoint -->
<AppShell.Footer> <!-- <footer>, opcional -->
</AppShell>
```
- **Landmark**: 1 `banner` (`<header>`) · 1 `main` · el `navigation` del
`Sidebar` · `complementary` ×2 NOMBRADOS (panel del Sidebar + Aside) · 1
`contentinfo`. Sin heading propio: un shell no es una sección.
- **v1**: los dos modelos de scroll, el colapso del nav por las costuras del
`Sidebar`, el aside plegable, y los skip-links generados de las regiones
MONTADAS.
- **Excepción B-10 declarada**: `SHELL_ALLOWLIST` ya existe en
`blocks-check.ts:44` y está probada por fixture — el shell puede componer
`banner`, `site-footer`… sin tocar el guard.
#### Las cuatro decisiones firmadas (2026-08-18)
| # | Decisión | Firmado | Por qué |
| ------ | ------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Q1** | Frontera de servicios | **el block NO cablea servicios** | La doctrina queda intacta (`blocks.md` §Services + D-BLK.6). La integración del ecosistema se demuestra donde de verdad vive: en una **app de referencia** en app-land (abajo). Un block que leyera `App.session` sería una raíz de composición disfrazada. |
| **Q2** | Modelo de scroll | **`scroll="main"` y `scroll="body"`, por prop** | `main` (rejilla `100dvh`, sin `fixed`, sin números) es lo que cierra **A-95** por construcción; `body` es lo que `docs-shell` (F4.1) necesita para anclas, TOC pegada y la barra de URL móvil. Es el eje que Mantine expone como `mode` y Atlassian como `isFixed` por slot. |
| **Q3** | Colocación de la cabecera | **`alt`: `<header>` dentro del inset** | El provider del `Sidebar` **ES** la fila flex (`soma/components/sidebar/components/sidebar.svelte` renderiza el `<div data-sidebar>`), así que el `Trigger` sólo vive dentro de ella. Una cabecera a todo lo ancho POR ENCIMA del nav exigiría partir ese provider — cambio de canon que v1 no paga. Es lo que hacen shadcn y Mantine `layout='alt'`. |
| **Q4** | Skip-links | **canon `SkipLink` + uno por región montada** | Técnica **G124** de la WCAG 2.4.1 («links at the top of the page to each area of the content»), que es lo que Atlassian genera. Se emiten de las regiones que EXISTEN, no de una lista escrita: el mismo contrato que estampa el landmark estampa su enlace. |
Consecuencias registradas: el eje `layout` del `Sidebar` (provider ≠ fila)
queda como **gap de canon para v2**, y lo pedirá `docs-shell` si quiere topbar
a todo el ancho. `ScrollArea` sobre el main queda como Gap del block.
#### Comprobado en código antes de escribir esto (2026-08-18)
- `Sidebar.Inset` acepta **`child`** — el shell lo renderiza como caja y mete
dentro `<header>` + `<main>`, así que el `<main>` no acaba envolviendo a la
cabecera. Sin esto, Q3 no era ejecutable.
- `Box` expone `minHeight` / `position` / `overflow` (`LayoutLengthValue =
number | string`, así que `100dvh` pasa crudo) y `Grid` expone
`templateRows` / `templateColumns`: **la rejilla del shell no necesita CSS
propio** — D-BLK.2 se cumple sin excepción, como en `hero`.
- `Sticky` expone `root` (el scroll-host): bajo `scroll="main"` la cabecera se
pega contra el main, no contra el viewport.
- ⚠️ `Sticky.offset` y `AnchorNav.topOffset` son **números px**. Bajo
`scroll="body"` un raíl que deba anclarse a la altura de la cabecera necesita
ese número; el shell publica la altura como token, y **quien la consuma en px
es F4.1** — allí se decide si `offset` gana longitudes CSS (Gap ya declarado
en el README de `sticky`).
#### El suelo de referencia (contrastado 2026-08-18, fuentes leídas)
Mantine `AppShell` · shadcn `Sidebar`+blocks · AntD `Layout`/`ProLayout` ·
Polaris `Frame`/`TopBar` · Atlassian (`page-layout` DEPRECADO +
`navigation-system` actual) · Toolpad `DashboardLayout` · Tailwind Plus
(Stacked 9 · Sidebar 8 · Multi-Column 6).
- **Suelo (≥3 refs)**: propiedad de la altura de cabecera · colapso
offcanvas/icono/none · drawer móvil · breakpoint como prop · aside/panel
derecho · padding/inset · layouts alt/multi-columna · landmarks por
construcción · RTL. De esos, **el `Sidebar` ya nos da cuatro**; el shell debe
los otros cinco.
- **Suelo-2**: skip-links · persistencia · atajo · ranura de ribbon ·
**el modelo de scroll como API** · geometría como CSS vars (Mantine 8
`--app-shell-*`; Atlassian 7 con fallback).
- **Superación posible**: los landmarks y sus skip-links salen del MISMO
contrato (nadie lo hace: Polaris pide un `ref`, Atlassian pide `id` +
`skipLinkTitle` a mano); dos `complementary` nombrados; RTL lógico; tokens,
densidad y modo de serie.
- ⚠️ **Riesgo, y hay que mirarlo de frente**: **Skeleton RETIRÓ su `AppShell`
en v3** — «the number of issues introduced by it far outweigh its potential
gains» (a11y en navegadores móviles con `100vh`, historia de sticky header
confusa). Por eso `scroll` es explícito, la altura va en `100dvh` y en móvil
no se fuerza el scroll propio del main.
#### Correcciones al dossier que esta fase 0 obliga
El dossier (`RESEARCH-blocks-references.md`) se corrige en el mismo pase: su
fila de `app-shell` afirmaba que **ninguna referencia trae skip-link** (Polaris
y Atlassian sí) y atribuía el modo icon-rail a Mantine (no lo tiene). Detalle y
fecha, en el propio dossier.
#### La app de referencia (donde SÍ se integra el ecosistema)
`web/routes/blocks/app/`, publicada en el catálogo con `kind: 'page'` como la
landing. Es el «Cierre F3» adelantado y, sobre todo, **la primera raíz de
composición en modo attach del repo**: `createActiveApp` → `attachActiveUix` →
`<Uix>` → `Soma.create()` → `ActiveEidos.create({ modeSource, densitySource })`
→ `createActivePrefsDomProjection` (en attach NO se proyecta sola) →
`createPrefsStorageBridge` → `setActiveApp` · `setBus` · `setPermsContext` ·
`applyStandardOrca`.
Hoy **ningún** shell del repo hace nada de eso: los siete arrancan
`createActiveUix` standalone, ninguno usa el canon `Sidebar`, ninguno tiene
skip-link, y cada uno reescribe a mano su `localStorage` y su `Set<listener>`
de modo. La app de referencia existe para que el cableado se escriba UNA vez y
para que cada punto que duela quede documentado como candidato (un helper de
fuentes visuales, una guía de raíz de app), nunca resuelto en silencio.
### F3.2 `auth`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Función**: familia de formularios de identidad. Sub-blocks: `sign-in` ·
`sign-up` · `recover` · `otp`.
- **Compone**: `Card` + `Form` + `Field` + `PasswordField` + `PinInput`
(otp) + `ProofOfHuman` + `Button` + `Separator` + `Link` + slot de
proveedores sociales (`Button`s del app).
- **API**: `<AuthCard kind='sign-in'|…>` con partes (`.Title` · `.Fields` ·
`.Actions` · `.Footer`), o cuatro exports finos — decidir en su fase 0
mirando el API real de `Form` (lo que menos indirection genere gana:
no-premature-abstraction).
- **Regla dura**: TODO handler (submit, oauth, resend) llega del app; el
block no conoce transporte ni credenciales. La validación es del sistema
`Form` con schemas del app.
- **Demo**: los 4 kinds, estados invalid/submitting, claro/oscuro.
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que había que preguntar: si su alcance tenía delante `active-app` y `active-uix` enteros. No los tenía. Las cuatro decisiones que se firman, y lo que las obligó: - **Q1 — el block NO cablea servicios.** La integración del ecosistema se demuestra en una app de referencia en app-land, no dentro del block. Un shell que leyera `App.session` para dibujar el menú de usuario sería una raíz de composición disfrazada: funcionaría, y metería el arranque del ecosistema dentro de una pieza que ha de poder caer en cualquier app. - **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed, sin números) es lo que cierra A-95 por construcción; `body` es lo que `docs-shell` necesitará para anclas y TOC pegada. - **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho exigiría partir ese provider: queda como gap de canon para v2. - **Q4 — canon `skip-link` + uno por región montada** (técnica G124). De paso, tres cosas que estaban mal escritas y ahora lo dicen: - El dossier afirmaba que **ninguna referencia trae skip-link** y que era superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera un menú entero. La superación real es generarlos del mismo contrato que estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí miró está deprecado. - **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni cablea servicios. - **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`, `mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo. - **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta `update-password`, que no es un lujo — es donde aterriza el enlace de recuperación, y sin ella el camino de `recover` no termina. `blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría, landmarks, alturas) y dónde está la línea (la raíz de composición es de la app). Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box` expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell no necesita CSS propio—, y `Sticky` expone `root`. docs:check 0 errores, 0 avisos (635 docs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **⚠️ Abierto para su fase 0 (anotado 2026-08-18)**: la ficha dice CUATRO
vistas y el dominio tiene SEIS. El `ViewType` de Supabase —la enumeración
más limpia del oficio— es `sign_in · sign_up · magic_link ·
forgotten_password · update_password · verify_otp`: nos faltan **magic-link**
y **update-password**, y esta última no es un lujo (es la pantalla a la que
aterriza el enlace de recuperación; sin ella el camino de `recover` no
termina). Se decide en su fase 0, no aquí. Dos hilos de canon la esperan y
hay que mirarlos antes: `Form`/SIUM con `progressive` no expone mensajes en
un envío inválido, y el provider de `ProofOfHuman` escribe su propio
`status`, así que el veredicto del app no se sostiene.
### F3.3 `data-table`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Función**: la tabla de trabajo completa: toolbar (búsqueda + filtros +
visibilidad de columnas + bulk actions con selección) + `Table` +
`Pagination` + `EmptyState` integrado.
- **Compone**: `Table` + `SearchField` + `DropdownMenu` + `Button` +
`Pagination` + `EmptyState` (F1.2) + `Group`/`Toolbar`.
- **⚠️ Fase 0 OBLIGADA y más profunda**: leer `soma/components/table` +
`$libs/datagrid` ANTES de diseñar el API — el block debe montar sobre las
capacidades reales (sorting/selection/paging que ya existan). Cada
capacidad que falte = FLAG con disposición (candidata a canon), nunca
lógica de datos dentro del block. El estado de datos es del app; el block
orquesta la UI.
- **API**: `<DataTable>` + `.Toolbar` (+`.Search`/`.Filters`/`.Columns`) +
`.Content` (la Table compuesta por el app con su API real) + `.Selection`
(barra bulk) + `.Footer` (Pagination) + `.Empty`.
- **Demo**: dataset realista (≥100 filas), búsqueda+filtro+selección+bulk,
estado vacío tras filtrar.
### F3.4 `dashboard`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `AutoGrid`/`Grid` + `Card` + `Metrics`/`CountUp` + `Chart`
(tipos ya existentes) + `Feed` + `EmptyState`.
- **API**: `<Dashboard>` + `.Stat` + `.Panel` (card con `.PanelHeader` +
children libres para chart/feed).
- **v1**: composición fija de ejemplo con slots; nada de layout persistible
ni drag (Gap explícito; si un día entra, el drag es de `drag-drop`).
### F3.5 `settings`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Container` (medida estrecha) + `Section` + `Heading` +
`Separator` + filas `Field`/controles + `Callout` (F1.4, intent `threat`)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `AlertDialog` (confirmación destructiva) + `Button`.
- **API**: `<Settings>` + `.Section` (+`.SectionTitle`/`.SectionDescription`)
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `.Row` (label+control+ayuda) + `.DangerZone`.
- **Regla**: cada control es el componente del ecosistema (nunca nativos —
B-2); el guardado (por fila o global) lo decide el app vía `Form`.
### F3.6 `user-menu`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Avatar` + `DropdownMenu` (+ items con `Icon`, `Separator`,
`Kbd` opcional).
- **API**: `<UserMenu>` + `.Trigger` (Avatar + nombre) + `.Items` (children
= items del DropdownMenu real).
- **Nota D-BLK.6**: tema/idioma/logout = items que el APP cablea; el block
da la estructura y los puntos de montaje.
### F3.7 `notifications`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Popover` (desktop) / `Drawer` (móvil) + `Badge` (contador en
el trigger `IconButton`) + `Feed` + `EmptyState` + `Button` («marcar
leídas» — string del app).
- **API**: `<Notifications>` + `.Trigger` + `.Panel` (+`.Header`/`.List`/
`.Empty`/`.Footer`).
- **Trampa conocida**: panel más ancho que el min-width del Popover →
recorte; pasar `width` por el prop tokenizado del `PopoverContent` (la
lección ya está aprendida — no re-descubrirla).
### F3.8 `wizard`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Stepper` + `Form` + `Group` de `Button`s (atrás/siguiente/
finalizar) + `Progress` opcional.
- **API**: `<Wizard>` + `.Steps` (Stepper compuesto) + `.Step` (contenido) +
`.Nav`.
- **Fase 0**: leer el API real de `Stepper` y de `Form` (validación por
paso) — el block solo coordina visibilidad de paso + navegación; el
estado de validez lo dicta `Form`.
### F3.9 `error-page`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Result` (F1.3) + `Group` de `Button`/`Link` + `SearchField`
opcional (404).
- **API**: `<ErrorPage status=…>` + children de acciones.
- **v1**: 404 · 403 · 500 · offline.
### F3.10 `kanban`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `DragDrop` + `Grid`/`Group` de columnas + `Card` +
`VirtualList` (columnas largas) + `Badge` + `EmptyState` por columna.
- **⚠️ Fase 0 OBLIGADA**: leer `soma/components/drag-drop` a fondo; el
block mapea sus primitivas reales (zonas, handles, anuncios a11y del
provider). Toda carencia = FLAG (candidata a canon), no workaround.
- **API**: `<Kanban>` + `.Column` (+`.ColumnHeader`) + `.Card` (children
libres). El modelo de datos y la persistencia del orden son del app.
- **Demo**: 3 columnas, drag entre columnas, columna vacía, teclado.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
_(Diferidos de F3, registrados en F5: `scheduler` — bloqueado hasta que
`chronos` aterrice; `chat-room` — NO va aquí: la familia `chat-_`es canon y
su roadmap vive en`next-features.md` §7.)\*
**Cierre F3**: `blocks-check` verde; demo de `app-shell` montando dentro
`data-table` + `dashboard` + `notifications` + `user-menu` como página de
integración; navegador claro/oscuro/375/1280 + teclado (tab por toda la
página de integración sin trampas de foco).
---
## F4 — Blocks de docs (3)
**Depende de**: F1 completo (prose, anchor-nav, **nav-tree**, callout) +
F2.1. Conecta con la iniciativa docs-corpus=site (el corpus es la semilla
del sitio); estos blocks son su vehículo de UI.
### F4.1 `docs-shell`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `AppShell` (F3.1, excepción B-10) especializado: **`NavTree`
(F1.8)** como árbol de docs + `Prose` (main) + `AnchorNav` (TOC derecha) +
`Breadcrumb` + trigger de búsqueda (`Command` — ya existe) + prev/next
(`Group` de dos `Card`/`Link`).
- **E-5 firmada**: el atajo ⌘K del trigger de búsqueda = listener app-land
documentado en el README del block; el art `shortcuts` conserva su
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que había que preguntar: si su alcance tenía delante `active-app` y `active-uix` enteros. No los tenía. Las cuatro decisiones que se firman, y lo que las obligó: - **Q1 — el block NO cablea servicios.** La integración del ecosistema se demuestra en una app de referencia en app-land, no dentro del block. Un shell que leyera `App.session` para dibujar el menú de usuario sería una raíz de composición disfrazada: funcionaría, y metería el arranque del ecosistema dentro de una pieza que ha de poder caer en cualquier app. - **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed, sin números) es lo que cierra A-95 por construcción; `body` es lo que `docs-shell` necesitará para anclas y TOC pegada. - **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho exigiría partir ese provider: queda como gap de canon para v2. - **Q4 — canon `skip-link` + uno por región montada** (técnica G124). De paso, tres cosas que estaban mal escritas y ahora lo dicen: - El dossier afirmaba que **ninguna referencia trae skip-link** y que era superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera un menú entero. La superación real es generarlos del mismo contrato que estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí miró está deprecado. - **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni cablea servicios. - **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`, `mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo. - **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta `update-password`, que no es un lujo — es donde aterriza el enlace de recuperación, y sin ella el camino de `recover` no termina. `blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría, landmarks, alturas) y dónde está la línea (la raíz de composición es de la app). Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box` expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell no necesita CSS propio—, y `Sticky` expone `root`. docs:check 0 errores, 0 avisos (635 docs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
disparador F5 (≥2 consumidores reales). ⚠️ **DESFASADA (visto en la fase 0
de F3.1, 2026-08-18)**: `Command.Dialog` **ya trae el atajo**
(`shortcut`, por defecto `'mod+k'`, con `open` bindable y `onOpenChange`;
`eidos/components/command/types.ts:41-52,96-99`). El listener a mano no hay
que escribirlo: se pasa el prop, o se pone a `null` para apagarlo. Lo que
sigue vigente de E-5 es lo otro: el art `shortcuts` no nace por esto.
- **API**: `<DocsShell>` + `.Nav` + `.Article` (prose) + `.Toc` + `.Search`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- `.PrevNext`.
- **v1**: 3 columnas desktop → TOC colapsada y sidebar-drawer en móvil.
### F4.2 `code-showcase`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Compone**: `Tabs` (Preview/Code) + `Surface` (lienzo de preview) +
`CodeBlock` + `Clipboard` + `Toolbar` opcional.
- **API**: `<CodeShowcase>` + `.Preview` (children vivos) + `.Code`
(children CodeBlock).
- **v1**: preview + código + copiar. (Chips de tema/dirección/densidad del
lienzo = Gap, notado — pediría servicios, D-BLK.6.)
### F4.3 `props-table`
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Función**: tabla de referencia de un componente **generada del morfo** —
parts, data-attrs, ARIA, keyboard y eventos salen de `compileMorfo`
(ventaja estructural única de este framework: el contrato ES
introspectable).
- **Compone**: `Table` + `Code`/`Kbd` + `Badge`.
- **API**: `<PropsTable morfo={…}>` (aquí el input data-driven es legítimo:
el "árbol" es el contrato canónico, no contenido inventado).
- **v1**: tablas morfo-backed (parts/attrs/keyboard/eventos con
familia·verbo·intent). Tabla de props TS = **Gap tracked** (requiere
extracción de tipos; no improvisar — flag para decidir tooling).
**Cierre F4**: una página de docs REAL montada con los 3 (un doc del corpus
renderizado en `docs-shell` con showcase y props-table de un componente
PASS), verificada en navegador.
---
## F5 — Backlog condicionado (no construir sin disparador)
Componentes canon candidatos (cada uno entraría por la ruta de 9 fases):
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Pieza | Disparador |
| ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `lightbox` (visor imagen zoom/galería) | cuando un block de media/galería real lo pida |
| `tour` (onboarding spotlight) | cuando el app-shell tenga consumidor real con onboarding |
| `hover-card` genérico | cuando un tercer caso no-URL aparezca (hoy `link-preview` cubre) |
| `description-list` | primer detail-view real (pareja natural de `data-table`) |
| `loading-overlay` | primera pantalla con carga bloqueante real |
| `transfer-list` | primer admin real con asignación dual |
| `mention` | ya registrado en `next-features.md` §7 (chat) — no duplicar aquí |
| `masonry` · `bottom-nav` · `swipe-actions` · `pull-to-refresh` · `image-compare` · `marquee` (¿pack?) · `watermark` · `signature-pad` | demanda real; varios son candidatos a pack, no a canon — decidir con la regla de admisión de packs |
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| art `shortcuts` (registro global de atajos + cheat-sheet con `Kbd`) | cuando ≥2 consumidores reales (command palette global + docs) lo pidan |
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Blocks diferidos: `scheduler` (bloqueado por `chronos`) · `billing` ·
`file-manager` · `profile-card` (cuando haya consumidor). **`logo-cloud` salió
de esta lista el 2026-08-17**: su premisa («pide `Marquee`») no casaba con las
referencias — entra por F2b tramo C.
Diferidos de F2b (2026-08-17), con disparador: **stats-band split-with-image** y
**stats-band timeline/stepped** — el dossier declara ese block «Paridad OK», así
que son adoptables, no suelo; entran cuando una demo real las pida.
---
## 6. Verificación de cierre del plan (definition of done global)
1. `npm run check` sin regresión; `npm run blocks:check` verde; `npm run
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
component:audit` → los 7 de F1 PASS (o NEEDS-WORK solo por D-\* de la fase
demos global); `docs:check` verde.
2. Las tres páginas de integración (F2 landing · F3 app · F4 docs) renderizan
compuestas SOLO de canon+blocks, verificadas visualmente en claro/oscuro,
375/1280 y teclado.
3. Doctrina publicada y enlazada (architecture/blocks.md en el mapa; CLAUDE.md
al día); cada block con README B-9 completo.
4. Ningún gap silenciado: todo ❌/⚠️ de las fases 0 tiene decisión firmada o
fila en F5/next-features.
## 7. Registro
- 2026-07-21 — Plan creado (análisis de ecosistema + decisión de usuario:
tier `blocks`). Iniciativa en `next-features.md` §8.
- 2026-07-21 — **F0.1 HECHA**: las 7 D-BLK firmadas (repaso en sesión).
D-BLK.1 **enmendada** frente a la propuesta: el tier vive en
`src/uix/blocks/` (no `src/blocks/`); referencias del plan actualizadas
(tabla de tiers, B-4, blocks-check, F0.3/F0.4). Resto firmado como
propuesto. Siguiente: F0.2 (doctrina en `architecture/blocks.md`).
- 2026-07-21 — **F0 CERRADA** (misma sesión): doctrina
`docs/architecture/blocks.md` · alias `$blocks` (vite + svelte.config +
CLAUDE.md) · `src/uix/blocks/README.md` (mapa + template B-9) ·
`scripts/blocks-check.ts` + `npm run blocks:check` (self-testing) ·
galería `web/routes/blocks/` con layout de bootstrap propio
(`+layout@.svelte`, espejo mínimo del de `/uix`, sin packs sema) · fila
E1 en `docs/README.md`. Nota de commit: `docs/README.md` y
`docs/next-features.md` quedaron FUERA del commit de F0 — ambos traían
diff foráneo de otra sesión imposible de separar por staging de archivo
completo; sus hunks propios (fila E1 + §8) viajan con el árbol de trabajo
hasta que su dueño commitee. Siguiente: **F1** (orden recomendado:
`empty-state` → `result` → `callout` → `sticky` → `prose` → `anchor-nav`
→ `sidebar`).
- 2026-07-21 — **Estudio de referencia COMPLETO** (encargo del usuario:
"a la par y cuanto menos superarlo"): 6 pistas de investigación web en
paralelo → `docs/process/RESEARCH-blocks-references.md` (suelos de
paridad a nivel de prop por ítem, pitfalls, superación). Regla nueva en
§4: toda fase 0 contrasta contra el dossier. Puntero de colisión añadido
a F1.7. **Enmiendas E-1…E-5 presentadas al usuario** (E-1 nav-tree ·
E-2 filosofía de alcance v1 · E-3 categorías nuevas F2 · E-4
registry/llms/MCP · E-5 ⌘K) — firmas pendientes; se registran aquí.
- 2026-07-21 — **E-1…E-5 FIRMADAS** (todas según recomendación): **E-1**
componente canónico `nav-tree` data-driven (F1.8; F1 pasa de 7 a 8; el
naming se valida en su fase 0 como extensión de D-BLK.7); **E-2** suelo
de paridad = v1 (regla en el intro de F1); **E-3** F2 pasa de 10 a 14
(banner · team · contact · content-section; resto de la unión a F5);
**E-4** distribución registry/llms.txt/MCP registrada como iniciativa
propia en `next-features.md` §9; **E-5** ⌘K = listener app-land (nota en
F4.1). Doctrina (`architecture/blocks.md` promotion path) y galería
actualizadas.
uix(empty-state): F1.2 · componente base display (morfo+eidos) al suelo del dossier Primera pieza F1 del plan blocks (PLAN-blocks.md; alcance E-2 = suelo de paridad, dossier §P3). Ruta de 9 fases completa: - morfo: 5 partes display (provider/media/title/description/actions), scope ['eidos'], 0 eventos con justificacion pasiva (patron renderEmptyState de las refs headless), Title role:'heading', texts.label - langs: components.empty-state.label (es/en) — el indice tambien recoge la retirada foranea del import de words (inseparable por staging de archivo; coherente con la migracion palabras ya enviada; words.ts sigue en su arbol) - eidos: compound EmptyState + Media(kind icon|media, placa 2x glifo) + Title(level 2-6, default h3 — modelo Atlaskit, tamano visual desacoplado) + Description(measure 45ch) + Actions(label -> role=group +aria-label, buttonGroupLabel); recipe sobre el bundle --size-* (titulo un paso discreto arriba; sm=in-collection, lg=hero); tokens publicos minimos (gap/actions-gap/media-bg/media-fg/media-radius/description-measure) - demo v2 9 tabs (harness: SystemAxes/MotionPanel/SemaPanel; snippet con paridad; escena in-collection via Card) + entrada nav (grupo Status) - README: Baseline · Comparativa (shadcn/Chakra/Atlaskit/AntD/Polaris) · Decisiones · Passive justification · Gaps con disposicion Verificacion: component:audit PASS · eidos-lint 5 morfo-backed + 4 eidos-only sancionados · morfo:check + morfo:vocabulary verdes · recipe-css-contract/api-contract/visual-attrs 37/37 · svelte-check 76E/51W = baseline exacto (cero regresion) · navegador claro Y oscuro por estilos computados (titulo 18->24px, placa 40->64px, chips vivos, cero errores de consola). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- 2026-07-21 — **F1.2 `empty-state` HECHA** (ruta de 9 fases completa,
suelo E-2 del dossier §P3): morfo display 5 partes (`scope: ['eidos']`,
0 eventos justificados, Title `role:'heading'`) + langs `label` + eidos
compound (`Media kind=icon|media` con placa 2× glifo · `Title level`
2–6 default h3, modelo Atlaskit · `Description` measure 45ch ·
`Actions label` → role=group, buttonGroupLabel) + recipe sobre el bundle
`--size-*` (título un paso discreto arriba) + demo v2 de 9 tabs + README
(Comparativa 5 refs · Passive justification · Gaps con disposición).
Verificado: `component:audit` **PASS** · eidos-lint 5 morfo-backed + 4
eidos-only sancionados · `svelte-check` 76E/51W = baseline exacto ·
navegador claro Y oscuro por estilos computados (título 18→24px, placa
40→64px, chips vivos). Siguiente: **F1.3 `result`** (comparte esqueleto;
fase 0 contra dossier §P3).
uix(result): F1.3 · estado terminal de flujo/pagina (morfo+eidos) al suelo del dossier Segunda pieza F1 del plan blocks (alcance E-2 = suelo de paridad, dossier §P3). Comparte el esqueleto de empty-state a proposito: EmptyState describe AUSENCIA de datos, Result reporta un RESULTADO — un lenguaje de layout, dos contratos (duplicacion consciente registrada en el README; revisable al 3er consumidor). - morfo: 6 partes display (provider/media/title/description/actions/extra; extra SIN archetype — parte genuinamente propia), scope ['eidos'], 0 eventos justificados (el resultado ya OCURRIO antes de renderizar) - status: enum de 7 = paridad AntD CON `warning` (decision firme del dossier) y HTTP renombrados semanticos (forbidden/not-found/server-error, nunca '404' stringly); default 'info'; data-status = attr eidos-only - media default: compone el mapa doctrinal IntentIcon (success→fulfill · error→threat · warning→risk) + Info de catalogo; HTTP = codigo mono grande NEUTRO aria-hidden (situaciones, no fallos — AntD jamas pinta 404 de rojo); children reemplazan el default entero (context local eidos-only con getter reactivo) - Title default h2 (vs h3 de empty-state — Result suele SER la pagina); Actions con label→role=group; Extra alineado a inicio (detalle que lee) - recipe: glifo display 3× xl bundle; tokens publicos gap/actions-gap/measures + {status}-color como forwarders de rol retintables; sin eje size (paridad AntD, gap diferido) - demo v2 9 tabs con copy por status + entrada nav (Status) + README (Comparativa · Decisiones · Passive justification · Gaps con disposicion) Verificacion: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed + 5 eidos-only sancionados · recipe/api/visual-attrs 37/37 · morfo:check verde (los 7 fallos listados son deuda foranea preexistente) · svelte-check 76E/51W = baseline exacto · navegador: success=fulfill verde 84px · error=threat rojo · not-found=«404» mono neutro, copy conmutando, cero errores de consola. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- 2026-07-21 — **F1.3 `result` HECHA** (ruta completa, suelo E-2 §P3):
morfo 6 partes (`extra` sin archetype — parte genuinamente propia) ·
status enum de 7 CON `warning` (decisión del dossier firme), default
`info`, HTTP renombrados semánticos y renderizados NEUTROS (código mono
grande, jamás rojo — doctrina AntD) · media default compone el mapa
doctrinal `IntentIcon` (fulfill/threat/risk) + `Info` de catálogo,
children lo reemplazan entero (context local eidos-only con getter
reactivo) · Title default h2 (vs h3 de empty-state — deliberado) ·
esqueleto duplicado conscientemente (README lo registra; revisable al
3er consumidor) · sin eje size (paridad AntD). Verificado:
`component:audit` **PASS 0E/0W** · eidos-lint 6 morfo-backed + 5
eidos-only · guards recipe 37/37 · navegador: success=fulfill verde
84px · error=threat rojo · not-found=«404» mono NEUTRO aria-hidden,
copy conmutando, cero errores consola. Siguiente: **F1.4 `callout`**
(decisión IMPORTANT/5º hueco + role=note; dossier §P3).
uix(callout): F1.4 · admonicion inline (morfo+eidos) al suelo del dossier Tercera pieza F1 del plan blocks (alcance E-2, dossier §P3). Banner = tira de anuncio de pagina; Callout = el aside del documento (split deliberado). - morfo: 4 partes (provider role=note / icon / title / content) + texts con TITULOS DEFAULT LOCALIZADOS por intent (note/tip/warning/caution — patron GitHub de label visible: la semantica nunca viaja solo en color/icono); 0 eventos justificados (dismissible DIFERIDO a pasada soma+sema por la regla de admision — dismiss ES un evento real) - el hueco IMPORTANT resuelto con el modelo Radix, sin 5o enum: `intent` (neutral|affirm|risk|threat — mapea NOTE/TIP/WARNING/CAUTION) + `color` override SOLO bajo neutral (doctrina §4: intent evaluativo gana); IMPORTANT = intent neutral + color + titulo propio (preset en la demo) - pintura sobre la maquinaria C6/THM-2 (patron badge): forwarders por color + slots `_palette-track/text/solid` → el generador emite la cascada de 8 roles Y el forward presence-guarded al shared layer — 33 escalas y colores custom con CERO CSS extra - a11y: role=note NOMBRADO via aria-labelledby → Title SOLO mientras esta montado (registro reactivo por context local); escalacion tipada role=status|alert|none (el role=alert estatico de shadcn = bug de referencia que NO copiamos); Title NO es heading (protege el outline que escaneara anchor-nav); icono decorativo = mapa doctrinal IntentIcon - fix cazado en navegador: el `+=` del registro leia el estado dentro del tracking del $effect del hijo → effect_update_depth silencioso (contador a -997, attr nunca estampado) — untrack() en register/cleanup; leccion registrada en memoria (incidente 2 de la clase) - recipe: grid con acento logico border-inline-start (RTL-correcto), nesting tolerado (Docusaurus); demo v2 9 tabs con preset IMPORTANT + PalettePicker showIntent=false; README completo Verificacion: component:audit PASS 0E/0W · eidos-lint 8 morfo-backed/0 invalid · guards 37/37 · svelte-check 76E/51W = baseline exacto · navegador: «Nota»/«Atencion» localizados · risk=ambar hue 45-60 · IMPORTANT=plum hue 326 resuelto por shared layer · labelledby=titleId · cero errores de consola. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- 2026-07-21 — **F1.4 `callout` HECHA** (ruta completa, suelo E-2 §P3):
morfo 4 partes + `texts` con **títulos default localizados por intent**
(note/tip/warning/caution — el patrón GitHub de label visible; la
semántica nunca viaja solo en color) · **el hueco IMPORTANT resuelto con
el modelo Radix**: `intent` (4, mapea el vocabulario de facto) + `color`
override SOLO bajo neutral (doctrina §4 — intent evaluativo gana),
montado sobre la maquinaria C6/THM-2 de badge (forwarders + `_palette-*`
→ 8 roles + 33 escalas + custom con CERO CSS extra; el forward
presence-guarded se emitió solo) · `role="note"` nombrado por el Title
vía `aria-labelledby` SOLO mientras está montado (registro reactivo por
context; **bug cazado en navegador**: el `+=` del registro leía el
estado dentro del tracking del `$effect` del hijo → effect_update_depth
y contador a −997 — fix `untrack`, lección para la memoria) · Title NO
heading (protege el outline de anchor-nav) · icono = mapa doctrinal
`IntentIcon` · escalación tipada `role: status|alert|none` (el
`role="alert"` estático de shadcn NO se copia) · dismissible DIFERIDO a
pasada soma+sema (regla de admisión). Verificado: audit **PASS 0E/0W** ·
lint 8 morfo-backed/0 invalid · guards 37/37 · navegador: «Nota»/«
Atención» localizados, risk=ámbar hue 45-60, **IMPORTANT=plum hue 326
por shared layer**, labelledby=titleId. Siguiente: **F1.1 `sticky`**
(orden del plan: desbloquea F2; dossier §P3 — offsets, scroll-host,
centinela por borde, espejo scroll-state).
- 2026-07-21 — **F1.1 `sticky` HECHA** (commit `998b11d9b`, PASS 0E/0W). El
primer componente BEHAVIORAL (soma+eidos). Precedido de un fan-out de
investigación (workflow, 6 lectores) que fijó el mapa de construcción:
template = FeedSentinelProvider; motor = `$adom.observeIntersection` (nunca
scroll+getBoundingClientRect); escritura de attrs por `syncAttrs` (nunca
`dom.apply` crudo). Diseño: un StickyProvider registra AMBAS partes
(box+sentinel) en un runtime; `$effect` retorna el observer como teardown;
callback async voltea `stuck=$state`; **rootMargin DERIVADO del offset** (la
línea de disparo del centinela = la línea de pin — acoplamiento
matemático); centinela = HERMANO de flujo del box (dos nodos raíz), no hijo;
offset viaja como `--_sticky-offset` (dato A8); recipe SOLO posiciona (el
consumidor decora `[data-sticky][data-stuck]`); `data-stuck`/`data-edge`
espejan `@container scroll-state(stuck: top/bottom)` (polyfill del contrato
CSS futuro). Verificación clave: **5 tests, incl. 1 real-IntersectionObserver
en chromium foreground** (no-stuck→scroll→stuck→back, prueba end-to-end de la
geometría — lo que el Browser pane suspendido NO puede). Decisión declarada:
el test usa el `installSomaHarness` compartido (los 86 tests lo hacen) — en
tensión con la regla CLAUDE.md «never fake translators»; flaggeada al usuario
para ratificar. Notas de commit: nav sidebar fuera (flip CRLF ajeno de 2125
líneas); índices langs/soma reconstruidos sticky-only (entangle con `aura`
sin commitear de sesión paralela). **Consola:** detectado un warning
reactivo residual en `callout` (registerTitle en $effect) — a investigar.
Siguiente: **F1.5 `prose`** (o F1.6 anchor-nav / F1.8 nav-tree; los 3
restantes que quedan de F1 tras prose son anchor-nav, nav-tree, sidebar).
- 2026-07-21 — **Review adversarial de sticky** (workflow `wrssrn7tx`, 3
lentes + verify): 1 hallazgo CONFIRMADO (minor) — pin en eje lógico vs
observer físico, diverge en writing-modes verticales. **Corregido**
(`6cba9a0b1`): recipe a `top`/`bottom` físico + margins físicos; 3 claims
«logical/RTL» del demo corregidos. Re-verificado PASS 0E/0W + test real-IO.
(1 lente teardown-ssr falló por API stall; caminos cubiertos por tests.)
**F1.1 sticky CERRADO.**
- 2026-07-21 — **F1.5 `prose` HECHA** (commit `5b84749ee`, PASS 0E/0W). Quinto
F1 (eidos-only). EL DIFERENCIADOR verificado en navegador: reglas de elemento
a especificidad CERO (`:where([data-prose] EL)`) → un `<Callout>` embebido
conserva su tinte/borde/grid AUTOMÁTICAMENTE (su `[data-callout]` gana a
`:where`), en claro Y oscuro — sin `not-prose`, cosa que ninguna ref puede.
Escala EM-relativa (un font-size raíz re-deriva todo, no la escala 5× de
tailwind); overflow de tabla estilo GitHub; dark gratis por roles (no
`-invert`); propiedades lógicas. Decisión `:where` (no `@scope`, diferido a
Gaps). Lección: recipe token defs (base.ts) admiten literales sin tripar
R-2.7; los literales em en el .css se justifican con `/* literal */` (21
añadidos). **F1 = 5/8.** Restan: anchor-nav (F1.6) · nav-tree (F1.8) ·
sidebar (F1.7).
- 2026-07-22 — **F1.6 `anchor-nav` HECHA** (commit `fe2d43b7a`, PASS 0E/0W).
Sexto F1, segundo BEHAVIORAL (soma+eidos) tras sticky. TOC scrollspy:
landmark `<nav>` + anchors nativos; detección por IntersectionObserver de
banda (`rootMargin -{topOffset}px 0px -70% 0px`), NUNCA scroll-listener +
`getBoundingClientRect` (doctrina anti-reflow — Mantine/Docusaurus lo
violan). `aria-current="location"` (spec-preciso; solo 1 de 5 refs pone
aria-current). Rail por-link con acento en `data-active`; indent por
`data-level`. Verificación: **test real-IO chromium** (`anchor-nav-io`) —
banda + zona muerta al fondo + fallback inicial; el pane suspendido daba un
`getComputedStyle` de color OBSOLETO (falsa alarma, la regla `[data-active]`
sí aplica — el `font-weight:500` lo probaba). **Review adversarial** (4
dimensiones × verificador escéptico, 9/16 confirmadas): arregladas →
ref-count en register/unregister (hrefs duplicados / churn ya no tiran un
target vivo, +test); transición y `outline-offset` tokenizados
(`--duration-fast`/`--ease-default`, `calc(--focus-ring-width * -1)`).
Documentadas como límite v1 (Gaps) → salto instantáneo al fondo (Ctrl+Fin)
en zona muerta + orden-enlaces==orden-secciones (ambas piden cambio de
contrato de observación v2). `data-level` fuera de rango degrada a flush
(no arreglado, simplicidad). **Además** (`37e35d7a2`): `npm run check`
global destapó 2 errores de tipo en F1.1 sticky (sentinelRef debía ser
`State` no `Active`; eidos types importaba `StickyProps` en vez del export
renombrado `ProviderProps`) — míos, arreglados. Mis archivos suman 0
errores de check (80→73; los 73 son deuda foránea de sesiones paralelas).
**F1 = 6/8.** Restan: nav-tree (F1.8) · sidebar (F1.7).
- 2026-07-22 — **F1.8 `nav-tree` EN CURSO** (WIP commiteado, sin cerrar).
Componente COMPLETO en verde en gates estáticos (audit PASS · eidos-lint 20/0
· svelte-check 0 · contracts mis-partes limpias); morfo+soma+eidos+pack de
sema+4 READMEs+todos los registros escritos. Falta demo + verificación en
NAVEGADOR + review adversarial + cierre. Data-driven (E-1); disclosure propio
(getter de estado + eventos `emerge`, pack espeja collapsible); filas propias
(no `Link`/`Badge` compuestos — Badge diferido a Gap); `activeHref` como
costura. Corregido de paso: README de soma de anchor-nav faltaba (contracts
lo destapó). **Handoff detallado: `docs/process/CONTINUE-nav-tree.md`.** ⚠️ 2
fallos contracts AJENOS (menubar/radio-group, sesiones paralelas).
uix(nav-tree): F1.8 CERRADA · demo + navegador + review adversarial Cierra el árbol de navegación data-driven (E-1) de F1: demo canónica de 9 pestañas con el mapa real de docs (43 nodos, 3 niveles), verificación en navegador real y review adversarial (5 dimensiones × 3 verificadores escépticos; 22 hallazgos brutos, 10 confirmados) con todos los confirmados arreglados. Arreglos del review - sema: la parte `group` —target de los eventos emerge— se registraba SIN `ref`, así que `runtime.trigger` lanzaba `SomaRuntimeTargetError` en silencio y el pack no sonaba nunca (cero `data-event-*` en el grupo frente a los de collapsible). El provider posee ahora el ref del `<ul>`. - eidos: en una fila navegable el chevron resolvía `inline-size: 100%` como flex-basis y ocupaba media fila (101 de 231 px en «Soma»), robándole clics al enlace. Toggle compacto con suelo de diana de 24 px (WCAG 2.5.8). - soma: una clave duplicada podía volver cíclico `parentByKey` y colgar la pestaña dentro de `trailKeys` (deriva en render) → clave sufijada + aviso del logger + guarda de ciclo en el paseo. - soma: el colapso es CONTEXTUAL (recuerda el `activeKey` bajo el que se hizo): cerrar la sección que lees se respeta, pero caduca al navegar DENTRO del grupo, para que la página actual nunca quede sin fila visible. Sigue siendo query pura, sin `$effect` que escriba estado. - soma: `child` recibe también `children` (el árbol renderizado); antes dejaba el landmark vacío, porque un árbol data-driven no lo puede reautorar el consumidor. - morfo + langs: el nombre accesible del chevron se declara en el contrato y se localiza («Alternar sección {label}»); ya no duplica el del enlace. - eidos: RTL completo — el glyph espeja solo (bordes lógicos), lo que no espeja es el giro, así que bajo `[dir='rtl']` las dos rotaciones se intercambian. El Gap «dirección del chevron en RTL» queda RESUELTO. - demo: paridad de snippet con los controles vivos; fuera el token fantasma `--nav-tree-rail-width` del docblock del recipe. badge en v1 (decisión del usuario, delegada) Está en el suelo de paridad del dossier §P5 (los 6 refs lo llevan). Se resuelve con SNIPPET, no con recursión a nivel de eidos: el morfo declara la parte `badge`, soma renderiza el snippet recibido (sin él, el valor crudo — sigue siendo headless) y el wrapper de eidos pasa el `Badge` canónico. Va DENTRO del control de la fila, así su texto entra en el nombre accesible («TSC, New, enlace»). `disabled` se descarta en v1 (fuera del suelo, y un enlace de navegación deshabilitado es semánticamente dudoso); ambos quedan registrados en la tabla de Gaps. Verificado: `component:audit` PASS · eidos-lint 26 morfo-backed / 0 invalid · `svelte-check` 0 errores en estos archivos · `vitest src/uix/eidos` 353/353 · navegador real (Playwright): trail auto-expandido, sema estampando en el grupo, teclado nativo, foco visible, claro y oscuro, RTL, 375 px sin desbordes, 0 errores de consola. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-22 — **F1.8 `nav-tree` HECHA** (cierre de la ruta: demo v2 de 9 tabs
con el mapa real de docs —43 nodos, 3 niveles—, verificación en navegador
REAL y review adversarial de 5 dimensiones × 3 verificadores escépticos,
22 hallazgos brutos → **10 confirmados**, 12 rechazados). Arreglado del
review: (1) **el emit de sema nunca ocurría** — la parte `group` es el
target de los eventos y se registraba SIN `ref`, así que `runtime.trigger`
lanzaba `SomaRuntimeTargetError` en silencio (lo cazó el navegador: cero
`data-event-*` en el grupo frente a los de collapsible); (2) **el chevron se
comía media fila** (`inline-size:100%` resolvía como flex-basis → 101px de
231 en «Soma»: pulsar el hueco tras la etiqueta plegaba en vez de navegar) →
toggle compacto de 24px con suelo de diana WCAG 2.5.8; (3) **ciclo potencial
en `parentByKey`** (clave duplicada = nodo padre de sí mismo → `trailKeys`
giraba para siempre y congelaba la pestaña) → clave sufijada + aviso del
logger + guarda de ciclo en el paseo; (4) **colapso ahora CONTEXTUAL**
(recuerda el `activeKey` bajo el que se hizo: cerrar la sección que lees se
respeta, pero caduca al navegar DENTRO — si no, la página actual se quedaba
sin fila visible y el trail auto-expandido quedaba anulado; sigue siendo
query pura); (5) **`child` ya no se comía el árbol** (recibe `children`
además de `props` — un árbol data-driven no lo puede reautorar el consumidor);
(6) **nombre accesible del chevron** declarado en morfo + localizado
(«Alternar sección {label}») en vez de duplicar el nombre del enlace;
(7) paridad de snippet en la demo + token fantasma `--nav-tree-rail-width`
fuera del docblock. **RTL completo** (el glyph se dibuja con bordes lógicos:
espeja solo; lo que no espeja es el GIRO → bajo `[dir='rtl']` las dos
rotaciones se intercambian) — el Gap «dirección del chevron en RTL» queda
RESUELTO, no diferido. **Decisión de usuario (delegada)**: `badge` SÍ entra
en v1 (está en el suelo del dossier §P5, los 6 refs lo llevan) resuelto con
**snippet**, no con recursión de eidos — soma declara la parte `badge` y
renderiza el snippet recibido (sin él, el valor crudo: sigue headless), el
wrapper de eidos pasa el `Badge` canónico; va DENTRO del control de la fila
para que su texto entre en el nombre accesible («TSC, New, enlace»).
`disabled` **también entra**, por decisión explícita del usuario tras
presentarle el criterio: el nodo conserva la fila pero pierde el `href` (no
hay nada que activar con Enter, clic ni «abrir en pestaña nueva» — un
`pointer-events:none` de CSS solo tapa el ratón) y añade `aria-disabled` +
`data-disabled`; el toggle del grupo NUNCA se deshabilita (disclosure es
control de vista, no destino) y un nodo sin `href` lo ignora.
uix(nav-tree): F1.8 CERRADA · demo + navegador + review adversarial Cierra el árbol de navegación data-driven (E-1) de F1: demo canónica de 9 pestañas con el mapa real de docs (43 nodos, 3 niveles), verificación en navegador real y review adversarial (5 dimensiones × 3 verificadores escépticos; 22 hallazgos brutos, 10 confirmados) con todos los confirmados arreglados. Arreglos del review - sema: la parte `group` —target de los eventos emerge— se registraba SIN `ref`, así que `runtime.trigger` lanzaba `SomaRuntimeTargetError` en silencio y el pack no sonaba nunca (cero `data-event-*` en el grupo frente a los de collapsible). El provider posee ahora el ref del `<ul>`. - eidos: en una fila navegable el chevron resolvía `inline-size: 100%` como flex-basis y ocupaba media fila (101 de 231 px en «Soma»), robándole clics al enlace. Toggle compacto con suelo de diana de 24 px (WCAG 2.5.8). - soma: una clave duplicada podía volver cíclico `parentByKey` y colgar la pestaña dentro de `trailKeys` (deriva en render) → clave sufijada + aviso del logger + guarda de ciclo en el paseo. - soma: el colapso es CONTEXTUAL (recuerda el `activeKey` bajo el que se hizo): cerrar la sección que lees se respeta, pero caduca al navegar DENTRO del grupo, para que la página actual nunca quede sin fila visible. Sigue siendo query pura, sin `$effect` que escriba estado. - soma: `child` recibe también `children` (el árbol renderizado); antes dejaba el landmark vacío, porque un árbol data-driven no lo puede reautorar el consumidor. - morfo + langs: el nombre accesible del chevron se declara en el contrato y se localiza («Alternar sección {label}»); ya no duplica el del enlace. - eidos: RTL completo — el glyph espeja solo (bordes lógicos), lo que no espeja es el giro, así que bajo `[dir='rtl']` las dos rotaciones se intercambian. El Gap «dirección del chevron en RTL» queda RESUELTO. - demo: paridad de snippet con los controles vivos; fuera el token fantasma `--nav-tree-rail-width` del docblock del recipe. badge en v1 (decisión del usuario, delegada) Está en el suelo de paridad del dossier §P5 (los 6 refs lo llevan). Se resuelve con SNIPPET, no con recursión a nivel de eidos: el morfo declara la parte `badge`, soma renderiza el snippet recibido (sin él, el valor crudo — sigue siendo headless) y el wrapper de eidos pasa el `Badge` canónico. Va DENTRO del control de la fila, así su texto entra en el nombre accesible («TSC, New, enlace»). `disabled` se descarta en v1 (fuera del suelo, y un enlace de navegación deshabilitado es semánticamente dudoso); ambos quedan registrados en la tabla de Gaps. Verificado: `component:audit` PASS · eidos-lint 26 morfo-backed / 0 invalid · `svelte-check` 0 errores en estos archivos · `vitest src/uix/eidos` 353/353 · navegador real (Playwright): trail auto-expandido, sema estampando en el grupo, teclado nativo, foco visible, claro y oscuro, RTL, 375 px sin desbordes, 0 errores de consola. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
Verificado: `component:audit` **PASS** · eidos-lint **26 morfo-backed / 0
invalid** · `svelte-check` 0 errores en mis archivos · `vitest src/uix/eidos`
353/353 · navegador real (Playwright, el pane suspendido NO sirve: congela
rAF y el scroll-into-view parecía roto) → trail auto-expandido, sema
`emerge-expand/collapse` estampando en `group`, teclado nativo (Tab/Enter/
Espacio), foco visible, claro+oscuro, RTL, 375px sin desbordes, 0 errores de
consola. **F1 = 7/8.** Resta: sidebar (F1.7).
- 2026-07-22 — **F1.7 `sidebar` — fase 0 + morfo + soma** (commit `2bc5fe6bd`).
Fase 0 presentada al usuario ANTES de construir, con 4 decisiones firmadas:
**17 partes** (13 estructurales + `menu-action` · `menu-badge` · `menu-sub` ·
`menu-sub-button`), **submenú FLOTANTE en modo icono ya en v1** (superación:
shadcn los oculta y deja ramas inalcanzables), **sema
`emerge-expand/collapse`** sobre `panel`, y **ritmo por fases con parada tras
morfo+soma**. Contrato: DOS ejes (`data-state` + `data-collapsible` SIEMPRE
estampado) + `data-side` + `data-mobile`; `panel` = `<aside>` nombrado y
`content` = `<nav>` nombrado con header/footer FUERA (ninguna ref trae
landmark); `rail` = `<button>` real en el orden de tabulación (shadcn:
`tabIndex=-1`); `aria-current="page"` del MISMO prop que `data-active`;
`menu-badge` dentro del control de la fila; `separator`/`input` NO son partes
(se componen `Separator` y `Field`). Soma: costuras
`open`/`defaultOpen`/`onOpenChange`/`toggle()` desde v1 (sin ellas la
persistencia y el atajo de app-land serían rediseño posterior), `mobile` del
breakpoint del SISTEMA (`uix.dom.isAtLeast`) y no de un `matchMedia` propio,
submenú flotante sobre `createFloatingShellRoot` con Escape que devuelve el
foco, e ids per-instancia para los dos `partRef` que son last-write-wins
(precedente `navigation-menu`). Verde: `morfo:check` carga el contrato ·
`component:audit --only sidebar` PASS 0E/0W · `svelte-check` 0 errores
propios · `contracts.test` solo los 2 fallos ajenos. **Siguiente**: eidos
(raíl, anchos tokenizados, modo icono, Drawer móvil) + demo + review.
- 2026-07-23 — **F1.7 `sidebar` HECHA · F1 CERRADA (8/8)**. Eidos + demo +
navegador + review adversarial (commits `2bc5fe6bd` morfo+soma ·
`21bbf9f50` eidos+demo · `293593307` alto completo · `dcce75cc2` foco del
flyout · `245c22471` controles de demo · `75e1fe32a` review). El review
(6 dimensiones × 3 verificadores, 135 agentes) dio **43 brutos → 31
confirmados**, todos arreglados. Los gordos: (1) `collapsible='none'`
dejaba el sidebar INALCANZABLE en móvil (toggle inerte + raíl oculto +
drawer sin trigger) → la inercia del modo se limita al escritorio; (2) ese
mismo modo hacía MENTIR a `data-state`/`aria-expanded` sobre un panel
visible → el modo pinta el estado; (3) el flyout de modo icono solo cerraba
desde el propio flyout → puntero, foco y Escape se resuelven ahora en el
`menu-item`, que contiene fila y sub, y el flag se limpia al cambiar de
presentación; (4) las filas del raíl no tenían NOMBRE (un tooltip solo
describe) → el texto de `tooltip` pasa también a `aria-label`; (5) el panel
off-canvas colapsado seguía en el tab order → `visibility: hidden` con la
transición retrasada; (6) el signo del off-canvas era físico → en RTL
barría el panel por encima de la página; (7) el flyout era la única
superficie flotante sin `z-index`. Verificado en navegador real todo lo
anterior. Gates: audit PASS 0E/0W · eidos-lint 47 morfo-backed / 0 invalid ·
svelte-check 0 errores propios · vitest src/uix/eidos 353/353. **F1 = 8/8:
la fase queda CERRADA.** Siguiente: F2 (blocks de sitio, 14).
uix(sidebar): un token de ESPACIO no es la altura de un control, y el raíl de iconos no es una constante El componente vivía fuera del canon de talla: la fila clavaba `min-block-size: var(--space-8)` —un token de la escala de ESPACIO haciendo de altura de CONTROL, que es otra escala de densidad— y `font-size: var(--font-size-sm)` escrito en el recipe, donde el guard del bundle no lo ve. El rótulo clavaba `xs`, el trigger `sm`, el badge `xs`, y la acción no componía `Button`: se quedaba con los 13,33px del user-agent y sin acuse perceptivo propio. Ahora el sidebar tiene eje `size` (xs..lg, default `md`, `data-size` en el WRAPPER —nunca en el morfo, o la resolución de soma lo pisaría con undefined—) y cada coordenada de la fila sale del bundle `--size-{k}-*`. Medido: 26/12 · 30/14 · 36/16 · 44/20, con el glifo siguiendo (14/16/18/20) por `--icon-size`, que es la costura que ya usa `Button` porque un `<Icon>` pinta su tamaño INLINE y gana a cualquier regla de hoja. El raíl de iconos deja de ser `3rem`: se DERIVA (fila + 2×padding), así que da 42 · 46 · 52 · 60 y a `md` ya no recorta la fila que tiene que contener. Tres composiciones más, por la regla que ya existía (container→part capada en `md`, precedente `Dialog.Close`): el `Trigger` deriva su talla, la `MenuAction` pasa a ser `IconButton ghost` —y con ella llegan su cromo, su anillo, su capa de estado y su `contact-activate`—, y el `MenuBadge` deriva con UNA excepción firmada: baja un paso, porque con la regla al pie el chip sale a 36/16 junto a una fila de 36/16 e iguala al control en vez de anotarlo. El hover deja de ser un `--color-surface-raised` a mano y pasa a la capa canónica `--state-hover`. Al medirlo salió un defecto que no buscaba: la fila activa no daba NINGUNA respuesta al hover, porque su acento usaba el shorthand `background`, que resetea el longhand `background-image` donde vive la capa. Con `background-color` la capa compone encima, que es justo para lo que existe. Verificado con Playwright headless (la pantalla del navegador estaba oculta y ahí rAF se congela: las medidas de layout salían falsas). Gates: eidos-lint 49 morfo-backed / 0 invalid · component:audit PASS 0E/0W · vitest src/uix/eidos 434/435 (el rojo es `skin-media-player`, ajeno, idéntico en base) · rtl:check 0 · docs:check 0/639 · `check` con delta CERO contra la base medida con stash (72/72). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- 2026-08-19 — **`sidebar` reabierto y corregido en su propio eje**, fuente
[`PLAN-sidebar.md`](./PLAN-sidebar.md): la fila era MUDA (fase 1 lo decidió
en silencio sobre la premisa falsa «item = Link puro») y el componente vivía
fuera del canon de talla. Ahora declara el par del libro (`contact-activate`
anclado en la fila + `shift-navigate` en el shell), el flyout de modo icono
tiene sus `emerge-*-sub` (SILENT), y el eje `size` (xs..lg, default `md`)
lleva la fila al bundle `--size-{k}-*` con el raíl de iconos DERIVADO. Cierra
también la fila de Gaps del `app-shell` («`Sidebar.MenuButton` es MUDO y
uix(navigation-menu): navegar no fija nada, y el megamenú que se abre solo no debería sonar T-1 de PLAN-sidebar.md §6. El `Link` emitía `commit-select + affirm` al navegar. El libro lo nombra: TABLA 9.3, «Success de navegación — cambio de contexto con logro», corregido como «shift + commit.fulfill si procede»; cap. 27 §2 «shift no es commit… ir a otra pantalla no implica que la operación haya terminado»; y la apertura de cap. 10, «una navegación no es exitosa por cambiar de pantalla». Nada queda fijado al pulsar un enlace del nav: `aria-current="page"` es la huella que deja el router DESPUÉS, no un estado que la parte fije. Ahora habla el par de cap. 22 §9 —«Link → contact.press seguido de shift.navigate: la presión no es la navegación»—, el mismo que la fila del Sidebar declaró ayer: - `contact-activate` ANCLADO en el enlace pulsado (parte repetida → `partInstance`). - `shift-navigate` en el `provider`, sin anclar: `data-event-*` es UNA ranura por elemento, y el sujeto del cruce es el `<nav>` que cruza como unidad, no el control que lo inicia. Sólo si el `<a>` tiene `href` — aquí no es un opt, viaja en restProps, así que se lee del elemento que la parte ya tenía atado. Esto REABRE la decisión del 2026-08-05 (ledger A-54), no la pisa: mover el evento fuera del marco estaba bien; la familia era el defecto. El pack estuvo a punto de borrarse, y esa habría sido la equivocación de la sesión. Al quitarle la regla del commit se quedaba con dos selectores vacíos y propuse retirarlo como ruido. El autor paró y pidió releer: la pregunta no era qué escribe el fichero, sino qué debe decir la aparición del megamenú. Se abre por HOVER, y medido con el default de familia un barrido del puntero por dos triggers y salir de la barra emitía `open` · `open` · `close` — tres sonidos, 6 osciladores, sin un solo clic. Cap. 17 §3 («emerge — normalmente no necesita sonido»), cap. 26 §7, cap. 32 §10-§11, cap. 5 §11. Así que `emerge-open` y `emerge-close` son SILENT, la postura que `tooltip` y el flyout del `sidebar` (D-SB.3) ya tienen para el mismo gesto. El pack DIFIERE del default y vuelve a ser un pack. Enmienda D.6 de book-deviations.md (PROJECT_CANON): `navigation-menu` sale del alcance del `commit.subtle`, y quedan corregidas tres cláusulas suyas caducadas — el `emerge-open/close` declarado desde el 2026-08-05, el `commit.subtle` que murió en 86fa9a1b7 sin sustituto, y el `emerge.soft` (0.08) que el catálogo absorbió (la base `open` es 0.09). `menubar` tiene la misma clase de defecto en su trigger y NO se toca: otro componente, otra sesión. Medido en Chromium headless (la pantalla oculta congela rAF; sonda desde la raíz): - clic en enlace → `contact-activate` (familia contact, sin intent) sobre el `[data-navigation-menu-link]` con `press-squeeze` corriendo EN el enlace a +8 y +30 ms y retirado a +120 · `shift-navigate` sobre `[data-navigation-menu]` · 4 osciladores · cero estampados de familia `commit`. - Enter sobre el enlace enfocado: idéntico (`handleListKeydown` no intercepta Enter; el ancla nativa dispara el click). Sin `href`: sólo contacto, 2 osc. - RTL: mismo par, sin `data-event-direction` — un `href` no tiene sentido propio. - El SILENT demostrado por diferencial con la MISMA maquinaria: el barrido sigue estampando `emerge-open`/`emerge-close` y baja de 6 osciladores a 0. - Enlace pulsado con el panel abierto: cuatro ocurrencias en tres nodos distintos (open@trigger · contact@link · shift@nav · close@trigger), 4 osciladores — las dos silenciosas no compiten con el par del enlace. La demo clavaba las cuatro previsualizaciones en el `<li>`: con eventos en tres partes, el ▶ del contacto habría encogido el item entero. Ahora resuelve la parte del contrato compilado (mismo arreglo que la revisión adversarial del sidebar). Medido: contacto→link, shift→nav, emerge→trigger. El README de soma describía el estado ANTERIOR al 2026-08-05 (target `item`, emisión en `openNow`, cascada por `closest()`): reescrito entero. Gates: check 72 errores con CERO en los ficheros tocados (el de nav-menu que queda vive en eidos/navigation-menu-content.svelte, `sideOffset`, intacto) · suite nav 8/8, cuatro contratos nuevos · sema 327/327 · morfo:check exit 0 · morfo:vocabulary 0 (el WARN es de cropper) · component:audit --only navigation-menu PASS · eidos-lint 32 morfo-backed / 0 invalid · docs:check 0/640 · rtl:check 0 · blocks:check 0/19. Queda ABIERTO y no es de este eje: `emerge-open` estampa en el TRIGGER y le corre `present-rise` encima —el botón «aparece» como si fuera él la superficie—, y la receta pinta abierto/seleccionado con `background` plano sobre un radio fijo. Los dos son anteriores a este commit y piden su propio eje (la doctrina ya tiene la forma: `targetFallback`, resolución por estado de montaje). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`NavigationMenu.Link` no»).
- 2026-08-19 — **T-1 EJECUTADA**, y con ella la fila de Gaps queda cerrada por
los dos lados: `navigation-menu` ya no emite `commit-select + affirm` al
navegar — el antipatrón «Success de navegación» (TABLA 9.3 del libro, no
cap. 10 §12 como decía esta línea) — sino el mismo par que la fila del raíl,
`contact-activate` anclado en el enlace y `shift-navigate` en el `<nav>`.
Enmienda la desviación PROJECT_CANON **D.6**, que prescribía `commit.subtle`
para este componente. Medido en Chromium.
docs(blocks): registro de F2.1, doctrina de demo del tier y handoff Documentación al día de lo que se cerró hoy y handoff para retomar mañana. - `PLAN-blocks.md` §7: **F2.1 `site-header` HECHA** con sus siete commits, la fase 0 contra el dossier §P1, el landmark que le faltaba a `NavigationMenu` (arreglado en el canon, no parcheado en el block), el hueco del CTA que navega y parece botón (registrado, no falseado) y el defecto de framework que destapó la demo. - `theming/changelog.md` §46: las 44 variables de cascada de `Box` dejan de heredarse. Un `Section` regalaba su padding a cada descendiente —la galería arrastraba ~300px de aire desde F0— y los hijos heredaban anchos y `display` ajenos. `@property { inherits: false }`, radio verificado sin regresiones. Lección: una variable que un componente escribe para SÍ MISMO debe declararse `inherits: false`; si no, deja de ser un prop y se vuelve un contagio. - `architecture/blocks.md` B-9: la demo de un block se construye sobre el harness compartido y el block se enseña A SANGRE — nunca dentro de un marco con relleno ni de una caja con scroll, porque eso cambia lo que el block hace. - `src/uix/blocks/README.md`: anatomía de la demo (harness, `{Name}Site`, ruta `preview`, `DocRow`, catálogo único, ejes en el shell). - `CONTINUE-blocks.md` (nuevo): handoff — qué toca (F2.2 `hero`), la plantilla de ficheros para copiar, las reglas que ya costaron sangre (a sangre, iframe solo para anchos de dispositivo, cada prop un control, nada de backticks en `<Text>`, ojo con las variables que heredan), la deuda declarada que es decisión del usuario y el estado exacto de los gates. Gates al parar: `blocks:check` verde · `svelte-check` 73 errores, todos deuda ajena (0 propios) · `vitest src/uix/eidos` 353/353 · `contracts.test` con los 3 fallos ajenos conocidos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-23 — **F2.1 `site-header` HECHA · primer block del tier** (commits
`3cfc030b3` block+landmark · `016658a0c` controles · `f98c844ed` fix de Box ·
`50574806d` harness · `33328df7a` cromo de sección · `d574982ab` escenario a
ras · `ae7b4f8c3` vista previa honesta). Composición pura: `Sticky` (solo si
`sticky`, sin wrapper inútil) + `Container` + `Group` + `Box` (el interruptor
ancho/estrecho por `display` responsive, B-6) + `Drawer`; marca, nav,
acciones y navegación móvil entran como snippets del app (B-7: cero cadenas
propias). Fase 0 contra el dossier §P1: adoptado el FLYOUT (la brecha de
paridad) sin reimplementarlo — el app pasa un `NavigationMenu`.
**Encontrado al componer, arreglado en el canon**: `NavigationMenu` no emitía
landmark (su morfo declara `defaultElement: 'nav'` y soma pintaba un `div`).
feat(button): el CTA que navega y parece botón, por composición (opción D) El hueco que registró `site-header`: un «Empezar gratis» de cabecera tiene que NAVEGAR y parecer botón. `Button` no crece un `href` —`Link` posee la navegación, y un ancla se activa con Enter, no con espacio, lo cual es correcto—: presta la pintura por composición. Lo que hacía de esa forma un downgrade era que el `child` (asChild) descartaba la decoración; se completa el slot. - **eidos `<Button>`**: el `child` recibe ahora `content`, el cuerpo YA decorado (icono · etiqueta · endIcon · spinner) en su propio snippet que comparten las dos ramas de render. Así `<a href {...props}>{@render content()}</a>` conserva TODOS los slots en vez de sustituirlos (antes la flecha del sitio alpha estaba escrita a mano). Nuevo tipo exportado `ButtonChildProps`. - **soma / morfo**: en la forma `child` el elemento es del consumidor, así que soma deja de estampar `type` (un `<a type="button">` es una pista de MIME falsa). El componente pasa `type: undefined` cuando hay `child`; el provider lo REENVÍA verbatim (antes lo re-defaulteaba a `'button'` y pisaba el drop — el default vive en el destructure del componente); el morfo declara el attr `type` condicional (`prop-truthy`). - **docs**: ejemplo rancio de `index.ts` corregido (anunciaba un `asChild`/ `variant="link"` que no existen); sección «CTA que navega» en el README de eidos con el patrón y el footgun documentado (un `<button>` en un `<form>` vía `child` se pone su propio `type`); nota en el README de soma. - **site-header**: el CTA de la demo usa ya la forma real (`<a>` sólido con flecha), y el hueco pasa de «candidato a canon» a CERRADO por composición — `Button` sigue sin `href` y `Link` sigue poseyendo la navegación, las dos decisiones firmadas se mantienen. Actualizados PLAN/CONTINUE-blocks. - **demo de Button**: control `child (asChild → <a>)` vivo, snippet del código y fila de a11y explicando por qué el ancla activa solo con Enter. Verificado en navegador (dev, restart para módulos frescos): asChild ON → `<a href="#pricing">` sin `type`, pintura sólida completa (bg primary, tinta blanca, 36px, padding 16px), y el slot de icono SOBREVIVE dentro del ancla (`[data-button-icon]` + svg + body); asChild OFF → `<button type="button">` intacto (sin regresión de submit implícito); el CTA real de `site-header` sale `<a>` con la flecha final y 0 errores de consola. `blocks:check` verde · `vitest src/uix/morfo` 114/114 · `svelte-check` sin errores propios nuevos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
**Hueco registrado, no falseado → CERRADO el mismo día**: el CTA que NAVEGA
y parece botón. Se sirve por COMPOSICIÓN, no por prop nueva: el `child`
(asChild) de `Button` entrega ahora un snippet `content` con el cuerpo ya
decorado, así que `<a href {...props}>{@render content()}</a>` recibe la
pintura sólida completa y conserva icono/etiqueta/spinner (antes el `child`
los sustituía: por eso la flecha del sitio alpha estaba escrita a mano); y
soma deja de estampar `type` en un elemento que no es suyo (`<a
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
type="button">` es una pista de MIME falsa) — el morfo lo declara
feat(button): el CTA que navega y parece botón, por composición (opción D) El hueco que registró `site-header`: un «Empezar gratis» de cabecera tiene que NAVEGAR y parecer botón. `Button` no crece un `href` —`Link` posee la navegación, y un ancla se activa con Enter, no con espacio, lo cual es correcto—: presta la pintura por composición. Lo que hacía de esa forma un downgrade era que el `child` (asChild) descartaba la decoración; se completa el slot. - **eidos `<Button>`**: el `child` recibe ahora `content`, el cuerpo YA decorado (icono · etiqueta · endIcon · spinner) en su propio snippet que comparten las dos ramas de render. Así `<a href {...props}>{@render content()}</a>` conserva TODOS los slots en vez de sustituirlos (antes la flecha del sitio alpha estaba escrita a mano). Nuevo tipo exportado `ButtonChildProps`. - **soma / morfo**: en la forma `child` el elemento es del consumidor, así que soma deja de estampar `type` (un `<a type="button">` es una pista de MIME falsa). El componente pasa `type: undefined` cuando hay `child`; el provider lo REENVÍA verbatim (antes lo re-defaulteaba a `'button'` y pisaba el drop — el default vive en el destructure del componente); el morfo declara el attr `type` condicional (`prop-truthy`). - **docs**: ejemplo rancio de `index.ts` corregido (anunciaba un `asChild`/ `variant="link"` que no existen); sección «CTA que navega» en el README de eidos con el patrón y el footgun documentado (un `<button>` en un `<form>` vía `child` se pone su propio `type`); nota en el README de soma. - **site-header**: el CTA de la demo usa ya la forma real (`<a>` sólido con flecha), y el hueco pasa de «candidato a canon» a CERRADO por composición — `Button` sigue sin `href` y `Link` sigue poseyendo la navegación, las dos decisiones firmadas se mantienen. Actualizados PLAN/CONTINUE-blocks. - **demo de Button**: control `child (asChild → <a>)` vivo, snippet del código y fila de a11y explicando por qué el ancla activa solo con Enter. Verificado en navegador (dev, restart para módulos frescos): asChild ON → `<a href="#pricing">` sin `type`, pintura sólida completa (bg primary, tinta blanca, 36px, padding 16px), y el slot de icono SOBREVIVE dentro del ancla (`[data-button-icon]` + svg + body); asChild OFF → `<button type="button">` intacto (sin regresión de submit implícito); el CTA real de `site-header` sale `<a>` con la flecha final y 0 errores de consola. `blocks:check` verde · `vitest src/uix/morfo` 114/114 · `svelte-check` sin errores propios nuevos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
condicional. `Button` sigue sin `href` y `Link` sigue poseyendo la
navegación: las dos decisiones firmadas se mantienen. Doctrina en el README
de eidos Button §«CTA que navega»; en uso en la demo del block.
docs(blocks): registro de F2.1, doctrina de demo del tier y handoff Documentación al día de lo que se cerró hoy y handoff para retomar mañana. - `PLAN-blocks.md` §7: **F2.1 `site-header` HECHA** con sus siete commits, la fase 0 contra el dossier §P1, el landmark que le faltaba a `NavigationMenu` (arreglado en el canon, no parcheado en el block), el hueco del CTA que navega y parece botón (registrado, no falseado) y el defecto de framework que destapó la demo. - `theming/changelog.md` §46: las 44 variables de cascada de `Box` dejan de heredarse. Un `Section` regalaba su padding a cada descendiente —la galería arrastraba ~300px de aire desde F0— y los hijos heredaban anchos y `display` ajenos. `@property { inherits: false }`, radio verificado sin regresiones. Lección: una variable que un componente escribe para SÍ MISMO debe declararse `inherits: false`; si no, deja de ser un prop y se vuelve un contagio. - `architecture/blocks.md` B-9: la demo de un block se construye sobre el harness compartido y el block se enseña A SANGRE — nunca dentro de un marco con relleno ni de una caja con scroll, porque eso cambia lo que el block hace. - `src/uix/blocks/README.md`: anatomía de la demo (harness, `{Name}Site`, ruta `preview`, `DocRow`, catálogo único, ejes en el shell). - `CONTINUE-blocks.md` (nuevo): handoff — qué toca (F2.2 `hero`), la plantilla de ficheros para copiar, las reglas que ya costaron sangre (a sangre, iframe solo para anchos de dispositivo, cada prop un control, nada de backticks en `<Text>`, ojo con las variables que heredan), la deuda declarada que es decisión del usuario y el estado exacto de los gates. Gates al parar: `blocks:check` verde · `svelte-check` 73 errores, todos deuda ajena (0 propios) · `vitest src/uix/eidos` 353/353 · `contracts.test` con los 3 fallos ajenos conocidos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
**Defecto de framework destapado por la demo**: las 44 variables de cascada
de `Box` heredaban, así que un `Section` regalaba su padding a cada
descendiente (la galería arrastraba ~300px de aire desde F0) →
`@property { inherits: false }`, radio verificado sin regresiones
(`docs/theming/changelog.md` §46).
**Superficie de demo del tier (doctrina nueva, B-9 ampliada)**: harness
compartido en `web/routes/blocks/_lib/` + cromo de sección con los ejes que
mueven el framework (tema, idioma, dirección, densidad) + catálogo único.
El block se enseña **a sangre en la página**, nunca dentro de un marco con
relleno ni de una caja con scroll: medido, un `Card` desplazaba 21px un
header con `offset: 0`, y un `sticky` dentro de un div con scroll es un
comportamiento que nadie vive. Los anchos de dispositivo se sirven desde una
ruta `preview` propia (iframe opt-in: en dev, dos documentos sin empaquetar
agotan las conexiones del navegador).
**Siguiente**: F2.2 `hero` — handoff en `docs/process/CONTINUE-blocks.md`.
feat(blocks): F2.2 `hero` — center · split · background, por slots Segundo block del tier. API por SLOTS DE SNIPPET, no compound: la forma se razonó con el usuario y con el paisaje de referencia. Compound se reserva para partes que COORDINAN (estado/contexto/ARIA entre ellas — Accordion, Dialog); un hero son cinco slots de layout que no se hablan entre sí y que el root arregla, así que snippets. Además el campo entero shippea marketing como copy-paste plano —nadie aplica compound a una sección—, y los slots por zona son la historia de personalización que distingue al tier del copy-paste. `<Hero layout="center|split|background" level container size>` + `eyebrow/title/description/actions/media/background/children`. - El block **envuelve** título y subtítulo en `Heading`/`Text`: así posee el `id` que nombra el `<section aria-labelledby>` y el nivel del encabezado, mientras la app pone las palabras (B-7). `eyebrow`/`actions`/`media` son contenido libre (ahí los componentes del canon SON la API). - `center` = `Stack` centrado, media debajo (ancho-capado); `split` = `Grid` de dos columnas (una sola sin media), apila en estrecho. - **`background` (cover)** — añadido por scope-approval del usuario: la media a sangre detrás de la copy, con velo de contraste (`--color-overlay` a `--opacity-scrim`), texto `on-solid` y `object-fit: cover` vía un `<style>` justificado (D-BLK.2). Son las ÚNICAS reglas que el block posee, todas sobre tokens del ecosistema — cero color a mano. Capas por orden de fuente, sin `z-index`. Demo (`web/routes/blocks/hero/`): full-bleed en la página + ruta `preview` para anchos de dispositivo, cada prop un control vivo, y el backdrop del layout cover dogfooda el sistema de color — es un `Surface color="primary" gradient` (finish aurora = `--gradient-aurora`, derivado de los roles del tema; cambia con la paleta). Mini-site compartido por las dos superficies (`HeroSite.svelte`). Encontrado al componer, registrado no resuelto: `Box`/`Surface` `flex`/`grow` no hicieron crecer un hijo flex (bars a 0-width, `flex: 0 1 auto`; la prop no la usa ningún componente shipped). La demo usó `Grid` (tracks `1fr`). Flag en el README del block y en el handoff para revisar el cableado de `--box-flex`. Verificado en navegador (Playwright headless, módulos frescos): center/split/ background × claro/oscuro × LTR/RTL, media on/off, y el landmark nombrado (región con `aria-labelledby` que resuelve al título). `blocks:check` verde (2 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-23 — **F2.2 `hero` HECHA** (mismo día). API por **slots de snippet**
(no compound): `eyebrow`/`title`/`description`/`actions`/`media`/`background` +
`layout: 'center' | 'split' | 'background'` + `level`/`container`/`size`. La
decisión de forma se razonó con el usuario: compound se reserva para partes que
COORDINAN (Accordion, Dialog); un hero son slots de layout que el root arregla
→ snippets. El campo entero shippea marketing como copy-paste plano; nadie
aplica compound a una sección → los slots por zona SON la historia de
personalización del tier. El block **envuelve** título/subtítulo en
`Heading`/`Text` (posee el `id` del landmark y el nivel; la app pone las
palabras); `eyebrow`/`actions`/`media` son contenido libre. Landmark:
`<section aria-labelledby>` nombrado por el título (verificado: región con
nombre accesible). **Layout `background` (cover)** añadido por scope-approval
del usuario: media a sangre detrás, velo `--color-overlay` a `--opacity-scrim`,
texto `on-solid` y `object-fit: cover` vía `<style>` justificado (D-BLK.2) —
las ÚNICAS reglas que el block posee, todas sobre tokens del ecosistema, cero
color a mano. La demo dogfooda el sistema de color: el backdrop es un `Surface`
`color="primary" gradient` (finish aurora, `--gradient-aurora` derivado de los
roles del tema). **Registrado, no resuelto**: `Box`/`Surface` `flex`/`grow` no
hicieron crecer un hijo flex (bars a 0-width, `flex: 0 1 auto`; prop sin usar
en shipped) → la demo usó `Grid` (tracks `1fr`); flag para revisar el cableado
de `--box-flex`. Gates: `blocks:check` verde (2 blocks) · `svelte-check` sin
errores propios. **Siguiente**: F2.3 `feature-grid` (primer compound del tier:
`.Item` repetido — LA brecha #1 del dossier, split/alternante texto-screenshot).
feat(blocks): F2.3 `feature-grid` — el primer block compound del tier `<FeatureGrid>` + `.Header` + `.Items` + `.Item` + `.ItemIcon`/`.ItemTitle`/ `.ItemText`: una cabecera sobre una rejilla responsive de features (icono · título · texto). Es la sección que responde «qué hace», la nº1 tras el hero. Primer block COMPOUND del tier, y aplica la regla de forma que fijamos: el `.Item` se REPITE (el app mapea sobre N features) → gana sub-componentes, donde `hero`/`site-header` usan slots de snippet (partes fijas de layout). Las partes no coordinan —sin contexto ni estado entre ellas—: la rejilla es del padre, las celdas del app. `.Items` es el envoltorio honesto de la rejilla (`AutoGrid`), que deja la cabecera fuera sin un `grid-column: 1/-1` a pelo. - El block coloca (Section · Container · AutoGrid · Surface · Heading · Text); el app pone todo el contenido por children (B-7). - `.Items` fluido por `minChildWidth` (tantas columnas como quepan) o `columns` fijas; `.Item` `align` start/center; `.ItemIcon` chip `Surface`; `.ItemTitle` `Heading` h3; `.ItemText` `Text` apagado. - `align` de sección (center/start) coloca la cabecera coherente con las columnas. Dos cosas encontradas al construir, resueltas: - **Los sub-componentes de bloque extienden los props del componente canon que envuelven** (`BoxProps`, `StackProps`, `HeadingProps`…), **no `HTMLAttributes`**: el `style: string|null` del atributo HTML crudo choca con el `style: string` del canon al hacer spread (+ "union type too complex"). - **`.ItemIcon` por defecto `solid`, no `soft`**: el soft-primary en claro es casi blanco (oklch 0.99) → el chip era invisible; solid da el chip con glifo on-solid (la tinta de contraste la pone `Surface`). Demo (`web/routes/blocks/feature-grid/`): full-bleed + ruta `preview`, con control de columnas (fluido/2/3/4), align, nº de items y dir; 6 features con iconos del canon. Hueco a decisión del usuario (en los Gaps del block): el **feature-split/ alternante** (texto junto a un screenshot, lados alternos) — la brecha nº1 del dossier — es otra disposición (filas de 2 columnas, no rejilla de iconos): probablemente un block hermano `feature-split`. Presentado, no resuelto. Verificado en navegador (Playwright, módulos frescos): center/start × claro/ oscuro × LTR/RTL, columnas fluidas y fijas, 3/4/6 items, chips visibles. `blocks:check` verde (3 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-23 — **F2.3 `feature-grid` HECHA** (mismo día). **Primer block COMPOUND
del tier**: `<FeatureGrid>` + `.Header` + `.Items` + `.Item` + `.ItemIcon`/
`.ItemTitle`/`.ItemText`. Aplica la regla de forma: `.Item` se REPITE (el app
mapea sobre N features) → gana compound; las partes no coordinan (sin
contexto/estado entre ellas), la rejilla es del padre. `.Items` es el envoltorio
honesto de la rejilla (`AutoGrid`) — deja la cabecera fuera sin un
`grid-column: 1/-1` a pelo. El block coloca (Section·Container·AutoGrid·Surface·
Heading·Text), el app pone contenido (B-7). `align` de sección (center/start)
coloca la cabecera coherente con las columnas. **Sub-componentes de bloque
extienden los props del componente canon que envuelven, NO `HTMLAttributes`**:
el `style: string|null` del atributo HTML crudo choca con el `style: string` del
canon (+ "union too complex") al hacer spread. **ItemIcon default `solid`**: el
soft-primary en claro es casi blanco (oklch 0.99) → invisible; solid da chip con
glifo on-solid (la tinta de contraste la pone `Surface`). Demo: control de
columnas (fluido/2/3/4), align, nº items, dir; 6 features con iconos del canon.
Gates: `blocks:check` verde (3 blocks) · `svelte-check` sin errores propios.
**Hueco a decisión del usuario**: el **feature-split/alternante** (texto junto a
screenshot, lados alternos) — la brecha nº1 del dossier — es otra disposición
(filas de 2 columnas, no rejilla de iconos): probablemente block hermano
`feature-split`. Presentado como scope-approval, no resuelto aquí. **Siguiente**:
F2.4 `pricing`.
feat(blocks): F2.3b `feature-split` — la brecha nº1 del dossier, con Mockup La sección que las 5 refs shippean y que va justo tras el hero: una afirmación de producto junto a un screenshot, alternando lados fila a fila. Otra disposición que la rejilla de iconos de `feature-grid`, así que block hermano (decisión del usuario), no una variante turbia dentro de aquél. Compound —la `.Row` se repite—: `<FeatureSplit>` + `.Row` (`reversed`, slot `media`) + `.Eyebrow` + `.Title` + `.Text` + `.Features`/`.Feature` (checklist) + `.Actions`. El block coloca; la app pone la copy y la media. - `reversed` mueve la media al lado de inicio vía `grid-column` (Box expone `gridColumn`/`order`), dejando la copy SIEMPRE primera en el DOM — el orden de lectura y el foco no cambian aunque el screenshot salte de lado. - El check de cada `.Feature` es decorativo (`aria-hidden`): la palabra lleva el significado. - La media se COMPONE con el primitivo `Mockup`: la demo enseña cromo de navegador (dashboard) y de teléfono (app), cerrando el hueco de «tratamiento de media» del dossier de raíz en vez de falsearlo. `.Row` usa un tipo limpio (no `HTMLAttributes`) porque su slot `media` colisiona con el atributo HTML homónimo; el resto de sub-partes extienden los props del componente canon que envuelven (lección de feature-grid). Demo (`web/routes/blocks/feature-split/`): full-bleed + ruta `preview`, con control del nº de filas y del lado inicial; 3 filas con Mockup navegador/teléfono. Verificado en navegador (Playwright, módulos frescos): filas alternas en claro/oscuro × LTR/RTL, `reversed`, el orden de lectura copy-primero, y los dos cromos de Mockup. `blocks:check` verde (4 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-23 — **Primitivo canon `Mockup` + F2.3b `feature-split` HECHOS** (mismo
día; scope-approval del usuario: «split + primitivo de media reutilizable»).
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
La clave del dossier: _la brecha del hero/features es el TRATAMIENTO DE MEDIA,
no el conteo de layouts_. En vez de falsear un screenshot por demo, se cierra
feat(blocks): F2.3b `feature-split` — la brecha nº1 del dossier, con Mockup La sección que las 5 refs shippean y que va justo tras el hero: una afirmación de producto junto a un screenshot, alternando lados fila a fila. Otra disposición que la rejilla de iconos de `feature-grid`, así que block hermano (decisión del usuario), no una variante turbia dentro de aquél. Compound —la `.Row` se repite—: `<FeatureSplit>` + `.Row` (`reversed`, slot `media`) + `.Eyebrow` + `.Title` + `.Text` + `.Features`/`.Feature` (checklist) + `.Actions`. El block coloca; la app pone la copy y la media. - `reversed` mueve la media al lado de inicio vía `grid-column` (Box expone `gridColumn`/`order`), dejando la copy SIEMPRE primera en el DOM — el orden de lectura y el foco no cambian aunque el screenshot salte de lado. - El check de cada `.Feature` es decorativo (`aria-hidden`): la palabra lleva el significado. - La media se COMPONE con el primitivo `Mockup`: la demo enseña cromo de navegador (dashboard) y de teléfono (app), cerrando el hueco de «tratamiento de media» del dossier de raíz en vez de falsearlo. `.Row` usa un tipo limpio (no `HTMLAttributes`) porque su slot `media` colisiona con el atributo HTML homónimo; el resto de sub-partes extienden los props del componente canon que envuelven (lección de feature-grid). Demo (`web/routes/blocks/feature-split/`): full-bleed + ruta `preview`, con control del nº de filas y del lado inicial; 3 filas con Mockup navegador/teléfono. Verificado en navegador (Playwright, módulos frescos): filas alternas en claro/oscuro × LTR/RTL, `reversed`, el orden de lectura copy-primero, y los dos cromos de Mockup. `blocks:check` verde (4 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
de raíz con un componente del canon.
- **`Mockup`** (`src/uix/eidos/components/mockup/` + morfo eidos-only, 0-event,
como `aspect-ratio`): enmarca media en cromo de dispositivo —
`chrome: 'plain' | 'browser' | 'phone'` + `url` para la barra. Recipe dibuja
la barra (dots + pill de URL), el bisel del teléfono y el notch; todo sobre
tokens (`--radius-*`, `--color-border-*`, `--shadow-*`, `--primitive-*`).
eidos-lint: 0 invalid / 0 class-hooks (mismo patrón que aspect-ratio, todo
eidos-only). Retrofiteado en la demo del `hero` (dogfood).
- **`feature-split`** (compound: `.Row` repite): `<FeatureSplit>` + `.Row`
(`reversed`, slot `media`) + `.Eyebrow`/`.Title`/`.Text`/`.Features`/
`.Feature`/`.Actions`. `reversed` mueve la media al inicio vía
`grid-column` (Box tiene `gridColumn`/`order`), dejando la copy PRIMERA en
el DOM (orden de lectura intacto). El check de `.Feature` es decorativo
(aria-hidden). La media se compone con `Mockup`.
- **BUG de `hero` arreglado de paso** (lo tapaba mi filtro de svelte-check con
backslashes mal escapados): los snippets `title` y `background` colisionaban
con los atributos HTML homónimos (`title?: string` / `background`) →
`string & Snippet`. Fix: `Omit<HTMLAttributes, 'children' | 'title'>` +
renombrar el slot `background`→`backdrop`. Lección para blocks: **un slot de
snippet cuyo nombre sea un atributo HTML necesita Omit o un nombre distinto**.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Gates: `blocks:check` verde (4 blocks) · `svelte-check` sin errores propios
(73 = deuda ajena) · eidos lint.test + morfo 131/131. **Siguiente**: F2.4
`pricing`.
feat(blocks): F2.4 `pricing` — el primer compound CON CONTEXTO del tier La sección de planes: un toggle de periodo sobre una fila de planes, uno destacado. Es el primer block cuyas partes se COORDINAN de verdad (no solo se repiten como en feature-grid/split): el `Switch` escribe el periodo de facturación y cada `PlanPrice` lo lee. Esa coordinación es lo que gana un contexto compartido — la forma más fuerte de compound. `<Pricing bind:period>` + `.Header` + `.Switch` + `.Plans` + `.Plan`(featured, badge) + `.PlanName`/`.PlanDescription`/`.PlanPrice`/`.PlanFeatures`/ `.PlanFeature`/`.PlanAction`. - **Contexto reactivo** (`context.ts`): la raíz provee el periodo como getter sobre un `$bindable`; el `Switch` (un `ToggleGroup`) lo escribe, el `PlanPrice` lo lee y muestra el snippet `monthly` o `annual`. Mismo patrón que `CardGroup`. `period` es bindable por si la app quiere observarlo. - **El block NUNCA formatea moneda**: la app compone `FormatNumber` dentro de los snippets de precio (B-7). El block posee el switch, no el dinero. - `.PlanAction` fija el CTA al borde inferior de la tarjeta (`margin-block-start: auto`) para que una fila de planes alinee sus botones aunque tengan distinto nº de features; `.Plan` con `align="start"` deja los checks en columna limpia; `featured` da acento (borde primary) + elevación. Demo (`web/routes/blocks/pricing/`): full-bleed + ruta `preview`, tres planes (Pro destacado en el centro) con el toggle mensual/anual vivo. Hueco a decisión del usuario (Gaps del block): la **tabla de comparación** (features × planes) — la brecha recurrente del dossier en pricing. Es una tabla, no una fila de tarjetas: candidato a hermano `pricing-table`. Presentado, no resuelto. Verificado en navegador: el toggle cambia los TRES precios a la vez (0/29/99 → 0/23/79), tarjetas de igual alto con CTAs alineados, featured con acento, en claro/oscuro × LTR/RTL. `blocks:check` verde (5 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-24 — **Fix de `Grid` + F2.4 `pricing` HECHOS**.
- **Fix canon `Grid`** (theming/changelog §47): `align`/`justify`/`alignContent`
NO aplicaban — los atajos `place-items`/`place-content` con fallback
`revert-layer` (cuando su prop no está puesto) revertían los longhands a su
inicial, pisándolos. Ahora el fallback COMPONE los vars de los longhands. 353
tests eidos verdes, grids por defecto sin cambio. Encontrado alineando
`feature-split`.
- **`pricing`** (compound **CON CONTEXTO** — el primero del tier cuyas partes se
COORDINAN, no solo se repiten): `<Pricing bind:period>` + `.Header` +
`.Switch` + `.Plans` + `.Plan`(featured, badge) + `.PlanName`/`.PlanDescription`/
`.PlanPrice`/`.PlanFeatures`/`.PlanFeature`/`.PlanAction`. El `Switch`
(`ToggleGroup`) escribe el periodo en un contexto reactivo (`context.ts`,
getter sobre el `$bindable`) y cada `PlanPrice` lo lee → muestra el snippet
`monthly`/`annual`. **El block NO formatea moneda**: la app compone
`FormatNumber` en los snippets (B-7). `.PlanAction` fija el CTA al borde
inferior (`margin-block-start: auto`) para alinear los botones; `.Plan` con
`align="start"` deja los checks en columna. Verificado: el toggle cambia los 3
precios a la vez (0/29/99 → 0/23/79), featured con acento+elevación, claro/
oscuro/RTL. **Hueco a decisión del usuario**: tabla de comparación
(features × planes) — otra disposición, candidato a hermano `pricing-table`.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Gates: `blocks:check` verde (5 blocks) · `svelte-check` sin errores propios.
**Siguiente**: F2.5 `testimonials`.
feat(blocks): F2.5 `testimonials` — rejilla de citas con autor Prueba social: una cabecera sobre una rejilla responsive de tarjetas de cita, cada una con su autor (avatar · nombre · cargo). Compound —la `.Item` se repite, como feature-grid— y SIN contexto (las partes no coordinan). `<Testimonials>` + `.Header` + `.Items` + `.Item` + `.Quote` + `.Author`(slot `avatar` + `.AuthorName`/`.AuthorRole`). - `AutoGrid` de tarjetas de igual alto; `.Author` fijada al borde inferior de la tarjeta (`margin-block-start: auto`) para que una fila de citas de distinto largo alinee las caras. - La cara es del app: un `<Avatar>` con imagen o un `Avatar.Fallback` de iniciales va en el slot `avatar` (B-7). - La tarjeta del quote usa `variant="outline"` — el `soft neutral` es casi invisible en claro. Encontrado al componer (en el README del block): el tema activa solo un SUBCONJUNTO de escalas donor (`green/indigo/orange/plum/teal`); una escala no activada (`cyan/ruby/amber/jade`) en `color` cae en SILENCIO a `primary` (la regla `[data-color]` hace `var(--scale-…, primary)`). Config del tema, no bug de componente; la demo usa escalas activadas. Hueco a decisión del usuario (Gaps): la **cita única en spotlight** (grande, centrada, logo+avatar+autor) — la variante modal del dossier, el grid es minoría. Otra disposición → candidato a hermano `testimonial-spotlight`. Demo (`web/routes/blocks/testimonials/`): full-bleed + ruta `preview`, cinco citas con avatares de iniciales en cinco colores distintos. Verificado en claro/oscuro × LTR/RTL, caras alineadas al fondo. `blocks:check` verde (6 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-24 — **F2.5 `testimonials` HECHO**. Compound (`.Item` repite, SIN
contexto — como feature-grid): `<Testimonials>` + `.Header` + `.Items` +
`.Item` + `.Quote` + `.Author`(slot `avatar` + `.AuthorName`/`.AuthorRole`).
`AutoGrid` de tarjetas de igual alto; `.Author` fijada al borde inferior
(`marginTop="auto"`) para alinear las caras aunque las citas midan distinto. La
cara es del app (`Avatar` con imagen o `Avatar.Fallback` de iniciales). `Card`
para el quote = `variant="outline"` (soft neutral es casi invisible en claro).
**Encontrado**: el tema activa solo un SUBCONJUNTO de escalas donor —
`green/indigo/orange/plum/teal`; una escala no activada (`cyan/ruby/amber/jade`)
en `color` cae en silencio a `primary` (la regla `[data-color]` hace
`var(--scale-…, primary)`). No es bug de componente, es config del tema; usar
escalas activadas. **Hueco a decisión del usuario**: la cita única en spotlight
(grande, centrada, logo+avatar+autor) — la variante modal del dossier, otra
disposición → candidato a hermano `testimonial-spotlight`. Verificado en
claro/oscuro/RTL, caras alineadas, 5 colores distintos. Gates: `blocks:check`
verde (6 blocks) · `svelte-check` sin errores propios. **Siguiente**: F2.6 `faq`.
feat(blocks): F2.6 `faq` — proxy fino del Accordion del canon La sección de preguntas: una cabecera sobre un acordeón de P/R en columna estrecha. Compound (`.Item` repite) y —la regla al envolver un componente interactivo— un PROXY FINO del `Accordion` del canon: el block lee su API y la pasa tal cual, no reinventa el disclosure. `<Faq>` + `.Header` + `.List` + `.Item`. - `.List` **ES** el `Accordion`: toda su API pasa sin gate — `type` (single/multiple), `bind:value`, `collapsible`, `variant`, `size`. Defaults de FAQ: single, collapsible (el abierto se puede cerrar), outline. - `.Item` proxya `Accordion.Item > Header > Trigger`(snippet `question`) / `Content`(children) y autogenera el `value` (clave de estado) con `$props.id()` si no se pasa — la única conveniencia sobre el andamiaje. - El teclado (flechas, Home/End, Enter/Espacio), `aria-expanded` y `aria-controls` salen del `Accordion`; el block no toca la a11y del disclosure. `Container` estrecho (`md`) para una columna legible. Demo (`web/routes/blocks/faq/`): full-bleed + ruta `preview`, cinco preguntas en una columna, con la cola «¿aún tienes dudas?» que la app pone tras `.List`. Huecos a decisión del usuario (Gaps): la **lista estática 2/3 columnas** (6 de 7 en TW NO son acordeón, sino P/R siempre abiertas) — otra disposición, prop `layout` o hermano. Verificado en navegador: el acordeón abre/cierra, claro/oscuro × LTR/RTL. `blocks:check` verde (7 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
- 2026-07-24 — **F2.6 `faq` HECHO**. Compound (`.Item` repite) y **proxy fino del
`Accordion` del canon** (regla del handoff: leer su API, no reinventar el
disclosure): `<Faq>` + `.Header` + `.List` + `.Item`. `.List` **ES** el
`Accordion` — toda su API pasa tal cual (`type`, `bind:value`, `collapsible`,
`variant`, `size`; defaults FAQ: single, collapsible, outline). `.Item` proxya
`Accordion.Item > Header > Trigger`(snippet `question`) / `Content`(children), y
autogenera el `value` con `$props.id()` si no se pasa. `Container` estrecho
(`md`). El teclado/ARIA del disclosure salen del `Accordion`, el block no los
toca. **Hueco a decisión del usuario**: la lista estática 2/3 columnas (6 de 7
en TW NO son acordeón) — otra disposición, prop `layout` o hermano. La cola
«¿aún tienes dudas?» es app-land (la demo la pone tras `.List`). Verificado: el
acordeón abre/cierra, claro/oscuro/RTL. Gates: `blocks:check` verde (7 blocks) ·
`svelte-check` sin errores propios. **Siguiente**: F2.7 `stats-band`.
feat(blocks): F2.7 `stats-band` — la cifra que se cuenta sola La fila de números que respalda lo que la página acaba de afirmar. Compound (`.Stat` repite, sin contexto): `<StatsBand>` + `.Stat` + `.Value` + `.Label`. - **Compone, no reinventa**: la semántica de KPI es el `Metrics` del canon, leído de su API y pasado tal cual (precedente `faq`/`Accordion`). `Metrics` es surfaceless, que es exactamente lo que una banda necesita: no son tarjetas de panel. Un delta, un icono o un sparkline son `Metrics.Delta`/`.Icon`/`.Chart` que la app compone dentro — el block no los re-expone. - **La superación literal del dossier**: `<StatsBand.Value count={12500} />` compone `CountUp`, así que la cifra se cuenta sola al entrar en pantalla. NINGUNA referencia puede shipearlo: todas entregan markup estático. El formato locale-aware y el salto directo bajo reduced-motion vienen del `CountUp`, no de aquí. - **`count` es opt-in, nunca default**: una cifra que se anima sin que el lector lo pida es ruido, y algunas no son contables («99,98 %»). Sin `count`, la cifra la pone la app por children. - **Ni una palabra ni un separador salen del block** (B-7): las palabras por children, el formato por `uix.format` dentro del `CountUp`. - Escalonado estructural: `data-stagger` en la banda y cada `.Stat` es un `Motion trigger="viewport"`, así que las cifras aterrizan una tras otra sin un solo milisegundo escrito a mano. Verificado en navegador (con la banda bajo el pliegue, para cazar el conteo desde el primer fotograma): 4 cifras con índices estructurales 0,1,2,3 contando —9377→11.678 · 255→318 · 36→45—, el porcentaje quieto por no ser contable, y el separador de millares por locale (`11.678`). Cero errores de página. `blocks:check` verde (8 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 months ago
- 2026-07-29 — **F2.7 `stats-band` HECHO**. Compound (`.Stat` repite, sin
contexto): `<StatsBand>` + `.Stat` + `.Value` + `.Label`. Compone el `Metrics`
del canon para la semántica de KPI —surfaceless, que es lo correcto: una banda
no son tarjetas de panel— y `CountUp` para la cifra que se cuenta sola al
entrar en pantalla: **la superación literal del dossier**, porque ninguna
referencia puede shipearla (todas entregan markup estático). `count` es
OPT-IN, nunca default (una cifra que se anima sin que el lector lo pida es
ruido, y «99,98 %» no es contable). El block NO formatea ni escribe: el
separador de millares sale de `uix.format` DENTRO del `CountUp` y las palabras
entran por children (B-7). Escalonado estructural (`data-stagger` en la banda,
cada `.Stat` un `Motion trigger="viewport"`). Verificado en navegador: 4 cifras
con índices 0,1,2,3 contando (9377→11.678 · 255→318 · 36→45), el porcentaje
quieto, separador por locale, cero errores de página. Gates: `blocks:check`
verde (8 blocks) · `svelte-check` sin errores propios. **Siguiente**: F2.8 `cta`.
feat(blocks): `cta` — el panel que pide el siguiente paso, y tres mentiras que destapó al medirlo F2.8. Slots de snippet (eyebrow · title · description · actions · children), misma forma que `hero`: las partes de un CTA no repiten ni coordinan, así que un compound no se gana nada. Dos disposiciones: `center` para cerrar una página y `justified` —copia al inicio, acciones al final— que es el hueco que nombraba el dossier. Un solo `Motion trigger="viewport"`: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Lo que salió al verificarlo en navegador, medido y no a ojo: - `variant='soft'` **fuera de la API**. Su track queda a `oklch(0.9932)` contra un `--color-surface-default` de `oklch(0.9911)`: 0.002 de luminancia, o sea ningún panel en claro. Y `Surface` no tiene borde al que caer. Cortar el prop es más barato que shipear un estado que se esfuma. - La ranura `contrast` de la paleta es blanco en TODO escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: `primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos; `neutral` 3.32 · `teal` 3.07 fallan en claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan. - `Text align` es inerte por defecto: renderiza un `span`, y `text-align` no hace nada sobre una caja inline. `align="center"` dejaba la copia a la izquierda dentro del layout centrado, sin avisar. Rodeado con `as="p"`. Y `Group` no apila: a 420px la etiqueta de la acción secundaria se parte contra el botón primario, así que las acciones van en `Flex direction={{ base: 'column', sm: 'row' }}`. `hero` compone las suyas con `Group` — anotado. Los tres hallazgos de canon quedan en los gaps del README del block y en `PLAN-blocks-quality.md` §6 (F15/F16/F17), sin tocar nada fuera del tier. Verificado: `center` y `justified` en claro/oscuro/RTL y a 420px, tres colores, entrada disparada, descripción en `<p>` centrada, cero errores de página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-30 — **F2.8 `cta` HECHO**. Slots de snippet (`eyebrow` · `title` ·
`description` · `actions` · `children`), misma forma que `hero`: las partes de
un CTA no repiten ni coordinan. Compone `Section` + `Container` + `Motion` +
`Surface` + `Heading` + `Text` + `Flex`. Dos disposiciones: `center` (`Stack`,
cierre de página) y **`justified`** (`Grid` 2 col., el hueco que nombraba el
dossier), que apila en estrecho. Un solo `Motion trigger="viewport"` con
`scale-fade`: el panel llega ENTERO, porque un CTA es una sola afirmación y
repartir sus tres partes se leería como duda. Landmark `section aria-labelledby`
al título que el propio block escribe.
**Tres hallazgos medidos en navegador, no a ojo** (los tres van a los gaps del
README y a §6 de `PLAN-blocks-quality.md`):
1. `variant='soft'` **eliminado de la API**: su track queda a `oklch(0.9932)`
contra un `--color-surface-default` de `oklch(0.9911)` — 0.002 L, o sea
ningún panel en claro — y `Surface` no tiene borde al que caer. Un panel
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
sosegado _con borde_ no tiene primitivo (`Card outline` acota pero no acepta
feat(blocks): `cta` — el panel que pide el siguiente paso, y tres mentiras que destapó al medirlo F2.8. Slots de snippet (eyebrow · title · description · actions · children), misma forma que `hero`: las partes de un CTA no repiten ni coordinan, así que un compound no se gana nada. Dos disposiciones: `center` para cerrar una página y `justified` —copia al inicio, acciones al final— que es el hueco que nombraba el dossier. Un solo `Motion trigger="viewport"`: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Lo que salió al verificarlo en navegador, medido y no a ojo: - `variant='soft'` **fuera de la API**. Su track queda a `oklch(0.9932)` contra un `--color-surface-default` de `oklch(0.9911)`: 0.002 de luminancia, o sea ningún panel en claro. Y `Surface` no tiene borde al que caer. Cortar el prop es más barato que shipear un estado que se esfuma. - La ranura `contrast` de la paleta es blanco en TODO escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: `primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos; `neutral` 3.32 · `teal` 3.07 fallan en claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan. - `Text align` es inerte por defecto: renderiza un `span`, y `text-align` no hace nada sobre una caja inline. `align="center"` dejaba la copia a la izquierda dentro del layout centrado, sin avisar. Rodeado con `as="p"`. Y `Group` no apila: a 420px la etiqueta de la acción secundaria se parte contra el botón primario, así que las acciones van en `Flex direction={{ base: 'column', sm: 'row' }}`. `hero` compone las suyas con `Group` — anotado. Los tres hallazgos de canon quedan en los gaps del README del block y en `PLAN-blocks-quality.md` §6 (F15/F16/F17), sin tocar nada fuera del tier. Verificado: `center` y `justified` en claro/oscuro/RTL y a 420px, tres colores, entrada disparada, descripción en `<p>` centrada, cero errores de página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
acabado de degradado).
2. La ranura `contrast` de la paleta es `#ffffff` en **todo** escalón sólido, así
que un lienzo de luminancia media deja el cuerpo por debajo de AA: medido,
`primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos, mientras
`neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 fallan en
claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan.
3. `Text align` es **inerte** por defecto: `Text` renderiza un `span` y
`text-align` no hace nada sobre una caja inline, así que `align="center"`
dejaba la copia alineada a la izquierda dentro del layout centrado. Resuelto
con `as="p"` (que es lo que ese contenido es).
También: `Group` no apila — las acciones necesitaron
`Flex direction={{ base: 'column', sm: 'row' }}` porque a 420px la etiqueta de
la acción secundaria se parte contra el botón primario (`hero` compone sus
acciones con `Group`, así que arrastra el mismo comportamiento — anotado para su
siguiente pasada). El CTA que navega usa el patrón `Button child` + `content`
con `intent="fulfill"`, igual que `hero`. Verificado en navegador: `center` y
`justified` en claro/oscuro/RTL + 420px, tres colores, entrada disparada
(`data-animation-pending` fuera), descripción en `<p>` centrada, cero errores de
página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores
propios. **Siguiente**: F2.9 `newsletter`.
feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho F2.9. Slots de snippet (eyebrow · title · description · field · submit · note · children), `center` y `justified`, y `panel` como interruptor real: el panel de marca es la cara de las referencias, pero el control, la ayuda y el error están calibrados para la superficie de página, así que apagarlo es un estado de primera y no un fallback. La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la pista libre y la acción abraza su contenido —eso es lo que hace que un alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado de un campo de correo deja inservibles a los dos. El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/ touched, la agregación de errores y el foco al primer error; `Field` posee el cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`. El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí. ## Superación del dossier: la etiqueta El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel` —arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar. ## Tres hallazgos de canon más, medidos - **La fundación de eidos no trae reset de modelo de caja y lo asume del app.** `[data-field-control]` declara `inline-size: 100%` + padding, así que bajo `content-box` el control mide 30px más que su contenedor: el campo se metía por debajo del botón de envío. Campo 480 / control 510 en la galería frente a 502 / 502 en los docs de componentes, que sí resetean (igual que `web/routes/active/styles.css`). Arreglado en app-land con `web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés, anotada en el handoff. - **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya construido: el componente solo lo reenvía al `createForm` que hace él mismo. El envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no expone el prop: el handler va en el `createForm` del app. - **Los mensajes de SIUM son idlangref.** La vía correcta es `uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una dirección de correo válida»—, pero la demo de docs del propio `Form` parte la cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón equivocado y siempre muestra inglés. Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6 (F18/F19/F20), sin tocar nada fuera del tier. Verificado en navegador el arco completo: correo inválido → error traducido con `role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo válido → confirmación del app (`Callout` afirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-30 — **F2.9 `newsletter` HECHO**. Slots de snippet; `center` +
`justified`; `panel` es un interruptor real (no un fallback). La fila es
`Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la
pista libre y la acción abraza su contenido, y bajo `sm` pasa a una columna
porque un botón al lado de un campo de correo deja inservibles a los dos. El
block **no valida ni emite sema**: `Form` posee el runtime y ya declara
`commit-submit`/`signal-invalid`; el app posee esquema, valores y handler.
**Superación del dossier**: la nota de privacidad (el único hueco que el
dossier nombraba para este block) **y la etiqueta**. Las referencias shipean la
fila escondiendo la etiqueta con `sr-only` o dejando solo un placeholder;
aquí se compone `Field floatingLabel` —la etiqueta arranca dentro del control y
sube al borde al enfocar o rellenar—, así que la fila queda alineada CON
etiqueta real y asociada. Verificado con pulsaciones reales en Playwright: 10px
dentro en reposo → −11px sobre el borde al enfocar y al rellenar. (Esto corrige
la advertencia de 2026-07-05 de que el flotado no era verificable sin navegador
real: el pane suspendido no valía, Playwright headless con teclas de verdad sí.)
**Tres hallazgos de canon más, medidos** (F18/F19/F20 en
`PLAN-blocks-quality.md` §6):
1. **La fundación de eidos no trae reset de modelo de caja.** Recetas como
`[data-field-control]` declaran `inline-size: 100%` + padding, así que bajo
`content-box` el control mide 30px más que su contenedor y se metía por
debajo del botón: campo 480 / control 510 en la galería frente a 502 / 502
en los docs de componentes, que sí resetean. El framework lo **asume** del
app. Arreglado en app-land (`web/routes/blocks/_lib/reset.css`), con A/B
sobre los 10 previews y 5 páginas de shell: cambia el newsletter y nada más.
Hubo que importarlo DOS veces porque la galería arranca UIX en línea en vez
de pasar por `BootUix` (deuda del arnés, anotada).
2. **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya
construido: el componente solo lo reenvía al `createForm` que hace él mismo.
El envío validaba, limpiaba y no anunciaba nada.
3. **Los mensajes de SIUM son idlangref**; la vía correcta es
`uix.langs.t(issue.message, issue.params)` (verificado: sale «Debe ser una
dirección de correo válida»), pero la demo de docs del propio `Form` parte la
cadena a mano tras el `|` y por eso siempre muestra inglés.
Verificado en navegador el arco completo: correo inválido → error TRADUCIDO con
`role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error;
correo válido → confirmación del app (`Callout` afirmativo con la dirección) y
error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no y 420px (fila
apilada). Cero errores de página. Gates: `blocks:check` verde (10 blocks) ·
`svelte-check` sin errores propios · prettier limpio en mis ficheros.
**Siguiente**: F2.10 `site-footer`.
feat(blocks): `site-footer` — el pie de la página, con el selector de idioma vivo F2.10. Es el block donde la regla de forma del tier se ve mejor: `Column` SE REPITE (el app mapea sobre N grupos), así que es parte compound; la marca, la banda de alta, lo social, lo legal y el `extra` no se repiten ni coordinan, así que son slots. El landmark es un `<footer>` de verdad: `contentinfo` sin pedir nada. Las columnas van en `AutoGrid minChildWidth`, **sin un solo breakpoint**: el mismo código sirve para 2 grupos o para 5, y en móvil caen a dos. La entrada es un `Motion trigger="viewport"` solo en la región superior y **sin escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie lee un pie columna por columna. ## Los dos huecos que nombraba el dossier, cerrados - **La banda de alta al boletín EN el pie** (3 de 7 en Tailwind Plus) como slot `signup`, con su propia fila a lo ancho: en la columna de la marca (~230px) la fila de correo tendría que apilarse. El formulario es del app; este block NO importa el block `newsletter` (B-10) ni inventa validación. - **El selector de idioma** como `extra` (el slot libre de D-BLK.6), y está VIVO: escribe `uix.prefs.setIntent('language', …)`, el eje real del ecosistema. Verificado — al elegir «English», `document.documentElement.lang` pasa a `en`. ## Tres números que salieron de medir, no de suponer - El `container` bajó de `xl` a **`lg`**: con `xl` el pie no se alineaba con ninguna sección de la página compuesta encima. - La rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`**: con `2fr`, un pie de cuatro grupos se partía en 3+1. - El gap entre columnas es **6, no 8**: con 8, cinco grupos no comparten fila al ancho `lg` (728px justos). Horizontal más apretado que vertical, porque el gap horizontal es el que decide cuántos grupos caben. ## Dos hallazgos de canon (F21/F22 en `PLAN-blocks-quality.md` §6) - **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading` aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`, `Container`, `Section`— renderiza un `<div>` fijo. Consecuencia: una columna de enlaces no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un block solo puede elegir entre divs o escribir markup que luego no puede estilar. - **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.** `getDisplayText()` resuelve contra un registro de etiquetas que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado no hay ninguno montado: `value=['es']` pintaba «es» en vez de «Español». Rodeado con el `child` de `Select.Value`. Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal, separador `aria-hidden`, los tres `IconButton` con nombre obligatorio, y el idioma cambiando de verdad. Cero errores de página. Gates: `blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-30 — **F2.10 `site-footer` HECHO**. El block donde la regla de forma del
tier se ve mejor: `.Column` SE REPITE → parte compound; marca, alta, social,
legal y extra no se repiten ni coordinan → slots. Landmark `<footer>` real, así
que es `contentinfo` sin pedir nada. Las columnas van en `AutoGrid`
(`minChildWidth`, **cero breakpoints**): el mismo código sirve para 2 grupos o
para 5, y en móvil caen a dos. Entrada solo en la región superior y **sin
escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie
lee un pie columna por columna.
**Los dos huecos que nombraba el dossier, cerrados**: la banda de alta al
boletín EN el pie (3 de 7 en TW) como slot `signup` —el app compone su propio
`Form`, el block NO importa el block `newsletter` (B-10)— y el **selector de
idioma** como `extra`, vivo sobre el eje real: escribe
`uix.prefs.setIntent('language', …)` y se verificó que mueve
`document.documentElement.lang` a `en`.
**Tres números que salieron de medir, no de suponer**: el `container` bajó de
`xl` a **`lg`** (con `xl` el pie no se alineaba con ninguna sección de la página
compuesta); la rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`** (con `2fr` un
pie de cuatro grupos se partía en 3+1); y el gap de columnas es **6, no 8**
(con 8, cinco grupos no comparten fila al ancho `lg` — 728px justos).
**Hallazgos** (F21/F22 en `PLAN-blocks-quality.md` §6): (1) **ningún primitivo de
layout puede cambiar de elemento** — `Text`/`Heading` aceptan `as`, pero `Box` y
todo lo construido sobre él es un `<div>` fijo, así que una columna de enlaces no
puede ser `<ul>/<li>`; (2) **un `Select` controlado muestra el VALOR crudo** hasta
que se abre una vez: `getDisplayText()` resuelve contra el registro de etiquetas
que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado
no hay ninguno montado. La demo lo rodea con el `child` de `Select.Value`.
Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL
y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal,
separador `aria-hidden`, los tres `IconButton` con nombre, y el selector de
idioma cambiando el idioma de verdad. Cero errores de página. Gates:
`blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier
limpio. **Siguiente**: F2.11 `banner`.
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la superficie entera del `Banner` con `Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni estrecharlos—, lo mete en columna con `Container` (`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con `Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`. La visibilidad NO es suya: es la decisión que ya tomó el componente («composición, no un booleano `dismissible`»), así que el app envuelve en su `{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad. Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un aviso persistente sin descarte es tan legítimo como uno descartable. La demo lo enseña **encima del `site-header` de verdad**, con página para desplazarse. Un aviso dentro de un recuadro no se parece a un aviso. ## El hallazgo que la fase 0 anticipó `Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede relajar a una región normal. Con el `site-header` en la misma página —que es la única colocación que shipean las referencias— quedan **dos landmarks `banner`**. Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos con `aria-label`, y eso hace la demo. ## Y una lección de método que costó una sesión Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres eran falsos**. La causa era la misma: medir demasiado pronto. Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar, así que están desde el primer frame; `type`, `aria-label` y los handlers los aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La espera válida es un atributo que solo pueda haber puesto la runtime —`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de esperar la condición y no el reloj: fallé al elegir la condición. De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier. Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no (con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px, la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el descarte con la página recolocándose, y los controles de la demo moviendo la vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check` sin errores propios · `docs:check` sin errores míos · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-31 — **F2.11 `banner` HECHO**. El block más FINO del tier a propósito:
cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la
superficie del `Banner` con `Omit<BannerProps, 'children'>` (no re-declara
`intent`/`variant`/`size` ni los estrecha), lo mete en columna con `Container`
(`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro),
envuelve con `Wrap` en vez de una fila rígida, y cablea `Banner.Close` solo si
llega `onDismiss`. La demo lo enseña **encima del `site-header` de verdad**, con
página para desplazarse: un aviso dentro de un recuadro no se parece a un aviso.
**Hallazgo real (fase 0 lo anticipó)**: `Banner` estampa `role="banner"` DESPUÉS
de sus rest props, así que no se puede relajar; con el `site-header` en la misma
página quedan **dos landmarks `banner`** (medido). Único paliativo hoy:
nombrarlos con `aria-label`.
**Y una lección de método que costó una sesión**: reporté tres «defectos del
canon» (`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo
`Button`, `IconButton` tragándose el `onclick`) y **los tres eran falsos**. La
causa: medía demasiado pronto. Los `data-variant`/`data-size` los escribe el
componente de eidos al renderizar —están desde el primer frame—, pero `type`,
`aria-label` y los handlers los aplica la **runtime del morfo en un efecto
posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando; a
~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La
espera válida es un atributo que solo pueda haber puesto la runtime
(`waitForFunction(() => boton.hasAttribute('type'))`), no que el nodo exista ni
que se vea. Regla afinada en `CONTINUE-blocks.md`.
De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre
accesible en `BlockDemo.svelte` — afectaba a las 12 demos del tier.
Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no
(con `no` el botón **deja de renderizarse**, no se oculta), claro/oscuro/RTL y
420px, la tira siempre por encima de la cabecera, cero desbordamiento
horizontal, y los controles de la demo moviendo la vista previa en línea. Gates:
`blocks:check` verde (12 blocks) · `svelte-check` sin errores propios ·
`docs:check` sin errores míos · prettier limpio. **Siguiente**: F2.12 `team`.
feat(blocks): `team` — las personas, y un contexto que sirve para una sola cosa F2.12. Compound porque `Member` SE REPITE (el app mapea sobre N), como `feature-grid`. Pero es el primero del tier cuyo **contexto existe para una sola cosa**: el `align` de la sección viaja a cada `Member` y a su fila de enlaces, así que la foto, el nombre, el cargo y los enlaces comparten eje sin que el app repita la alineación en cada tarjeta. Un `align` explícito en una parte siempre gana. Esa es la diferencia con `feature-grid`, cuyos ítems solo se repiten y por eso no llevan contexto: **la regla de forma se aplica parte por parte, no block por block**. ## Dos decisiones de forma - **Sin `.MemberAvatar`.** El plan la dibujaba; no tendría nada que añadir sobre `<Avatar size radius>` y escondería su API (`Image`, `Fallback`, los estados de carga). El app compone el `Avatar` del canon directamente dentro del `Member`. Se envuelve lo que el block DEFAULTEA —tipografía, retícula, eje—, no lo que solo reenvía. - **El nombre es un `Heading level={3}`** apagado con `size="sm"`: una persona en una retícula tiene nombre, y las referencias lo marcan igual. El nivel es estructura del documento; el tamaño, tipografía. Así un lector de pantalla puede saltar de persona a persona. Y un detalle que separa cumplir de sobresalir: la fila de enlaces va pegada al fondo de la tarjeta (`marginTop: auto`). Con biografías de distinto largo las filas quedaban a alturas distintas y la retícula se leía descuadrada; sin biografías las tarjetas ya miden lo mismo y la regla no cambia nada. ## Encontrado al verificar **Un `columns` fijo no colapsa.** A 420px, cuatro columnas dejan celdas de ~90px con el nombre partido en tres líneas y el avatar desbordando. El default del block es fluido (`minChildWidth`), así que el fallo era de la demo: ahora pasa `columns={{ base: 2, md: N }}` — para eso están los props responsivos. Anotado en el README. Verificado en navegador, esperando el atributo que pone la runtime del morfo y no el reloj: 2/3/4 columnas, `align` center/start propagándose por contexto al eje de cada miembro, biografía sí/no, claro/oscuro/RTL y 420px, seis avatares, doce botones de icono **con nombre propio por persona** («Escribir a Ada Lovelace», no «correo»), escalonado estructural con índices 0,1,2…, cero desbordamiento horizontal y cero errores de página. Gates: `blocks:check` verde (13 blocks) · `svelte-check` sin errores propios · `docs:check` 0 · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-31 — **F2.12 `team` HECHO**. Compound porque `Member` SE REPITE, y el
primero del tier cuyo **contexto sirve para una sola cosa**: el `align` de la
sección viaja a cada `Member` y a su fila de enlaces, así que la foto, el
nombre, el cargo y los enlaces comparten eje sin que el app repita la
alineación en cada tarjeta. Un `align` explícito en una parte siempre gana. Es
la diferencia con `feature-grid`, cuyos ítems solo se repiten y por eso no
tienen contexto: **la regla se aplica parte por parte, no block por block**.
**Dos decisiones de forma**: (1) el nombre es un `Heading level={3}` apagado
con `size="sm"` —una persona en una retícula tiene nombre y las referencias lo
marcan igual—, nivel = estructura, tamaño = tipografía; (2) la fila de enlaces
va pegada al fondo (`marginTop: auto`), porque con biografías de distinto largo
las filas quedaban a alturas distintas y la retícula se leía descuadrada — sin
biografías las tarjetas ya miden lo mismo y la regla no cambia nada.
**Encontrado al verificar**: un `columns` FIJO no colapsa. A 420px, cuatro
columnas dejan celdas de ~90px con el nombre partido en tres líneas y el avatar
desbordando. El default del block es fluido (`minChildWidth`), así que el fallo
era de la demo: ahora pasa `columns={{ base: 2, md: N }}` — para eso están los
props responsivos. Anotado en el README.
Verificado en navegador (esperando el atributo que pone la runtime, no el
reloj): 2/3/4 columnas, `align` center/start propagándose por contexto al eje de
cada miembro, biografía sí/no, claro/oscuro/RTL y 420px, seis avatares, doce
botones de icono **con nombre propio por persona**, escalonado estructural con
índices 0,1,2…, cero desbordamiento y cero errores de página. Gates:
`blocks:check` verde (13 blocks) · `svelte-check` sin errores propios ·
`docs:check` 0 · prettier limpio. **Siguiente**: F2.13 `contact`.
feat(blocks): contact CERRADO — la coordinacion entra en el contrato B El estado se le pregunta al ESQUEMA, no al Form. `form.isValid` con `validationBehaviour: 'progressive'` significa «aun no se ha encontrado nada mal»: un formulario vacio e intacto no tiene errores, asi que se declaraba valido y `incomplete` era INALCANZABLE al cargar — la seccion pedia resolver la verificacion con los tres campos vacios, justo el hueco que la doctrina existe para cerrar. Ahora la raiz valida los valores con `validateSync` de SIUM: sincrono y sin escribir errores (`form.validate()` habria encendido los tres campos en rojo en el primer frame). `state.test.ts` fija la maquina con 9 casos — primer test unitario del tier, porque `state.ts` es su primera logica pura: el orden de prioridad, que solo `ready` deja enviar, y que todo estado bloqueado tiene frase. Contrato B (architecture/blocks.md): seccion «Coordination» con las tres cosas que posee un block que coordina (estado, forma de sus datos, palabras), B-5 admite el esquema por defecto, B-7 enmendado (posee las palabras de SUS estados como idlangref con fallback ingles) y la convencion de servicios acotada: el traductor es el UNICO servicio sancionado. Dice tambien a quien NO aplica — los blocks de layout no coordinan nada. i18n por la via real: el arnes registra el namespace `blocks` en las DOS cadenas de arranque (galeria y previews, que duplican el boot) y la demo se lee en castellano en vez de caer al fallback ingles. Ademas: README y pestanas de doc rehechos al reparto nuevo, ficha F2.13 y bitacora en PLAN-blocks.md, y el control vivo que faltaba para un prop publico — `verification` con reto / sin reto, porque omitir el prop no es lo mismo que pasar un estado que nunca verifica. Verificado en navegador, arco completo y las dos ramas: sin reto vacio → un campo → tres campos (se desbloquea) → «Enviando…» → «Enviado»; con reto, con los tres campos llenos el boton sigue bloqueado y la razon pasa a «Resuelve la verificacion de arriba». `verifying` y `rejected` quedan cubiertos por test unitario, no por el reto en vivo: el provider de ProofOfHuman escribe `status` el mismo y el veredicto del app no se sostiene (hilo de canon abierto). Gates: blocks:check verde (14 blocks) · vitest 9/9 · docs:check 0 · svelte-check sin errores propios · prettier limpio en los ficheros propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-31 — **CAMBIO DE DOCTRINA (decisión del usuario)**: «si al final es una
lista de componentes… el bloque es coordinación, estado, y data también». Con
el `contact` delante se vio el motivo: con el estado 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é. Escrito en
`architecture/blocks.md` §«Coordination», con B-5 (el block puede traer la
FORMA de sus datos) y B-7 (posee las palabras de SUS estados, como idlangref
con fallback inglés) enmendados y la convención de servicios acotada: el
traductor es el único servicio sancionado. **No aplica a todos**: los blocks de
layout no coordinan nada y inventarles estado sería el error contrario.
- 2026-07-31 — **F2.13 `contact` HECHO**, el primero que COORDINA. Posee tres
cosas: la máquina de la sección (`state.ts`, 7 estados derivados de una sola
fuente), la forma por defecto de sus datos (`schema.ts`) y las palabras de cada
estado. `CONTACT_REASON` y `CONTACT_ACTION` son `Record` EXHAUSTIVOS: el mismo
mapa que bloquea el botón escribe la frase, así que un estado mudo no se puede
ni escribir. Las partes leen el estado por contexto — ninguna lo recalcula ni
inventa un `disabled`. A la demo se le cayeron el esquema, las etiquetas, el
`disabled`, el flag de envío y la frase: **249 líneas menos**, y lo que queda es
lo que de verdad es del app.
**Encontrado al verificar — y era real**: la máquina leía `form.isValid`, que
con `validationBehaviour: 'progressive'` significa «aún no se ha encontrado nada
mal». Un formulario vacío e intacto no tiene errores → se declaraba válido →
`incomplete` era INALCANZABLE al cargar y la sección pedía resolver la
verificación con los tres campos vacíos: justo el hueco que la doctrina existe
para cerrar. Arreglado preguntando al ESQUEMA (`validateSync` de SIUM:
síncrono, y sin escribir errores en el formulario — `form.validate()` habría
encendido los tres campos en rojo al cargar). Fijado con `state.test.ts`
(9 casos), primera lógica pura del tier y por tanto su primer test unitario.
**Lección de verificación (cara)**: perseguí un fallo inexistente durante
varias rondas porque mi guion esperaba `button[type="submit"]` con atributo
`type` como puerta de hidratación — y **ese atributo viene ya en el HTML del
servidor**, así que la espera se cumplía sobre el documento estático y yo
tecleaba antes de que Svelte tomara los inputs: los valores entraban en el DOM
y `form.values` seguía vacío. Matiza la regla del handoff: no basta con que el
atributo lo escriba la runtime en cliente, tiene que ser algo que **NO exista
en el SSR**. La puerta honesta aquí fue `<style id="uix-blocks-display">` (lo
escribe un `$effect` de `BootUix`) más un ida y vuelta reactivo real. También
costó una pista falsa: el servidor de dev servía módulos rancios y el `console.log`
del derivado salía por el **stdout del servidor** (SSR), no por la consola del
navegador — que es exactamente lo que delató el diagnóstico.
Verificado en navegador, arco completo y las dos ramas: **sin reto** vacío →
un campo → tres campos (se desbloquea) → «Enviando…» → «Enviado» con la frase
afirmativa; **con reto**, con los tres campos llenos el botón sigue bloqueado y
la razón pasa a «Resuelve la verificación de arriba» — es decir, nombra el
bloqueo REAL y en orden. Cero errores de página. Los estados `verifying` y
`rejected` quedan cubiertos por test unitario, no por el reto en vivo: el
provider de `ProofOfHuman` escribe `status` él mismo y el veredicto del app no
se sostiene (hilo de canon abierto). i18n: el arnés registra el namespace
`blocks` en las DOS cadenas de arranque (galería y previews) y la demo se lee
en castellano. Añadido el control vivo que faltaba para un prop público:
`verification` con reto / sin reto — y omitir el prop NO es lo mismo que pasar
un estado que nunca verifica. Gates: `blocks:check` verde (14 blocks) ·
`svelte-check` sin errores propios · `state.test.ts` 9/9 · prettier limpio en
los ficheros propios. **Siguiente**: F2.14 `content-section`.
feat(blocks): content-section cierra F2 — la rejilla de escape, y las escalas tipograficas salen de Text El ultimo block de sitio, y el que mas cerca estuvo de ser un envoltorio: `Section` + `Container` + `Prose` no habria anadido nada, porque `Prose` ya trae su medida de lectura. Lo que falta en el ecosistema es el ESCAPE — dentro de un `Container` nada puede ser mas ancho que el, asi que una figura a sangre hay que sacarla del articulo y ponerla de hermana, y el orden de lectura se rompe. La seccion es una rejilla de 5 pistas (canalon · flanco · MEDIDA · flanco · canalon) y cada parte dice hasta donde llega: `measure` col 3, `wide` col 2/5, `full` col 1/-1. Todas las longitudes son tokens del sistema (`--measure-*`, `--container-width-*`, `--container-padding-inline`). `.Body` y `.Media` son compound porque SE REPITEN; la cabecera son slots de snippet. `.Body` apaga la medida de `Prose`: dos duenos del mismo ancho se pelean y ganaria el mas estrecho en silencio. Los tres ejes son RESPONSIVE (`measure`, `wide`, y el `width` de cada parte), resueltos con `eidos.resolve()` — el mismo camino que usan los primitivos de eidos, asi que los unicos breakpoints en juego son los canonicos. Es lo que un valor unico no puede decir: `{ base: 'full', md: 'wide' }` = foto a sangre en movil y contenida de `md` arriba. CANON: un componente no es libreria de otro. `Heading` importaba CINCO escalas tipograficas de `Text` (`TextTracking`, `TextLeading`, `TextWrap`, `TextNumeric`, `TextMeasure`) y al tipar este block repeti la violacion. Las cinco se mueven a `eidos/lib/types.ts`, que es donde su propio encabezado dice que van y donde ya esta documentado el precedente del mismo fallo (`ColorRole` re-escrito a mano que derivo a 8 miembros contra 9). Sin shim de compatibilidad: `text` y `heading` las importan de la libreria. Cuatro defectos que solo dijo el navegador: 1. `gap` separa tambien las COLUMNAS y una parte que las cruza se lleva esos huecos encima — `wide` media 1088 en vez de los 1024 del contenedor con el que debe alinearse, y a 420px `full` llegaba a 532 dentro de una rejilla de 404 y hacia scrollear la pagina. Van `rowGap` + `columnGap={0}`. 2. Una pista `min(medida, 100%)` ignora sus propios canalones: a 420px la central se quedaba los 404 enteros y la rejilla se iba a 436. 3. Un token de container es un ancho EXTERIOR: con `--container-width-lg` a pelo la figura `wide` sobresalia 16px por cada lado respecto al texto de la seccion hermana de debajo, a 1280/1440/1920. La pista ancha resta ahora `2 * --container-padding-inline` (992 en `lg`) y el desfase es 0. Lo cazo el usuario mirando la pantalla: yo habia medido el block contra si mismo, no contra la pagina. 4. `100%` significa algo distinto dentro de cada parte — en `measure`/`wide` ya es una pista, en `full` es la rejilla entera con canalones —, asi que capar el pie igual en los dos casos lo dejaba a 340 contra una columna de 372 en uno y a 404 en el otro. Verificado en navegador: barrido 375→1920 sin desbordamiento; alineacion pie↔prosa identica en 3 viewports × 3 medidas × 3 anchuras (18/18); figura `wide` a ras del texto de la seccion hermana en 1280/1440/1920; y el valor responsive cambia exactamente en el `md` canonico (a sangre a 767, contenida a 768). Claro, oscuro, 420px y 1920 mirados, y la geometria confirmada tambien en el Chrome del usuario. Semantica propia `figure`/`figcaption` (no interactivos → estructura de documento, B-8) con el hueco senalado: el canon no tiene primitiva `Figure`. Contexto de CONFIGURACION, no de estado — es un block de layout y no coordina nada. Gates: blocks:check verde (15 blocks) · svelte-check sin errores en lo tocado · eidos 28/29 suites (la que falla es `audio-player` sin morfo, del hilo de audio, ajena) · docs:check 0 · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-07-31 — **F2.14 `content-section` HECHO → F2 CERRADA 15/15**. El último
de sitio, y el que más cerca estuvo de ser un envoltorio: `Section` +
`Container` + `Prose` no habría añadido nada, porque `Prose` ya trae su medida
de lectura. Lo que sí falta en el ecosistema es el **escape**: dentro de un
`Container` nada puede ser más ancho que él, así que una figura a sangre hay
que sacarla del artículo y ponerla de hermana — y el orden de lectura se
rompe. La sección es una REJILLA de 5 pistas (canalón · flanco · MEDIDA ·
flanco · canalón) y cada parte dice hasta dónde llega: `measure` col 3,
`wide` col 2/5, `full` col 1/-1. Todas las longitudes son tokens del sistema
(`--measure-*`, `--container-width-*`, `--container-padding-inline`).
`.Body` y `.Media` son compound porque SE REPITEN (un artículo alterna prosa y
figuras en el orden en que se lee); la cabecera son slots de snippet.
`.Body` apaga la medida de `Prose`: dos dueños del mismo ancho se pelean y
ganaría el más estrecho en silencio.
**Tres defectos que solo dijo el navegador**, ninguno visible leyendo el
código: (1) **`gap` separa también las COLUMNAS**, y una parte que las cruza
se lleva esos huecos encima — `wide` medía **1088** en vez de los 1024 del
contenedor con el que debe alinearse, y a 420px `full` llegaba a **532** en
una rejilla de 404 y hacía scrollear la página; las columnas aquí son un
instrumento de medida, no cosas puestas al lado, así que van con `rowGap` y
`columnGap={0}`. (2) una pista `min(medida, 100%)` **ignora sus propios
canalones**: a 420px la central se quedaba con los 404 enteros y los canalones
empujaban la rejilla a 436. (3) **`100%` significa algo distinto dentro de
cada parte** — en `measure`/`wide` ya es una pista, en `full` es la rejilla
entera con canalones incluidos —, así que capar el pie igual en los dos casos
lo dejaba a 340 contra una columna de 372 en uno y a 404 en el otro.
Verificado tras los arreglos en **3 viewports × 3 medidas × 3 anchuras**: el
pie tiene el mismo ancho y la misma x que la prosa en las 18 combinaciones,
cero desbordamiento horizontal. Claro,
oscuro y 420px mirados. Semántica propia `figure`/`figcaption` (no
interactivos → estructura de documento, B-8) con el hueco señalado: **el canon
no tiene primitiva `Figure`**. Contexto de CONFIGURACIÓN, no de estado (la
medida, para el pie) — es un block de layout y no coordina nada. Gates:
`blocks:check` verde (15 blocks) · `svelte-check` sin errores propios ·
`docs:check` 0 · prettier limpio. **F2 CERRADA**; siguiente F3 (blocks de
aplicación) o la página compuesta de prueba del cierre de F2.
- 2026-07-31 — **Cambio de CANON pedido por el usuario: un componente no es
librería de otro.** Al tipar `content-section` importé `TextMeasure` desde
`components/text/types`, y el usuario lo cortó: no se importan librerías de
otros componentes; lo compartido se saca a una librería común. Al mirarlo, la
violación ya existía y era mayor que la mía — **`Heading` importaba CINCO
escalas tipográficas de `Text`** (`TextTracking`, `TextLeading`, `TextWrap`,
`TextNumeric`, `TextMeasure`). Las cinco se han movido a
`src/uix/eidos/lib/types.ts`, que es donde su propio encabezado dice que van
(«only types that would otherwise duplicate across components live here») y
donde ya está el precedente documentado del mismo fallo (`ColorRole`
re-escrito a mano que derivó a 8 miembros contra 9). Sin shim de compat:
`text/types.ts` las importa, `heading/types.ts` también, y el bloque igual.
Arrastraba además un error mío anterior: había declarado
`ContentMeasure = 'narrow' | 'normal' | 'wide'`, copia literal del canónico, y
tipado `wide` como `string` libre con default `'var(--container-width-lg)'` —
lo que empujaba la construcción del nombre del token a un fichero de RUTA.
Ahora `measure` es `TextMeasure`, `wide` es `ContainerSize` y el único `var()`
vive en `grid.ts`. Gates tras el movimiento: `check` sin errores en lo tocado
(74 globales, ninguno mío) · 28 de 29 suites de eidos verdes (la que falla es
`audio-player` sin morfo, del hilo de audio, `c4d64ba98`) · `blocks:check` 15.
- 2026-07-31 — **`content-section`: los tres ejes pasan a RESPONSIVE** (decisión
del usuario: «debe de ser responsive»). `measure`, `wide` y el `width` de cada
parte aceptan `ResponsiveProp`, resueltos con `eidos.resolve()` — el mismo
camino que usan los primitivos de eidos, así que los únicos breakpoints en
juego son los canónicos. Es lo que un valor único no puede decir:
`{ base: 'full', md: 'wide' }` = foto a sangre en móvil y contenida de `md`
arriba. **Verificado contra el breakpoint canónico**: a 767 va a sangre, a 768
se contiene. Expuesto como control vivo en la demo.
- 2026-08-01 — **`content-section`: un cuarto defecto, y lo cazó el usuario
mirando la pantalla.** Yo había verificado la geometría INTERNA del block
(medida, escape, pie, breakpoints, responsive) pero nunca lo había mirado
contra la página: la pista `wide` usaba `--container-width-lg` a pelo, que es
el ancho EXTERIOR del `Container`, así que la figura sobresalía **16px por
cada lado** respecto al texto de la sección hermana de debajo — a 1280, 1440 y
1920, siempre los mismos 16. El README afirmaba justo lo contrario («lines up
with the sections above and below»). Arreglado restando
`2 * --container-padding-inline`: la pista ancha en `lg` son **992**, no 1024,
y el desfase pasa a 0 en los tres anchos. Verificado también en el Chrome del
usuario.
**Lección de método**: medir el block contra SÍ MISMO no basta. Un block vive
en una página, así que la comprobación que faltaba era poner una sección
normal debajo y comparar los bordes izquierdos. Y **generé la captura en
oscuro y nunca la abrí** — la regla dice screenshot + MIRAR, y yo me quedé en
la primera mitad. Ojo también con inspeccionar en una pestaña de fondo:
`visibilityState: 'hidden'` suspende el rAF y deja los `data-animation-pending`
puestos con `opacity: 0`, lo que parece un bug de visibilidad y no lo es (en
primer plano, 0 pendientes).
docs(blocks): auditoria del tier congelada — 29 confirmados, 50 sin verificar Auditoria de los 15 blocks con 50 agentes: dos dimensiones en paralelo —doctrina (contrato B + regla de coordinacion, por lectura) y percepcion (sema en vivo en navegador, interaccion por interaccion)— y despues un verificador ESCEPTICO por hallazgo, con el encargo de refutarlo y la lista de falsos positivos conocidos del repo. 90 en bruto → 40 verificados → 29 CONFIRMADOS + 11 refutados (28%) Ese 28% es la razon de existir de la fase adversarial: sin ella habrian entrado once acusaciones falsas. ⚠️ 50 hallazgos quedaron SIN VERIFICAR, 18 de ellos marcados ALTA. El tope de 40 lo puso mi script, no el trabajo. No son menos graves: estan sin comprobar. `AUDIT-blocks-2026-08-01.md` es la UNICA copia — el journal del workflow era de sesion y ya no existe. Incluye los refutados con su motivo para que nadie los vuelva a reportar. Lo gordo confirmado: - `contact` importa `$libs/forms`, fuera de la lista de la frontera dura 1. - El nombre accesible del submit de `contact` esta congelado en «Enviar» en los cinco estados: `Form.Submit` impone su `aria-label`, asi que las palabras de estado que el block existe para poseer NO llegan al lector de pantalla. El block cumple su promesa solo a la vista. - `site-header` congela la pagina: con el cajon abierto, cruzar el `breakpoint` esconde cajon/overlay/trigger con `display:none` pero deja el Drawer abierto y el scroll del body bloqueado. Reproducido con swipe tactil real rotando un telefono; la unica salida es Escape, que en tactil no existe. - `hero` en `layout="background"`: CTA secundaria a 1.78:1, bajo AA, y el token que la demo escribe a mano para arreglarlo es inerte. - `height="100%"` sobre `Card` es INERTE (atributo HTML, no prop) en `pricing` y `testimonials`: las tarjetas no igualan alto mientras README y demo afirman lo contrario. - B-8 sin declarar (landmark + jerarquia de encabezados) en seis READMEs. La dimension CRUZADA —los 15 en una pagina compuesta— NO se ejecuto, y es ademas la condicion de cierre de F2 del plan. La auditoria confirmo su propia premisa: la capa perceptiva no se habia ejercido NUNCA porque la galeria arrancaba sin packs de sema desde F0, y ahi esta lo mas feo (todavia sin verificar): el nav del header emitiendo `commit-select`+`affirm` con earcon audible al pasar el RATON por encima, un `Enter` en `newsletter` emitiendo dos `commit-submit` con intents contradictorios, y 4 de 7 elementos del header mudos. El handoff `CONTINUE-blocks.md` arranca ahora por la auditoria, con el orden recomendado (verificar los 50 → arreglar por severidad → pagina compuesta → catalogo → v2) y la deuda declarada del arnes, que triplica la cadena de arranque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-01 — **AUDITORÍA del tier (50 agentes)**. Dos dimensiones sobre los 15
blocks —doctrina y percepción— con un verificador escéptico por hallazgo:
**90 en bruto → 40 verificados → 29 CONFIRMADOS y 11 refutados (28% de falsos
positivos)**. Congelada en `AUDIT-blocks-2026-08-01.md`, que es la única copia
(el journal del workflow era de sesión). ⚠️ **50 quedaron SIN VERIFICAR, 18 de
ellos marcados ALTA** — el tope de 40 lo puso mi script, no el trabajo.
Lo gordo confirmado: `contact` importa `$libs/forms` fuera de la frontera dura 1
· el nombre accesible del submit de `contact` está congelado en «Enviar» en los
cinco estados porque `Form.Submit` impone su `aria-label`, así que **las palabras
de estado que el block existe para poseer no llegan al lector de pantalla** ·
`site-header` congela la página si el cajón está abierto al cruzar el breakpoint
(reproducido con swipe táctil real) · `hero` en `background` deja la CTA
secundaria a 1.78:1 · `height="100%"` sobre `Card` es INERTE en `pricing` y
`testimonials` mientras README y demo afirman lo contrario · B-8 sin declarar en
seis READMEs.
La dimensión CRUZADA (los 15 en una página compuesta) NO se ejecutó: sigue
pendiente, y es además la condición de cierre de F2 del plan.
**Confirmó la premisa de la auditoría**: la capa perceptiva no se había ejercido
NUNCA (la galería arrancaba sin packs de sema desde F0), y ahí está lo más feo
sin verificar — el nav del header emitiendo `commit-select` + `affirm` con earcon
audible al pasar el RATÓN por encima, un `Enter` en `newsletter` emitiendo dos
`commit-submit` con intents contradictorios, y 4 de 7 elementos del header mudos.
feat(blocks): el guard vigila la frontera, y siete filas del ledger caen medidas FASE 5 DEL SANEAMIENTO — `blocks:check` deja de vigilar solo la forma del tier y pasa a vigilar su frontera, su documentacion y su catalogo. Tres reglas, cada una con su fixture negativo en el selfTest(): 1. Lista blanca de importaciones (B-4). La frontera dura 1 era prosa; ahora es codigo: $uix, los arts publicos, $libs/forms, svelte y los relativos del propio block. Los arts se DERIVAN de src/arts/*, no se listan a mano — una lista escrita se queda atras el dia que aterriza un art. El tier ya cumplia: cero violaciones al encenderla. 2. README completo (B-9) + declaracion de landmark (B-8). Salieron 7 de 15 sin ella; escritas leyendo la fuente de cada block, no la plantilla. 3. Ficha en el catalogo de demos (B-9), en los DOS sentidos: un block que el catalogo no publica es invisible para el rail, y un slug publicado sin block detras es un enlace muerto. De paso el escaner deja de leer los comentarios como codigo: los index.ts documentan su uso con un `// import { Cta } from '$blocks/cta'`, y una lista blanca que lee prosa habria empezado a acusar a los ejemplos. Verificado EN ROJO sobre el arbol real, no solo contra los fixtures: un zod y un $libs/days metidos en hero/types.ts salen con su linea exacta mientras el mismo import dentro de un comentario NO salta; el catalogo falla en las dos direcciones; y al romper isAllowedSpec a proposito el self-test aborta el guard en vez de dar verde. ARREGLOS DEL CONTRASTE DOCTRINAL — siete filas, cada una con medicion o gate: - A-88 team: `height="100%"` en el Stack del Member. El div que pinta Motion es un bloque pelado, asi que el Stack se quedaba a altura de contenido y el `margin-top:auto` de los enlaces repartia CERO. Medido: la desviacion entre filas de iconos pasa de 20px a 0, y el margen reparte 20,297px. - A-94/A-28 (feature-grid, team, testimonials): los tres declaraban su tipo como `= AutoGridProps` y ponian {...rest} ANTES de las props que fijan, asi que un consumidor podia escribir align="start" y no obtener nada. Cerrados con Pick<AutoGridProps, ...>, que conserva los tipos exactos del canon. - A-84 feature-grid: el eje `align` llega a las partes por contexto, como en team, en vez de que el typedoc instruya al app a repetirlo. Medido: con align=center el align-items computado es center en cabecera Y celdas. - A-19 faq: `marginX="auto"` en el Header — Box no centra solo. Medido: el Box de 768px resuelve margin-inline 248px/248px donde antes daba 0. - A-27 testimonials y A-86 pricing: el typedoc decia `soft` donde el codigo hace `outline`, y el ejemplo de la puerta de entrada no compilaba. - A-91 banner (fila nueva): el block hereda el eje intent/color que el canon declara fuera del sistema abierto en su propio typedoc, sin registrarlo. LEDGER — dos lecciones de instrumento escritas dentro, porque ambas produjeron hallazgos falsos publicados: 1. El dev server sirve codigo ANTERIOR a HEAD si reusa cache de Vite. Tres filas (A-36, A-69, A-85) se dieron por vivas estando arregladas. El delator fue aritmetico: las duraciones medidas eran EXACTAMENTE las que el comentario del arreglo cita como estado anterior. 2. Contar nodos WebAudio no es medir sonido: la sintesis crea dos osciladores por earcon, un sound pack de muestras cambia la aritmetica entera, y crear nodos no es sonar. El contador responde "hubo actividad" y nada mas. Y el error de metodo del que ambas son sintoma, tambien registrado: se fue a re-medir A-67 sin leer su ficha, que ya traia los gains del catalogo, el empate de applyDominance y la regla de CANON.md incumplida — mejor razonado que la nota que lo sustituia. Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 69 errores / 57 avisos = linea base exacta, ninguno en lo tocado · docs:check 0/0 · prettier limpio en todo lo del commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-09 — **Fase 5 del saneamiento: `blocks:check` endurecido** (el último
punto del plan de saneamiento sin empezar; el detalle de las fases 1–4 vive en
`AUDIT-blocks-ledger.md`, que es la fuente viva desde el 2026-08-05). Tres
reglas nuevas, cada una con su fixture negativo en el `selfTest()` que el guard
corre antes de escanear:
1. **Lista blanca de importaciones (B-4)** — la frontera dura 1 deja de ser sólo
prosa: `$uix`, los arts públicos, `$libs/forms`, `svelte` y los relativos del
propio block; lo demás es error con el especificador por delante. **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. 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; escritas leyendo la fuente
de cada block (cierra A-14, que sólo pedía tres).
3. **Ficha en el catálogo de demos (B-9)** — el árbol y
`web/routes/blocks/_lib/catalog.ts` 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.
Verificado **en rojo sobre el árbol real**, no sólo contra los fixtures: un
`zod` y un `$libs/days` metidos en `hero/types.ts` salen señalados con su línea
exacta mientras el mismo import dentro de un comentario NO salta; el catálogo
falla en las dos direcciones; y al romper `isAllowedSpec` a propósito, el
self-test aborta el guard en vez de dar verde. Gates: `blocks:check` verde
(15 blocks / 114 ficheros) · `vitest src/uix/blocks` 20/20 · `docs:check` 0/0 ·
`svelte-check` 74E/54W = la línea base medida justo antes.
docs(blocks): la variedad se firma antes de construirla — F2b en cuatro tramos El usuario señaló que el dossier de referencias nos dejaba muy por debajo en variedad. El contraste fila a fila de su §P1 contra los 15 blocks dio 6 filas hechas y 4 resueltas por composición: lo que quedaba pendiente eran DECISIONES de julio, no medidas. Ninguna de las ocho variantes era un hallazgo nuevo — todas estaban ya en el README de su block esperando una firma. Pero ocho variantes no cierran la brecha que se ve, que es de CARDINALIDAD, y el método para esa ya estaba escrito en el dossier (P6.1: demostrar la equivalencia variante-a-variante en vez de competir en número de dumps). La primera versión de la sección no lo trajo y se reescribió entera el mismo día. F2b queda con cuatro tramos: A las variantes de suelo, B la matriz de equivalencia por block —el entregable central, con su plantilla y el guard encendido al final para no dejar 18 READMEs en rojo durante la ola—, C el catálogo que se reabre y D la página compuesta. La regla de forma gana el término que faltaba y que más trabajo hace: una variante alcanzable componiendo es una RECETA demostrada, cero código. Sin él cada fila de las referencias parece pedir un layout nuevo y el tier engorda por imitación. Tres decisiones de catálogo: logo-cloud vuelve (se difirió sobre una premisa que las refs desmienten, y su valor propio es la tinta monocroma derivada de tokens, que nadie más puede dar); blog entra por la misma puerta que team; cookie-consent entra con doctrina propia, porque la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción. onboarding NO entra: es un wizard con otro nombre, y duplicar función es la inflación que este modelo existe para evitar. Y el cierre de F2 deja de mentir: cerrada EN UNIDADES. La página compuesta que exigía nunca se construyó, así que el claim de que los blocks ensamblan sin fricción se declara sin demostrar en vez de darse por probado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **F2b establecida (firma pendiente)**. Contraste fila a fila de
la tabla §P1 del dossier contra los 15 blocks construidos, leyendo el
`types.ts` y los Gaps del README de cada uno: de sus 11 filas, 6 hechas y 4
resueltas por composición / app-land. El resto es la ola **V1…V8**, con su
regla de forma (misma anatomía → `layout`; otra anatomía → hermano; una
variante que obedece al estado de su sección → PARTE, por B-10) y su orden
(baratos primero, `Pricing.Compare` el último). Ninguna fila es un hallazgo
nuevo: las ocho estaban registradas en el README de su block desde julio —
lo que faltaba era la decisión, no la medida. Añadido también el **orden
recomendado de F3**, con `app-shell` primero porque desbloquea A-95 del
ledger y F4.1. La tabla de estado de §0 decía «F0 pendiente (nada
construido)» y F2 «pendiente»: corregidas al estado real.
- 2026-08-17 — **F2b REESCRITA y FIRMADA** el mismo día. La primera versión
(entrada anterior) atacaba sólo el suelo de §P1 con ocho variantes, y eso NO
cierra la brecha que motivó la revisión: la de **cardinalidad percibida**. El
método para esa brecha ya estaba en el dossier (P6.1 — demostrar la
equivalencia variante-a-variante en vez de competir en número de dumps) y no
se trajo. La ola pasa a cuatro tramos: **A** variantes de suelo · **B** la
matriz de equivalencia por block (el entregable central) · **C** el catálogo
que se reabre · **D** la página compuesta. La regla de forma gana su término
que faltaba y que más trabajo hace: **variante alcanzable componiendo →
RECETA demostrada, cero código** — sin él el tier engorda por imitación.
Fichas corregidas: **V1** con sus dos decisiones de diseño destapadas (quién
declara las columnas si `.Plans` y `.Compare` conviven; el colapso móvil) y
tanda propia; **V2** deja de venderse como «misma anatomía» (gana `logo`,
pierde la `Card`) aunque sigue siendo `layout`; **V3** la unión discriminada
alcanza también a `value`/`disabled` de `.Item`; **V4** tiene que decidir si
el formulario REEMPLAZA a `actions`, como en las referencias; **V8** se MIDE
antes de declararse gratis (`PricingPlansProps` es un `Pick` cerrado);
**V6/V7 salen de la ola** a F5 — el dossier declara `stats-band` «Paridad
OK», son adoptables y no suelo, y meterlas diluía E-2.
**Tres decisiones firmadas**: **`logo-cloud` REABIERTO** como block estático
(su premisa de diferimiento no casa con las refs; el valor propio es la tinta
monocroma derivada de tokens, y `Marquee` queda como preset de motion, no
como block) · **`blog` ENTRA** (misma naturaleza que `team`, que ya entró) ·
**`cookie-consent` ENTRA con doctrina propia** (no es una sección: overlay
legal con estado, y la ley europea vive en el TIPO — rechazar al mismo nivel
que aceptar, no esenciales apagadas por construcción) · **`onboarding` NO
entra**: es un `wizard` con contenido de bienvenida, y duplicar una función
con otro nombre es la inflación que este modelo existe para evitar.
**La página compuesta entra como puerta de cierre de F2b** y el cierre de F2
queda enmendado: cerrada EN UNIDADES, con el claim de integración declarado
sin demostrar en vez de dado por probado.
- 2026-08-17 — **F2b: fichas de ejecución escritas** (las cinco que faltaban
para que la ola fuera plan de implementación al nivel de F2): plantilla de
la matriz en el tramo B (5 columnas; vive como `## Equivalencias` en el
README de cada block; `blocks:check` la exige como ÚLTIMA tanda del tramo,
verificada en rojo) · fichas N1 `logo-cloud` (⚠️ la tinta monocroma se
promociona al CANON antes del block — un block no posee CSS), N2 `blog`
(fechas por `FormatDate`/`RelativeTime`; `<article>` choca con F21 → fase 0
decide `as` en `Card` o gap declarado), N3 `cookie-consent` (la ley en el
TIPO: `onRejectAll` requerido, misma prominencia por construcción, no
esenciales apagadas; `Escape` no decide) · ficha de la página compuesta
(`web/routes/blocks/landing/`, criterio de NATURALIDAD — los que no caben
quedan registrados como no-compuestos, la segunda página es Gap con
disparador; verificación con medida por punto: outline, landmarks —los dos
`role="banner"`—, costuras de `sectionSize`, cascadas por viewport, A-95,
sonda de sema, teclado entero).
- 2026-08-17 — **F2b · V4 `form-in-hero` HECHA** (primera fila de la ola). Slot
`form` en `hero`: el app compone `Form` + `Field` + `Form.Submit` enteros y el
block sólo da sitio y medida — **no toma el handle del formulario**, porque
leer la validez de la sección lo convertiría en un block que COORDINA y
tendría que poseer máquina y palabras; un hero no coordina nada. Medido en
Playwright headless sobre píxeles pintados: tinta escrita 15.88:1 claro /
16.28:1 oscuro · relleno del campo contra el lienzo de `background` 9.61:1 ·
la fila colapsa a 375 (campo 325 / botón 325) y comparte a 1280 (335 / 131,
misma fila) · medida propia 480px en `center` contra sección de 1264 y 472px
en `split` (= la columna: el cap `sm` es inerte ahí) · Enter llega al handler
del app y un correo inválido lo para con el error traducido y `aria-invalid` ·
RTL espeja la fila. **Dos gaps de canon registrados en el README del block**:
`Form.Submit` no reenvía al `Button` ni `intent` ni eje de anchura (el botón
no llena su celda al colapsar; `newsletter` mide lo mismo, y el `justify` de
`Grid` es `justify-content`, así que no se arregla desde fuera) — la demo usa
lo que hay (`style`, que sí se reenvía). **Y el contexto de inversión mordió
por segunda vez**: un `Text` sin `color` explícito bajo `background` mide
1.62:1 contra 10:1 con `on-solid`. ⚠️ Anotada además la lección de
INSTRUMENTO: dos sondas dieron números seguros y falsos antes de la buena
(parser rgb sobre un sistema que emite `oklch`, y canvas `fillStyle` que
tampoco normaliza oklch aquí → medía NEGRO contra todo); la señal de que la
sonda es honesta es que la etiqueta del submit sale a 5.18:1, la cifra que el
repo ya documenta para `primary`.
blocks(pricing): un plan solo no se sostenía — la fila necesitaba un tope Segunda fila de la ola F2b, y la que existía para comprobar una sospecha. El README llevaba desde julio diciendo que el plan único «ya lo cubre el layout», diferido por eso. La ficha exigía medirlo antes de darlo por gratis, y no lo cubría. La causa está en la lista de pistas, no en cuántos planes hay: la fila es repeat(auto-fill, minmax(17rem, 1fr)), y auto-fill RESERVA toda pista que quepa, la ocupe alguien o no. Medido a 1280 sobre 992px de fila, un plan solo daba una tarjeta de 315px pegada al borde inicial con 677px de vacío al lado; fijar columns a 1 solo cambiaba eso por una tarjeta de 992px, que es un cartel. Meterlo en un Container sm tampoco: a 640px vuelven a caber dos pistas. Lo que faltaba era un tope sobre la FILA, y la parte no lo dejaba pasar: sus props están cerradas a propósito desde A-94, porque el envoltorio esparce el resto antes de las props que fija y un width del consumidor tipaba y se descartaba en silencio. Así que se abre nombrando: maxWidth entra en el tipo y viaja explícito al AutoGrid, que es exactamente lo que A-94 dejó dicho — el tipo dice lo que la parte honra. El marginX auto va con él sin condición: sin tope la fila ya llena la medida y auto no resuelve a nada. El número lo pone la app. Un block que horneara uno estaría decidiendo cuánto mide un plan; la demo pasa 28rem para uno y 44rem para dos, que es lo que las referencias le dan a cada arreglo, y gana un control vivo de recuento que también viaja en la URL de la vista previa. Medido después: un plan 448px centrado, dos planes 704px en dos pistas de 340 centradas, y tres planes 992px idénticos a antes — el tope es opt-in y no toca el caso que ya existía. A 375 es inerte, porque allí una sola pista ya toma el ancho. Queda anotada la lección en el README y en el handoff, porque no es de este block: una fila «diferida» puede ser una medición equivocada y no un aplazamiento. Nadie había mirado pricing con menos de tres planes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **F2b · V8 `single-price` HECHA, y la ficha acertó al exigir que
se midiera ANTES**: «un solo `.Plan` y el layout ya lo sostiene» —la
disposición que el README del block llevaba desde julio— era falsa. La fila es
`repeat(auto-fill, minmax(17rem, 1fr))` y **`auto-fill` reserva toda pista que
quepa**, la ocupe alguien o no: medido a 1280 sobre 992px de fila, un plan solo
daba una card de 315px pegada al borde con 677px de vacío al lado, y
`columns={1}` sólo cambiaba eso por una card de 992px. Ninguna de las dos es la
card centrada de las referencias. Resuelto abriendo `.Plans` a **`maxWidth`**
(tope de la FILA, con `marginX="auto"` incondicional para centrarla),
reenviado NOMBRADO al `AutoGrid` — nunca por `{...rest}`, que es justo lo que
A-94 dejó dicho. El número lo pone el app: la demo pasa `28rem` para un plan y
`44rem` para dos. Medido después: 448px centrados con uno, 704px (dos pistas de 340) con dos, y **992px idénticos con tres** — el tope es opt-in y no toca el
caso que ya existía; a 375 es inerte. Control vivo nuevo en la demo (nº de
planes) y en la URL del preview. ⚠️ Lección que se queda: **una fila
«diferida» puede ser una medición equivocada, no un aplazamiento** — nadie
había mirado este block con menos de tres planes.
- 2026-08-17 — **F2b · V5 `cta` split-with-media HECHA**. Tercer `layout` +
slot `media`, con dos decisiones que la ficha no traía: la copia **conserva
sus acciones en la MISMA columna** (separarlas dejaría el botón bajo la
captura, lejos de la frase que lo pide) y **`split` sin `media` cae a
`justified`**, no a una rejilla con una pista vacía — la regla que ya sigue el
`split` del `hero`. Medido: a 1280 dos pistas de 436px en un panel de 992; a
375 una pista, con el CTA POR ENCIMA de la media al apilar; idéntico en claro
y oscuro. Demo: `split` en el control de layout + un `Mockup` en el slot.
- 2026-08-17 — **`Card` gana el eje `lift`, y `pricing` lo reenvía** (petición del
usuario: «¿por qué no hay hover en la card del precio? debería estar al menos
como opción»). Empezó con una afirmación mía FALSA —«`Card` no expone estado de
puntero»—: el realce existía desde siempre, con sus dos tokens en la fundación,
pero **colgaba entero de `[data-interactive]`**, que promueve la tarjeta a
`<button>` y emite `commit-select`. Una tarjeta de plan lleva su CTA dentro, así
que ese paquete es inaplicable: `<button>` en `<button>`. La elevación pasa a su
propio attr **visual-wrapper** (`data-lift`, fuera del morfo por la doctrina del
2026-08-15, con fila en `component-visual-attrs.test.ts` — la primera de `card`);
`interactive` lo implica, así que nada cambia para una tarjeta clicable. Sin
tokens nuevos. `PricingPlanProps` gana `lift` y la demo su control vivo. Detalle
y medidas: `theming/changelog.md` §49.
**Y destapó un hueco del propio guard**: el comentario que explica por qué NO se
usa un nativo nombra `` `<button>` ``, y **B-2 leía comentarios como código** —
su barrido quitaba sólo `<!-- … -->` de UNA línea, mientras `stripComments`, que
maneja todas las formas y conserva los saltos, ya existía tres líneas más abajo
y se usaba sólo para los imports. Es la lección de la fase 5 aplicada a una regla
y no a la otra. Arreglado, con **dos** fixtures nuevos (nativo nombrado en
comentario multilínea = prosa · nativo REAL debajo de un comentario multilínea =
error) y **probado en rojo sobre el árbol real**: un `<button>` inyectado en
`hero.svelte` sale con su línea exacta (158) y el árbol se restaura después.
blocks(pricing): el plan dice dónde estás, y el que no puedes elegir dice por qué Firma del usuario: faltaba la decoración del elegido, y faltaba poder marcar un plan como no disponible — si estoy cambiando de plan, el que ya tengo no debería poder seleccionarse. Lo segundo cambia la posición doctrinal del block. Su README decía desde julio que nada en una tabla de precios puede bloquearse y que por tanto no hay nada que explicar. Eso vale mientras la sección es sólo una comparación; se cae en cuanto el visitante ya está en un plan. Y ahí manda lo que costó contact: una acción bloqueada debe su frase, o es un agujero — algo que no se puede hacer y nada en pantalla diciendo por qué. El plan gana un estado: disponible, actual o elegido. Un enum y no tres booleanos, porque tres booleanos admiten lo que no existe, como elegido y no disponible a la vez. Y ortogonal a featured, que es la recomendación de quien vende: el plan recomendado puede ser justamente el que ya tienes. Se deriva una sola vez en el plan y viaja por contexto. La acción se lo entrega al botón del app por parámetro de su snippet, así que nadie vuelve a deducir en el punto de uso si se puede pulsar, y las palabras del botón siguen siendo del app. La frase, en cambio, es del block: idlangref con respaldo en inglés dentro de un Record exhaustivo, el mismo mapa que bloquea la acción es el que escribe el motivo, así que un estado que bloquea en silencio no se puede ni expresar. Su test lo afirma en las dos direcciones. Y una parte nueva la dice y posee el id al que apunta el aria-describedby del botón deshabilitado, sin renderizar nada cuando no hay nada que decir. La decoración la pone el canon: el elegido reenvía el anillo de Card, el actual su tratamiento deshabilitado. Nada de un data-pricing-selected propio: sería un atributo que nadie lee, porque un block no pinta CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale con cursor de no permitido, botón deshabilitado y aria-describedby que casa con el id de la frase, traducida por el arnés; al elegir otro, su tarjeta gana el anillo de dos píxeles con el primary de su paleta y las demás no. Queda anotado un gap del canon que salió al medir esto y que no toco: Card disabled nunca atenúa. La regla existe y su cursor sí aplica, pero la animación de montaje fija la opacidad en uno con prioridad de animación y se come la del estado; aislado con no-emerge, atenúa. Es el patrón que la doctrina de motion llama frágil-conocido —dos dueños peleándose por una propiedad en un nodo— y alcanza a toda Card deshabilitada del ecosistema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **`pricing` gana el estado del plan y, con él, PALABRAS** (firma del
usuario: la decoración del elegido y la propiedad de «no disponible» para
upgrade / downgrade). Su README decía lo contrario desde julio —«nada en una
tabla de precios puede ser bloqueado, así que no hay nada que explicar»— y eso
se cae en cuanto el visitante ya está EN un plan: el actual no se puede elegir.
Ahí manda la lección de `contact`: **una acción bloqueada debe su frase**.
Forma: **`.Plan state`** = `available | current | selected`, un enum y no tres
booleanos (permitirían «elegido y no disponible» a la vez), **ortogonal a
`featured`** —que es la recomendación de quien vende, no dónde está quien
compra—; el estado se deriva UNA vez en `.Plan` y viaja por contexto; `.PlanAction`
se lo entrega al `Button` del app por parámetro del snippet, así que nadie
re-deduce un `disabled` en el punto de uso y las palabras del botón siguen
siendo del app (B-7); **`.PlanReason`** (parte nueva) dice la frase y posee el id
al que apunta el `aria-describedby` del botón bloqueado — y no renderiza nada
cuando no hay nada que decir. Las palabras son del block, idlangref con fallback
inglés en un `Record` EXHAUSTIVO (`plan-state.ts` + `plan-state.test.ts`, que
afirma en las dos direcciones: todo estado que bloquea tiene frase, y todo
estado que no bloquea calla). **La decoración es del CANON**: `selected` →
el anillo de `Card`, `current` → su tratamiento deshabilitado. Nada de
`data-pricing-selected`: sería un attr que nadie lee, porque un block no pinta
CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale
con `cursor: not-allowed`, botón deshabilitado y `aria-describedby` que CASA con
el id de la frase traducida; al elegir otro, su tarjeta gana el anillo primary
de 2px y el resto no.
⚠️ **Gap de canon medido aquí, sin tocar: `Card disabled` NUNCA atenúa.** La
regla existe y su `cursor: not-allowed` sí aplica, pero la animación de montaje
(`card-emerge`, `fill-mode: both`) fija `opacity: 1` con prioridad de animación
y se come la del estado — probado aislándolo: con `data-no-emerge` en el mismo
nodo, atenúa a 0.4. Es el patrón KNOWN-FRAGILE de la doctrina de motion (dos
dueños, una propiedad, un nodo) y alcanza a TODA `Card` deshabilitada del
ecosistema. Reparación natural: atenuar por `filter: opacity(...)`, que la
animación no posee, sin tokens nuevos. Espera firma.
blocks(testimonials): una cita sola no va en una tarjeta La brecha que el dossier le apuntaba a este block: la cita única es la variante modal de todas las referencias y el grid la minoría, dos de ocho en Tailwind Plus. Entra como disposición, no como hermano. La anatomía no cambia —cita, autor, avatar—, sólo cómo se colocan, y el precedente es el fondo del hero, que también redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide rotación o carrusel, que es otro componente. Viaja por contexto de configuración, el patrón del align de feature-grid y team, no la coordinación de pricing. Y viaja porque el spotlight no es una piel sobre las mismas cajas: la rejilla deja de serlo y de estampar el ritmo estructural —que es para una FILA, no para una cita—, el ítem pierde la tarjeta, la cita sube de tamaño y se centra, y el autor pasa debajo, donde ya no hay una hilera de caras que alinear. Pedirle a la app que repita esa decisión en cinco partes es el agujero que A-84 cerró en feature-grid. Enmarcar una cita sola la hace parecer un ítem de una lista que no está ahí, así que ahí no hay tarjeta y el tipo lo dice, en vez de aceptar una superficie que luego descarta. La cita se centra con un párrafo de verdad: sobre una caja en línea el centrado no aplica, y sería mentira. Slot nuevo para la marca de quien firma, en las dos disposiciones: es lo que hace que una cita se lea como evidencia y no como una opinión, y es la pieza que ponen todas las referencias. Medido: en grid, cinco tarjetas, veinte píxeles, medida de mil veinticuatro y ritmo escalonado; en spotlight, cero tarjetas, una cita de veintiocho centrada en párrafo, medida de setecientos sesenta y ocho, autor centrado y sin escalonado; a 375 la cita cae a veinticuatro. El grid queda idéntico a antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **F2b · V2 `testimonials` spotlight HECHA**. La brecha del dossier
para este block: la cita única es la variante MODAL de las referencias y el grid
la minoría (2 de 8 en TW). Entra como **`layout="grid" | "spotlight"`**, no como
hermano — la anatomía no cambia (cita · autor · avatar), sólo la disposición, y
el precedente es el `background` del `hero`, que también redefine lo que hacen
sus partes; el hermano se reabre SÓLO si un día pide rotación o carrusel, que es
otro componente. **Viaja por contexto de CONFIGURACIÓN** (el patrón del `align`
de `feature-grid`/`team`, no la coordinación de `pricing`) porque `spotlight` no
es una piel: `Items` deja de ser rejilla **y de estampar `data-stagger`** —el
ritmo estructural es para una FILA—, `Item` **pierde la `Card`** —enmarcar una
cita sola la hace parecer un ítem de una lista que no existe—, `Quote` sube a
`xl` y se centra **con `as="p"`** (F17: sin él el centrado sería mentira) y
`Author` pasa debajo. La medida de la sección se estrecha a `md`. Slot nuevo
**`logo`** en `.Item`, en las dos disposiciones: la marca de quien firma es lo
que hace que la cita se lea como evidencia. Medido: grid → 5 tarjetas, 20px,
container 1024, stagger; spotlight → **0 tarjetas**, una cita de 28px centrada en
`<p>`, container 768, autor centrado, sin stagger; a 375 la cita cae a 24px. El
grid queda idéntico.
⚠️ **Aviso de árbol compartido**: `svelte-check` marcó 73 durante esta tanda y
ninguno es del tier — otra sesión está a medias con un componente `background`
(morfo, langs y `eidos/generated` tocados). Medido con todo en stash: la base
real sin nadie es 60. **No volver a hacer `git stash --include-untracked` en este
árbol**: arrastra el trabajo sin commitear de la otra sesión.
blocks(faq): la lista estática, y un tipo que deja de prometer lo que no da La brecha del dossier para este block, y la mayoría del formato: seis de siete en Tailwind Plus no son acordeón, sino una rejilla de preguntas y respuestas siempre abiertas. El eje vive en la LISTA, no en la raíz. Primero porque la lista es lo único que cambia —la cabecera se lee igual—, y segundo porque esa colocación es la que permite que el tipo diga la verdad: pasa a ser una unión discriminada. Bajo acordeón atraviesa toda la superficie del Accordion del canon; bajo lista no se ofrece ninguna de esas props, porque una rejilla estática no tiene estado abierto que enlazar, nada que colapsar ni modo simple o múltiple. Ofrecer una superficie y descartarla en silencio es justo lo que A-94 dejó dicho que no se hace, y aquí es donde iba a morder después. El ítem lee la disposición del contexto: no puede discriminar sobre la unión de la lista sin obligar a la app a repetir el arreglo en cada pregunta. Su value y su disabled quedan documentados como del acordeón, la misma convención que el fondo del hero y la media del cta. Y bajo lista la pregunta es un encabezado de verdad, porque ahí ES el encabezado de su respuesta; bajo acordeón el canon la mete dentro del disparador y esa semántica es suya, así que el block no inventa una segunda. Medido a 1280: en acordeón, cinco disparadores, ningún encabezado, medida de setecientos sesenta y ocho y respuestas ocultas hasta pulsar; en lista, ningún disparador, cinco encabezados, tres pistas de trescientos nueve, medida de mil veinticuatro y todas las respuestas visibles sin tocar nada. A 375 cae a una columna conservando ambas cosas. El acordeón queda idéntico. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **F2b · V3 `faq` lista estática HECHA**. La brecha del dossier y la
mayoría del formato: **6 de 7 en TW no son acordeón**. Entra como
**`.List layout="accordion" | "list"`**, y el sitio del eje importa: vive en la
LISTA, no en la raíz, porque la lista es lo único que cambia —el header se lee
igual— y porque esa colocación es la que permite que **el TIPO diga la verdad**.
`FaqListProps` pasa a ser **unión discriminada**: bajo `accordion` atraviesa
toda la superficie del `Accordion` del canon; bajo `list` **no se ofrece
ninguna**, porque una rejilla estática no tiene estado abierto que enlazar, nada
que colapsar ni modo simple/múltiple. Es A-94 aplicado donde iba a morder
después. El `Item` lee la disposición del CONTEXTO —no puede discriminar sobre
la unión de la lista sin obligar a la app a repetir el arreglo en cada
pregunta— y sus `value`/`disabled` quedan documentados como del arreglo
`accordion`, la misma convención que el `backdrop` del `hero` y el `media` del
`cta`. **Bajo `list` la pregunta es un `h3` de verdad**: en una lista estática ES
el encabezado de su respuesta, mientras que bajo `accordion` el canon la mete en
el disparador y esa semántica es suya — el block no inventa una segunda. Medido
a 1280: accordion → 5 disparadores, 0 encabezados, medida 768, respuestas
ocultas; list → 0 disparadores, **5 `h3`**, tres pistas de 309px, medida 1024 y
**todas las respuestas visibles sin pulsar**; a 375 cae a una columna
conservando ambas cosas. El acordeón queda idéntico.
blocks(pricing): la tabla de comparación, y las palabras que una marca no dice La brecha recurrente del dossier en pricing, y la última fila del primer tramo de la ola. Es una PARTE y no un hermano porque tiene que obedecer al periodo de la sección, y sólo una parte puede leer ese contexto: B-10 le prohíbe a un hermano importar el block que lo posee. Vivir dentro de la sección es además lo que permite que la app meta el precio en una celda y cambie con el conmutador, sin que esta página cablee nada. La app construye la tabla y el block la compone. B-5 admite datos justo donde el canon compuesto ya es data-driven, y la tabla del canon lo es: exige la instancia que devuelve createTable. Así que la parte no inventa un formato de matriz propio ni traduce nada de ida y vuelta, que es el acoplamiento que esa regla existe para evitar. Las tarjetas y la tabla conviven, como en las referencias, alimentadas por el mismo array de planes. Dos cosas que el plan había esbozado y que construirlas demostró equivocadas, y quedan escritas en vez de calladas. No hay un Record de ejes visuales por plan: acentuar una columna es componer canon, que es justo lo que ya permite el snippet de cabecera, y ese Record habría sido una segunda fuente de lo que la app declara en su plan. Y el block no fija la columna: el motor es del app, y meter mano ahí sería el block decidiendo cómo se comporta la tabla de otro. Lo que sí posee es lo que la app no puede: el nombre accesible de la tabla y las palabras que una marca de visto o de guion no dice en voz alta. Ese glifo lleva todo el significado de la celda y es silencio para un lector de pantalla, así que va oculto y la frase viaja en su etiqueta, desde un mapa exhaustivo con respaldo en inglés y su test en las dos direcciones. Y absorbe el cast del canon siendo genérico: la tabla tipa su ranura sin parámetro mientras el motor sí lo devuelve, así que todo consumidor castea, empezando por la demo del propio componente. Aquí se castea una vez a la entrada y se deshace a la salida, para que el snippet de celda devuelva la fila con la forma del app. Medido a 1280 y 375: ocho filas por cuatro columnas, tabla nombrada, marcas que se anuncian, y en móvil la tabla desplaza con la columna de características quieta y la última columna alcanzable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **F2b · V1 `Pricing.Compare` HECHA — tramo A CERRADO (6/6)**. La
brecha recurrente del dossier en pricing. **PARTE y no hermano**: tiene que
obedecer al periodo de la sección y sólo una parte lee ese contexto (B-10 le
prohíbe a un hermano importar el block que lo posee) — vivir dentro de
`<Pricing>` es también lo que permite que la app meta un `PlanPrice` en una
celda y cambie con el conmutador, sin cablear nada. **La app construye la tabla
y el block la COMPONE**: B-5 admite datos justo donde el canon compuesto ya es
data-driven, y `Table` lo es (exige la instancia de `createTable`), así que
`.Compare` no inventa formato de matriz ni traduce. `.Plans` y `.Compare`
CONVIVEN, alimentados por el mismo array.
⚠️ **Dos cosas que el plan sketchó y construirlas demostró equivocadas**,
registradas y no calladas: (1) **sin `Record` por plan** — acentuar es componer
canon, que es lo que ya permite el snippet `columnHeader`; el `Record` habría
sido una segunda fuente de lo que la app declara en su `.Plan`; (2) **el block
NO pinnea**: el motor es del app y mutarlo desde el block sería decidir cómo se
comporta su tabla. La app declara `enablePinning` + `initialColumnPinning`.
**Posee** el nombre accesible de la tabla y las palabras que una marca ✓/— no
dice (`PRICING_COMPARE_VALUE`, `Record` exhaustivo con test en las dos
direcciones; el glifo va `aria-hidden`). **Y absorbe el cast del canon** siendo
genérico: `Table` tipa `TableInstance<unknown>` mientras `createTable<T>`
devuelve `TableInstance<T>` — todo consumidor castea, su propia demo incluida —,
así que `.Compare` castea UNA vez a la entrada y lo deshace a la salida para que
el snippet `cell` devuelva la fila en la forma del app.
**Dos defectos de CANON salieron de aquí** (`theming/changelog.md` §51): la
tabla **no tenía escape horizontal** —a 375 medía 420 en un contenedor de 327 y
93px eran inalcanzables, con `overflow: hidden` en los dos ejes— **ARREGLADO**
(`overflow-x: auto` + `overflow-y: hidden`, guard probado por mutación); y **la
columna fijada no aguanta en RTL** —se ancla con `left` FÍSICO y al desplazar
(`scrollLeft` −94) su borde pasó de 350 a 444 y salió de pantalla— **FILADO**,
porque es el eje de dirección y tiene doctrina propia. Además, `Table` **no
exportaba los tipos que su API exige**: el guard B-4 rechazó `$libs/datagrid`
por nombre y tenía razón — se corrige exponiéndolos desde el canon, no
ensanchando la frontera.
⚠️ **Nota de proceso, segunda reincidencia**: pasé prettier por
`docs/theming/changelog.md` otra vez y reformateó 400 líneas ajenas. Restaurado
y reaplicado a mano. **Ese fichero no se formatea.**
blocks(tramo B): las quince matrices, y el guard que ya las exige El entregable central de la ola queda cerrado. Cada block declara ahora, en su propio README, qué composición nuestra alcanza cada variante que shippean las referencias, con la receta exacta, dónde se ha visto y en qué estado queda. Y la regla que gobierna la tabla no admite promesas: una receta que no se ha visto en el navegador no entra. El saldo es lo que la ola buscaba desde que se abrió la pregunta. Unas noventa y cinco filas cubiertas con receta y prueba. Cuatro superaciones con nombre, que ninguna referencia puede expresar porque shippean markup estático: el plan actual que no se puede elegir, las dos secciones que explican por qué un envío está bloqueado, y las cifras que cuentan respetando la preferencia de movimiento. Una veintena de gaps, cada uno con su nombre: la mitad son recetas que existen y no se han enseñado, que es trabajo de demo, y la otra mitad candidatos de canon que ya conocíamos. Y ocho «no se ofrece» con su motivo escrito, que es una postura del sistema y no un agujero — un carrusel en la apertura es movimiento automático sin control de pausa, un mapa es una dependencia externa, filtrar preguntas es estado de datos del app. La primera tabla justificó el tramo entero. El hero declaraba desde julio que el mockup de móvil estaba hecho, y la demo sólo había enseñado el cromo de navegador. El primitivo ofrece tres. Se añadió el control, se midió, y sólo entonces se escribió la fila: una receta declarada no es una receta demostrada, y separarlas es exactamente para lo que este tramo existía. El guard exige ya la sección, encendido al final como mandaba el plan —hacerlo antes habría dejado quince readmes en rojo toda la ola— y con su fixture negativo. Cambiar la regla hizo caer tres fixtures antiguos, que es el self-test haciendo su trabajo, y se actualizaron. Probado en rojo sobre el árbol real: quitándole la sección a team sale su línea exacta, y restaurada vuelve a verde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-17 — **F2b · TRAMO B CERRADO: las 15 matrices de equivalencia + el guard
encendido**. El entregable central de la ola: por cada block, una sección
`## Equivalencias` con cinco columnas (variante de la referencia · quién la
shippea · **la receta exacta** · dónde se ha visto · estado), y la regla dura
operando — **una receta que no se ha visto en el navegador NO entra en la
tabla**. Saldo: ~95 filas, de las que **4 son superación** con nombre (el plan
actual no elegible de `pricing`, la explicación del envío bloqueado de
`newsletter` y de `contact`, y las cifras que cuentan de `stats-band`: ninguna
referencia puede expresarlas porque shippean markup estático), **~20 gaps con
nombre** —la mitad son «la receta existe y falta enseñarla», que es trabajo de
demo, y la otra mitad candidatos de canon ya conocidos (`visually-hidden`,
`layout-primitive-element`, `card-static-elevation`, `scroll-hide`)— y **8 «no
se ofrece» con su motivo escrito**, que es una postura del sistema y no un
hueco: el carrusel en el hero (movimiento automático sin control de pausa), el
mapa en `contact` (dependencia externa), el buscador de `faq` (estado de datos
del app)…
⚠️ **La primera tabla ya justificó el tramo**: el `hero` declaraba en sus Gaps
desde julio que el mockup de móvil estaba HECHO, y **la demo sólo había
enseñado el cromo de navegador**. `Mockup` ofrece tres; se añadió el control, se
midió (`data-chrome="phone"`, 320px) y sólo entonces se escribió la fila. Una
receta declarada no es una receta demostrada: eso es lo que este tramo separa.
**El guard exige ya la sección** (`README_SECTIONS` gana `## Equivalencias`),
encendido AL FINAL como el plan mandaba —encenderlo antes habría dejado 15
READMEs en rojo toda la ola—, con fixture negativo propio, los tres fixtures
antiguos actualizados (el self-test los cazó él solo al cambiar la regla) y
**probado en rojo sobre el árbol real**: quitándole la sección a `team` sale su
línea exacta, y restaurado vuelve a verde.
blocks(logo-cloud): la banda de marcas vuelve, y su parte difícil no era la disposición Se difirió en F2 sobre una premisa que las referencias desmienten —que su versión honesta pedía un marquee o quedaba en un wrap trivial—: Tailwind Plus, Untitled y Flowbite lo shippean estático. Una decisión cuya premisa cae se corrige. Pero el argumento para construirlo no es la paridad. Lo difícil de un muro de logos es la tinta: veinte marcas en veinte paletas gritan sobre la página y entre ellas. Ese tratamiento acaba de entrar en la fundación como contexto, así que el bloque queda fino y es exactamente la forma correcta — cuando lo difícil es un tratamiento, el tratamiento va a la fundación y el bloque sólo lo estampa. Aquí no hay una línea de CSS, que es además lo que el contrato exige. Dos decisiones medidas, no elegidas. Las marcas van en un wrap y no en una rejilla, porque llegan con proporciones dispares y unas pistas iguales dejan huecos irregulares alrededor de las estrechas. Y la parte del ítem se gana su sitio defaulteando el eje de bloque, que es lo único que un logo nunca trae consigo: un vectorial carga su viewBox, un raster sus píxeles, y uno al lado del otro aterrizan a cinco alturas distintas. Verificado con ocho marcas de anchos y colores deliberadamente dispares: las ocho a una sola altura conservando sesenta y cuatro píxeles de diferencia de ancho, que es cada una guardando su proporción. El marquee se queda como hueco con disparador y con su forma decidida: si algún día entra será un preset de movimiento sobre la fila, no un bloque nuevo, y con la preferencia de movimiento reducido parándolo. Y el hueco de la utilidad para nombres sólo audibles sale aquí por tercera vez en la ola. Tres consumidores reales lo piden ya. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-18 — **F2b · C · N1 `logo-cloud` HECHO, y el eje de tinta con él**. El
block se difirió en F2 sobre una premisa que las referencias desmienten («su
versión honesta pide `Marquee`»): TW (6), Untitled y Flowbite lo shippean
ESTÁTICO. Reabierto — pero el argumento para construirlo no es la paridad: **la
parte difícil de un muro de logos es la TINTA, no la disposición**.
**Ese tratamiento fue al CANON, a la fundación, no al block ni a `Image`.**
`data-ink` es el hermano de `data-on`: uno re-entinta porque cambió el LIENZO,
el otro porque el CONTENIDO son marcas ajenas. Medido antes de elegir sitio:
`Image` **no tiene eje de tinta** (ni prop ni regla) y tampoco era su casa — es
un componente con máquina de carga para fotos, mientras que un logo suele ser un
`<svg>` en línea, y el mecanismo es DOBLE (`currentColor` para vectorial,
`filter` para raster). Un primitivo `Logo` nuevo se descartó por lo mismo: no
habría cargado más que el contexto. **Cero tokens nuevos** (`--opacity-muted` y
`--opacity-full` ya estaban en el tier semántico de la escala). Detalle y
medidas: `theming/changelog.md` §52.
**El block queda FINO**, que es la forma correcta cuando lo difícil es un
tratamiento: estampa `data-ink` en su sección y coloca un `Wrap`. `.Items` es
`Wrap` y **no rejilla** —medido: proporciones dispares dejan huecos irregulares
en pistas iguales— y `.Item` se gana la parte **DEFAULTEANDO** el eje de bloque,
lo único que un logo nunca trae consigo. Verificado con 8 marcas de anchos y
colores distintos: `mono` → gris al 0.65 con la tinta del tema (y otra en
oscuro), `brand` → su color, puntero → color entero; **una sola altura (28px)
con 64px de diferencia de ancho**, que es cada marca guardando su proporción.
⚠️ **Dos lecciones**: (1) `renderBlock` del generador **no añade `;`** entre
declaraciones —cada una trae el suyo salvo la última—, y sin ellos las reglas
LLEGAN a la hoja y no pintan nada; anotado en el propio generador. (2) El hueco
**`visually-hidden`** sale aquí por TERCERA vez en la ola (`newsletter`,
`site-footer`, `logo-cloud`): es candidato de canon con tres consumidores reales.
blocks(article-grid): la banda editorial, y el elemento que el canon no podía dar Lo que las referencias llaman blog. Sus dos preguntas de fase cero quedan resueltas, y ninguna necesitó tocar el canon. El nombre. Blog nombra una sección entera de un sitio, y esto es la fila de artículos recientes dentro de otra página; el tier nombra por función de página y el elemento que renderiza es un article, así que article-grid hace coincidir nombre, función y markup. La palabra blog sobrevive donde un consumidor la busca: la primera línea del readme y la etiqueta del catálogo. El artículo semántico. Un teaser tiene que ser un article o se lee como una caja de texto suelto, pero la tarjeta del canon es un div fijo y eso parecía obligar a elegir entre añadirle un eje de elemento o declarar un hueco. Construirlo enseñó la tercera vía, que no cuesta nada: el bloque renderiza el article y compone la tarjeta dentro. El tier lleva desde su primer bloque renderizando sus propios elementos de seccionado, así que el elemento es del bloque y la superficie sigue siendo del canon. La regla amuralla al primitivo, no a la composición. Y tres decisiones más que sí son de diseño. La tarjeta entera no es un enlace, porque el teaser lleva además un chip y una firma y anidar interactivos dentro de un enlace es marcado inválido: es justo donde acaban las referencias que hacen clicable toda la tarjeta, así que el enlace vive en el título y hay exactamente uno por teaser. El realce al puntero viene encendido, al revés que en precios, porque un teaser sí es algo a lo que se va. Y el bloque no formatea fechas: las compone la app con el componente del canon. Dos errores míos en la tanda, los dos cazados mirando la captura y no la sonda. El ancho mínimo por defecto sólo cabía dos veces en la medida, cuando las referencias lideran con tres columnas; medido y corregido, ahora da tres, dos y una según el ancho. Y el segundo intento de arreglarlo no llegó a aplicarse sin que me diera cuenta, porque lancé el reemplazo sin aserción y el formateador había juntado las props en una línea: culpé a la caché del servidor antes de mirar el fichero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- 2026-08-18 — **F2b · C · N2 `article-grid` HECHO** (lo que las refs llaman
«blog»). Sus **dos decisiones de fase 0, resueltas**: (1) **el nombre** — «blog»
nombra una sección de SITIO y esto es la fila de artículos recientes DENTRO de
otra página; el tier nombra por función de página y el elemento que renderiza es
un `<article>`, así que `article-grid` hace coincidir nombre, función y markup.
«Blog» sobrevive donde se busca: primera línea del README y etiqueta del
catálogo. (2) **`<article>` vs F21 — sin tocar canon**: el plan reservaba elegir
entre añadir `as` a `Card` o declarar gap, y construirlo enseñó una tercera vía
sin coste: **el block renderiza el `<article>` y compone el `Card` DENTRO**. El
tier ya renderiza sus propios elementos de seccionado (`<footer>`, `<header>`,
`<section>`), así que el elemento es del block y la superficie del canon. F21
amuralla al primitivo, no a la composición.
Además: **la tarjeta entera NO es un enlace** —el teaser lleva chip y firma, y
anidar interactivos en un enlace es marcado inválido, que es donde acaban las
refs que hacen clicable toda la tarjeta—, el enlace vive en el título (1 por
teaser, medido); **`lift` viene ENCENDIDO** al revés que en `pricing`, porque un
teaser SÍ es algo a lo que se va; y **el block no formatea fechas** (la app
compone `RelativeTime`, verificado: «anteayer», «hace 6 días»).
⚠️ **Dos errores míos en esta tanda, los dos cazados MIRANDO la captura y no la
sonda**: el `minChildWidth` por defecto (`20rem`) sólo cabía DOS veces en la
medida `lg` —`(992+32)/(320+32) = 2.9`— cuando las refs lideran con tres; a
`18rem` mide 3/2/1 pistas en 1280/768/375. Y el segundo intento de arreglarlo
**no se aplicó y no me enteré**: lancé el `replace` sin `assert` y prettier había
juntado el destructuring en una línea. Culpé a la caché del dev server antes de
mirar el fichero. **Todo `replace` lleva `assert`.**

Powered by TurnKey Linux.