# 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.
- **Estado**: F0 pendiente (nada construido). Actualizar esta tabla al cerrar
cada tanda, estilo `PLAN-component-coherence.md` .
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
| 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) |
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** | Blocks de sitio (**14**: 10 + banner·team·contact·content-section por E-3) | pendiente (F2 solo requiere F1.1) |
| **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.
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
| # | 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. |
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
| **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). |
| **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.
docs(blocks): dossier de referencia — 6 pistas, suelos de paridad + enmiendas E-1..E-5
Encargo del usuario (2026-07-21): estudio de referencia para "estar al menos
a la par y cuanto menos superarlo". 6 agentes de investigación web en
paralelo sobre fuentes oficiales vivas (varias verificadas a nivel de código
fuente: sidebar.tsx del registry shadcn, styles.js de tailwind-typography,
sources de Mantine/Starlight/Docusaurus/Ark).
- docs/process/RESEARCH-blocks-references.md — el dossier: P1 catálogos de
marketing (conteos+variantes) · P2 aplicación (ProTable/ProLayout,
dashboard-01, Tremor 303, Clerk/Supabase) · P3 F1 ligeros (APIs a nivel de
prop; scroll-state(stuck) como vocabulario de plataforma; role=alert de
shadcn = bug a no copiar) · P4 prose+sidebar (not-prose/:where()+donut;
shadcn 23 partes) · P5 docs shells (Fumadocs benchmark; colision F1.7 vs
nav-tree/B-5; ruta svelte2tsx para props TS) · P6 ecosistema Svelte
(shadcn-svelte 58 blocks solo-app; hueco "sistema integrado" VACIO) +
sintesis: 5 conclusiones, enmiendas E-1..E-5 (presentadas, firmas
pendientes), 10 angulos de superacion.
- PLAN-blocks.md — regla nueva §4 (toda fase 0 contrasta contra el dossier),
puntero de colision en F1.7, registro.
Verificacion: docs:check 0/0 (512 docs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **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».
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
_(`logo-cloud` sigue en F5: su versión honesta pide `Marquee` o queda en un
`Wrap` trivial que no justifica block todavía — decisión anotada, revisable.
El resto de la unión del dossier — bento, gallery, cookie-consent, popups,
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
careers, events, comparison, timeline — queda en F5 con disparador, E-3.)_
**Cierre F2**: `blocks-check` verde; galería `web/routes/blocks/` con los 14;
una página compuesta de PRUEBA (header + hero + features + pricing + faq +
cta + footer juntos) que valide que los blocks ensamblan sin fricción — esa
página ES el test de integración del tier y se mira en claro/oscuro/375/1280.
---
## F3 — Blocks de aplicación (10)
Mismas reglas y ficha que F2. **Depende de** : F1.2/F1.3/F1.4 (estados),
F1.7 (`sidebar`) para F3.1.
### F3.1 `app-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
- **Función**: esqueleto de aplicación: sidebar + topbar + contenido (+ aside
opcional).
- **Compone**: `Sidebar` (F1.7) + `Sticky` + `Container` /`Grid` +
`ScrollArea` + `Group` + slots.
- **API**: `<AppShell>` + `.Sidebar` + `.Topbar` + `.Content` + `.Aside` .
- **Landmark**: `header/nav/main/aside` + skip-link (según decisión F2.1);
jerarquía documentada.
- **v1**: sidebar colapsable + topbar afijada + main con scroll propio;
móvil = sidebar en drawer (lo trae F1.7).
- **Excepción B-10 declarada**: los shells componen otros elementos de F1 y
blocks — allowlist en `blocks-check` .
### 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.
### 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
disparador F5 (≥2 consumidores reales).
- **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 |
Blocks diferidos: `scheduler` (bloqueado por `chronos` ) · `logo-cloud`
(bloqueado por decisión marquee) · `billing` · `file-manager` ·
`profile-card` (cuando haya consumidor).
---
## 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` ).
docs(blocks): dossier de referencia — 6 pistas, suelos de paridad + enmiendas E-1..E-5
Encargo del usuario (2026-07-21): estudio de referencia para "estar al menos
a la par y cuanto menos superarlo". 6 agentes de investigación web en
paralelo sobre fuentes oficiales vivas (varias verificadas a nivel de código
fuente: sidebar.tsx del registry shadcn, styles.js de tailwind-typography,
sources de Mantine/Starlight/Docusaurus/Ark).
- docs/process/RESEARCH-blocks-references.md — el dossier: P1 catálogos de
marketing (conteos+variantes) · P2 aplicación (ProTable/ProLayout,
dashboard-01, Tremor 303, Clerk/Supabase) · P3 F1 ligeros (APIs a nivel de
prop; scroll-state(stuck) como vocabulario de plataforma; role=alert de
shadcn = bug a no copiar) · P4 prose+sidebar (not-prose/:where()+donut;
shadcn 23 partes) · P5 docs shells (Fumadocs benchmark; colision F1.7 vs
nav-tree/B-5; ruta svelte2tsx para props TS) · P6 ecosistema Svelte
(shadcn-svelte 58 blocks solo-app; hueco "sistema integrado" VACIO) +
sintesis: 5 conclusiones, enmiendas E-1..E-5 (presentadas, firmas
pendientes), 10 angulos de superacion.
- PLAN-blocks.md — regla nueva §4 (toda fase 0 contrasta contra el dossier),
puntero de colision en F1.7, registro.
Verificacion: docs:check 0/0 (512 docs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- 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).
- 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).
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` .
- 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).