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

84 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 pendiente (nada construido). Actualizar esta tabla al cerrar cada tanda, estilo PLAN-component-coherence.md.
Fase Contenido Estado
F0 Infraestructura del tier: doctrina + alias + guard + rutas demo HECHA 2026-07-21 (F0.1–F0.7; cross-ref en comparison.md omitido a propósito — sin aporte hasta que exista catálogo)
F1 8 componentes base del CANON que los blocks necesitan (7 + nav-tree por E-1) CERRADA — 8/8 HECHAS (empty-state · result · callout · sticky · prose · anchor-nav · nav-tree · sidebar, PASS las ocho; ver registro)
F2 Blocks de sitio (14: 10 + banner·team·contact·content-section por E-3) pendiente (F2 solo requiere F1.1)
F3 Blocks de aplicación (10) pendiente
F4 Blocks de docs (3) pendiente
F5 Backlog condicionado (componentes media/mobile + blocks diferidos) pendiente

1. Qué es un block (doctrina propuesta — aterriza en docs/architecture/blocks.md en F0)

Un block es una composición nombrada de componentes del canon que desempeña una función de página: no aporta primitivas nuevas, aporta ensamblaje correcto (layout, landmarks, jerarquía de headings, responsive, puntos de contenido). Consume el framework; el framework nunca lo referencia.

La tabla de tiers queda:

Tier Valor Contrato Entra por
Canon (src/uix/) densidad de contrato (eventos, ARIA, teclado, tokens que otros consumen) morfo + matriz de aceptación ruta de 9 fases (docs/building-a-component.md)
Packs (src/packs/) decoración parametrizada de hoja contrato P packs-check
Blocks (src/uix/blocks/) composición de función de página contrato B (§3) blocks-check

Regla de admisión (espejo de la de packs): si al construir un block hace falta comportamiento nuevo con superficie de contrato — un evento real, una máquina de estados, un data-attr que el CSS necesita seleccionar, una obligación a11y de widget — esa pieza se construye ANTES como componente canónico por la ruta de 9 fases, y el block la compone. Nunca se le crece el privilegio al block. (Es exactamente la regla "compose existing components; flag gaps" de docs/guides/component-guide.md §4, elevada a frontera de tier.) La F1 de este plan existe porque ese triage ya está hecho: los 7 gaps detectados se construyen primero.

Lo que un block SÍ posee semánticamente: landmarks y estructura de documento (header/nav/main/aside/footer, jerarquía h1–h6, skip-link, aria-label de región). Los componentes no pueden saber el contexto de página; los blocks sí. Es su única superficie a11y propia.

Un block PUEDE tener estado de vista local (pestaña activa, toggle mensual/anual) usando los props/eventos públicos de los componentes que compone. Lo que NO puede es materializar ese estado con DOM/CSS propio que requiera contrato (→ regla de admisión).


2. Decisiones D-BLK — FIRMADAS 2026-07-21

Repasadas y firmadas por el usuario en el kickoff (F0.1 HECHA). D-BLK.1 quedó enmendada respecto a la propuesta original del plan (que proponía src/blocks/); el resto se firmó tal como estaba propuesto. D-BLK.3/4/5 derivan de doctrina ya vigente y se firmaron por no-objeción.

# Decisión FIRMADO
D-BLK.1 Ubicación y alias src/uix/blocks/ + alias $blocks (decisión de usuario: el tier es UI y vive junto a las capas). La prueba de encapsulación se conserva íntegra: borrar src/uix/blocks/ deja npm run check verde y NADA del canon (morfo/soma/sema/eidos/active-uix/langs) lo importa — blocks es un tier bajo src/uix/, no una quinta capa.
D-BLK.2 Estilos Layout-components-first: el layout se hace componiendo Container/Section/Stack/Flex/Grid/AutoGrid/Wrap/Group/Separator/AspectRatio/Surface y sus props. Un block NO trae .css propio; un <style> scoped puntual exige justificación en su README y nunca selecciona internals de componentes compuestos.
D-BLK.3 API Componente compuesto con partes anidadas: <SiteHeader> / <SiteHeader.Nav> / <SiteHeader.Actions>; contenido SIEMPRE por children, nunca árboles de datos (items={...} solo donde el componente canónico compuesto ya es data-driven). (Derivada de la regla compositional-not-data-driven ya vigente.)
D-BLK.4 Demos web/routes/blocks/{kebab}/+page.svelte + galería índice en web/routes/blocks/. (La ruta web/routes/alpha/ sigue TERMINADA y prohibida — no tocarla.)
D-BLK.5 Idiomas/strings Un block no posee NINGÚN string visible: todo texto llega del app como children/props. Si un string parece inevitable, es superficie de contrato → lo posee el componente canónico subyacente (vía texts: del morfo + langs). (Consecuencia mecánica de B-1: sin morfo no hay texts:.)
D-BLK.6 Servicios v1 sin servicios: los blocks NO consumen uix.prefs/langs/eidos directamente; el cableado (tema/idioma, submit de auth, transporte) llega como handlers/props del app. Revisable si ≥2 blocks demuestran necesidad real (misma vara que la 2-de-3).
D-BLK.7 Naming F1 sticky · anchor-nav · empty-state · result · callout · prose · sidebar — confirmados. (Los matices de naming siguen revisables en la fase 0 de cada uno, como toda fase 0.)

Cualquier enmienda futura a una D-BLK se registra aquí con fecha ANTES de seguir construyendo (regla dura: los desvíos de alcance se declaran, nunca en silencio).


3. El contrato B (suelo de calidad de un block — guard: blocks-check)

B Obligación
B-1 Sin morfo, sin pack sema, sin fila en component:audit. Un block es composición; el comportamiento con contrato se promociona al canon ANTES (regla de admisión §1).
B-2 Todo elemento interactivo es un componente eidos del catálogo (Button, Link, Field, …). Elementos nativos interactivos crudos (button/input/select/textarea/a) = error de blocks-check. (Excepción única: el HTML que Prose recibe ya renderizado — ese contenido es del app.)
B-3 Los componentes compuestos se consumen AS-IS por sus props públicos (variant/size/color/…). Prohibido re-estilizar sus internals desde el block (ni CSS ni style=). Los colores son siempre roles/tokens vía props — un block no decide color fuera del sistema.
B-4 Dependencia unidireccional: src/uix/blocks/* importa $uix, $adom y arts públicos; nada del canon (src/uix/{morfo,soma,sema,eidos,active-uix,langs}) ni de src/{arts,libs,packs} importa de src/uix/blocks/. Borrar el tier deja check verde — la prueba de encapsulación se mantiene aunque viva bajo src/uix/. Entre blocks tampoco se importa (B-10).
B-5 Contenido por composición (children/snippets). Nunca root={tree} ni props-árbol propias.
B-6 Responsive con los mecanismos del framework (props responsive de los componentes de layout, breakpoints canónicos). Cero matchMedia/listeners propios — si hiciera falta observar algo, es señal de componente canónico (→ admisión).
B-7 Cero strings propios (D-BLK.5).
B-8 Landmarks correctos: elemento sectioning + aria-label/aria-labelledby cuando hay más de un landmark del mismo tipo; jerarquía de headings coherente y documentada en el README del block (qué nivel emite y cómo se ajusta).
B-9 Cada block: README.md (secciones: Función · Mapa de composición — qué componentes canónicos usa y con qué props — · Decisiones · Gaps-con-disposición) + demo con profundidad de testbed (cada prop pública = control vivo; guía: docs/guides/demo-authoring.md, adaptada — sin las 9 tabs completas de componente, mínimo: escena realista + panel de props + código copiable).
B-10 Un block no importa otro block. Si dos blocks comparten estructura, la pieza compartida o es un componente canónico o se duplica conscientemente (anotado en Gaps). Excepción declarada: los shells (app-shell, docs-shell) SÍ componen blocks/componentes de F1 por diseño — se lista explícitamente en su README.
B-11 Motion: solo vía los props motion/presets de los componentes compuestos o Cascade para coreografía de entrada. Cero @keyframes/transitions propias (la regla R-4.5 del canon aplica moralmente aunque el audit no corra aquí).

blocks-check (F0.5) verifica mecánicamente: B-2 (AST/regex de elementos nativos interactivos), B-4 (dirección de imports), B-1 (no hay ficheros bajo src/uix/morfo/components/ reclamados por blocks; no imports de sema/components), D-BLK.2 (no .css bajo src/uix/blocks/; <style> solo con /* justified: … */), B-9 (README + ruta demo existen), B-10 (imports entre blocks solo en la allowlist de shells).


4. Reglas de trabajo (TODAS las sesiones de este plan)

Lectura obligatoria antes de tocar nada (leer los docs directamente — nunca delegar la lectura a agentes):

  • Siempre: CLAUDE.md (llega solo) · este plan · docs/README.md (mapa).
  • F1 (componentes canon): docs/building-a-component.md — LA puerta; cada fase de la ruta nombra su doc y su guard. No saltarse la fase 0 (tabla comparativa vs ≥3 referencias; cada ❌/⚠️ del scope recibe decisión del usuario ANTES de construir).
  • F2–F4 (blocks): docs/architecture/blocks.md (existirá tras F0) + los README de CADA componente que el block compone (el mapa de composición se escribe leyendo, no de memoria) + referencias de blocks equivalentes (shadcn blocks · Tailwind UI/Plus · Flowbite blocks · PrimeBlocks · Relume) — comparativa ANTES de diseñar y al declarar done.
  • Dossier de referencia (2026-07-21): TODA fase 0 (F1–F4) contrasta contra docs/process/RESEARCH-blocks-references.md ANTES de diseñar — 6 pistas de investigación con suelos de paridad a nivel de prop, pitfalls y ángulos de superación por ítem. La fase 0 verifica contra el dossier (y solo investiga de cero lo que el dossier no cubra); las brechas de suelo que el dossier señala para el ítem se resuelven en su scope-approval.

