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

1683 lines
115 KiB

This file contains ambiguous Unicode characters!

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

# PLAN — Tier `blocks`: composición reutilizable (infraestructura + componentes base + catálogo)
> **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`.
| Fase | Contenido | Estado |
| ------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **F0** | Infraestructura del tier: doctrina + alias + guard + rutas demo | **HECHA 2026-07-21** (F0.1–F0.7; cross-ref en `comparison.md` omitido a propósito — sin aporte hasta que exista catálogo) |
| **F1** | **8** componentes base del CANON que los blocks necesitan (7 + `nav-tree` por E-1) | **CERRADA — 8/8 HECHAS** (empty-state · result · callout · **sticky** · **prose** · **anchor-nav** · **nav-tree** · **sidebar**, PASS las ocho; ver registro) |
| **F2** | Blocks de sitio (**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
_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:
| 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.
| # | Decisión | FIRMADO |
| ----------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **D-BLK.1** | Ubicación y alias | **`src/uix/blocks/`** + alias `$blocks` (decisión de usuario: el tier es UI y vive junto a las capas). La prueba de encapsulación se conserva íntegra: borrar `src/uix/blocks/` deja `npm run check` verde y NADA del canon (`morfo/soma/sema/eidos/active-uix/langs`) lo importa — blocks es un tier bajo `src/uix/`, no una quinta capa. |
| **D-BLK.2** | Estilos | **Layout-components-first**: el layout se hace componiendo `Container/Section/Stack/Flex/Grid/AutoGrid/Wrap/Group/Separator/AspectRatio/Surface` y sus props. Un block NO trae `.css` propio; un `<style>` scoped puntual exige justificación en su README y nunca selecciona internals de componentes compuestos. |
| **D-BLK.3** | API | Componente compuesto con partes anidadas: `<SiteHeader>` / `<SiteHeader.Nav>` / `<SiteHeader.Actions>`; contenido SIEMPRE por children, nunca árboles de datos (`items={...}` solo donde el componente canónico compuesto ya es data-driven). (Derivada de la regla compositional-not-data-driven ya vigente.) |
| **D-BLK.4** | Demos | `web/routes/blocks/{kebab}/+page.svelte` + galería índice en `web/routes/blocks/`. (La ruta `web/routes/alpha/` sigue TERMINADA y prohibida — no tocarla.) |
| **D-BLK.5** | Idiomas/strings | Un block no posee NINGÚN string visible: todo texto llega del app como children/props. Si un string parece inevitable, es superficie de contrato → lo posee el componente canónico subyacente (vía `texts:` del morfo + langs). (Consecuencia mecánica de B-1: sin morfo no hay `texts:`.) |
| **D-BLK.6** | Servicios | **v1 sin servicios**: los blocks NO consumen `uix.prefs`/langs/eidos directamente; el cableado (tema/idioma, submit de auth, transporte) llega como handlers/props del app. Revisable si ≥2 blocks demuestran necesidad real (misma vara que la 2-de-3). |
| **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`)
| B | Obligación |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **B-1** | Sin morfo, sin pack sema, sin fila en `component:audit`. Un block es composición; el comportamiento con contrato se promociona al canon ANTES (regla de admisión §1). |
| **B-2** | Todo elemento interactivo es un componente eidos del catálogo (`Button`, `Link`, `Field`, …). Elementos nativos interactivos crudos (`button/input/select/textarea/a`) = **error** de `blocks-check`. (Excepción única: el HTML que `Prose` recibe ya renderizado — ese contenido es del app.) |
| **B-3** | Los componentes compuestos se consumen AS-IS por sus props públicos (`variant/size/color/…`). Prohibido re-estilizar sus internals desde el block (ni CSS ni `style=`). Los colores son siempre roles/tokens vía props — un block no decide color fuera del sistema. |
| **B-4** | Dependencia unidireccional: `src/uix/blocks/*` importa `$uix`, `$adom` y arts públicos; nada del canon (`src/uix/{morfo,soma,sema,eidos,active-uix,langs}`) ni de `src/{arts,libs,packs}` importa de `src/uix/blocks/`. Borrar el tier deja `check` verde — la prueba de encapsulación se mantiene aunque viva bajo `src/uix/`. Entre blocks tampoco se importa (B-10). |
| **B-5** | Contenido por composición (children/snippets). Nunca `root={tree}` ni props-árbol propias. |
| **B-6** | Responsive con los mecanismos del framework (props responsive de los componentes de layout, breakpoints canónicos). Cero `matchMedia`/listeners propios — si hiciera falta observar algo, es señal de componente canónico (→ admisión). |
| **B-7** | Cero strings propios (D-BLK.5). |
| **B-8** | Landmarks correctos: elemento sectioning + `aria-label`/`aria-labelledby` cuando hay más de un landmark del mismo tipo; jerarquía de headings coherente y documentada en el README del block (qué nivel emite y cómo se ajusta). |
| **B-9** | Cada block: `README.md` (secciones: **Función · Mapa de composición** — qué componentes canónicos usa y con qué props — **· Decisiones · Gaps-con-disposición**) + demo con profundidad de testbed (cada prop pública = control vivo; guía: `docs/guides/demo-authoring.md`, adaptada — sin las 9 tabs completas de componente, mínimo: escena realista + panel de props + código copiable). |
| **B-10** | Un block no importa otro block. Si dos blocks comparten estructura, la pieza compartida o es un componente canónico o se duplica conscientemente (anotado en Gaps). Excepción declarada: los shells (`app-shell`, `docs-shell`) SÍ componen blocks/componentes de F1 por diseño — se lista explícitamente en su README. |
| **B-11** | Motion: solo vía los props `motion`/presets de los componentes compuestos o `Cascade` para coreografía de entrada. Cero `@keyframes`/transitions propias (la regla R-4.5 del canon aplica moralmente aunque el audit no corra aquí). |
`blocks-check` (F0.5) verifica mecánicamente: B-2 (AST/regex de elementos
nativos interactivos), B-4 (dirección de imports), B-1 (no hay ficheros bajo
`src/uix/morfo/components/` reclamados por blocks; no imports de
`sema/components`), D-BLK.2 (no `.css` bajo `src/uix/blocks/`; `<style>` solo
con `/* justified: … */`), B-9 (README + ruta demo existen), B-10 (imports
entre blocks solo en la allowlist de shells).
---
## 4. Reglas de trabajo (TODAS las sesiones de este plan)
**Lectura obligatoria antes de tocar nada** (leer los docs directamente —
nunca delegar la lectura a agentes):
- Siempre: `CLAUDE.md` (llega solo) · este plan · `docs/README.md` (mapa).
- F1 (componentes canon): `docs/building-a-component.md` — LA puerta; cada
fase de la ruta nombra su doc y su guard. No saltarse la fase 0 (tabla
comparativa vs ≥3 referencias; cada ❌/⚠️ del scope recibe decisión del
usuario ANTES de construir).
- F2–F4 (blocks): `docs/architecture/blocks.md` (existirá tras F0) + los
README de CADA componente que el block compone (el mapa de composición se
escribe leyendo, no de memoria) + referencias de blocks equivalentes
(shadcn blocks · Tailwind UI/Plus · Flowbite blocks · PrimeBlocks · Relume)
— comparativa ANTES de diseñar y al declarar done.
- **Dossier de referencia (2026-07-21)**: TODA fase 0 (F1–F4) contrasta
contra `docs/process/RESEARCH-blocks-references.md` ANTES de diseñar — 6
pistas de investigación con suelos de paridad a nivel de prop, pitfalls y
ángulos de superación por ítem. La fase 0 verifica contra el dossier (y
solo investiga de cero lo que el dossier no cubra); las brechas de suelo
que el dossier señala para el ítem se resuelven en su scope-approval.
**Proceso por tanda** (una tanda = un componente o un block):
1. `git reset -q` + verificar HEAD (sesiones concurrentes; NUNCA amend).
2. Fase 0 del ítem: comparativa + scope al usuario si hay decisiones.
3. Construir. UN fichero → verificar → resto (no-cascade). Componer, jamás
re-implementar; gap detectado = FLAG al usuario, no workaround inline.
4. Verificar: `npm run check` + scope vitest del ítem + guard del tier
(`component:audit --only {kebab}` para F1 · `blocks-check` para F2+) +
**navegador de verdad**: screenshot y MIRARLO, claro Y oscuro
(`colorScheme:'dark'`), móvil y desktop para blocks (resize 375/1280).
5. Demo con profundidad de testbed (B-9); docs del framework en el MISMO pase
(README del ítem + mapa si procede).
6. Commit (convención viva del repo): `uix({kebab}): …` para F1,
`blocks({kebab}): …` para F2+, `docs(blocks): …` para doctrina. Stage
SOLO los paths propios (nunca `git add -A`; excluir `words/`, `palabras/`,
`web/routes/alpha/`).
7. Actualizar la tabla de estado de este plan (y `next-features.md` §8 al
cerrar cada fase).
**Prohibiciones**: no tocar `palabras/`, `chronos/`, `media-player`
(foráneos/WIP — componerlos solo cuando estén landed; hoy chronos NO lo
está); no crear servicios/mocks falsos en tests (instancias reales vía
`createActiveUix`); no `--no-verify`; no borrar nada sin instrucción
explícita; responder en castellano, código y docs en inglés.
---
## F0 — Infraestructura del tier
**Objetivo**: que exista el tier con doctrina, guard y sitio donde vivir —
vacío pero verde.
| 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.
- **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:
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`)
- 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
ú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`
- **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`
- **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`
- **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`
- **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`
- `.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`
- **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`
- **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`
- **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.
### F2.8 `cta` — HECHO (2026-07-30)
- **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).
### F2.9 `newsletter` — HECHO (2026-07-30)
- **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.
- 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`.
### F2.10 `site-footer` — HECHO (2026-07-30)
- **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.
### 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).
### F2.12 `team` — HECHO (2026-07-31)
- **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.
### 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).
### 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».
_(`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,
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`
- **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`
- **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`
- **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`
- **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`
- **Compone**: `Container` (medida estrecha) + `Section` + `Heading` +
`Separator` + filas `Field`/controles + `Callout` (F1.4, intent `threat`)
- `AlertDialog` (confirmación destructiva) + `Button`.
- **API**: `<Settings>` + `.Section` (+`.SectionTitle`/`.SectionDescription`)
- `.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`
- **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`
- **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`
- **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`
- **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`
- **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.
_(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`
- **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`
- `.PrevNext`.
- **v1**: 3 columnas desktop → TOC colapsada y sidebar-drawer en móvil.
### F4.2 `code-showcase`
- **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`
- **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):
| 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 |
| 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
component:audit` → los 7 de F1 PASS (o NEEDS-WORK solo por D-\* de la fase
demos global); `docs:check` verde.
2. Las tres páginas de integración (F2 landing · F3 app · F4 docs) renderizan
compuestas SOLO de canon+blocks, verificadas visualmente en claro/oscuro,
375/1280 y teclado.
3. Doctrina publicada y enlazada (architecture/blocks.md en el mapa; CLAUDE.md
al día); cada block con README B-9 completo.
4. Ningún gap silenciado: todo ❌/⚠️ de las fases 0 tiene decisión firmada o
fila en F5/next-features.
## 7. Registro
- 2026-07-21 — Plan creado (análisis de ecosistema + decisión de usuario:
tier `blocks`). Iniciativa en `next-features.md` §8.
- 2026-07-21 — **F0.1 HECHA**: las 7 D-BLK firmadas (repaso en sesión).
D-BLK.1 **enmendada** frente a la propuesta: el tier vive en
`src/uix/blocks/` (no `src/blocks/`); referencias del plan actualizadas
(tabla de tiers, B-4, blocks-check, F0.3/F0.4). Resto firmado como
propuesto. Siguiente: F0.2 (doctrina en `architecture/blocks.md`).
- 2026-07-21 — **F0 CERRADA** (misma sesión): doctrina
`docs/architecture/blocks.md` · alias `$blocks` (vite + svelte.config +
CLAUDE.md) · `src/uix/blocks/README.md` (mapa + template B-9) ·
`scripts/blocks-check.ts` + `npm run blocks:check` (self-testing) ·
galería `web/routes/blocks/` con layout de bootstrap propio
(`+layout@.svelte`, espejo mínimo del de `/uix`, sin packs sema) · fila
E1 en `docs/README.md`. Nota de commit: `docs/README.md` y
`docs/next-features.md` quedaron FUERA del commit de F0 — ambos traían
diff foráneo de otra sesión imposible de separar por staging de archivo
completo; sus hunks propios (fila E1 + §8) viajan con el árbol de trabajo
hasta que su dueño commitee. Siguiente: **F1** (orden recomendado:
`empty-state` → `result` → `callout` → `sticky` → `prose` → `anchor-nav`
→ `sidebar`).
- 2026-07-21 — **Estudio de referencia COMPLETO** (encargo del usuario:
"a la par y cuanto menos superarlo"): 6 pistas de investigación web en
paralelo → `docs/process/RESEARCH-blocks-references.md` (suelos de
paridad a nivel de prop por ítem, pitfalls, superación). Regla nueva en
§4: toda fase 0 contrasta contra el dossier. Puntero de colisión añadido
a F1.7. **Enmiendas E-1…E-5 presentadas al usuario** (E-1 nav-tree ·
E-2 filosofía de alcance v1 · E-3 categorías nuevas F2 · E-4
registry/llms/MCP · E-5 ⌘K) — firmas pendientes; se registran aquí.
- 2026-07-21 — **E-1…E-5 FIRMADAS** (todas según recomendación): **E-1**
componente canónico `nav-tree` data-driven (F1.8; F1 pasa de 7 a 8; el
naming se valida en su fase 0 como extensión de D-BLK.7); **E-2** suelo
de paridad = v1 (regla en el intro de F1); **E-3** F2 pasa de 10 a 14
(banner · team · contact · content-section; resto de la unión a F5);
**E-4** distribución registry/llms.txt/MCP registrada como iniciativa
propia en `next-features.md` §9; **E-5** ⌘K = listener app-land (nota en
F4.1). Doctrina (`architecture/blocks.md` promotion path) y galería
actualizadas.
- 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).
- 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).
- 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.
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).
- 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`).
**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
type="button">` es una pista de MIME falsa) — el morfo lo declara
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.
**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`.
- 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).
- 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`.
- 2026-07-23 — **Primitivo canon `Mockup` + F2.3b `feature-split` HECHOS** (mismo
día; scope-approval del usuario: «split + primitivo de media reutilizable»).
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
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**.
Gates: `blocks:check` verde (4 blocks) · `svelte-check` sin errores propios
(73 = deuda ajena) · eidos lint.test + morfo 131/131. **Siguiente**: F2.4
`pricing`.
- 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`.
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`.
- 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`.
- 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`.
- 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
sosegado _con borde_ no tiene primitivo (`Card outline` acota pero no acepta
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`.
- 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`.
- 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`.
- 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`.
- 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`.
- 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`.
- 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).
- 2026-08-01 — **AUDITORÍA del tier (50 agentes)**. Dos dimensiones sobre los 15
blocks —doctrina y percepción— con un verificador escéptico por hallazgo:
**90 en bruto → 40 verificados → 29 CONFIRMADOS y 11 refutados (28% de falsos
positivos)**. Congelada en `AUDIT-blocks-2026-08-01.md`, que es la única copia
(el journal del workflow era de sesión). ⚠️ **50 quedaron SIN VERIFICAR, 18 de
ellos marcados ALTA** — el tope de 40 lo puso mi script, no el trabajo.
Lo gordo confirmado: `contact` importa `$libs/forms` fuera de la frontera dura 1
· el nombre accesible del submit de `contact` está congelado en «Enviar» en los
cinco estados porque `Form.Submit` impone su `aria-label`, así que **las palabras
de estado que el block existe para poseer no llegan al lector de pantalla** ·
`site-header` congela la página si el cajón está abierto al cruzar el breakpoint
(reproducido con swipe táctil real) · `hero` en `background` deja la CTA
secundaria a 1.78:1 · `height="100%"` sobre `Card` es INERTE en `pricing` y
`testimonials` mientras README y demo afirman lo contrario · B-8 sin declarar en
seis READMEs.
La dimensión CRUZADA (los 15 en una página compuesta) NO se ejecutó: sigue
pendiente, y es además la condición de cierre de F2 del plan.
**Confirmó la premisa de la auditoría**: la capa perceptiva no se había ejercido
NUNCA (la galería arrancaba sin packs de sema desde F0), y ahí está lo más feo
sin verificar — el nav del header emitiendo `commit-select` + `affirm` con earcon
audible al pasar el RATÓN por encima, un `Enter` en `newsletter` emitiendo dos
`commit-submit` con intents contradictorios, y 4 de 7 elementos del header mudos.

Powered by TurnKey Linux.