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

152 KiB

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/F1/F2 cerradas. 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 (15: 10 + banner·team·contact·content-section por E-3 + feature-split) CERRADA EN UNIDADES — 15/15 (+ saneamiento, guard endurecido y contraste doctrinal 15/15; ledger AUDIT-blocks-ledger.md). La página compuesta que su cierre exigía NO se construyó: la integración vive en F2b
F2b Ola de paridad de variedad: variantes de suelo (A) + matriz de equivalencia (B) + logo-cloud·blog·cookie-consent (C) + página compuesta (D) FIRMADA 2026-08-17 — pendiente de ejecutar
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 (Buttons).
  • 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.
  • 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 §«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 REABIERTO el 2026-08-17 y movido a F2b: la premisa con la que se difirió —«su versión honesta pide Marquee»— no casa con las referencias, que lo shippean como grid estático (TW 6 · Untitled · Flowbite). blog y cookie-consent entran también por F2b. El resto de la unión del dossier — bento, gallery, popups, careers, events, comparison, timeline — sigue en F5 con disparador, E-3.)

Cierre F2: blocks-check verde; galería web/routes/blocks/ con los 15.

⚠️ Enmendado el 2026-08-17: este cierre exigía además una página compuesta de PRUEBA (header + hero + features + pricing + faq + cta + footer juntos) y esa página NO se construyó — quedó fuera de alcance por decisión del usuario. F2 está cerrada en unidades: cada block verificado contra sí mismo. La integración —lo único que puede probar que los blocks ensamblan sin fricción— es la puerta de cierre de F2b (tramo D). Mientras esa página no exista, el claim del tier está sin demostrar, y aquí se dice así en vez de dar por probado lo que no se probó.


F2b — Ola de paridad de variedad (firmada 2026-08-17)