Proceso por tanda (una tanda = un componente o un block):

  1. git reset -q + verificar HEAD (sesiones concurrentes; NUNCA amend).
  2. Fase 0 del ítem: comparativa + scope al usuario si hay decisiones.
  3. Construir. UN fichero → verificar → resto (no-cascade). Componer, jamás re-implementar; gap detectado = FLAG al usuario, no workaround inline.
  4. Verificar: npm run check + scope vitest del ítem + guard del tier (component:audit --only {kebab} para F1 · blocks-check para F2+) + navegador de verdad: screenshot y MIRARLO, claro Y oscuro (colorScheme:'dark'), móvil y desktop para blocks (resize 375/1280).
  5. Demo con profundidad de testbed (B-9); docs del framework en el MISMO pase (README del ítem + mapa si procede).
  6. Commit (convención viva del repo): uix({kebab}): … para F1, blocks({kebab}): … para F2+, docs(blocks): … para doctrina. Stage SOLO los paths propios (nunca git add -A; excluir words/, palabras/, web/routes/alpha/).
  7. Actualizar la tabla de estado de este plan (y next-features.md §8 al cerrar cada fase).

Prohibiciones: no tocar palabras/, chronos/, media-player (foráneos/WIP — componerlos solo cuando estén landed; hoy chronos NO lo está); no crear servicios/mocks falsos en tests (instancias reales vía createActiveUix); no --no-verify; no borrar nada sin instrucción explícita; responder en castellano, código y docs en inglés.


F0 — Infraestructura del tier

Objetivo: que exista el tier con doctrina, guard y sitio donde vivir — vacío pero verde.

Paso Producir Verificación
F0.1 Firma D-BLK (§2) con el usuario; enmendar el plan si procede HECHA 2026-07-21 — firmas en §2 (D-BLK.1 enmendada: src/uix/blocks/)
F0.2 docs/architecture/blocks.md — la doctrina de §1 + §3 en formato espejo de packs.md (frontmatter E1, regla de admisión, hard boundaries, contrato B, "promotion path" = la F1 como ejemplo vivido). Enlazar sin copiar: canon → CANON.md, ruta → building-a-component.md npm run docs:check (links)
F0.3 Alias $blocks → src/uix/blocks en el const aliases de vite.config.ts (fuente de verdad) + svelte.config.js en sync + fila en la tabla de aliases de CLAUDE.md npm run check
F0.4 src/uix/blocks/README.md — mapa del tier (inventario vivo = el árbol, como packs) + template de README de block (B-9) —
F0.5 scripts/blocks-check.ts + npm script blocks:check — los checks mecánicos listados en §3. Espejo estructural de packs-check. Test negativo: un fixture con <button> crudo debe fallar npm run blocks:check verde en tier vacío + test negativo rojo
F0.6 web/routes/blocks/+page.svelte — galería índice (dogfooding: componer Container/Section/Card/… del propio catálogo para la galería) navegador claro/oscuro
F0.7 Cablear docs: fila E1 en el mapa de docs/README.md (architecture/blocks.md) + nota del tier en el diagrama de arquitectura de CLAUDE.md (línea blocks/ → …) + cross-ref en docs/comparison.md si aporta npm run docs:check

Cierre F0: check + docs:check + blocks:check verdes; galería renderiza vacía con mensaje de "en construcción" compuesto con el catálogo.

CERRADA 2026-07-21 — evidencia: docs:check 0/0 (511 docs); svelte-check 76E/51W = baseline exacto (los archivos nuevos compilan limpios; los 76 son deuda foránea preexistente del árbol sin commitear); blocks:check verde (self-test 10 fixtures OK — el <button> crudo SE detecta — y 5 532 archivos de canon/arts/libs/packs escaneados sin violaciones de dirección). Galería verificada en navegador vía árbol de accesibilidad + estilos computados en AMBOS modos (light: base-light, h1 oklch(0.24…); dark: base-dark, h1 oklch(0.95…)); la captura de píxeles del panel embebido expiró (renderer suspendido en segundo plano — clase conocida) — pendiente de un vistazo humano o Playwright en la primera sesión F1. El listener prefers-color-scheme del layout funciona al boot; el evento change en vivo no dispara con el panel suspendido (peculiaridad del entorno, mismo patrón raw-matchMedia que el layout de /uix).


F1 — Componentes base del CANON (8)

Estos NO son blocks: entran por src/uix/{morfo,soma,sema,eidos} siguiendo la ruta completa de 9 fases de docs/building-a-component.md (fase 0 Decide → 8 Acceptance), con component:audit --only {kebab} como oráculo. Las fichas siguientes NO sustituyen la ruta — la parametrizan: dan el gap, la membresía esperada, el boceto de morfo, las referencias mínimas de la fase 0 y las trampas conocidas del ecosistema que aplican. El vocabulario cerrado (archetypes, familias, verbos, intents, holds) se toma SIEMPRE de docs/canon/vocabularies.md (generado del código) — las fichas nombran candidatos, el builder valida contra la lista real.

Orden recomendado: F1.2 → F1.3 → F1.4 (display, baratos, establecen ritmo) → F1.1 (desbloquea F2) → F1.5 → F1.6 → F1.8 → F1.7 (desbloquean F4/F3).

E-2 firmada (2026-07-21): los v1 de F1–F4 se dimensionan al suelo de paridad del dossier (lo que ≥2 referencias convergen) — cada fase 0 trae la subida concreta al scope-approval; quedarse bajo suelo exige decisión registrada, nunca omisión.

F1.1 sticky — afijado con estado

  • Gap: wrapper position:sticky que SABE cuándo está afijado (data-stuck) para que la cabecera cambie elevación/fondo al pegarse.
  • Membresía: soma + eidos (comportamiento real: observación + estado).
  • Fase 0, comparar: AntD Affix · Mantine Affix · la técnica sentinel con IntersectionObserver (CSS-Tricks/web.dev) · shadcn (no lo tiene — anotarlo como diferencial).
  • Morfo (boceto): parts provider (el contenedor sticky) + sentinel (1px observado, aria-hidden); data: data-stuck (presente/ausente), data-edge (top | bottom). Sin eventos v1 (afijarse es hecho de layout, no ocurrencia perceptiva del usuario — si algún consumidor pide señal sema, se revisa con el criterio D.4). Sin keyboard, sin texts. Partes display → sin archetype (un archetype interactivo arrastra estilos de item; y el archetype content pisa position — justo lo que sticky no puede permitirse).
  • Soma: provider observa el sentinel vía dom.observe (IntersectionObserver por adom) — JAMÁS scroll listener + getBoundingClientRect síncrono (regla de reflow: lecturas de layout solo post-layout vía dom.measure/rAF). data-stuck lo escriben los effects (dom.apply), único escritor.
  • Eidos: wrapper fino; tokens --sticky-top-offset / --sticky-z-index (alias de la escala --z-index-* semántica, no número crudo). La receta NO decide sombra/fondo del contenido — eso lo hace el consumidor seleccionando su propio estado visual con data-stuck presente (documentar el patrón en el README).
  • Demo: página larga con header/toolbar afijable, chip mostrando el estado, edge top y bottom.
  • Trampas: mergeProps clobberea stamps — attrs visuales del wrapper fuera del morfo salvo que crucen a soma (aquí data-stuck SÍ es soma).

F1.2 empty-state — estado vacío

  • Gap: patrón universal icono/título/descripción/acción para listas y paneles sin datos. Hoy no existe nada.
  • Membresía: morfo + eidos, sin provider soma (display puro; hay precedente sancionado: metrics es scope: ['sema','eidos'] sin soma). Morfo-first aplica igual: hasta las hojas llevan morfo.
  • Fase 0, comparar: Chakra EmptyState · AntD Empty · HeroUI · patrones de empty state de Material.
  • Morfo (boceto): parts provider + media (icon o ilustración) + title + description + actions. Sin eventos, sin keyboard, sin archetype en partes display. texts: NO — los strings los trae el app (es contenido, no chrome del componente).
  • Eidos: receta pequeña espejo de la más cercana ya enviada (estudiar banner/card antes de escribir una línea); centrado, spacing tokenizado --empty-state-*, tamaño vía canon size si aporta (probablemente solo sm/md). actions compone Button del catálogo vía children.
  • Demo: en contexto real — un table/grid-list sin filas mostrando el empty-state, más la escena aislada con controles.

F1.3 result — página de resultado

  • Gap: estado terminal de página/flujo (éxito, error, 403/404/500). Pareja de empty-state, distinto rol: cierra un flujo, no describe ausencia de datos.
  • Membresía: como F1.2 (morfo + eidos display).
  • Fase 0, comparar: AntD Result · patrones de error page de Tailwind UI · HeroUI.
  • Morfo (boceto): parts provider + media + title + description + actions + extra. Prop status → data-status (success | error | info | forbidden | not-found | server-error). Cuidado doctrinal: data-status NO es el intent perceptivo — si en algún momento gana eventos con evaluación, el intent viaja por fromProp:intent del morfo, no reciclando status (regla data-color ≠ perceptual-intent). v1 sin eventos.
  • Eidos: color del media por rol canónico según status (roles, no hex); iconografía por status con override por children.
  • Demo: los 6 status + composición con Button home/back.

