165 KiB
PLAN — Tier blocks: composición reutilizable (infraestructura + componentes base + catálogo)
Kickoff para sesión nueva: "Lee
docs/process/PLAN-blocks.mdy continúa la fase que toque." Decisión de usuario (2026-07-21): existe un tier nuevoblocks— 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-shelles un chasis compartido; el tierpacks(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.mdANTES 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):
git reset -q+ verificar HEAD (sesiones concurrentes; NUNCA amend).- Fase 0 del ítem: comparativa + scope al usuario si hay decisiones.
- Construir. UN fichero → verificar → resto (no-cascade). Componer, jamás re-implementar; gap detectado = FLAG al usuario, no workaround inline.
- Verificar:
npm run check+ scope vitest del ítem + guard del tier (component:audit --only {kebab}para F1 ·blocks-checkpara F2+) + navegador de verdad: screenshot y MIRARLO, claro Y oscuro (colorScheme:'dark'), móvil y desktop para blocks (resize 375/1280). - Demo con profundidad de testbed (B-9); docs del framework en el MISMO pase (README del ítem + mapa si procede).
- Commit (convención viva del repo):
uix({kebab}): …para F1,blocks({kebab}): …para F2+,docs(blocks): …para doctrina. Stage SOLO los paths propios (nuncagit add -A; excluirwords/,palabras/,web/routes/alpha/). - 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:stickyque 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· MantineAffix· 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, sintexts. Partes display → sinarchetype(un archetype interactivo arrastra estilos de item; y el archetypecontentpisaposition— justo lo que sticky no puede permitirse). - Soma: provider observa el sentinel vía
dom.observe(IntersectionObserver por adom) — JAMÁS scroll listener +getBoundingClientRectsíncrono (regla de reflow: lecturas de layout solo post-layout víadom.measure/rAF).data-stucklo 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 condata-stuckpresente (documentar el patrón en el README). - Demo: página larga con header/toolbar afijable, chip mostrando el estado, edge top y bottom.
- Trampas:
mergePropsclobberea stamps — attrs visuales del wrapper fuera del morfo salvo que crucen a soma (aquídata-stuckSÍ 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:
metricsesscope: ['sema','eidos']sin soma). Morfo-first aplica igual: hasta las hojas llevan morfo. - Fase 0, comparar: Chakra
EmptyState· AntDEmpty· 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/cardantes de escribir una línea); centrado, spacing tokenizado--empty-state-*, tamaño vía canonsizesi aporta (probablemente solosm/md).actionscomponeButtondel catálogo vía children. - Demo: en contexto real — un
table/grid-listsin 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. Propstatus→data-status(success | error | info | forbidden | not-found | server-error). Cuidado doctrinal:data-statusNO es el intent perceptivo — si en algún momento gana eventos con evaluación, el intent viaja porfromProp:intentdel 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
Buttonhome/back.
F1.4 callout — admonición inline
- Gap: aviso DENTRO del contenido (info/tip/aviso/peligro).
Banneres el anuncio de página; esto es la nota de documento — imprescindible paraprosey 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· shadcnAlert· admonitions de Docusaurus/Starlight ·bannerpropio (leer su README: qué decisiones ya están tomadas para avisos y cuáles NO trasladan). - Morfo (boceto): parts
provider(rolenote) +icon+title+content. La evaluación ES semántica aquí: propintentrestringido a un subset canónico (candidatos:neutral | affirm | risk | threat; validar contravocabularies.md) estampado víafromProp: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
bannere 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 — · MantineTypographyStylesProvider· 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/prealineados con los tokens decode/code-block(leer sus recetas ANTES; si hay que duplicar valores, es un gap a flag, no un copy-paste); imágenesmax-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· MantineTableOfContents· Starlight/Docusaurus TOC. APG: no hay patrón de widget — es unnavlandmark conaria-current; anotarlo en el README (válvula A-1.4 con nota, como pagination/stepper). - Morfo (boceto): parts
provider(nav,aria-labelvíatexts:— 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 componeLinkdel catálogo). Data:data-activeen item;aria-current. Eventos: click de link = navegación (familia/verbo a decidir en fase 1 contraSEMA_VERBS— candidato familiashift; validar).expression:según criterio D.4. - Soma: registro de secciones objetivo (por id o por
attachPart), observación víadom.observe(IntersectionObserver, umbrales al gusto del provider) — mismas reglas anti-reflow que F1.1. El activo es estado del provider; los effects estampandata-active. Scroll programático al click víadom(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) · MantineAppShell.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 — componeButtonconasChild/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 unLink— decidir en fase 1 (si item =Linkpuro, 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 COMPONEDrawer(no lo reimplementa) — el provider decide qué montar por breakpoint responsive del sistema, no matchMedia propio. - Eidos: raíl con
will-changecuidado (memoria:will-change: transformproduce jitter en raíles finos con DPR≠1 — override aauto); tooltips de item en modo raíl componenTooltip; 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-viewpropio. - Morfo (boceto): parts
provider(nav landmark,aria-labelvíatexts:) +list+item+trigger(grupo colapsable) +link(componeLink;aria-current="page"del MISMO estado quedata-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.
- slot badge (compone
- 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íadom. - 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 conButton) +Badge(chip anuncio) +Image/AspectRatio. - API:
<Hero>(proplayout: '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:
centerysplit(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/.ItemTexto 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+Iconcheck +Text). - API:
<Pricing>+.Switch(billing toggle; estado de vista local permitido §1) +.Plan(propfeatured?) +.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
FormatNumberdel app). - Demo: 3 planes, featured al centro, toggle vivo.
F2.5 testimonials
- Compone:
Section+AutoGrid+Card+Avatar+Text+Group. - API:
<Testimonials>+.Header+.Item(+.ItemAuthorcon 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;
typedel accordion expuesto tal cual.
F2.7 stats-band
- Compone:
Section+Group/AutoGrid+Metrics+CountUp+Text. - API:
<StatsBand>+.Stat(valor + etiqueta;CountUpopt-in por prop). - v1: banda horizontal 2–4 stats; los formatos numéricos llegan ya
formateados o vía
FormatNumbercompuesto 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 snippeteyebrow·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 quehero).variantNO existe: medido, el canvassoftno acota panel (bitácora).
F2.9 newsletter — HECHO (2026-07-30)
- Compone: como F2.8 +
Form(variant="plain") +Gridde fila1fr auto; elFieldde correo y elForm.Submitlos compone el app en sus slots. - API:
<Newsletter layout panel color gradient level container size>+form(OBLIGATORIO, decreateForm) +schema(opcional) + slotseyebrow·title·description·field·submit·note·children. Desviación del plan: slots, no.Title/.Description/.Form(las partes no repiten ni coordinan). No exponeonValidSubmit:Form.Providerlo ignora cuando recibe unformya construido, así que el handler va en elcreateFormdel app. - Nota: separado de
ctaporque el form introduce a11y y estados (invalid/submitting) que el CTA puro no tiene. - El block no inventa validación:
Formposee runtime, esquema, dirty/ touched, agregación de errores y foco al primer error;Fieldposee el cableado ARIA. Tampoco emite sema:commit-submit/signal-invalidya los declara el morfo delForm.
F2.10 site-footer — HECHO (2026-07-30)
- Compone:
<footer>+Section+Container+Motion+Grid(1fr 3fr: marca | columnas) +AutoGrid(las columnas) +Separator+Flex(barra inferior). - API:
<SiteFooter container size minColumnWidth>+ slotsbrand·signup·social·legal·extra+ partes compound.Columny.ColumnTitle. Desviación del plan:.Social/.Legal/.Extrason SLOTS, no partes — no se repiten ni coordinan;.Columnsí se repite, así que es compound.extraes 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
Bannerdel canon (reenviando su superficie entera víaOmit<BannerProps, 'children'>) +Container+Wrap+Banner.Close. - API:
<SiteBanner container onDismiss>+ slotsbadge·children·action. El dismiss lo posee elBanner(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. ElAvatarlo compone el APP dentro delMember. - 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+.Formy dejaba «validación y submit = handlers del app». Se cumple lo segundo (el envío sigue siendo del app poronSend), 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..Infose partió en.Details/.Detailporque la fila se repite, y aparecieron.Fields,.Submity.Reasonporque coordinan (leen el estado por contexto). - La máquina (
state.ts):incomplete · unverified · verifying · rejected · ready · sending · sent, conCONTACT_REASONyCONTACT_ACTIONcomoRecordexhaustivos — un estado bloqueado sin frase no se puede escribir. Cubierta porstate.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 conprogressivesignifica «aún no se ha encontrado nada mal» — un formulario vacío se declaraba válido yincompleteera INALCANZABLE al cargar: la sección pedía resolver la verificación con los tres campos vacíos. Arreglado preguntando al esquema (validateSyncde 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íaSection+Container+Prose, pero dentro de unContainernada 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 (measurecol 3 ·widecol 2/5 ·fullcol 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 primitivaFigure. - 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
aligndeteam. Es un block de LAYOUT y no coordina nada, como dicearchitecture/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:
- 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
layoutnuevo o un hermano, y el tier engorda por imitación en vez de por función. - Misma anatomía, otro ARREGLO → prop
layouten el mismo block —hero(center|split|background),ctaynewsletter(center|justified). - Otra anatomía → block hermano —
feature-splitfrente afeature-grid: filas alternantes con lista de features y acciones no son celdas de icono. - 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 |
Plan de ejecución de V1 — Pricing.Compare (firmado 2026-08-17)
Fase 0 hecha, y con una corrección propia: el README de Table lista el
pinning de columnas como gap diferido, y es falso a fecha de hoy — el engine
tiene enablePinning por columna e initialColumnPinning, soma escribe
position: sticky con su offset (buildColumnStyle) y el recipe pinta
[data-pinned] con un borde que revela lo que hay detrás. La primera versión de
esta fase 0 se apoyó en el README y dio un dato equivocado; el paso 1 lo corrige.
Las dos decisiones que la ficha reservaba, resueltas por doctrina:
- (a) Quién declara los planes → el
createTabledel app, y.Comparelo COMPONE. B-5 admiteitems={…}«sólo donde el canon compuesto ya es data-driven», yTablelo es: exige la instancia decreateTable. Así que.CompareNO inventa un formato de matriz propio ni lo traduce por dentro: el app crea la tabla como en cualquier otra del ecosistema y.Comparela recibe..Plansy.CompareCONVIVEN (las refs muestran ambos), alimentados por el MISMO array del app — sin registro por contexto (acoplamiento por orden de montaje) y sin sustitución. Descartadas: matriz propia (viola B-5 y duplica el engine) · registro de.Planen contexto (acopla dos partes que no se hablan). - (b) Móvil → scroll horizontal con la columna de características FIJADA.
Es lo que hacen las referencias y el canon ya lo da (
enablePinning+initialColumnPinning: { left: [...] }); cero código nuevo, cero gap. Descartado el colapso a lista por plan: el mismo contenido dos veces, y una tabla que deja de ser tabla pierde la navegación por celdas que es la razón de existir de una comparación.
Qué es .Compare (y qué no):
- PARTE de
pricing(término 4 de la regla de forma): coordina con el periodo — la fila de precio lee el contexto y muestra la cifra mensual o anual; el app entrega ambas por snippet, como enPlanPrice. - Coloca: fija la columna de características, y sobre las columnas de plan
aplica
featured(acento) ystate(currentatenuado ·selectedcon anillo) — los mismos ejes que.Plan, así que el estado se declara UNA vez por plan y viaja a las dos vistas. - Posee la semántica: la tabla lleva su
label(nombre accesible; el canonTablelo tiene) y las celdas booleanas (✓ / —) elaria-labelque sus glifos no dan — palabras del block por idlangref con fallback inglés, comoPRICING_PLAN_REASON. - No traduce, no formatea, no inventa: precios por snippet, palabras del app.
Pasos, cada uno con su verificación:
- Corregir el README de
Table(fila «Column pinning» → HECHO, con las tres capas nombradas) →docs:check. compare-langs: idlangrefs#?blocks.pricing.compare.included|Included·.notIncluded|Not included(para elaria-labelde las celdas booleanas) → fila enblocks-langs.tsdel arnés y test que afirma prefijo + fallback..Compare(pricing-compare.svelte+ tipos): recibetable(TableInstance),label, y unplansalineado con las columnas —Recordpor id de columna →{ featured?, state? }— para pintar acento/estado; fija la primera columna si el app no pinneó ninguna (default sensato, opt-out conpinFeatures={false}); la fila de precio componePlanPrice; las celdas booleanas componenIcon.Check+aria-label→svelte-check0 enpricingblocks:checkverde.
- Demo:
createTabledel app con la matriz real (features × 3 planes) · control vivocompararsí/no ·.Plansy.Comparea la vez · el toggle de periodo mueve la fila de precio de la tabla Y las cards a la vez → medido. - Medir (Playwright, píxeles y AX): 1280 → la tabla es
<table>real concaption/aria-label, columnas de plan condata-featured/estado, precio cambia con el periodo · 375 → la tabla scrollea en horizontal y la columna de características queda FIJA (position: sticky,left: 0, borde de[data-pinned]) · lectura AT: cada celda anuncia fila + columna, ✓/— con nombre · claro/oscuro · RTL (la columna fija va aright). - README + registro (§«The comparison table» con las dos decisiones y las medidas; fila de Gaps → SHIPPED; entrada en §7 del plan; handoff).
Riesgos que el agente debe saber:
- La otra sesión está tocando
hero/yeidos/generated(componentebackground): stage SÓLOsrc/uix/blocks/pricing,web/routes/blocks/pricing,web/routes/blocks/_lib/blocks-langs.ts,src/uix/eidos/components/table/README.mdy los docs de proceso. Nada degit add -A, nada destash. TableconmaxHeightenvuelve su propio scroll; sin él, pega al ancestro. Aquí NO haymaxHeight: el scroll horizontal lo da eloverflow-xdel contenedor de la tabla — comprobar qué envuelve el recipe antes de asumir.- El pinning en RTL: soma escribe
${pinned}:${offset}pxcon la posición FÍSICA (left/right). El plan pide medir en RTL, no asumir que espeja.
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-bandsplit-with-image — arreglo de 2 columnas. - V7
stats-bandtimeline / stepped — el canonTimelineexiste 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:
- 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.
- 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.
- 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
## Equivalenciasen el README de cada block. La plantilla del tier (src/uix/blocks/README.md) la gana, yblocks:checkla 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
gapsin 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) +WrapoAutoGrid(la fila; fase 0 decide midiendo con logos reales) +Motion. Los logos son contenido del app:Imagedel 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(candidatotone) o primitivo nuevo; si es primitivo, entra por la ruta de 9 fases antes que el block. Para<svg>inline el camino natural escurrentColor+ 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>(propscontainerSize·sectionSize+ el eje de tinta) + slotseyebrow/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 —altdel 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 detestimonials.Author). Fechas: el app componeFormatDate/RelativeTime— el block no formatea fechas, comopricingno formatea moneda. - API:
<Blog>+.Header+.Posts+.Post(compound: SE REPITE) +.PostMedia+.PostMeta(categoría + fecha) +.PostTitle(elLinkdel 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;Cardrenderizadiv): fase 0 decide entreasenCard(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
layoutsi 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.
Ficha N3 cookie-consent
- 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ó):
onAcceptAllYonRejectAllREQUERIDOS — 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);essentiales 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) +DialogoDrawer(preferencias; fase 0 decide con la guía delante) +Switchpor categoría +Button+Group+Text/Link(política del app). - Foco y descarte: la barra NO roba el foco al aparecer (no es modal);
Escapeno 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 delDialog/Drawerque 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@.svelteque 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),h2por 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"(blockbanner+<header>del site-header). - Ritmo vertical: los
sectionSizeconsecutivos comparados en las costuras (el método que destapó los 16px decontent-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): siapp-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: ¿doblecommit-submit?). - Claro/oscuro · 375/1280 · RTL · teclado de punta a punta (sin trampas de foco; el drawer del header devuelve el foco).
- Outline de headings: UN
- Gates:
blocks:checkverde · 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/datagridysoma/components/drag-dropantes 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 deForm(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
Formcon 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+EmptyStateintegrado. - Compone:
Table+SearchField+DropdownMenu+Button+Pagination+EmptyState(F1.2) +Group/Toolbar. - ⚠️ Fase 0 OBLIGADA y más profunda: leer
soma/components/table+$libs/datagridANTES 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+ filasField/controles +Callout(F1.4, intentthreat)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 conIcon,Separator,Kbdopcional). - 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 triggerIconButton) +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
widthpor el prop tokenizado delPopoverContent(la lección ya está aprendida — no re-descubrirla).
F3.8 wizard
- Compone:
Stepper+Form+GroupdeButtons (atrás/siguiente/ finalizar) +Progressopcional. - API:
<Wizard>+.Steps(Stepper compuesto) +.Step(contenido) +.Nav. - Fase 0: leer el API real de
Steppery deForm(validación por paso) — el block solo coordina visibilidad de paso + navegación; el estado de validez lo dictaForm.
F3.9 error-page
- Compone:
Result(F1.3) +GroupdeButton/Link+SearchFieldopcional (404). - API:
<ErrorPage status=…>+ children de acciones. - v1: 404 · 403 · 500 · offline.
F3.10 kanban
- Compone:
DragDrop+Grid/Groupde columnas +Card+VirtualList(columnas largas) +Badge+EmptyStatepor columna. - ⚠️ Fase 0 OBLIGADA: leer
soma/components/drag-dropa 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 (Groupde dosCard/Link). - E-5 firmada: el atajo ⌘K del trigger de búsqueda = listener app-land
documentado en el README del block; el art
shortcutsconserva 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+Toolbaropcional. - 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)
npm run checksin regresión;npm run blocks:checkverde;npm run component:audit→ los 7 de F1 PASS (o NEEDS-WORK solo por D-* de la fase demos global);docs:checkverde.- 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.
- Doctrina publicada y enlazada (architecture/blocks.md en el mapa; CLAUDE.md al día); cada block con README B-9 completo.
- 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 ennext-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/(nosrc/blocks/); referencias del plan actualizadas (tabla de tiers, B-4, blocks-check, F0.3/F0.4). Resto firmado como propuesto. Siguiente: F0.2 (doctrina enarchitecture/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íaweb/routes/blocks/con layout de bootstrap propio (+layout@.svelte, espejo mínimo del de/uix, sin packs sema) · fila E1 endocs/README.md. Nota de commit:docs/README.mdydocs/next-features.mdquedaron 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-treedata-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 ennext-features.md§9; E-5 ⌘K = listener app-land (nota en F4.1). Doctrina (architecture/blocks.mdpromotion path) y galería actualizadas. -
2026-07-21 — F1.2
empty-stateHECHA (ruta de 9 fases completa, suelo E-2 del dossier §P3): morfo display 5 partes (scope: ['eidos'], 0 eventos justificados, Titlerole:'heading') + langslabel+ eidos compound (Media kind=icon|mediacon placa 2× glifo ·Title level2–6 default h3, modelo Atlaskit ·Descriptionmeasure 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:auditPASS · eidos-lint 5 morfo-backed + 4 eidos-only sancionados ·svelte-check76E/51W = baseline exacto · navegador claro Y oscuro por estilos computados (título 18→24px, placa 40→64px, chips vivos). Siguiente: F1.3result(comparte esqueleto; fase 0 contra dossier §P3). -
2026-07-21 — F1.3
resultHECHA (ruta completa, suelo E-2 §P3): morfo 6 partes (extrasin archetype — parte genuinamente propia) · status enum de 7 CONwarning(decisión del dossier firme), defaultinfo, HTTP renombrados semánticos y renderizados NEUTROS (código mono grande, jamás rojo — doctrina AntD) · media default compone el mapa doctrinalIntentIcon(fulfill/threat/risk) +Infode 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:auditPASS 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.4callout(decisión IMPORTANT/5º hueco + role=note; dossier §P3). -
2026-07-21 — F1.4
calloutHECHA (ruta completa, suelo E-2 §P3): morfo 4 partes +textscon 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) +coloroverride 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íaaria-labelledbySOLO mientras está montado (registro reactivo por context; bug cazado en navegador: el+=del registro leía el estado dentro del tracking del$effectdel hijo → effect_update_depth y contador a −997 — fixuntrack, lección para la memoria) · Title NO heading (protege el outline de anchor-nav) · icono = mapa doctrinalIntentIcon· escalación tipadarole: status|alert|none(elrole="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.1sticky(orden del plan: desbloquea F2; dossier §P3 — offsets, scroll-host, centinela por borde, espejo scroll-state). -
2026-07-21 — F1.1
stickyHECHA (commit998b11d9b, 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 porsyncAttrs(nuncadom.applycrudo). Diseño: un StickyProvider registra AMBAS partes (box+sentinel) en un runtime;$effectretorna el observer como teardown; callback async volteastuck=$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-edgeespejan@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 elinstallSomaHarnesscompartido (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 conaurasin commitear de sesión paralela). Consola: detectado un warning reactivo residual encallout(registerTitle en $effect) — a investigar. Siguiente: F1.5prose(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 atop/bottomfí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
proseHECHA (commit5b84749ee, 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 — sinnot-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-navHECHA (commitfe2d43b7a, 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 endata-active; indent pordata-level. Verificación: test real-IO chromium (anchor-nav-io) — banda + zona muerta al fondo + fallback inicial; el pane suspendido daba ungetComputedStylede color OBSOLETO (falsa alarma, la regla[data-active]sí aplica — elfont-weight:500lo 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 youtline-offsettokenizados (--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-levelfuera de rango degrada a flush (no arreglado, simplicidad). Además (37e35d7a2):npm run checkglobal destapó 2 errores de tipo en F1.1 sticky (sentinelRef debía serStatenoActive; eidos types importabaStickyPropsen vez del export renombradoProviderProps) — 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-treeEN 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 + eventosemerge, pack espeja collapsible); filas propias (noLink/Badgecompuestos — Badge diferido a Gap);activeHrefcomo 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-treeHECHA (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 partegroupes el target de los eventos y se registraba SINref, así queruntime.triggerlanzabaSomaRuntimeTargetErroren silencio (lo cazó el navegador: cerodata-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 enparentByKey(clave duplicada = nodo padre de sí mismo →trailKeysgiraba para siempre y congelaba la pestaña) → clave sufijada + aviso del logger + guarda de ciclo en el paseo; (4) colapso ahora CONTEXTUAL (recuerda elactiveKeybajo 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)childya no se comía el árbol (recibechildrenademás deprops— 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-widthfuera 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):badgeSÍ 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 partebadgey renderiza el snippet recibido (sin él, el valor crudo: sigue headless), el wrapper de eidos pasa elBadgecanónico; va DENTRO del control de la fila para que su texto entre en el nombre accesible («TSC, New, enlace»).disabledtambién entra, por decisión explícita del usuario tras presentarle el criterio: el nodo conserva la fila pero pierde elhref(no hay nada que activar con Enter, clic ni «abrir en pestaña nueva» — unpointer-events:nonede CSS solo tapa el ratón) y añadearia-disabled+data-disabled; el toggle del grupo NUNCA se deshabilita (disclosure es control de vista, no destino) y un nodo sinhreflo ignora. Verificado:component:auditPASS · eidos-lint 26 morfo-backed / 0 invalid ·svelte-check0 errores en mis archivos ·vitest src/uix/eidos353/353 · navegador real (Playwright, el pane suspendido NO sirve: congela rAF y el scroll-into-view parecía roto) → trail auto-expandido, semaemerge-expand/collapseestampando engroup, 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 (commit2bc5fe6bd). 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), semaemerge-expand/collapsesobrepanel, y ritmo por fases con parada tras morfo+soma. Contrato: DOS ejes (data-state+data-collapsibleSIEMPRE estampado) +data-side+data-mobile;panel=<aside>nombrado ycontent=<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 quedata-active;menu-badgedentro del control de la fila;separator/inputNO son partes (se componenSeparatoryField). Soma: costurasopen/defaultOpen/onOpenChange/toggle()desde v1 (sin ellas la persistencia y el atajo de app-land serían rediseño posterior),mobiledel breakpoint del SISTEMA (uix.dom.isAtLeast) y no de unmatchMediapropio, submenú flotante sobrecreateFloatingShellRootcon Escape que devuelve el foco, e ids per-instancia para los dospartRefque son last-write-wins (precedentenavigation-menu). Verde:morfo:checkcarga el contrato ·component:audit --only sidebarPASS 0E/0W ·svelte-check0 errores propios ·contracts.testsolo los 2 fallos ajenos. Siguiente: eidos (raíl, anchos tokenizados, modo icono, Drawer móvil) + demo + review. -
2026-07-23 — F1.7
sidebarHECHA · F1 CERRADA (8/8). Eidos + demo + navegador + review adversarial (commits2bc5fe6bdmorfo+soma ·21bbf9f50eidos+demo ·293593307alto completo ·dcce75cc2foco del flyout ·245c22471controles de demo ·75e1fe32areview). 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 adata-state/aria-expandedsobre 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 elmenu-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 detooltippasa también aaria-label; (5) el panel off-canvas colapsado seguía en el tab order →visibility: hiddencon 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 sinz-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-headerHECHA · primer block del tier (commits3cfc030b3block+landmark ·016658a0ccontroles ·f98c844edfix de Box ·50574806dharness ·33328df7acromo de sección ·d574982abescenario a ras ·ae7b4f8c3vista previa honesta). Composición pura:Sticky(solo sisticky, sin wrapper inútil) +Container+Group+Box(el interruptor ancho/estrecho pordisplayresponsive, 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 unNavigationMenu. Encontrado al componer, arreglado en el canon:NavigationMenuno emitía landmark (su morfo declaradefaultElement: 'nav'y soma pintaba undiv). 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: elchild(asChild) deButtonentrega ahora un snippetcontentcon el cuerpo ya decorado, así que<a href {...props}>{@render content()}</a>recibe la pintura sólida completa y conserva icono/etiqueta/spinner (antes elchildlos sustituía: por eso la flecha del sitio alpha estaba escrita a mano); y soma deja de estampartypeen un elemento que no es suyo (<a type="button">es una pista de MIME falsa) — el morfo lo declara condicional.Buttonsigue sinhrefyLinksigue 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 deBoxheredaban, así que unSectionregalaba 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 enweb/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, unCarddesplazaba 21px un header conoffset: 0, y unstickydentro de un div con scroll es un comportamiento que nadie vive. Los anchos de dispositivo se sirven desde una rutapreviewpropia (iframe opt-in: en dev, dos documentos sin empaquetar agotan las conexiones del navegador). Siguiente: F2.2hero— handoff endocs/process/CONTINUE-blocks.md. -
2026-07-23 — F2.2
heroHECHA (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 enHeading/Text(posee eliddel landmark y el nivel; la app pone las palabras);eyebrow/actions/mediason contenido libre. Landmark:<section aria-labelledby>nombrado por el título (verificado: región con nombre accesible). Layoutbackground(cover) añadido por scope-approval del usuario: media a sangre detrás, velo--color-overlaya--opacity-scrim, textoon-solidyobject-fit: coverví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 unSurfacecolor="primary" gradient(finish aurora,--gradient-auroraderivado de los roles del tema). Registrado, no resuelto:Box/Surfaceflex/growno hicieron crecer un hijo flex (bars a 0-width,flex: 0 1 auto; prop sin usar en shipped) → la demo usóGrid(tracks1fr); flag para revisar el cableado de--box-flex. Gates:blocks:checkverde (2 blocks) ·svelte-checksin errores propios. Siguiente: F2.3feature-grid(primer compound del tier:.Itemrepetido — LA brecha #1 del dossier, split/alternante texto-screenshot). -
2026-07-23 — F2.3
feature-gridHECHA (mismo día). Primer block COMPOUND del tier:<FeatureGrid>+.Header+.Items+.Item+.ItemIcon/.ItemTitle/.ItemText. Aplica la regla de forma:.Itemse REPITE (el app mapea sobre N features) → gana compound; las partes no coordinan (sin contexto/estado entre ellas), la rejilla es del padre..Itemses el envoltorio honesto de la rejilla (AutoGrid) — deja la cabecera fuera sin ungrid-column: 1/-1a pelo. El block coloca (Section·Container·AutoGrid·Surface· Heading·Text), el app pone contenido (B-7).alignde sección (center/start) coloca la cabecera coherente con las columnas. Sub-componentes de bloque extienden los props del componente canon que envuelven, NOHTMLAttributes: elstyle: string|nulldel atributo HTML crudo choca con elstyle: stringdel canon (+ "union too complex") al hacer spread. ItemIcon defaultsolid: el soft-primary en claro es casi blanco (oklch 0.99) → invisible; solid da chip con glifo on-solid (la tinta de contraste la poneSurface). Demo: control de columnas (fluido/2/3/4), align, nº items, dir; 6 features con iconos del canon. Gates:blocks:checkverde (3 blocks) ·svelte-checksin 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 hermanofeature-split. Presentado como scope-approval, no resuelto aquí. Siguiente: F2.4pricing. -
2026-07-23 — Primitivo canon
Mockup+ F2.3bfeature-splitHECHOS (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, comoaspect-ratio): enmarca media en cromo de dispositivo —chrome: 'plain' | 'browser' | 'phone'+urlpara 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 delhero(dogfood).feature-split(compound:.Rowrepite):<FeatureSplit>+.Row(reversed, slotmedia) +.Eyebrow/.Title/.Text/.Features/.Feature/.Actions.reversedmueve la media al inicio víagrid-column(Box tienegridColumn/order), dejando la copy PRIMERA en el DOM (orden de lectura intacto). El check de.Featurees decorativo (aria-hidden). La media se compone conMockup.- BUG de
heroarreglado de paso (lo tapaba mi filtro de svelte-check con backslashes mal escapados): los snippetstitleybackgroundcolisionaban con los atributos HTML homónimos (title?: string/background) →string & Snippet. Fix:Omit<HTMLAttributes, 'children' | 'title'>+ renombrar el slotbackground→backdrop. Lección para blocks: un slot de snippet cuyo nombre sea un atributo HTML necesita Omit o un nombre distinto. Gates:blocks:checkverde (4 blocks) ·svelte-checksin errores propios (73 = deuda ajena) · eidos lint.test + morfo 131/131. Siguiente: F2.4pricing.
-
2026-07-24 — Fix de
Grid+ F2.4pricingHECHOS.- Fix canon
Grid(theming/changelog §47):align/justify/alignContentNO aplicaban — los atajosplace-items/place-contentcon fallbackrevert-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 alineandofeature-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. ElSwitch(ToggleGroup) escribe el periodo en un contexto reactivo (context.ts, getter sobre el$bindable) y cadaPlanPricelo lee → muestra el snippetmonthly/annual. El block NO formatea moneda: la app componeFormatNumberen los snippets (B-7)..PlanActionfija el CTA al borde inferior (margin-block-start: auto) para alinear los botones;.Planconalign="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 hermanopricing-table. Gates:blocks:checkverde (5 blocks) ·svelte-checksin errores propios. Siguiente: F2.5testimonials.
- Fix canon
-
2026-07-24 — F2.5
testimonialsHECHO. Compound (.Itemrepite, SIN contexto — como feature-grid):<Testimonials>+.Header+.Items+.Item+.Quote+.Author(slotavatar+.AuthorName/.AuthorRole).AutoGridde tarjetas de igual alto;.Authorfijada al borde inferior (marginTop="auto") para alinear las caras aunque las citas midan distinto. La cara es del app (Avatarcon imagen oAvatar.Fallbackde iniciales).Cardpara 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) encolorcae en silencio aprimary(la regla[data-color]hacevar(--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 hermanotestimonial-spotlight. Verificado en claro/oscuro/RTL, caras alineadas, 5 colores distintos. Gates:blocks:checkverde (6 blocks) ·svelte-checksin errores propios. Siguiente: F2.6faq. -
2026-07-24 — F2.6
faqHECHO. Compound (.Itemrepite) y proxy fino delAccordiondel canon (regla del handoff: leer su API, no reinventar el disclosure):<Faq>+.Header+.List+.Item..ListES elAccordion— toda su API pasa tal cual (type,bind:value,collapsible,variant,size; defaults FAQ: single, collapsible, outline)..ItemproxyaAccordion.Item > Header > Trigger(snippetquestion) /Content(children), y autogenera elvaluecon$props.id()si no se pasa.Containerestrecho (md). El teclado/ARIA del disclosure salen delAccordion, 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, proplayouto 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:checkverde (7 blocks) ·svelte-checksin errores propios. Siguiente: F2.7stats-band. -
2026-07-29 — F2.7
stats-bandHECHO. Compound (.Statrepite, sin contexto):<StatsBand>+.Stat+.Value+.Label. Compone elMetricsdel canon para la semántica de KPI —surfaceless, que es lo correcto: una banda no son tarjetas de panel— yCountUppara 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).countes 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 deuix.formatDENTRO delCountUpy las palabras entran por children (B-7). Escalonado estructural (data-staggeren la banda, cada.StatunMotion 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:checkverde (8 blocks) ·svelte-checksin errores propios. Siguiente: F2.8cta. -
2026-07-30 — F2.8
ctaHECHO. Slots de snippet (eyebrow·title·description·actions·children), misma forma quehero: las partes de un CTA no repiten ni coordinan. ComponeSection+Container+Motion+Surface+Heading+Text+Flex. Dos disposiciones:center(Stack, cierre de página) yjustified(Grid2 col., el hueco que nombraba el dossier), que apila en estrecho. Un soloMotion trigger="viewport"conscale-fade: el panel llega ENTERO, porque un CTA es una sola afirmación y repartir sus tres partes se leería como duda. Landmarksection aria-labelledbyal 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):variant='soft'eliminado de la API: su track queda aoklch(0.9932)contra un--color-surface-defaultdeoklch(0.9911)— 0.002 L, o sea ningún panel en claro — ySurfaceno tiene borde al que caer. Un panel sosegado con borde no tiene primitivo (Card outlineacota pero no acepta acabado de degradado).- La ranura
contrastde la paleta es#ffffffen todo escalón sólido, así que un lienzo de luminancia media deja el cuerpo por debajo de AA: medido,primary5.18 ·indigo5.21 ·plum4.75 pasan en ambos modos, mientrasneutral3.32 ·secondary3.30 ·slate3.30 ·teal3.07 fallan en claro. El block reenvía cualquiercolor; la demo solo ofrece los que pasan. Text alignes inerte por defecto:Textrenderiza unspanytext-alignno hace nada sobre una caja inline, así quealign="center"dejaba la copia alineada a la izquierda dentro del layout centrado. Resuelto conas="p"(que es lo que ese contenido es).
También:
Groupno apila — las acciones necesitaronFlex direction={{ base: 'column', sm: 'row' }}porque a 420px la etiqueta de la acción secundaria se parte contra el botón primario (herocompone sus acciones conGroup, así que arrastra el mismo comportamiento — anotado para su siguiente pasada). El CTA que navega usa el patrónButton child+contentconintent="fulfill", igual quehero. Verificado en navegador:centeryjustifieden claro/oscuro/RTL + 420px, tres colores, entrada disparada (data-animation-pendingfuera), descripción en<p>centrada, cero errores de página. Gates:blocks:checkverde (9 blocks) ·svelte-checksin errores propios. Siguiente: F2.9newsletter. -
2026-07-30 — F2.9
newsletterHECHO. Slots de snippet;center+justified;paneles un interruptor real (no un fallback). La fila esGrid templateColumns={{ base: '1fr', sm: '1fr auto' }}: el campo se queda la pista libre y la acción abraza su contenido, y bajosmpasa 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:Formposee el runtime y ya declaracommit-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-onlyo dejando solo un placeholder; aquí se componeField 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):- La fundación de eidos no trae reset de modelo de caja. Recetas como
[data-field-control]declaraninline-size: 100%+ padding, así que bajocontent-boxel 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 porBootUix(deuda del arnés, anotada). onValidSubmites un no-op silencioso cuando se pasa unformya construido: el componente solo lo reenvía alcreateFormque hace él mismo. El envío validaba, limpiaba y no anunciaba nada.- 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 propioFormparte 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-describedbyy foco al primer error; correo válido → confirmación del app (Calloutafirmativo con la dirección) y error limpio. Claro/oscuro/RTL, tres colores,panelsí/no y 420px (fila apilada). Cero errores de página. Gates:blocks:checkverde (10 blocks) ·svelte-checksin errores propios · prettier limpio en mis ficheros. Siguiente: F2.10site-footer. - La fundación de eidos no trae reset de modelo de caja. Recetas como
-
2026-07-30 — F2.10
site-footerHECHO. El block donde la regla de forma del tier se ve mejor:.ColumnSE REPITE → parte compound; marca, alta, social, legal y extra no se repiten ni coordinan → slots. Landmark<footer>real, así que escontentinfosin pedir nada. Las columnas van enAutoGrid(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 propioForm, el block NO importa el blocknewsletter(B-10)— y el selector de idioma comoextra, vivo sobre el eje real: escribeuix.prefs.setIntent('language', …)y se verificó que muevedocument.documentElement.langaen.Tres números que salieron de medir, no de suponer: el
containerbajó dexlalg(conxlel pie no se alineaba con ninguna sección de la página compuesta); la rejilla superior pasó de1fr 2fra1fr 3fr(con2frun 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 ancholg— 728px justos).Hallazgos (F21/F22 en
PLAN-blocks-quality.md§6): (1) ningún primitivo de layout puede cambiar de elemento —Text/Headingaceptanas, peroBoxy todo lo construido sobre él es un<div>fijo, así que una columna de enlaces no puede ser<ul>/<li>; (2) unSelectcontrolado muestra el VALOR crudo hasta que se abre una vez:getDisplayText()resuelve contra el registro de etiquetas que llenan losSelect.Itemal montarse, y con elContenten un portal cerrado no hay ninguno montado. La demo lo rodea con elchilddeSelect.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 tresIconButtoncon nombre, y el selector de idioma cambiando el idioma de verdad. Cero errores de página. Gates:blocks:checkverde (11 blocks) ·svelte-checksin errores propios · prettier limpio. Siguiente: F2.11banner. -
2026-07-31 — F2.11
bannerHECHO. 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 delBannerconOmit<BannerProps, 'children'>(no re-declaraintent/variant/sizeni los estrecha), lo mete en columna conContainer(width="100%"+paddingX={0}: a sangre por fuera, en columna por dentro), envuelve conWrapen vez de una fila rígida, y cableaBanner.Closesolo si llegaonDismiss. La demo lo enseña encima delsite-headerde verdad, con página para desplazarse: un aviso dentro de un recuadro no se parece a un aviso.Hallazgo real (fase 0 lo anticipó):
Bannerestamparole="banner"DESPUÉS de sus rest props, así que no se puede relajar; con elsite-headeren la misma página quedan dos landmarksbanner(medido). Único paliativo hoy: nombrarlos conaria-label.Y una lección de método que costó una sesión: reporté tres «defectos del canon» (
aria-labelausente enBanner.Close,typedesaparecido de todoButton,IconButtontragándose elonclick) y los tres eran falsos. La causa: medía demasiado pronto. Losdata-variant/data-sizelos escribe el componente de eidos al renderizar —están desde el primer frame—, perotype,aria-labely los handlers los aplica la runtime del morfo en un efecto posterior. A ~1s:typeausente 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 enCONTINUE-blocks.md.De paso, un aviso de soma que sí era real y era mío:
Tabs.Listsin nombre accesible enBlockDemo.svelte— afectaba a las 12 demos del tier.Verificado en navegador: los 4 intents, los 3 tratamientos,
descartablesí/no (connoel 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:checkverde (12 blocks) ·svelte-checksin errores propios ·docs:checksin errores míos · prettier limpio. Siguiente: F2.12team. -
2026-07-31 — F2.12
teamHECHO. Compound porqueMemberSE REPITE, y el primero del tier cuyo contexto sirve para una sola cosa: elalignde la sección viaja a cadaMembery 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. Unalignexplícito en una parte siempre gana. Es la diferencia confeature-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 consize="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
columnsFIJO 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 pasacolumns={{ 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,
aligncenter/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:checkverde (13 blocks) ·svelte-checksin errores propios ·docs:check0 · prettier limpio. Siguiente: F2.13contact. -
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
contactdelante se vio el motivo: con el estado fuera, cadadisabledes 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 enarchitecture/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
contactHECHO, 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_REASONyCONTACT_ACTIONsonRecordEXHAUSTIVOS: 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 undisabled. A la demo se le cayeron el esquema, las etiquetas, eldisabled, 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 convalidationBehaviour: 'progressive'significa «aún no se ha encontrado nada mal». Un formulario vacío e intacto no tiene errores → se declaraba válido →incompleteera 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 (validateSyncde SIUM: síncrono, y sin escribir errores en el formulario —form.validate()habría encendido los tres campos en rojo al cargar). Fijado constate.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 atributotypecomo 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 yform.valuesseguí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$effectdeBootUix) más un ida y vuelta reactivo real. También costó una pista falsa: el servidor de dev servía módulos rancios y elconsole.logdel 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
verifyingyrejectedquedan cubiertos por test unitario, no por el reto en vivo: el provider deProofOfHumanescribestatusél mismo y el veredicto del app no se sostiene (hilo de canon abierto). i18n: el arnés registra el namespaceblocksen 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:verificationcon reto / sin reto — y omitir el prop NO es lo mismo que pasar un estado que nunca verifica. Gates:blocks:checkverde (14 blocks) ·svelte-checksin errores propios ·state.test.ts9/9 · prettier limpio en los ficheros propios. Siguiente: F2.14content-section. -
2026-07-31 — F2.14
content-sectionHECHO → F2 CERRADA 15/15. El último de sitio, y el que más cerca estuvo de ser un envoltorio:Section+Container+Proseno habría añadido nada, porqueProseya trae su medida de lectura. Lo que sí falta en el ecosistema es el escape: dentro de unContainernada 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:measurecol 3,widecol 2/5,fullcol 1/-1. Todas las longitudes son tokens del sistema (--measure-*,--container-width-*,--container-padding-inline)..Bodyy.Mediason compound porque SE REPITEN (un artículo alterna prosa y figuras en el orden en que se lee); la cabecera son slots de snippet..Bodyapaga la medida deProse: 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)
gapsepara también las COLUMNAS, y una parte que las cruza se lleva esos huecos encima —widemedía 1088 en vez de los 1024 del contenedor con el que debe alinearse, y a 420pxfullllegaba 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 conrowGapycolumnGap={0}. (2) una pistamin(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 — enmeasure/wideya es una pista, enfulles 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 primitivaFigure. Contexto de CONFIGURACIÓN, no de estado (la medida, para el pie) — es un block de layout y no coordina nada. Gates:blocks:checkverde (15 blocks) ·svelte-checksin errores propios ·docs:check0 · 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-sectionimportéTextMeasuredesdecomponents/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 —Headingimportaba CINCO escalas tipográficas deText(TextTracking,TextLeading,TextWrap,TextNumeric,TextMeasure). Las cinco se han movido asrc/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 (ColorRolere-escrito a mano que derivó a 8 miembros contra 9). Sin shim de compat:text/types.tslas importa,heading/types.tstambién, y el bloque igual. Arrastraba además un error mío anterior: había declaradoContentMeasure = 'narrow' | 'normal' | 'wide', copia literal del canónico, y tipadowidecomostringlibre con default'var(--container-width-lg)'— lo que empujaba la construcción del nombre del token a un fichero de RUTA. AhorameasureesTextMeasure,wideesContainerSizey el únicovar()vive engrid.ts. Gates tras el movimiento:checksin errores en lo tocado (74 globales, ninguno mío) · 28 de 29 suites de eidos verdes (la que falla esaudio-playersin morfo, del hilo de audio,c4d64ba98) ·blocks:check15. -
2026-07-31 —
content-section: los tres ejes pasan a RESPONSIVE (decisión del usuario: «debe de ser responsive»).measure,widey elwidthde cada parte aceptanResponsiveProp, resueltos coneidos.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 demdarriba. 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 pistawideusaba--container-width-lga pelo, que es el ancho EXTERIOR delContainer, 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 restando2 * --container-padding-inline: la pista ancha enlgson 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 losdata-animation-pendingpuestos conopacity: 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:
contactimporta$libs/formsfuera de la frontera dura 1 · el nombre accesible del submit decontactestá congelado en «Enviar» en los cinco estados porqueForm.Submitimpone suaria-label, así que las palabras de estado que el block existe para poseer no llegan al lector de pantalla ·site-headercongela la página si el cajón está abierto al cruzar el breakpoint (reproducido con swipe táctil real) ·heroenbackgrounddeja la CTA secundaria a 1.78:1 ·height="100%"sobreCardes INERTE enpricingytestimonialsmientras 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+affirmcon earcon audible al pasar el RATÓN por encima, unEnterennewsletteremitiendo doscommit-submitcon intents contradictorios, y 4 de 7 elementos del header mudos. -
2026-08-09 — Fase 5 del saneamiento:
blocks:checkendurecido (el último punto del plan de saneamiento sin empezar; el detalle de las fases 1–4 vive enAUDIT-blocks-ledger.md, que es la fuente viva desde el 2026-08-05). Tres reglas nuevas, cada una con su fixture negativo en elselfTest()que el guard corre antes de escanear:- Lista blanca de importaciones (B-4) — la frontera dura 1 deja de ser sólo
prosa:
$uix, los arts públicos,$libs/forms,sveltey los relativos del propio block; lo demás es error con el especificador por delante. Los arts se DERIVAN desrc/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). - 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). - Ficha en el catálogo de demos (B-9) — el árbol y
web/routes/blocks/_lib/catalog.tstienen 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.tsdocumentan 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
zody un$libs/daysmetidos enhero/types.tssalen 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 romperisAllowedSpeca propósito, el self-test aborta el guard en vez de dar verde. Gates:blocks:checkverde (15 blocks / 114 ficheros) ·vitest src/uix/blocks20/20 ·docs:check0/0 ·svelte-check74E/54W = la línea base medida justo antes. - Lista blanca de importaciones (B-4) — la frontera dura 1 deja de ser sólo
prosa:
-
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.tsy 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.Compareel ú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, conapp-shellprimero 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
.Plansy.Compareconviven; el colapso móvil) y tanda propia; V2 deja de venderse como «misma anatomía» (ganalogo, pierde laCard) aunque sigue siendolayout; V3 la unión discriminada alcanza también avalue/disabledde.Item; V4 tiene que decidir si el formulario REEMPLAZA aactions, como en las referencias; V8 se MIDE antes de declararse gratis (PricingPlansPropses unPickcerrado); V6/V7 salen de la ola a F5 — el dossier declarastats-band«Paridad OK», son adoptables y no suelo, y meterlas diluía E-2. Tres decisiones firmadas:logo-cloudREABIERTO como block estático (su premisa de diferimiento no casa con las refs; el valor propio es la tinta monocroma derivada de tokens, yMarqueequeda como preset de motion, no como block) ·blogENTRA (misma naturaleza queteam, que ya entró) ·cookie-consentENTRA 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) ·onboardingNO entra: es unwizardcon 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
## Equivalenciasen el README de cada block;blocks:checkla exige como ÚLTIMA tanda del tramo, verificada en rojo) · fichas N1logo-cloud(⚠️ la tinta monocroma se promociona al CANON antes del block — un block no posee CSS), N2blog(fechas porFormatDate/RelativeTime;<article>choca con F21 → fase 0 decideasenCardo gap declarado), N3cookie-consent(la ley en el TIPO:onRejectAllrequerido, misma prominencia por construcción, no esenciales apagadas;Escapeno 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 dosrole="banner"—, costuras desectionSize, cascadas por viewport, A-95, sonda de sema, teclado entero). -
2026-08-17 — F2b · V4
form-in-heroHECHA (primera fila de la ola). Slotformenhero: el app componeForm+Field+Form.Submitenteros 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 debackground9.61:1 · la fila colapsa a 375 (campo 325 / botón 325) y comparte a 1280 (335 / 131, misma fila) · medida propia 480px encentercontra sección de 1264 y 472px ensplit(= la columna: el capsmes inerte ahí) · Enter llega al handler del app y un correo inválido lo para con el error traducido yaria-invalid· RTL espeja la fila. Dos gaps de canon registrados en el README del block:Form.Submitno reenvía alButtonniintentni eje de anchura (el botón no llena su celda al colapsar;newslettermide lo mismo, y eljustifydeGridesjustify-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: unTextsincolorexplícito bajobackgroundmide 1.62:1 contra 10:1 conon-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 emiteoklch, y canvasfillStyleque 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 paraprimary. -
2026-08-17 — F2b · V8
single-priceHECHA, y la ficha acertó al exigir que se midiera ANTES: «un solo.Plany el layout ya lo sostiene» —la disposición que el README del block llevaba desde julio— era falsa. La fila esrepeat(auto-fill, minmax(17rem, 1fr))yauto-fillreserva 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, ycolumns={1}sólo cambiaba eso por una card de 992px. Ninguna de las dos es la card centrada de las referencias. Resuelto abriendo.PlansamaxWidth(tope de la FILA, conmarginX="auto"incondicional para centrarla), reenviado NOMBRADO alAutoGrid— nunca por{...rest}, que es justo lo que A-94 dejó dicho. El número lo pone el app: la demo pasa28rempara un plan y44rempara dos. Medido después: 448px centrados con uno, 704px (dos pistas de 340) con dos, y 992px idénticos con tres — el tope es opt-in y no toca el caso que ya existía; a 375 es inerte. Control vivo nuevo en la demo (nº de planes) y en la URL del preview. ⚠️ Lección que se queda: una fila «diferida» puede ser una medición equivocada, no un aplazamiento — nadie había mirado este block con menos de tres planes. -
2026-08-17 — F2b · V5
ctasplit-with-media HECHA. Tercerlayout+ slotmedia, con dos decisiones que la ficha no traía: la copia conserva sus acciones en la MISMA columna (separarlas dejaría el botón bajo la captura, lejos de la frase que lo pide) ysplitsinmediacae ajustified, no a una rejilla con una pista vacía — la regla que ya sigue elsplitdelhero. Medido: a 1280 dos pistas de 436px en un panel de 992; a 375 una pista, con el CTA POR ENCIMA de la media al apilar; idéntico en claro y oscuro. Demo:spliten el control de layout + unMockupen el slot. -
2026-08-17 —
Cardgana el ejelift, ypricinglo reenvía (petición del usuario: «¿por qué no hay hover en la card del precio? debería estar al menos como opción»). Empezó con una afirmación mía FALSA —«Cardno expone estado de puntero»—: el realce existía desde siempre, con sus dos tokens en la fundación, pero colgaba entero de[data-interactive], que promueve la tarjeta a<button>y emitecommit-select. Una tarjeta de plan lleva su CTA dentro, así que ese paquete es inaplicable:<button>en<button>. La elevación pasa a su propio attr visual-wrapper (data-lift, fuera del morfo por la doctrina del 2026-08-15, con fila encomponent-visual-attrs.test.ts— la primera decard);interactivelo implica, así que nada cambia para una tarjeta clicable. Sin tokens nuevos.PricingPlanPropsganalifty la demo su control vivo. Detalle y medidas:theming/changelog.md§49. Y destapó un hueco del propio guard: el comentario que explica por qué NO se usa un nativo nombra`<button>`, y B-2 leía comentarios como código — su barrido quitaba sólo<!-- … -->de UNA línea, mientrasstripComments, que maneja todas las formas y conserva los saltos, ya existía tres líneas más abajo y se usaba sólo para los imports. Es la lección de la fase 5 aplicada a una regla y no a la otra. Arreglado, con dos fixtures nuevos (nativo nombrado en comentario multilínea = prosa · nativo REAL debajo de un comentario multilínea = error) y probado en rojo sobre el árbol real: un<button>inyectado enhero.sveltesale con su línea exacta (158) y el árbol se restaura después. -
2026-08-17 —
pricinggana el estado del plan y, con él, PALABRAS (firma del usuario: la decoración del elegido y la propiedad de «no disponible» para upgrade / downgrade). Su README decía lo contrario desde julio —«nada en una tabla de precios puede ser bloqueado, así que no hay nada que explicar»— y eso se cae en cuanto el visitante ya está EN un plan: el actual no se puede elegir. Ahí manda la lección decontact: una acción bloqueada debe su frase. Forma:.Plan state=available | current | selected, un enum y no tres booleanos (permitirían «elegido y no disponible» a la vez), ortogonal afeatured—que es la recomendación de quien vende, no dónde está quien compra—; el estado se deriva UNA vez en.Plany viaja por contexto;.PlanActionse lo entrega alButtondel app por parámetro del snippet, así que nadie re-deduce undisableden el punto de uso y las palabras del botón siguen siendo del app (B-7);.PlanReason(parte nueva) dice la frase y posee el id al que apunta elaria-describedbydel botón bloqueado — y no renderiza nada cuando no hay nada que decir. Las palabras son del block, idlangref con fallback inglés en unRecordEXHAUSTIVO (plan-state.ts+plan-state.test.ts, que afirma en las dos direcciones: todo estado que bloquea tiene frase, y todo estado que no bloquea calla). La decoración es del CANON:selected→ el anillo deCard,current→ su tratamiento deshabilitado. Nada dedata-pricing-selected: sería un attr que nadie lee, porque un block no pinta CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale concursor: not-allowed, botón deshabilitado yaria-describedbyque CASA con el id de la frase traducida; al elegir otro, su tarjeta gana el anillo primary de 2px y el resto no. ⚠️ Gap de canon medido aquí, sin tocar:Card disabledNUNCA atenúa. La regla existe y sucursor: not-allowedsí aplica, pero la animación de montaje (card-emerge,fill-mode: both) fijaopacity: 1con prioridad de animación y se come la del estado — probado aislándolo: condata-no-emergeen el mismo nodo, atenúa a 0.4. Es el patrón KNOWN-FRAGILE de la doctrina de motion (dos dueños, una propiedad, un nodo) y alcanza a TODACarddeshabilitada del ecosistema. Reparación natural: atenuar porfilter: opacity(...), que la animación no posee, sin tokens nuevos. Espera firma. -
2026-08-17 — F2b · V2
testimonialsspotlight HECHA. La brecha del dossier para este block: la cita única es la variante MODAL de las referencias y el grid la minoría (2 de 8 en TW). Entra comolayout="grid" | "spotlight", no como hermano — la anatomía no cambia (cita · autor · avatar), sólo la disposición, y el precedente es elbackgrounddelhero, que también redefine lo que hacen sus partes; el hermano se reabre SÓLO si un día pide rotación o carrusel, que es otro componente. Viaja por contexto de CONFIGURACIÓN (el patrón delaligndefeature-grid/team, no la coordinación depricing) porquespotlightno es una piel:Itemsdeja de ser rejilla y de estampardata-stagger—el ritmo estructural es para una FILA—,Itempierde laCard—enmarcar una cita sola la hace parecer un ítem de una lista que no existe—,Quotesube axly se centra conas="p"(F17: sin él el centrado sería mentira) yAuthorpasa debajo. La medida de la sección se estrecha amd. Slot nuevologoen.Item, en las dos disposiciones: la marca de quien firma es lo que hace que la cita se lea como evidencia. Medido: grid → 5 tarjetas, 20px, container 1024, stagger; spotlight → 0 tarjetas, una cita de 28px centrada en<p>, container 768, autor centrado, sin stagger; a 375 la cita cae a 24px. El grid queda idéntico. ⚠️ Aviso de árbol compartido:svelte-checkmarcó 73 durante esta tanda y ninguno es del tier — otra sesión está a medias con un componentebackground(morfo, langs yeidos/generatedtocados). Medido con todo en stash: la base real sin nadie es 60. No volver a hacergit stash --include-untrackeden este árbol: arrastra el trabajo sin commitear de la otra sesión. -
2026-08-17 — F2b · V3
faqlista estática HECHA. La brecha del dossier y la mayoría del formato: 6 de 7 en TW no son acordeón. Entra como.List layout="accordion" | "list", y el sitio del eje importa: vive en la LISTA, no en la raíz, porque la lista es lo único que cambia —el header se lee igual— y porque esa colocación es la que permite que el TIPO diga la verdad.FaqListPropspasa a ser unión discriminada: bajoaccordionatraviesa toda la superficie delAccordiondel canon; bajolistno se ofrece ninguna, porque una rejilla estática no tiene estado abierto que enlazar, nada que colapsar ni modo simple/múltiple. Es A-94 aplicado donde iba a morder después. ElItemlee la disposición del CONTEXTO —no puede discriminar sobre la unión de la lista sin obligar a la app a repetir el arreglo en cada pregunta— y susvalue/disabledquedan documentados como del arregloaccordion, la misma convención que elbackdropdelheroy elmediadelcta. Bajolistla pregunta es unh3de verdad: en una lista estática ES el encabezado de su respuesta, mientras que bajoaccordionel canon la mete en el disparador y esa semántica es suya — el block no inventa una segunda. Medido a 1280: accordion → 5 disparadores, 0 encabezados, medida 768, respuestas ocultas; list → 0 disparadores, 5h3, tres pistas de 309px, medida 1024 y todas las respuestas visibles sin pulsar; a 375 cae a una columna conservando ambas cosas. El acordeón queda idéntico.