De dónde sale: el saldo de la tabla §P1 del dossier (RESEARCH-blocks-references.md) contra los 15 blocks ya construidos, medido el 2026-08-17 leyendo el types.ts y la tabla de Gaps del README de cada uno. De las 11 filas del dossier, 6 están hechas (frames de media vía el canon Mockup · hero layout="background" · feature-split, que ERA la brecha #1 · cta layout="justified" · newsletter note · site-footer signup + extra) y 4 se resolvieron por composición o app-land, declaradas en su README (flyout del header por NavigationMenu, announcement por el block banner, logo-strip y vídeo por los slots del hero, cola de soporte del faq).

⚠️ Ninguna fila de variante de esta ola es un hallazgo nuevo: estaban ya registradas con disposición en el README de su block desde julio. Lo que faltaba no era medir — era decidir.

⚠️ Esta sección se reescribió entera el mismo día en que se escribió. La primera versión atacaba sólo el suelo de §P1 — ocho variantes — y con eso NO se cierra la brecha que motivó la revisión, que es de cardinalidad percibida (TW: Heroes 12, Features 15; nosotros 1–3 arreglos por block). El propio dossier fija el método en P6.1 y la primera versión no lo trajo: no competir en número de dumps, sino demostrar la equivalencia variante-a-variante. De ahí que la ola tenga cuatro tramos y no uno.

La regla de forma de la ola — se firma ANTES que las filas

No es doctrina nueva: es el precedente que F2 ya vivió. Son cuatro términos, y el primero es el que más trabajo hace:

  1. Variante alcanzable componiendo lo que ya hay → RECETA demostrada, CERO código. Se prueba en la demo y se escribe en el README del block. Sin este término, cada fila de las referencias parece pedir un layout nuevo o un hermano, y el tier engorda por imitación en vez de por función.
  2. Misma anatomía, otro ARREGLO → prop layout en el mismo block — hero (center|split|background), cta y newsletter (center|justified).
  3. Otra anatomía → block hermano — feature-split frente a feature-grid: filas alternantes con lista de features y acciones no son celdas de icono.
  4. Variante que debe OBEDECER al estado de su sección → PARTE del block que lo posee, nunca hermano: un hermano no puede leer ese contexto y B-10 le prohíbe importarlo. (Gobierna V1.)

Tramo A — las variantes de suelo

# Block Qué falta Forma firmada Coste
V1 pricing tabla comparativa features×planes Pricing.Compare (parte, por el término 4). Tanda propia con fase 0: dos decisiones de diseño que la ficha no puede prejuzgar — (a) quién declara las columnas si .Plans y .Compare conviven (doble declaración de planes) y (b) qué pasa en móvil (las refs colapsan a lista-por-plan o a scroll) alto
V2 testimonials cita única en spotlight layout="grid" | "spotlight". ⚠️ No es «misma anatomía» exacta: gana un slot logo, pierde la Card y Items pasa a un solo protagonista. Aun así layout, con el precedente de hero layout="background", que también redefine partes. El hermano se reabre SÓLO si un día pide rotación / carousel medio
V3 faq lista estática 2/3 columnas layout="accordion" | "list", con .List conmutando entre Accordion y una rejilla de Q&A siempre abiertas. El punto fino es el TIPO: hoy FaqListProps = AccordionProps, y bajo list no aplican ni esas props ni value/disabled de .Item → unión discriminada, jamás superficie descartada en silencio (A-94) medio
V4 hero form-in-hero ✅ HECHA 2026-08-17. Slot form, el app compone Form + Field + Form.Submit entero (el hero NO toma el handle: tomarlo lo convertiría en un block que coordina). Aterriza en el sitio del CTA — tras la descripción, antes de actions — con medida propia sm; los dos conviven si llegan los dos. Detalle y medidas: README del block §«The sign-up slot» bajo
V5 cta split-with-media tercer layout="split" + slot media (el nombre que ya usa hero); el Mockup existe. Es suelo: el dossier la lista bajo
V8 pricing single-price ✅ HECHA 2026-08-17 — y NO era gratis. Medido: la fila es auto-fill, así que RESERVA las pistas que caben aunque nadie las ocupe → un plan solo daba 315px pegados al borde con 677px de vacío, y columns={1} lo cambiaba por una card de 992px. Resuelto con .Plans maxWidth (tope de fila, centrado, reenviado nombrado por A-94); el número lo pone el app. Detalle: README del block §«The row cap» ~0 → bajo

Fuera de la ola, a F5 con disparador — el dossier declara stats-band «Paridad OK», así que estas dos son adoptables, no suelo, y meterlas aquí diluiría E-2 (que dice suelo = v1, no «todo lo que exista en una referencia»):

  • V6 stats-band split-with-image — arreglo de 2 columnas.
  • V7 stats-band timeline / stepped — el canon Timeline existe y esto es otra sección, no una banda.

Tramo B — la matriz de equivalencia (el entregable central)

Es el tramo que responde a la brecha percibida, y sale del dossier P6.1: «cubrir la matriz con 1 block parametrizado + presets, documentando la equivalencia variante-a-variante; no competir en cardinalidad copy-paste».

Por cada block de F2, una tabla en su README: variante de la referencia → composición nuestra que la logra (con la receta real: props + qué se pasa por qué slot), y lo que NO se alcance sale como gap con nombre en vez de quedarse en la sensación de que «tienen más». Cada receta se demuestra en la demo del block — una receta no verificada en navegador no entra en la tabla.

Tres cosas que esto produce y las ocho variantes no producían:

  1. La prueba de la tesis del tier («variantes como props tipadas vs N dumps»), que hoy es una afirmación de la doctrina sin evidencia enseñable.
  2. La lista honesta de lo que de verdad no podemos hacer — que es la entrada legítima a nuevas filas, en vez de imitar catálogos.
  3. Material directo para la galería pública: la matriz ES la respuesta a «¿por qué 15 y no 133?».

La plantilla de la matriz — se fija aquí para no re-decidirla block a block:

Columna Qué lleva
Variante el nombre que la referencia le da («Hero with app screenshot», «Split with image»)
Ref TW · Flowbite · Untitled · PrimeBlocks · sv-blocks — las que la ofrecen (≥2 = era suelo)
Receta la composición nuestra EXACTA: props + slots (layout="split" + media con <Mockup chrome="phone">), o el término 2/3/4 aplicado
Demostrada en el control vivo de la demo o la página compuesta donde se ve — una receta sin demostración en navegador NO entra en la tabla
Estado cubierta · gap: <nombre> (candidata a variante o a canon, con disposición) · no se ofrece (con el motivo — p. ej. dark pattern)
  • Dónde vive: sección ## Equivalencias en el README de cada block. La plantilla del tier (src/uix/blocks/README.md) la gana, y blocks:check la exige como ÚLTIMA tanda del tramo — encenderla el primer día pondría 18 READMEs en rojo durante toda la ola; encenderla al final se verifica EN ROJO quitando la sección de un block (la regla de toda regla nueva del guard: un guard que inspecciona el conjunto vacío pasa).
  • Regla dura: una fila gap sin disposición no cierra el tramo; las filas «no se ofrece» llevan el motivo escrito, porque son una postura del sistema, no un hueco.
  • La fuente de variantes por block es la tabla §P1 del dossier + el catálogo vivo de cada referencia mirado al escribir la tabla (los conteos del dossier son de 2026-07-21; si una ref añadió una variante convergente nueva, se anota con fecha).

Tramo C — el catálogo que se reabre (tres decisiones firmadas)

# Block Decisión
N1 logo-cloud ENTRA, v1 estático. Se difirió sobre una premisa que las referencias desmienten (TW 6 · Untitled · Flowbite lo shippean como grid estático), y una decisión cuya premisa cae se corrige. El argumento positivo es más fuerte que el de paridad: el problema real de un logo-cloud es la tinta — logos de marcas dispares que gritan entre sí y desaparecen en dark, que las refs venden como segundo artefacto. Tratamiento monocromo derivado de los tokens del tema = la tesis del sistema donde nadie más puede. Marquee queda como Gap con disparador: si entra, es un preset de motion sobre este block, no un block nuevo
N2 blog ENTRA. Misma naturaleza que team — grid de entidades con metadatos (Card + Image + Badge + fecha + autor) — y team entró: negarle la entrada sería arbitrario. Es la categoría de marketing más visible que queda fuera. Fase 0 normal; el nombre se decide ahí (blog es el término de las referencias pero nombra ambiguo — la función es «grid de artículos»)
N3 cookie-consent ENTRA, con doctrina propia y tanda propia — NO es una sección, es un overlay legal con estado. El proyecto ya firmó la regla que le aplica en la familia commerce: la ley europea vive en el TIPO. Un cookie-consent de referencia hace lo que ningún dump hace: el tipo OBLIGA a que rechazar exista al mismo nivel que aceptar y a que las categorías no esenciales nazcan apagadas — legalidad por construcción, no por disciplina del consumidor. Fase 0 profunda (ePrivacy/GDPR + refs)
— onboarding NO entra como block. Es un wizard (F3.8) con contenido de bienvenida; Flowbite lo clasifica en marketing por accidente de catálogo. Crear onboarding sería duplicar una función con otro nombre — exactamente la inflación (los 16 sidebars) que nuestro modelo existe para evitar. Se resuelve como receta de wizard en el tramo B (+ tour de F5 si algún día hay spotlight)

Las fichas siguen el formato de F2 (Función · Compone · API · Landmark · v1 · Demo). Como toda ficha del plan, parametrizan la tanda, no la sustituyen: la fase 0 de cada una lee los README reales de lo que compone.

Ficha N1 logo-cloud

  • Función: banda de prueba social — logos de clientes/partners bajo un encabezado corto («Trusted by…» — palabras del app, B-7).
  • Compone: Section + Container + Box/Stack (header) + Wrap o AutoGrid (la fila; fase 0 decide midiendo con logos reales) + Motion. Los logos son contenido del app: Image del canon o <svg> inline.
  • ⚠️ La tinta vive en el CANON, no en el block. Un block no posee CSS (D-BLK.2), así que el tratamiento monocromo — la razón de reabrir este block — se promociona ANTES por la regla de admisión del tier. Fase 0 decide el hueco: prop del canon Image (candidato tone) o primitivo nuevo; si es primitivo, entra por la ruta de 9 fases antes que el block. Para <svg> inline el camino natural es currentColor + la tinta del tema; para raster, filtro. El opt-out por logo existe (marcas que exigen su color) y lo decide el consumidor, no el block.
  • API: <LogoCloud> (props containerSize · sectionSize + el eje de tinta) + slots eyebrow/title + .Items + .Item (compound: el logo SE REPITE). Alturas normalizadas por token — un logo no trae su tamaño.
  • Landmark: con título, <section aria-labelledby>; sin título es una banda decorativa y el README declara qué significa eso para la AT (los logos siguen necesitando nombre accesible — alt del app).
  • v1: grid/wrap estático. Marquee = Gap con disparador (preset de motion sobre este block, no block nuevo).
  • Demo: 6–8 logos reales de tamaños dispares (la normalización se VE), control del eje de tinta, claro/oscuro (la razón del block se ve ahí), RTL, 375/1280.

Ficha N2 blog (nombre a validar en su fase 0)

  • Función: rejilla de artículos recientes — la puerta editorial del sitio (posts, noticias, casos de estudio).
  • Compone: Section + Container + Box (header) + AutoGrid + Card + AspectRatio/Image (media) + Badge (categoría) + Heading + Text + Link + Group/Avatar (autor — el patrón de testimonials.Author). Fechas: el app compone FormatDate / RelativeTime — el block no formatea fechas, como pricing no formatea moneda.
  • API: <Blog> + .Header + .Posts + .Post (compound: SE REPITE) + .PostMedia + .PostMeta (categoría + fecha) + .PostTitle (el Link del app dentro — la card entera NO es un link: anidaría interactivos) + .PostExcerpt + .PostAuthor.
  • Landmark: <section aria-labelledby>. ⚠️ La semántica de <article> choca con F21 (los primitivos de layout no cambian de elemento; Card renderiza div): fase 0 decide entre as en Card (decisión de CANON, se presenta antes) o gap declarado en el README. No se finge con roles.
  • v1: UNA variante — el grid de 3. Featured-post y lista horizontal = Gaps con disposición (candidatos a layout si el dossier de su fase 0 los da como convergentes).
  • Demo: 3–6 posts con contenido real, con/sin media, autor on/off, claro/oscuro, 375/1280.
  • Función: el aviso de consentimiento — overlay legal con estado, no una sección. Barra anclada + panel de preferencias por categoría.
  • Fase 0 PROFUNDA (obligada, tanda propia): ePrivacy/GDPR + guía CNIL/EDPB ANTES de diseñar el API. Las referencias (Flowbite · Relume) se miran como anti-patrón: sus dumps priman «aceptar» y esconden «rechazar», que es exactamente lo que el tipo debe hacer inconstruible.
  • La ley vive en el TIPO (la doctrina que commerce ya firmó): onAcceptAll Y onRejectAll REQUERIDOS — el block no se puede montar sin camino de rechazo; las dos acciones se renderizan con la MISMA prominencia por construcción y el API no expone ningún knob para degradar «rechazar»; las categorías no esenciales nacen apagadas (el tipo no admite pre-marcado); essential es fija y no desactivable.
  • Coordina (doctrina de coordinación): máquina de la sección (unset · preferences · decided), la FORMA de sus datos (B-5: el esquema de categorías por defecto, sustituible) y las palabras de sus estados por idlangref #?blocks.cookie-consent.* con fallback inglés (B-7). La decisión se anuncia a la AT (uix.announce — el segundo servicio sancionado).
  • Persistencia y ejecución = del APP (D-BLK.6): el block emite onDecision(consent); jamás escribe cookies ni bloquea scripts.
  • Compone: Card/Surface (la barra) + la capa de colocación anclada (el eje affix canonizado 2026-08-15 — su fase 0 lee ese README y NO re-implementa colocación) + Dialog o Drawer (preferencias; fase 0 decide con la guía delante) + Switch por categoría + Button + Group + Text/Link (política del app).
  • Foco y descarte: la barra NO roba el foco al aparecer (no es modal); Escape no decide — cerrar sin elegir no es consentir, y eso también es estructura, no estilo.
  • Landmark: role="region" nombrada; el panel de preferencias hereda la semántica del Dialog/Drawer que compone.
  • v1: barra inferior no bloqueante + panel de preferencias + esquema de 4 categorías por defecto (essential · functional · analytics · marketing).
  • Demo: los tres caminos (aceptar todo · rechazar todo · granular) con la decisión emitida VISIBLE, teclado completo, AT (anuncio verificado), claro/oscuro, 375/1280, RTL.

Tramo D — la página compuesta (la puerta de cierre)

Es la condición de cierre que F2 escribió y no cumplió, y entra aquí. El claim del tier es «los blocks ensamblan sin fricción», y ese claim sólo se prueba compuesto: la jerarquía de headings CRUZANDO blocks, los landmarks únicos, el ritmo vertical entre sectionSize consecutivos, la cascada de motion sección a sección. Todo lo que vendemos como superación es indemostrable block a block.

No es teórico: el defecto de los 16px de content-section sólo apareció al poner otra sección debajo, y A-95 (el banner affix="top" tapando la cabecera pegada) es un defecto que sólo existe cruzado.

Va al final — componer antes de cambiar los blocks sería componer dos veces — es el vehículo natural de la matriz del tramo B, y se promociona a demo pública (P1: todas las referencias shippean páginas compuestas como artefacto de prueba; TW 10 · Untitled 105).

Ficha — la página compuesta

  • Ruta: web/routes/blocks/landing/ con el patrón de las previews (+layout@.svelte que resetea: la página se ve A SANGRE, sin marco de demo) + ficha en _lib/catalog.ts (B-9) para que el raíl la publique.
  • Qué es: una landing VEROSÍMIL de un producto — contenido real de principio a fin (el patrón {Name}Site), no un desfile de blocks. El criterio de inclusión es la naturalidad: los blocks que una landing real lleva (site-header + banner + hero + logo-cloud + feature-grid + feature-split + stats-band + testimonials + pricing + faq + cta + newsletter + site-footer + cookie-consent encima) entran; los que no le caben con naturalidad (team · contact · content-section · blog) quedan REGISTRADOS en el README de la página como no-compuestos — una segunda página compuesta (about/blog) es Gap con disparador, no alcance de esta.
  • Qué verifica (cada punto con su medida, no a ojo):
    • Outline de headings: UN h1 (el hero), h2 por sección — la política de heading-level que vendemos como superación, medida sobre el árbol de accesibilidad.
    • Landmarks únicos y nombrados — aquí se re-mide el hallazgo de los DOS role="banner" (block banner + <header> del site-header).
    • Ritmo vertical: los sectionSize consecutivos comparados en las costuras (el método que destapó los 16px de content-section: bordes medidos entre secciones vecinas, no cada block contra sí mismo).
    • Motion sección a sección: las cascadas disparan al entrar cada una en viewport, no todas a la vez (§D.13; Playwright headless — el pane suspendido congela IO y rAF).
    • A-95 (banner affix="top" + header pegado): si app-shell (F3.1) existe ya, se arregla y se cierra; si no, se re-confirma y queda.
    • La sonda de sema sobre el formulario de la página (el cabo de los hermanos del Field: ¿doble commit-submit?).
    • Claro/oscuro · 375/1280 · RTL · teclado de punta a punta (sin trampas de foco; el drawer del header devuelve el foco).
  • Gates: blocks:check verde · la página en el catálogo · las medidas de arriba anotadas en el README de la página con fecha e instrumento.

Orden

A V4 → V8 (medir primero) → V5 → V2 → V3 → V1 en tanda propia · B la matriz por tandas, un block por vez, según se tocan · C N1 logo-cloud → N2 blog → N3 cookie-consent (tanda propia) · D la página compuesta cierra.

Cada fila entra por el proceso por tanda de §4. La fase 0 ligera de las variantes ya está hecha y vive en el README de su block; N1/N2/N3 traen la suya, y la de N3 es profunda.

Cierre F2b

blocks:check verde; cada variante nueva con su control vivo en la demo (B-9, «cada prop público un control»); cada fila de Gaps del README pasada de «scope-approval pending» a la decisión firmada; la matriz del tramo B escrita y demostrada en los 15 + los 3 nuevos; la página compuesta verificada en claro/oscuro y 375/1280, con recorrido de teclado completo.

La ola no reabre ninguna fila CONFIRMADO del ledger — son ejes distintos y se miden aparte — con una excepción esperada: la página compuesta es donde A-95 deja de ser inarreglable, si para entonces existe app-shell (F3.1).


F3 — Blocks de aplicación (10)

Mismas reglas y ficha que F2. Depende de: F1.2/F1.3/F1.4 (estados), F1.7 (sidebar) para F3.1. Las cuatro dependencias están CERRADAS, así que F3 no está bloqueada por nada del canon.

Orden recomendado (2026-08-17): F3.1 app-shell PRIMERO — es la pieza que desbloquea dos cosas ajenas a su propia ficha: la fila A-95 del ledger (banner affix="top" tapa la cabecera pegada; no se arregla en ninguno de los dos blocks porque falta quien POSEA las alturas de página) y F4.1 docs-shell, que lo especializa. Después, por coste creciente y con las fases 0 profundas al final: F3.9 error-page (compone Result, ya hecho) → F3.6 user-menu → F3.7 notifications → F3.5 settings → F3.2 auth → F3.4 dashboard → F3.8 wizard → F3.3 data-table y F3.10 kanban, que son las dos con fase 0 OBLIGADA y más profunda (leer soma/components/table

  • $libs/datagrid y soma/components/drag-drop antes de diseñar el API).

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 (Buttons 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 Buttons (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 ennext-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) · billing · file-manager · profile-card (cuando haya consumidor). logo-cloud salió de esta lista el 2026-08-17: su premisa («pide Marquee») no casaba con las referencias — entra por F2b tramo C.

Diferidos de F2b (2026-08-17), con disparador: stats-band split-with-image y stats-band timeline/stepped — el dossier declara ese block «Paridad OK», así que son adoptables, no suelo; entran cuando una demo real las pida.


6. Verificación de cierre del plan (definition of done global)

  1. npm run check sin regresión; npm run blocks:check verde; npm run 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.

  • 2026-08-09 — Fase 5 del saneamiento: blocks:check endurecido (el último punto del plan de saneamiento sin empezar; el detalle de las fases 1–4 vive en AUDIT-blocks-ledger.md, que es la fuente viva desde el 2026-08-05). Tres reglas nuevas, cada una con su fixture negativo en el selfTest() que el guard corre antes de escanear:

    1. Lista blanca de importaciones (B-4) — la frontera dura 1 deja de ser sólo prosa: $uix, los arts públicos, $libs/forms, svelte y los relativos del propio block; lo demás es error con el especificador por delante. Los arts se DERIVAN de src/arts/*, no se listan a mano — una lista escrita se queda atrás el día que aterriza un art. El tier ya cumplía: cero violaciones al encenderla (cierra la disposición de A-05).
    2. README completo (B-9) + declaración de landmark (B-8) — las cuatro secciones de la plantilla y un párrafo **Landmark + headings**. Al encenderla salieron 7 de 15 sin declaración; escritas leyendo la fuente de cada block (cierra A-14, que sólo pedía tres).
    3. Ficha en el catálogo de demos (B-9) — el árbol y web/routes/blocks/_lib/catalog.ts tienen que coincidir en los DOS sentidos: un block que el catálogo no publica es invisible para el raíl, y un slug publicado sin block detrás es un enlace muerto.

    De paso, el escáner deja de leer los comentarios como código: los index.ts documentan su uso con un // import { Cta } from '$blocks/cta' y una lista blanca que lee prosa habría empezado a acusar a los ejemplos.

    Verificado en rojo sobre el árbol real, no sólo contra los fixtures: un zod y un $libs/days metidos en hero/types.ts salen señalados con su línea exacta mientras el mismo import dentro de un comentario NO salta; el catálogo falla en las dos direcciones; y al romper isAllowedSpec a propósito, el self-test aborta el guard en vez de dar verde. Gates: blocks:check verde (15 blocks / 114 ficheros) · vitest src/uix/blocks 20/20 · docs:check 0/0 · svelte-check 74E/54W = la línea base medida justo antes.

  • 2026-08-17 — F2b establecida (firma pendiente). Contraste fila a fila de la tabla §P1 del dossier contra los 15 blocks construidos, leyendo el types.ts y los Gaps del README de cada uno: de sus 11 filas, 6 hechas y 4 resueltas por composición / app-land. El resto es la ola V1…V8, con su regla de forma (misma anatomía → layout; otra anatomía → hermano; una variante que obedece al estado de su sección → PARTE, por B-10) y su orden (baratos primero, Pricing.Compare el último). Ninguna fila es un hallazgo nuevo: las ocho estaban registradas en el README de su block desde julio — lo que faltaba era la decisión, no la medida. Añadido también el orden recomendado de F3, con app-shell primero porque desbloquea A-95 del ledger y F4.1. La tabla de estado de §0 decía «F0 pendiente (nada construido)» y F2 «pendiente»: corregidas al estado real.

  • 2026-08-17 — F2b REESCRITA y FIRMADA el mismo día. La primera versión (entrada anterior) atacaba sólo el suelo de §P1 con ocho variantes, y eso NO cierra la brecha que motivó la revisión: la de cardinalidad percibida. El método para esa brecha ya estaba en el dossier (P6.1 — demostrar la equivalencia variante-a-variante en vez de competir en número de dumps) y no se trajo. La ola pasa a cuatro tramos: A variantes de suelo · B la matriz de equivalencia por block (el entregable central) · C el catálogo que se reabre · D la página compuesta. La regla de forma gana su término que faltaba y que más trabajo hace: variante alcanzable componiendo → RECETA demostrada, cero código — sin él el tier engorda por imitación. Fichas corregidas: V1 con sus dos decisiones de diseño destapadas (quién declara las columnas si .Plans y .Compare conviven; el colapso móvil) y tanda propia; V2 deja de venderse como «misma anatomía» (gana logo, pierde la Card) aunque sigue siendo layout; V3 la unión discriminada alcanza también a value/disabled de .Item; V4 tiene que decidir si el formulario REEMPLAZA a actions, como en las referencias; V8 se MIDE antes de declararse gratis (PricingPlansProps es un Pick cerrado); V6/V7 salen de la ola a F5 — el dossier declara stats-band «Paridad OK», son adoptables y no suelo, y meterlas diluía E-2. Tres decisiones firmadas: logo-cloud REABIERTO como block estático (su premisa de diferimiento no casa con las refs; el valor propio es la tinta monocroma derivada de tokens, y Marquee queda como preset de motion, no como block) · blog ENTRA (misma naturaleza que team, que ya entró) · cookie-consent ENTRA con doctrina propia (no es una sección: overlay legal con estado, y la ley europea vive en el TIPO — rechazar al mismo nivel que aceptar, no esenciales apagadas por construcción) · onboarding NO entra: es un wizard con contenido de bienvenida, y duplicar una función con otro nombre es la inflación que este modelo existe para evitar. La página compuesta entra como puerta de cierre de F2b y el cierre de F2 queda enmendado: cerrada EN UNIDADES, con el claim de integración declarado sin demostrar en vez de dado por probado.

  • 2026-08-17 — F2b: fichas de ejecución escritas (las cinco que faltaban para que la ola fuera plan de implementación al nivel de F2): plantilla de la matriz en el tramo B (5 columnas; vive como ## Equivalencias en el README de cada block; blocks:check la exige como ÚLTIMA tanda del tramo, verificada en rojo) · fichas N1 logo-cloud (⚠️ la tinta monocroma se promociona al CANON antes del block — un block no posee CSS), N2 blog (fechas por FormatDate/RelativeTime; <article> choca con F21 → fase 0 decide as en Card o gap declarado), N3 cookie-consent (la ley en el TIPO: onRejectAll requerido, misma prominencia por construcción, no esenciales apagadas; Escape no decide) · ficha de la página compuesta (web/routes/blocks/landing/, criterio de NATURALIDAD — los que no caben quedan registrados como no-compuestos, la segunda página es Gap con disparador; verificación con medida por punto: outline, landmarks —los dos role="banner"—, costuras de sectionSize, cascadas por viewport, A-95, sonda de sema, teclado entero).

  • 2026-08-17 — F2b · V4 form-in-hero HECHA (primera fila de la ola). Slot form en hero: el app compone Form + Field + Form.Submit enteros y el block sólo da sitio y medida — no toma el handle del formulario, porque leer la validez de la sección lo convertiría en un block que COORDINA y tendría que poseer máquina y palabras; un hero no coordina nada. Medido en Playwright headless sobre píxeles pintados: tinta escrita 15.88:1 claro / 16.28:1 oscuro · relleno del campo contra el lienzo de background 9.61:1 · la fila colapsa a 375 (campo 325 / botón 325) y comparte a 1280 (335 / 131, misma fila) · medida propia 480px en center contra sección de 1264 y 472px en split (= la columna: el cap sm es inerte ahí) · Enter llega al handler del app y un correo inválido lo para con el error traducido y aria-invalid · RTL espeja la fila. Dos gaps de canon registrados en el README del block: Form.Submit no reenvía al Button ni intent ni eje de anchura (el botón no llena su celda al colapsar; newsletter mide lo mismo, y el justify de Grid es justify-content, así que no se arregla desde fuera) — la demo usa lo que hay (style, que sí se reenvía). Y el contexto de inversión mordió por segunda vez: un Text sin color explícito bajo background mide 1.62:1 contra 10:1 con on-solid. ⚠️ Anotada además la lección de INSTRUMENTO: dos sondas dieron números seguros y falsos antes de la buena (parser rgb sobre un sistema que emite oklch, y canvas fillStyle que tampoco normaliza oklch aquí → medía NEGRO contra todo); la señal de que la sonda es honesta es que la etiqueta del submit sale a 5.18:1, la cifra que el repo ya documenta para primary.

  • 2026-08-17 — F2b · V8 single-price HECHA, y la ficha acertó al exigir que se midiera ANTES: «un solo .Plan y el layout ya lo sostiene» —la disposición que el README del block llevaba desde julio— era falsa. La fila es repeat(auto-fill, minmax(17rem, 1fr)) y auto-fill reserva toda pista que quepa, la ocupe alguien o no: medido a 1280 sobre 992px de fila, un plan solo daba una card de 315px pegada al borde con 677px de vacío al lado, y columns={1} sólo cambiaba eso por una card de 992px. Ninguna de las dos es la card centrada de las referencias. Resuelto abriendo .Plans a maxWidth (tope de la FILA, con marginX="auto" incondicional para centrarla), reenviado NOMBRADO al AutoGrid — nunca por {...rest}, que es justo lo que A-94 dejó dicho. El número lo pone el app: la demo pasa 28rem para un plan y 44rem para dos. Medido después: 448px centrados con uno, 704px (dos pistas de 340) con dos, y 992px idénticos con tres — el tope es opt-in y no toca el caso que ya existía; a 375 es inerte. Control vivo nuevo en la demo (nº de planes) y en la URL del preview. ⚠️ Lección que se queda: una fila «diferida» puede ser una medición equivocada, no un aplazamiento — nadie había mirado este block con menos de tres planes.

Powered by TurnKey Linux.