F1.4 callout — admonición inline

  • Gap: aviso DENTRO del contenido (info/tip/aviso/peligro). Banner es el anuncio de página; esto es la nota de documento — imprescindible para prose y el docs-shell.
  • Membresía: morfo + eidos display (sin soma; variante dismissible se DIFIERE — si se pidiera, el dismiss es evento real → soma + sema en esa pasada, no antes).
  • Fase 0, comparar: Radix Themes Callout · shadcn Alert · admonitions de Docusaurus/Starlight · banner propio (leer su README: qué decisiones ya están tomadas para avisos y cuáles NO trasladan).
  • Morfo (boceto): parts provider (role note) + icon + title + content. La evaluación ES semántica aquí: prop intent restringido a un subset canónico (candidatos: neutral | affirm | risk | threat; validar contra vocabularies.md) estampado vía fromProp:intent — así eidos tiñe con la MISMA mecánica evaluativa del sistema, no con un enum paralelo inventado.
  • Eidos: tinte por intent usando roles/escalas (fondo suave + borde + icono saturado — estudiar cómo tiñe banner e imitar la mecánica); tipografía del canon; --callout-* para spacing/radius.
  • Demo: los 4 intents × con/sin título × con contenido multilínea, e incrustado en un texto largo (anticipo de prose).

F1.5 prose — contenido largo estilizado

  • Gap: contenedor que estiliza HTML/markdown renderizado (h1–h6, p, listas, blockquote, tabla, código, img, hr) con los tokens del tema. Sin esto no hay docs ni blog.
  • Membresía: morfo mínimo + eidos (sin soma). El morfo declara UNA part provider; el trabajo vive en la receta.
  • Fase 0, comparar: Tailwind Typography (prose) — el patrón de referencia — · Mantine TypographyStylesProvider · Radix Themes.
  • Norma que lo hace legal: los selectores de elemento descendientes ([data-prose] h2, [data-prose] ul…) son selectores estructurales bajo una parte del morfo — sancionados por la norma S1 del eidos-lint (hook = data-attr del morfo o estructural bajo parte; clases NO).
  • Eidos: LA receta grande del grupo. Reglas: tipografía SOLO vía primitivos del canon (--font-*, escala tipográfica; cero literales — y los proporcionales que hagan falta con /* literal: */ justificado); medida de lectura --prose-max-width (~65ch) tokenizada; code/pre alineados con los tokens de code/code-block (leer sus recetas ANTES; si hay que duplicar valores, es un gap a flag, no un copy-paste); imágenes max-width:100%; tablas con overflow propio. El HTML interno es del app — aquí la regla B-2 no aplica (es la excepción documentada).
  • Demo: documento markdown real renderizado (headings, listas anidadas, tabla, código, blockquote, callout incrustado), claro/oscuro, densidades.

F1.6 anchor-nav — índice con scrollspy

  • Gap: TOC lateral con sección activa según scroll (docs, settings largos, landing largas).
  • Membresía: soma + eidos (observación + estado activo + navegación).
  • Fase 0, comparar: AntD Anchor · Mantine TableOfContents · Starlight/Docusaurus TOC. APG: no hay patrón de widget — es un nav landmark con aria-current; anotarlo en el README (válvula A-1.4 con nota, como pagination/stepper).
  • Morfo (boceto): parts provider (nav, aria-label vía texts: — aquí SÍ hay string de chrome: "On this page"/"En esta página" → catálogo langs) + list + item + link (¿archetype de item interactivo? — validar contra vocabularies; el link compone Link del catálogo). Data: data-active en item; aria-current. Eventos: click de link = navegación (familia/verbo a decidir en fase 1 contra SEMA_VERBS — candidato familia shift; validar). expression: según criterio D.4.
  • Soma: registro de secciones objetivo (por id o por attachPart), observación vía dom.observe (IntersectionObserver, umbrales al gusto del provider) — mismas reglas anti-reflow que F1.1. El activo es estado del provider; los effects estampan data-active. Scroll programático al click vía dom (scrollIntoView por ActiveDom), no window crudo.
  • Eidos: raíl vertical con indicador de activo (¡leer la memoria NavMenu Indicator: soma posiciona, eidos da forma, nunca transition de transform!); niveles h2/h3 con indentación tokenizada.
  • Demo: página larga real con prose (F1.5) + anchor-nav vivo.

F1.7 sidebar — navegación vertical de app

✅ E-1 FIRMADA (2026-07-21): la colisión sidebar-app vs árbol-docs (P5/B-5) se resuelve con el componente canónico nav-tree (F1.8) — este sidebar queda app-céntrico. Además: el dossier (P4) fija el suelo shadcn (23 partes, dos ejes estado/modo, costuras open/onOpenChange/ toggle expuestas desde v1) — la fase 0 dimensiona contra él por E-2.

  • Membresía: soma + eidos. Es el componente más pesado de F1 — reservarle tanda propia.
  • Fase 0, comparar: shadcn Sidebar (el patrón de referencia actual) · Mantine AppShell.Navbar · Ark/Radix (no lo tienen — anotar). Scope al usuario ANTES de construir: qué features de shadcn entran en v1 (propuesta v1: grupos + colapsable a raíl con tooltips + activo + slots header/footer; FUERA v1: keyboard shortcut global, persistencia — llega del app por D-BLK.6, submenús flotantes en raíl).
  • Morfo (boceto): parts provider + header + content + group + group-label + item + footer + trigger (botón colapso — compone Button con asChild/child pattern). Data: data-collapsed, data-rail, data-active (item). Eventos: toggle de colapso (verbo candidato del set real; evaluación neutral), activación de item si el item es más que un Link — decidir en fase 1 (si item = Link puro, la navegación no necesita evento propio del sidebar).
  • Soma: estado collapsed/rail; grupos colapsables PUEDEN componer el provider de collapsible (precedente compose-compound-in-component: NumberField dentro de Knob, con aislamiento de eventos); variante móvil COMPONE Drawer (no lo reimplementa) — el provider decide qué montar por breakpoint responsive del sistema, no matchMedia propio.
  • Eidos: raíl con will-change cuidado (memoria: will-change: transform produce jitter en raíles finos con DPR≠1 — override a auto); tooltips de item en modo raíl componen Tooltip; tokens --sidebar-* (width, rail-width, paddings).
  • Demo: shell de app simulada, toggle colapso, grupos, móvil (375px) con drawer, RTL.

F1.8 nav-tree — árbol de navegación data-driven (E-1, 2026-07-21)

  • Gap: navegación jerárquica profunda (docs-shell, árboles de páginas) con active-trail — lo que el dossier (P5) demostró que NO cabe en el sidebar-app: ≥3 niveles, active-trail auto-expandido, auto-colapso de hermanos, profundidad default-open, badges, scroll-active-into-view, presentación móvil. Data-driven por diseño (el app pasa el árbol como datos — precedente sancionado: Menubar; B-5 no se viola porque el componente canónico ES data-driven).
  • Membresía — fase 0 OBLIGADA contra nuestro propio catálogo: leer tree-view (ya existe: selección de nodos) ANTES de diseñar y delimitar la frontera tree-view (selección/widget tree) vs nav-tree (navegación por links, landmark nav) — o justificar extender tree-view. La decisión es del scope-approval.
  • Fase 0, comparar: sidebars de Starlight/Docusaurus/Fumadocs (dossier P5 — comportamientos y anatomía) + APG Disclosure Navigation (dossier P4/P3: botones aria-expanded/aria-controls, Esc devuelve foco, flechas opcionales) + tree-view propio.
  • Morfo (boceto): parts provider (nav landmark, aria-label vía texts:) + list + item + trigger (grupo colapsable) + link (compone Link; aria-current="page" del MISMO estado que data-active)
    • slot badge (compone Badge). Data: data-active, data-expanded, profundidad como CSS var tokenizada (no attr por nivel). Keyboard: patrón disclosure; roving opcional opt-in.
  • Soma: árbol como datos; el activo llega por costura del app (matcher de URL — el componente NO conoce el router); active-trail expande ancestros; colapso de grupos evalúa componer collapsible (compose-first); scroll-active-into-view vía dom.
  • Eidos: indent por nivel con token (--nav-tree-depth-offset, estilo Mantine pero tokenizado), línea/raíl de nivel, estados active/expanded.
  • Demo: árbol real de ~40 nodos y 3 niveles (p. ej. el propio mapa de docs), active-trail vivo, móvil 375px.

Cierre F1: los 8 con component:audit --only PASS (o NEEDS-WORK únicamente por reglas D-* de demos v3 si esa fase global sigue abierta — anotar en la tabla), morfo:check, eidos-lint por componente, suite npx vitest run src/uix/eidos verde, npm run check sin regresión sobre baseline.


F2 — Blocks de sitio (14)

Todos entran por el contrato B. Ficha = Función · Compone · API (partes) · Layout/landmark · v1 · Demo. Regla transversal: fase 0 ligera SIEMPRE (mirar el block equivalente en ≥2 catálogos de referencia de §4 y anotar en el README qué se adopta/descarta). Variantes: v1 = LA variante (una); ampliaciones = Gaps con disposición, no código especulativo.

Depende de: F1.1 (sticky) para F2.1; el resto de F2 no depende de F1.

F2.1 site-header

  • Función: cabecera de sitio con afijado y cambio de elevación al pegarse; colapso a menú móvil.
  • Compone: Sticky (F1.1) + Container + NavigationMenu + Button + Drawer (móvil) + Link + Separator.
  • API: <SiteHeader> (props: sticky?, container?) + .Brand + .Nav + .Actions + .MobileNav (children del drawer).
  • Layout/landmark: <header> + <nav aria-label>; skip-link como primer foco (decidir en su fase 0 si el skip-link vive aquí o en los shells — una sola respuesta, documentada).
  • v1: brand izquierda · nav centro · actions derecha · drawer móvil; estilización del estado pegado vía data-stuck (elevación/fondo con tokens de los componentes compuestos, no CSS nuevo).
  • Demo: página con scroll largo, claro/oscuro, 375/1280, RTL.

F2.2 hero

  • Función: sección de apertura con titular, subtítulo, acciones y media.
  • Compone: Section + Container + Stack/Grid + Heading (nivel configurable, default h1) + Text + Group (acciones con Button) + Badge (chip anuncio) + Image/AspectRatio.
  • API: <Hero> (prop layout: 'center' | 'split') + .Eyebrow + .Title + .Description + .Actions + .Media.
  • Layout/landmark: <section aria-labelledby={title.id}>; un solo h1 por página es responsabilidad del app — el README lo dice.
  • v1: center y split (la segunda existe porque discrimina el layout, no por lujo — es el criterio "casos que discriminen").
  • Demo: ambos layouts, con/sin media, con badge, dark.

F2.3 feature-grid

  • Compone: Section + Container + AutoGrid + Stack + Icon + Heading + Text.
  • API: <FeatureGrid> + .Header (title+description de sección) + .Item (con .ItemIcon/.ItemTitle/.ItemText o children libres).
  • v1: items planos (sin Card — la variante card es Gap).
  • Demo: 3/6 items, columnas responsive del AutoGrid.

F2.4 pricing

  • Compone: Section + CardGroup/Card + Heading + Text + Badge (plan destacado) + Button + ToggleGroup (mensual/anual) + filas de features (Stack + Group + Icon check + Text).
  • API: <Pricing> + .Switch (billing toggle; estado de vista local permitido §1) + .Plan (prop featured?) + .PlanPrice + .PlanFeatures
    • .PlanAction.
  • v1: 2–4 planes en fila responsive; el precio mostrado por periodo lo resuelve el app con el valor del toggle (block emite el cambio vía prop callback del ToggleGroup — sin formatear moneda: eso es FormatNumber del app).
  • Demo: 3 planes, featured al centro, toggle vivo.

F2.5 testimonials

  • Compone: Section + AutoGrid + Card + Avatar + Text + Group.
  • API: <Testimonials> + .Header + .Item (+.ItemAuthor con Avatar/nombre/cargo).
  • v1: grid; variante Carousel = Gap (el componente existe; entra cuando una demo real la pida).

F2.6 faq

  • Compone: Section + Container (medida estrecha) + Heading + Accordion.
  • API: <Faq> + .Header + .Item (proxy fino de Accordion.Item con children pregunta/respuesta — respetando el API real del Accordion, que se lee antes).
  • v1: una columna; type del accordion expuesto tal cual.

F2.7 stats-band

  • Compone: Section + Group/AutoGrid + Metrics + CountUp + Text.
  • API: <StatsBand> + .Stat (valor + etiqueta; CountUp opt-in por prop).
  • v1: banda horizontal 2–4 stats; los formatos numéricos llegan ya formateados o vía FormatNumber compuesto por el app.

F2.8 cta — HECHO (2026-07-30)

  • Compone: Section + Container + Motion + Surface (tratamiento de fondo del sistema — un CTA se distingue por acabado, y eso ya es vocabulario del framework) + Heading + Text + Flex (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> + Container + Grid (columnas: Heading sm + Stack de Link) + Separator + Group (IconButton sociales) + Text legal.
  • API: <SiteFooter> + .Column + .Social + .Legal + .Extra (slot libre: theme/lang pickers del app — D-BLK.6).
  • v1: 3–5 columnas responsive → apiladas en móvil.

F2.11 banner (E-3)

  • Compone: el componente Banner existente + Container + Link + Button (dismiss lo posee Banner si ya lo trae — leer su README).
  • API: <SiteBanner> + children; posición top pareja de site-header.
  • 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 (E-3)

  • Compone: Section + AutoGrid + Avatar + Heading + Text + Group (IconButton sociales por miembro).
  • API: <Team> + .Header + .Member (+.MemberAvatar/.MemberName/ .MemberRole/.MemberLinks).

F2.13 contact (E-3)

  • Compone: Section + Grid (info + form) + Form + Field + Textarea + Button + filas de datos de contacto (Icon + Text + Link).
  • API: <Contact> + .Info + .Form; validación y submit = sistema Form con handlers del app (misma regla que newsletter).

F2.14 content-section (E-3)

  • Compone: Section + Container (medida estrecha) + Prose (F1.5) + slots de media (Image/Figure fuera del flujo prose).
  • API: <ContentSection> + children (el HTML renderizado va al Prose).
  • Depende de: F1.5.

(logo-cloud sigue en F5: su versión honesta pide Marquee o queda en un Wrap trivial que no justifica block todavía — decisión anotada, revisable. El resto de la unión del dossier — bento, gallery, cookie-consent, popups, careers, events, comparison, timeline — queda en F5 con disparador, E-3.)

Cierre F2: blocks-check verde; galería web/routes/blocks/ con los 14; una página compuesta de PRUEBA (header + hero + features + pricing + faq + cta + footer juntos) que valide que los blocks ensamblan sin fricción — esa página ES el test de integración del tier y se mira en claro/oscuro/375/1280.


F3 — Blocks de aplicación (10)

Mismas reglas y ficha que F2. Depende de: F1.2/F1.3/F1.4 (estados), F1.7 (sidebar) para F3.1.

F3.1 app-shell

  • Función: esqueleto de aplicación: sidebar + topbar + contenido (+ aside opcional).
  • Compone: Sidebar (F1.7) + Sticky + Container/Grid + ScrollArea + Group + slots.
  • API: <AppShell> + .Sidebar + .Topbar + .Content + .Aside.
  • Landmark: header/nav/main/aside + skip-link (según decisión F2.1); jerarquía documentada.
  • v1: sidebar colapsable + topbar afijada + main con scroll propio; móvil = sidebar en drawer (lo trae F1.7).
  • Excepción B-10 declarada: los shells componen otros elementos de F1 y blocks — allowlist en blocks-check.

F3.2 auth

  • Función: familia de formularios de identidad. Sub-blocks: sign-in · sign-up · recover · otp.
  • Compone: Card + Form + Field + PasswordField + PinInput (otp) + ProofOfHuman + Button + Separator + Link + slot de proveedores sociales (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 en next-features.md §7.)

Cierre F3: blocks-check verde; demo de app-shell montando dentro data-table + dashboard + notifications + user-menu como página de integración; navegador claro/oscuro/375/1280 + teclado (tab por toda la página de integración sin trampas de foco).


F4 — Blocks de docs (3)

Depende de: F1 completo (prose, anchor-nav, nav-tree, callout) + F2.1. Conecta con la iniciativa docs-corpus=site (el corpus es la semilla del sitio); estos blocks son su vehículo de UI.

F4.1 docs-shell

  • Compone: AppShell (F3.1, excepción B-10) especializado: NavTree (F1.8) como árbol de docs + Prose (main) + AnchorNav (TOC derecha) + Breadcrumb + trigger de búsqueda (Command — ya existe) + prev/next (Group de dos Card/Link).
  • E-5 firmada: el atajo ⌘K del trigger de búsqueda = listener app-land documentado en el README del block; el art shortcuts conserva su disparador F5 (≥2 consumidores reales).
  • API: <DocsShell> + .Nav + .Article (prose) + .Toc + .Search
    • .PrevNext.
  • v1: 3 columnas desktop → TOC colapsada y sidebar-drawer en móvil.

F4.2 code-showcase

  • Compone: Tabs (Preview/Code) + Surface (lienzo de preview) + CodeBlock + Clipboard + Toolbar opcional.
  • API: <CodeShowcase> + .Preview (children vivos) + .Code (children CodeBlock).
  • v1: preview + código + copiar. (Chips de tema/dirección/densidad del lienzo = Gap, notado — pediría servicios, D-BLK.6.)

F4.3 props-table

  • Función: tabla de referencia de un componente generada del morfo — parts, data-attrs, ARIA, keyboard y eventos salen de compileMorfo (ventaja estructural única de este framework: el contrato ES introspectable).
  • Compone: Table + Code/Kbd + Badge.
  • API: <PropsTable morfo={…}> (aquí el input data-driven es legítimo: el "árbol" es el contrato canónico, no contenido inventado).
  • v1: tablas morfo-backed (parts/attrs/keyboard/eventos con familia·verbo·intent). Tabla de props TS = Gap tracked (requiere extracción de tipos; no improvisar — flag para decidir tooling).

Cierre F4: una página de docs REAL montada con los 3 (un doc del corpus renderizado en docs-shell con showcase y props-table de un componente PASS), verificada en navegador.


F5 — Backlog condicionado (no construir sin disparador)

Componentes canon candidatos (cada uno entraría por la ruta de 9 fases):

Pieza Disparador
lightbox (visor imagen zoom/galería) cuando un block de media/galería real lo pida
tour (onboarding spotlight) cuando el app-shell tenga consumidor real con onboarding
hover-card genérico cuando un tercer caso no-URL aparezca (hoy link-preview cubre)
description-list primer detail-view real (pareja natural de data-table)
loading-overlay primera pantalla con carga bloqueante real
transfer-list primer admin real con asignación dual
mention ya registrado en next-features.md §7 (chat) — no duplicar aquí
masonry · bottom-nav · swipe-actions · pull-to-refresh · image-compare · marquee (¿pack?) · watermark · signature-pad demanda real; varios son candidatos a pack, no a canon — decidir con la regla de admisión de packs
art shortcuts (registro global de atajos + cheat-sheet con Kbd) cuando ≥2 consumidores reales (command palette global + docs) lo pidan

Blocks diferidos: scheduler (bloqueado por chronos) · logo-cloud (bloqueado por decisión marquee) · billing · file-manager · profile-card (cuando haya consumidor).


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

  1. npm run check sin regresión; npm run blocks:check verde; npm run component:audit → los 7 de F1 PASS (o NEEDS-WORK solo por D-* de la fase demos global); docs:check verde.
  2. Las tres páginas de integración (F2 landing · F3 app · F4 docs) renderizan compuestas SOLO de canon+blocks, verificadas visualmente en claro/oscuro, 375/1280 y teclado.
  3. Doctrina publicada y enlazada (architecture/blocks.md en el mapa; CLAUDE.md al día); cada block con README B-9 completo.
  4. Ningún gap silenciado: todo ❌/⚠️ de las fases 0 tiene decisión firmada o fila en F5/next-features.

7. Registro

  • 2026-07-21 — Plan creado (análisis de ecosistema + decisión de usuario: tier blocks). Iniciativa en next-features.md §8.

  • 2026-07-21 — F0.1 HECHA: las 7 D-BLK firmadas (repaso en sesión). D-BLK.1 enmendada frente a la propuesta: el tier vive en src/uix/blocks/ (no src/blocks/); referencias del plan actualizadas (tabla de tiers, B-4, blocks-check, F0.3/F0.4). Resto firmado como propuesto. Siguiente: F0.2 (doctrina en architecture/blocks.md).

  • 2026-07-21 — F0 CERRADA (misma sesión): doctrina docs/architecture/blocks.md · alias $blocks (vite + svelte.config + CLAUDE.md) · src/uix/blocks/README.md (mapa + template B-9) · scripts/blocks-check.ts + npm run blocks:check (self-testing) · galería web/routes/blocks/ con layout de bootstrap propio (+layout@.svelte, espejo mínimo del de /uix, sin packs sema) · fila E1 en docs/README.md. Nota de commit: docs/README.md y docs/next-features.md quedaron FUERA del commit de F0 — ambos traían diff foráneo de otra sesión imposible de separar por staging de archivo completo; sus hunks propios (fila E1 + §8) viajan con el árbol de trabajo hasta que su dueño commitee. Siguiente: F1 (orden recomendado: empty-state → result → callout → sticky → prose → anchor-nav → sidebar).

  • 2026-07-21 — Estudio de referencia COMPLETO (encargo del usuario: "a la par y cuanto menos superarlo"): 6 pistas de investigación web en paralelo → docs/process/RESEARCH-blocks-references.md (suelos de paridad a nivel de prop por ítem, pitfalls, superación). Regla nueva en §4: toda fase 0 contrasta contra el dossier. Puntero de colisión añadido a F1.7. Enmiendas E-1…E-5 presentadas al usuario (E-1 nav-tree · E-2 filosofía de alcance v1 · E-3 categorías nuevas F2 · E-4 registry/llms/MCP · E-5 ⌘K) — firmas pendientes; se registran aquí.

  • 2026-07-21 — E-1…E-5 FIRMADAS (todas según recomendación): E-1 componente canónico nav-tree data-driven (F1.8; F1 pasa de 7 a 8; el naming se valida en su fase 0 como extensión de D-BLK.7); E-2 suelo de paridad = v1 (regla en el intro de F1); E-3 F2 pasa de 10 a 14 (banner · team · contact · content-section; resto de la unión a F5); E-4 distribución registry/llms.txt/MCP registrada como iniciativa propia en next-features.md §9; E-5 ⌘K = listener app-land (nota en F4.1). Doctrina (architecture/blocks.md promotion path) y galería actualizadas.

  • 2026-07-21 — F1.2 empty-state HECHA (ruta de 9 fases completa, suelo E-2 del dossier §P3): morfo display 5 partes (scope: ['eidos'], 0 eventos justificados, Title role:'heading') + langs label + eidos compound (Media kind=icon|media con placa 2× glifo · Title level 2–6 default h3, modelo Atlaskit · Description measure 45ch · Actions label → role=group, buttonGroupLabel) + recipe sobre el bundle --size-* (título un paso discreto arriba) + demo v2 de 9 tabs + README (Comparativa 5 refs · Passive justification · Gaps con disposición). Verificado: component:audit PASS · eidos-lint 5 morfo-backed + 4 eidos-only sancionados · svelte-check 76E/51W = baseline exacto · navegador claro Y oscuro por estilos computados (título 18→24px, placa 40→64px, chips vivos). Siguiente: F1.3 result (comparte esqueleto; fase 0 contra dossier §P3).

  • 2026-07-21 — F1.3 result HECHA (ruta completa, suelo E-2 §P3): morfo 6 partes (extra sin archetype — parte genuinamente propia) · status enum de 7 CON warning (decisión del dossier firme), default info, HTTP renombrados semánticos y renderizados NEUTROS (código mono grande, jamás rojo — doctrina AntD) · media default compone el mapa doctrinal IntentIcon (fulfill/threat/risk) + Info de catálogo, children lo reemplazan entero (context local eidos-only con getter reactivo) · Title default h2 (vs h3 de empty-state — deliberado) · esqueleto duplicado conscientemente (README lo registra; revisable al 3er consumidor) · sin eje size (paridad AntD). Verificado: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed + 5 eidos-only · guards recipe 37/37 · navegador: success=fulfill verde 84px · error=threat rojo · not-found=«404» mono NEUTRO aria-hidden, copy conmutando, cero errores consola. Siguiente: F1.4 callout (decisión IMPORTANT/5º hueco + role=note; dossier §P3).

  • 2026-07-21 — F1.4 callout HECHA (ruta completa, suelo E-2 §P3): morfo 4 partes + texts con títulos default localizados por intent (note/tip/warning/caution — el patrón GitHub de label visible; la semántica nunca viaja solo en color) · el hueco IMPORTANT resuelto con el modelo Radix: intent (4, mapea el vocabulario de facto) + color override SOLO bajo neutral (doctrina §4 — intent evaluativo gana), montado sobre la maquinaria C6/THM-2 de badge (forwarders + _palette-* → 8 roles + 33 escalas + custom con CERO CSS extra; el forward presence-guarded se emitió solo) · role="note" nombrado por el Title vía aria-labelledby SOLO mientras está montado (registro reactivo por context; bug cazado en navegador: el += del registro leía el estado dentro del tracking del $effect del hijo → effect_update_depth y contador a −997 — fix untrack, lección para la memoria) · Title NO heading (protege el outline de anchor-nav) · icono = mapa doctrinal IntentIcon · escalación tipada role: status|alert|none (el role="alert" estático de shadcn NO se copia) · dismissible DIFERIDO a pasada soma+sema (regla de admisión). Verificado: audit PASS 0E/0W · lint 8 morfo-backed/0 invalid · guards 37/37 · navegador: «Nota»/« Atención» localizados, risk=ámbar hue 45-60, IMPORTANT=plum hue 326 por shared layer, labelledby=titleId. Siguiente: F1.1 sticky (orden del plan: desbloquea F2; dossier §P3 — offsets, scroll-host, centinela por borde, espejo scroll-state).

  • 2026-07-21 — F1.1 sticky HECHA (commit 998b11d9b, PASS 0E/0W). El primer componente BEHAVIORAL (soma+eidos). Precedido de un fan-out de investigación (workflow, 6 lectores) que fijó el mapa de construcción: template = FeedSentinelProvider; motor = $adom.observeIntersection (nunca scroll+getBoundingClientRect); escritura de attrs por syncAttrs (nunca dom.apply crudo). Diseño: un StickyProvider registra AMBAS partes (box+sentinel) en un runtime; $effect retorna el observer como teardown; callback async voltea stuck=$state; rootMargin DERIVADO del offset (la línea de disparo del centinela = la línea de pin — acoplamiento matemático); centinela = HERMANO de flujo del box (dos nodos raíz), no hijo; offset viaja como --_sticky-offset (dato A8); recipe SOLO posiciona (el consumidor decora [data-sticky][data-stuck]); data-stuck/data-edge espejan @container scroll-state(stuck: top/bottom) (polyfill del contrato CSS futuro). Verificación clave: 5 tests, incl. 1 real-IntersectionObserver en chromium foreground (no-stuck→scroll→stuck→back, prueba end-to-end de la geometría — lo que el Browser pane suspendido NO puede). Decisión declarada: el test usa el installSomaHarness compartido (los 86 tests lo hacen) — en tensión con la regla CLAUDE.md «never fake translators»; flaggeada al usuario para ratificar. Notas de commit: nav sidebar fuera (flip CRLF ajeno de 2125 líneas); índices langs/soma reconstruidos sticky-only (entangle con aura sin commitear de sesión paralela). Consola: detectado un warning reactivo residual en callout (registerTitle en $effect) — a investigar. Siguiente: F1.5 prose (o F1.6 anchor-nav / F1.8 nav-tree; los 3 restantes que quedan de F1 tras prose son anchor-nav, nav-tree, sidebar).

  • 2026-07-21 — Review adversarial de sticky (workflow wrssrn7tx, 3 lentes + verify): 1 hallazgo CONFIRMADO (minor) — pin en eje lógico vs observer físico, diverge en writing-modes verticales. Corregido (6cba9a0b1): recipe a top/bottom físico + margins físicos; 3 claims «logical/RTL» del demo corregidos. Re-verificado PASS 0E/0W + test real-IO. (1 lente teardown-ssr falló por API stall; caminos cubiertos por tests.) F1.1 sticky CERRADO.

  • 2026-07-21 — F1.5 prose HECHA (commit 5b84749ee, PASS 0E/0W). Quinto F1 (eidos-only). EL DIFERENCIADOR verificado en navegador: reglas de elemento a especificidad CERO (:where([data-prose] EL)) → un <Callout> embebido conserva su tinte/borde/grid AUTOMÁTICAMENTE (su [data-callout] gana a :where), en claro Y oscuro — sin not-prose, cosa que ninguna ref puede. Escala EM-relativa (un font-size raíz re-deriva todo, no la escala 5× de tailwind); overflow de tabla estilo GitHub; dark gratis por roles (no -invert); propiedades lógicas. Decisión :where (no @scope, diferido a Gaps). Lección: recipe token defs (base.ts) admiten literales sin tripar R-2.7; los literales em en el .css se justifican con /* literal */ (21 añadidos). F1 = 5/8. Restan: anchor-nav (F1.6) · nav-tree (F1.8) · sidebar (F1.7).

  • 2026-07-22 — F1.6 anchor-nav HECHA (commit fe2d43b7a, PASS 0E/0W). Sexto F1, segundo BEHAVIORAL (soma+eidos) tras sticky. TOC scrollspy: landmark <nav> + anchors nativos; detección por IntersectionObserver de banda (rootMargin -{topOffset}px 0px -70% 0px), NUNCA scroll-listener + getBoundingClientRect (doctrina anti-reflow — Mantine/Docusaurus lo violan). aria-current="location" (spec-preciso; solo 1 de 5 refs pone aria-current). Rail por-link con acento en data-active; indent por data-level. Verificación: test real-IO chromium (anchor-nav-io) — banda + zona muerta al fondo + fallback inicial; el pane suspendido daba un getComputedStyle de color OBSOLETO (falsa alarma, la regla [data-active] sí aplica — el font-weight:500 lo probaba). Review adversarial (4 dimensiones × verificador escéptico, 9/16 confirmadas): arregladas → ref-count en register/unregister (hrefs duplicados / churn ya no tiran un target vivo, +test); transición y outline-offset tokenizados (--duration-fast/--ease-default, calc(--focus-ring-width * -1)). Documentadas como límite v1 (Gaps) → salto instantáneo al fondo (Ctrl+Fin) en zona muerta + orden-enlaces==orden-secciones (ambas piden cambio de contrato de observación v2). data-level fuera de rango degrada a flush (no arreglado, simplicidad). Además (37e35d7a2): npm run check global destapó 2 errores de tipo en F1.1 sticky (sentinelRef debía ser State no Active; eidos types importaba StickyProps en vez del export renombrado ProviderProps) — míos, arreglados. Mis archivos suman 0 errores de check (80→73; los 73 son deuda foránea de sesiones paralelas). F1 = 6/8. Restan: nav-tree (F1.8) · sidebar (F1.7).

  • 2026-07-22 — F1.8 nav-tree EN CURSO (WIP commiteado, sin cerrar). Componente COMPLETO en verde en gates estáticos (audit PASS · eidos-lint 20/0 · svelte-check 0 · contracts mis-partes limpias); morfo+soma+eidos+pack de sema+4 READMEs+todos los registros escritos. Falta demo + verificación en NAVEGADOR + review adversarial + cierre. Data-driven (E-1); disclosure propio (getter de estado + eventos emerge, pack espeja collapsible); filas propias (no Link/Badge compuestos — Badge diferido a Gap); activeHref como costura. Corregido de paso: README de soma de anchor-nav faltaba (contracts lo destapó). Handoff detallado: docs/process/CONTINUE-nav-tree.md. ⚠️ 2 fallos contracts AJENOS (menubar/radio-group, sesiones paralelas).

  • 2026-07-22 — F1.8 nav-tree HECHA (cierre de la ruta: demo v2 de 9 tabs con el mapa real de docs —43 nodos, 3 niveles—, verificación en navegador REAL y review adversarial de 5 dimensiones × 3 verificadores escépticos, 22 hallazgos brutos → 10 confirmados, 12 rechazados). Arreglado del review: (1) el emit de sema nunca ocurría — la parte group es el target de los eventos y se registraba SIN ref, así que runtime.trigger lanzaba SomaRuntimeTargetError en silencio (lo cazó el navegador: cero data-event-* en el grupo frente a los de collapsible); (2) el chevron se comía media fila (inline-size:100% resolvía como flex-basis → 101px de 231 en «Soma»: pulsar el hueco tras la etiqueta plegaba en vez de navegar) → toggle compacto de 24px con suelo de diana WCAG 2.5.8; (3) ciclo potencial en parentByKey (clave duplicada = nodo padre de sí mismo → trailKeys giraba para siempre y congelaba la pestaña) → clave sufijada + aviso del logger + guarda de ciclo en el paseo; (4) colapso ahora CONTEXTUAL (recuerda el activeKey bajo el que se hizo: cerrar la sección que lees se respeta, pero caduca al navegar DENTRO — si no, la página actual se quedaba sin fila visible y el trail auto-expandido quedaba anulado; sigue siendo query pura); (5) child ya no se comía el árbol (recibe children además de props — un árbol data-driven no lo puede reautorar el consumidor); (6) nombre accesible del chevron declarado en morfo + localizado («Alternar sección {label}») en vez de duplicar el nombre del enlace; (7) paridad de snippet en la demo + token fantasma --nav-tree-rail-width fuera del docblock. RTL completo (el glyph se dibuja con bordes lógicos: espeja solo; lo que no espeja es el GIRO → bajo [dir='rtl'] las dos rotaciones se intercambian) — el Gap «dirección del chevron en RTL» queda RESUELTO, no diferido. Decisión de usuario (delegada): badge SÍ entra en v1 (está en el suelo del dossier §P5, los 6 refs lo llevan) resuelto con snippet, no con recursión de eidos — soma declara la parte badge y renderiza el snippet recibido (sin él, el valor crudo: sigue headless), el wrapper de eidos pasa el Badge canónico; va DENTRO del control de la fila para que su texto entre en el nombre accesible («TSC, New, enlace»). disabled también entra, por decisión explícita del usuario tras presentarle el criterio: el nodo conserva la fila pero pierde el href (no hay nada que activar con Enter, clic ni «abrir en pestaña nueva» — un pointer-events:none de CSS solo tapa el ratón) y añade aria-disabled + data-disabled; el toggle del grupo NUNCA se deshabilita (disclosure es control de vista, no destino) y un nodo sin href lo ignora. Verificado: component:audit PASS · eidos-lint 26 morfo-backed / 0 invalid · svelte-check 0 errores en mis archivos · vitest src/uix/eidos 353/353 · navegador real (Playwright, el pane suspendido NO sirve: congela rAF y el scroll-into-view parecía roto) → trail auto-expandido, sema emerge-expand/collapse estampando en group, teclado nativo (Tab/Enter/ Espacio), foco visible, claro+oscuro, RTL, 375px sin desbordes, 0 errores de consola. F1 = 7/8. Resta: sidebar (F1.7).

  • 2026-07-22 — F1.7 sidebar — fase 0 + morfo + soma (commit 2bc5fe6bd). Fase 0 presentada al usuario ANTES de construir, con 4 decisiones firmadas: 17 partes (13 estructurales + menu-action · menu-badge · menu-sub · menu-sub-button), submenú FLOTANTE en modo icono ya en v1 (superación: shadcn los oculta y deja ramas inalcanzables), sema emerge-expand/collapse sobre panel, y ritmo por fases con parada tras morfo+soma. Contrato: DOS ejes (data-state + data-collapsible SIEMPRE estampado) + data-side + data-mobile; panel = <aside> nombrado y content = <nav> nombrado con header/footer FUERA (ninguna ref trae landmark); rail = <button> real en el orden de tabulación (shadcn: tabIndex=-1); aria-current="page" del MISMO prop que data-active; menu-badge dentro del control de la fila; separator/input NO son partes (se componen Separator y Field). Soma: costuras open/defaultOpen/onOpenChange/toggle() desde v1 (sin ellas la persistencia y el atajo de app-land serían rediseño posterior), mobile del breakpoint del SISTEMA (uix.dom.isAtLeast) y no de un matchMedia propio, submenú flotante sobre createFloatingShellRoot con Escape que devuelve el foco, e ids per-instancia para los dos partRef que son last-write-wins (precedente navigation-menu). Verde: morfo:check carga el contrato · component:audit --only sidebar PASS 0E/0W · svelte-check 0 errores propios · contracts.test solo los 2 fallos ajenos. Siguiente: eidos (raíl, anchos tokenizados, modo icono, Drawer móvil) + demo + review.

  • 2026-07-23 — F1.7 sidebar HECHA · F1 CERRADA (8/8). Eidos + demo + navegador + review adversarial (commits 2bc5fe6bd morfo+soma · 21bbf9f50 eidos+demo · 293593307 alto completo · dcce75cc2 foco del flyout · 245c22471 controles de demo · 75e1fe32a review). El review (6 dimensiones × 3 verificadores, 135 agentes) dio 43 brutos → 31 confirmados, todos arreglados. Los gordos: (1) collapsible='none' dejaba el sidebar INALCANZABLE en móvil (toggle inerte + raíl oculto + drawer sin trigger) → la inercia del modo se limita al escritorio; (2) ese mismo modo hacía MENTIR a data-state/aria-expanded sobre un panel visible → el modo pinta el estado; (3) el flyout de modo icono solo cerraba desde el propio flyout → puntero, foco y Escape se resuelven ahora en el menu-item, que contiene fila y sub, y el flag se limpia al cambiar de presentación; (4) las filas del raíl no tenían NOMBRE (un tooltip solo describe) → el texto de tooltip pasa también a aria-label; (5) el panel off-canvas colapsado seguía en el tab order → visibility: hidden con la transición retrasada; (6) el signo del off-canvas era físico → en RTL barría el panel por encima de la página; (7) el flyout era la única superficie flotante sin z-index. Verificado en navegador real todo lo anterior. Gates: audit PASS 0E/0W · eidos-lint 47 morfo-backed / 0 invalid · svelte-check 0 errores propios · vitest src/uix/eidos 353/353. F1 = 8/8: la fase queda CERRADA. Siguiente: F2 (blocks de sitio, 14).

  • 2026-07-23 — F2.1 site-header HECHA · primer block del tier (commits 3cfc030b3 block+landmark · 016658a0c controles · f98c844ed fix de Box · 50574806d harness · 33328df7a cromo de sección · d574982ab escenario a ras · ae7b4f8c3 vista previa honesta). Composición pura: Sticky (solo si sticky, sin wrapper inútil) + Container + Group + Box (el interruptor ancho/estrecho por display responsive, B-6) + Drawer; marca, nav, acciones y navegación móvil entran como snippets del app (B-7: cero cadenas propias). Fase 0 contra el dossier §P1: adoptado el FLYOUT (la brecha de paridad) sin reimplementarlo — el app pasa un NavigationMenu. Encontrado al componer, arreglado en el canon: NavigationMenu no emitía landmark (su morfo declara defaultElement: 'nav' y soma pintaba un div). Hueco registrado, no falseado → CERRADO el mismo día: el CTA que NAVEGA y parece botón. Se sirve por COMPOSICIÓN, no por prop nueva: el child (asChild) de Button entrega ahora un snippet content con el cuerpo ya decorado, así que <a href {...props}>{@render content()}</a> recibe la pintura sólida completa y conserva icono/etiqueta/spinner (antes el child los sustituía: por eso la flecha del sitio alpha estaba escrita a mano); y soma deja de estampar type en un elemento que no es suyo (<a type="button"> es una pista de MIME falsa) — el morfo lo declara condicional. Button sigue sin href y Link sigue poseyendo la navegación: las dos decisiones firmadas se mantienen. Doctrina en el README de eidos Button §«CTA que navega»; en uso en la demo del block. Defecto de framework destapado por la demo: las 44 variables de cascada de Box heredaban, así que un Section regalaba su padding a cada descendiente (la galería arrastraba ~300px de aire desde F0) → @property { inherits: false }, radio verificado sin regresiones (docs/theming/changelog.md §46). Superficie de demo del tier (doctrina nueva, B-9 ampliada): harness compartido en web/routes/blocks/_lib/ + cromo de sección con los ejes que mueven el framework (tema, idioma, dirección, densidad) + catálogo único. El block se enseña a sangre en la página, nunca dentro de un marco con relleno ni de una caja con scroll: medido, un Card desplazaba 21px un header con offset: 0, y un sticky dentro de un div con scroll es un comportamiento que nadie vive. Los anchos de dispositivo se sirven desde una ruta preview propia (iframe opt-in: en dev, dos documentos sin empaquetar agotan las conexiones del navegador). Siguiente: F2.2 hero — handoff en docs/process/CONTINUE-blocks.md.

  • 2026-07-23 — F2.2 hero HECHA (mismo día). API por slots de snippet (no compound): eyebrow/title/description/actions/media/background + layout: 'center' | 'split' | 'background' + level/container/size. La decisión de forma se razonó con el usuario: compound se reserva para partes que COORDINAN (Accordion, Dialog); un hero son slots de layout que el root arregla → snippets. El campo entero shippea marketing como copy-paste plano; nadie aplica compound a una sección → los slots por zona SON la historia de personalización del tier. El block envuelve título/subtítulo en Heading/Text (posee el id del landmark y el nivel; la app pone las palabras); eyebrow/actions/media son contenido libre. Landmark: <section aria-labelledby> nombrado por el título (verificado: región con nombre accesible). Layout background (cover) añadido por scope-approval del usuario: media a sangre detrás, velo --color-overlay a --opacity-scrim, texto on-solid y object-fit: cover vía <style> justificado (D-BLK.2) — las ÚNICAS reglas que el block posee, todas sobre tokens del ecosistema, cero color a mano. La demo dogfooda el sistema de color: el backdrop es un Surface color="primary" gradient (finish aurora, --gradient-aurora derivado de los roles del tema). Registrado, no resuelto: Box/Surface flex/grow no hicieron crecer un hijo flex (bars a 0-width, flex: 0 1 auto; prop sin usar en shipped) → la demo usó Grid (tracks 1fr); flag para revisar el cableado de --box-flex. Gates: blocks:check verde (2 blocks) · svelte-check sin errores propios. Siguiente: F2.3 feature-grid (primer compound del tier: .Item repetido — LA brecha #1 del dossier, split/alternante texto-screenshot).

  • 2026-07-23 — F2.3 feature-grid HECHA (mismo día). Primer block COMPOUND del tier: <FeatureGrid> + .Header + .Items + .Item + .ItemIcon/ .ItemTitle/.ItemText. Aplica la regla de forma: .Item se REPITE (el app mapea sobre N features) → gana compound; las partes no coordinan (sin contexto/estado entre ellas), la rejilla es del padre. .Items es el envoltorio honesto de la rejilla (AutoGrid) — deja la cabecera fuera sin un grid-column: 1/-1 a pelo. El block coloca (Section·Container·AutoGrid·Surface· Heading·Text), el app pone contenido (B-7). align de sección (center/start) coloca la cabecera coherente con las columnas. Sub-componentes de bloque extienden los props del componente canon que envuelven, NO HTMLAttributes: el style: string|null del atributo HTML crudo choca con el style: string del canon (+ "union too complex") al hacer spread. ItemIcon default solid: el soft-primary en claro es casi blanco (oklch 0.99) → invisible; solid da chip con glifo on-solid (la tinta de contraste la pone Surface). Demo: control de columnas (fluido/2/3/4), align, nº items, dir; 6 features con iconos del canon. Gates: blocks:check verde (3 blocks) · svelte-check sin errores propios. Hueco a decisión del usuario: el feature-split/alternante (texto junto a screenshot, lados alternos) — la brecha nº1 del dossier — es otra disposición (filas de 2 columnas, no rejilla de iconos): probablemente block hermano feature-split. Presentado como scope-approval, no resuelto aquí. Siguiente: F2.4 pricing.

  • 2026-07-23 — Primitivo canon Mockup + F2.3b feature-split HECHOS (mismo día; scope-approval del usuario: «split + primitivo de media reutilizable»). La clave del dossier: la brecha del hero/features es el TRATAMIENTO DE MEDIA, no el conteo de layouts. En vez de falsear un screenshot por demo, se cierra de raíz con un componente del canon.

    • Mockup (src/uix/eidos/components/mockup/ + morfo eidos-only, 0-event, como aspect-ratio): enmarca media en cromo de dispositivo — chrome: 'plain' | 'browser' | 'phone' + url para la barra. Recipe dibuja la barra (dots + pill de URL), el bisel del teléfono y el notch; todo sobre tokens (--radius-*, --color-border-*, --shadow-*, --primitive-*). eidos-lint: 0 invalid / 0 class-hooks (mismo patrón que aspect-ratio, todo eidos-only). Retrofiteado en la demo del hero (dogfood).
    • feature-split (compound: .Row repite): <FeatureSplit> + .Row (reversed, slot media) + .Eyebrow/.Title/.Text/.Features/ .Feature/.Actions. reversed mueve la media al inicio vía grid-column (Box tiene gridColumn/order), dejando la copy PRIMERA en el DOM (orden de lectura intacto). El check de .Feature es decorativo (aria-hidden). La media se compone con Mockup.
    • BUG de hero arreglado de paso (lo tapaba mi filtro de svelte-check con backslashes mal escapados): los snippets title y background colisionaban con los atributos HTML homónimos (title?: string / background) → string & Snippet. Fix: Omit<HTMLAttributes, 'children' | 'title'> + renombrar el slot background→backdrop. Lección para blocks: un slot de snippet cuyo nombre sea un atributo HTML necesita Omit o un nombre distinto. Gates: blocks:check verde (4 blocks) · svelte-check sin errores propios (73 = deuda ajena) · eidos lint.test + morfo 131/131. Siguiente: F2.4 pricing.
  • 2026-07-24 — Fix de Grid + F2.4 pricing HECHOS.

    • Fix canon Grid (theming/changelog §47): align/justify/alignContent NO aplicaban — los atajos place-items/place-content con fallback revert-layer (cuando su prop no está puesto) revertían los longhands a su inicial, pisándolos. Ahora el fallback COMPONE los vars de los longhands. 353 tests eidos verdes, grids por defecto sin cambio. Encontrado alineando feature-split.
    • pricing (compound CON CONTEXTO — el primero del tier cuyas partes se COORDINAN, no solo se repiten): <Pricing bind:period> + .Header + .Switch + .Plans + .Plan(featured, badge) + .PlanName/.PlanDescription/ .PlanPrice/.PlanFeatures/.PlanFeature/.PlanAction. El Switch (ToggleGroup) escribe el periodo en un contexto reactivo (context.ts, getter sobre el $bindable) y cada PlanPrice lo lee → muestra el snippet monthly/annual. El block NO formatea moneda: la app compone FormatNumber en los snippets (B-7). .PlanAction fija el CTA al borde inferior (margin-block-start: auto) para alinear los botones; .Plan con align="start" deja los checks en columna. Verificado: el toggle cambia los 3 precios a la vez (0/29/99 → 0/23/79), featured con acento+elevación, claro/ oscuro/RTL. Hueco a decisión del usuario: tabla de comparación (features × planes) — otra disposición, candidato a hermano pricing-table. Gates: blocks:check verde (5 blocks) · svelte-check sin errores propios. Siguiente: F2.5 testimonials.
  • 2026-07-24 — F2.5 testimonials HECHO. Compound (.Item repite, SIN contexto — como feature-grid): <Testimonials> + .Header + .Items + .Item + .Quote + .Author(slot avatar + .AuthorName/.AuthorRole). AutoGrid de tarjetas de igual alto; .Author fijada al borde inferior (marginTop="auto") para alinear las caras aunque las citas midan distinto. La cara es del app (Avatar con imagen o Avatar.Fallback de iniciales). Card para el quote = variant="outline" (soft neutral es casi invisible en claro). Encontrado: el tema activa solo un SUBCONJUNTO de escalas donor — green/indigo/orange/plum/teal; una escala no activada (cyan/ruby/amber/jade) en color cae en silencio a primary (la regla [data-color] hace var(--scale-…, primary)). No es bug de componente, es config del tema; usar escalas activadas. Hueco a decisión del usuario: la cita única en spotlight (grande, centrada, logo+avatar+autor) — la variante modal del dossier, otra disposición → candidato a hermano testimonial-spotlight. Verificado en claro/oscuro/RTL, caras alineadas, 5 colores distintos. Gates: blocks:check verde (6 blocks) · svelte-check sin errores propios. Siguiente: F2.6 faq.

  • 2026-07-24 — F2.6 faq HECHO. Compound (.Item repite) y proxy fino del Accordion del canon (regla del handoff: leer su API, no reinventar el disclosure): <Faq> + .Header + .List + .Item. .List ES el Accordion — toda su API pasa tal cual (type, bind:value, collapsible, variant, size; defaults FAQ: single, collapsible, outline). .Item proxya Accordion.Item > Header > Trigger(snippet question) / Content(children), y autogenera el value con $props.id() si no se pasa. Container estrecho (md). El teclado/ARIA del disclosure salen del Accordion, el block no los toca. Hueco a decisión del usuario: la lista estática 2/3 columnas (6 de 7 en TW NO son acordeón) — otra disposición, prop layout o hermano. La cola «¿aún tienes dudas?» es app-land (la demo la pone tras .List). Verificado: el acordeón abre/cierra, claro/oscuro/RTL. Gates: blocks:check verde (7 blocks) · svelte-check sin errores propios. Siguiente: F2.7 stats-band.

  • 2026-07-29 — F2.7 stats-band HECHO. Compound (.Stat repite, sin contexto): <StatsBand> + .Stat + .Value + .Label. Compone el Metrics del canon para la semántica de KPI —surfaceless, que es lo correcto: una banda no son tarjetas de panel— y CountUp para la cifra que se cuenta sola al entrar en pantalla: la superación literal del dossier, porque ninguna referencia puede shipearla (todas entregan markup estático). count es OPT-IN, nunca default (una cifra que se anima sin que el lector lo pida es ruido, y «99,98 %» no es contable). El block NO formatea ni escribe: el separador de millares sale de uix.format DENTRO del CountUp y las palabras entran por children (B-7). Escalonado estructural (data-stagger en la banda, cada .Stat un Motion trigger="viewport"). Verificado en navegador: 4 cifras con índices 0,1,2,3 contando (9377→11.678 · 255→318 · 36→45), el porcentaje quieto, separador por locale, cero errores de página. Gates: blocks:check verde (8 blocks) · svelte-check sin errores propios. Siguiente: F2.8 cta.

  • 2026-07-30 — F2.8 cta HECHO. Slots de snippet (eyebrow · title · description · actions · children), misma forma que hero: las partes de un CTA no repiten ni coordinan. Compone Section + Container + Motion + Surface + Heading + Text + Flex. Dos disposiciones: center (Stack, cierre de página) y justified (Grid 2 col., el hueco que nombraba el dossier), que apila en estrecho. Un solo Motion trigger="viewport" con scale-fade: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Landmark section aria-labelledby al título que el propio block escribe.

    Tres hallazgos medidos en navegador, no a ojo (los tres van a los gaps del README y a §6 de PLAN-blocks-quality.md):

    1. variant='soft' eliminado de la API: su track queda a oklch(0.9932) contra un --color-surface-default de oklch(0.9911) — 0.002 L, o sea ningún panel en claro — y Surface no tiene borde al que caer. Un panel sosegado con borde no tiene primitivo (Card outline acota pero no acepta acabado de degradado).
    2. La ranura contrast de la paleta es #ffffff en todo escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: medido, primary 5.18 · indigo 5.21 · plum 4.75 pasan en ambos modos, mientras neutral 3.32 · secondary 3.30 · slate 3.30 · teal 3.07 fallan en claro. El block reenvía cualquier color; la demo solo ofrece los que pasan.
    3. Text align es inerte por defecto: Text renderiza un span y text-align no hace nada sobre una caja inline, así que align="center" dejaba la copia alineada a la izquierda dentro del layout centrado. Resuelto con as="p" (que es lo que ese contenido es).

    También: Group no apila — las acciones necesitaron Flex direction={{ base: 'column', sm: 'row' }} porque a 420px la etiqueta de la acción secundaria se parte contra el botón primario (hero compone sus acciones con Group, así que arrastra el mismo comportamiento — anotado para su siguiente pasada). El CTA que navega usa el patrón Button child + content con intent="fulfill", igual que hero. Verificado en navegador: center y justified en claro/oscuro/RTL + 420px, tres colores, entrada disparada (data-animation-pending fuera), descripción en <p> centrada, cero errores de página. Gates: blocks:check verde (9 blocks) · svelte-check sin errores propios. Siguiente: F2.9 newsletter.

  • 2026-07-30 — F2.9 newsletter HECHO. Slots de snippet; center + justified; panel es un interruptor real (no un fallback). La fila es Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}: el campo se queda la pista libre y la acción abraza su contenido, y bajo sm pasa a una columna porque un botón al lado de un campo de correo deja inservibles a los dos. El block no valida ni emite sema: Form posee el runtime y ya declara commit-submit/signal-invalid; el app posee esquema, valores y handler.

    Superación del dossier: la nota de privacidad (el único hueco que el dossier nombraba para este block) y la etiqueta. Las referencias shipean la fila escondiendo la etiqueta con sr-only o dejando solo un placeholder; aquí se compone Field floatingLabel —la etiqueta arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila queda alineada CON etiqueta real y asociada. Verificado con pulsaciones reales en Playwright: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar. (Esto corrige la advertencia de 2026-07-05 de que el flotado no era verificable sin navegador real: el pane suspendido no valía, Playwright headless con teclas de verdad sí.)

    Tres hallazgos de canon más, medidos (F18/F19/F20 en PLAN-blocks-quality.md §6):

    1. La fundación de eidos no trae reset de modelo de caja. Recetas como [data-field-control] declaran inline-size: 100% + padding, así que bajo content-box el control mide 30px más que su contenedor y se metía por debajo del botón: campo 480 / control 510 en la galería frente a 502 / 502 en los docs de componentes, que sí resetean. El framework lo asume del app. Arreglado en app-land (web/routes/blocks/_lib/reset.css), con A/B sobre los 10 previews y 5 páginas de shell: cambia el newsletter y nada más. Hubo que importarlo DOS veces porque la galería arranca UIX en línea en vez de pasar por BootUix (deuda del arnés, anotada).
    2. onValidSubmit es un no-op silencioso cuando se pasa un form ya construido: el componente solo lo reenvía al createForm que hace él mismo. El envío validaba, limpiaba y no anunciaba nada.
    3. Los mensajes de SIUM son idlangref; la vía correcta es uix.langs.t(issue.message, issue.params) (verificado: sale «Debe ser una dirección de correo válida»), pero la demo de docs del propio Form parte la cadena a mano tras el | y por eso siempre muestra inglés.

    Verificado en navegador el arco completo: correo inválido → error TRADUCIDO con role="alert", aria-invalid, aria-describedby y foco al primer error; correo válido → confirmación del app (Callout afirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores, panel sí/no y 420px (fila apilada). Cero errores de página. Gates: blocks:check verde (10 blocks) · svelte-check sin errores propios · prettier limpio en mis ficheros. Siguiente: F2.10 site-footer.

Powered by TurnKey Linux.