50 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 pendiente (nada construido). Actualizar esta tabla al cerrar
cada tanda, estilo
PLAN-component-coherence.md.
| Fase | Contenido | Estado |
|---|---|---|
| F0 | Infraestructura del tier: doctrina + alias + guard + rutas demo | HECHA 2026-07-21 (F0.1–F0.7; cross-ref en comparison.md omitido a propósito — sin aporte hasta que exista catálogo) |
| F1 | 8 componentes base del CANON que los blocks necesitan (7 + nav-tree por E-1) |
en curso — F1.2 empty-state HECHA 2026-07-21 (PASS; ver registro) |
| F2 | Blocks de sitio (14: 10 + banner·team·contact·content-section por E-3) | pendiente (F2 solo requiere F1.1) |
| F3 | Blocks de aplicación (10) | pendiente |
| F4 | Blocks de docs (3) | pendiente |
| F5 | Backlog condicionado (componentes media/mobile + blocks diferidos) | pendiente |
1. Qué es un block (doctrina propuesta — aterriza en docs/architecture/blocks.md en F0)
Un block es una composición nombrada de componentes del canon que desempeña una función de página: no aporta primitivas nuevas, aporta ensamblaje correcto (layout, landmarks, jerarquía de headings, responsive, puntos de contenido). Consume el framework; el framework nunca lo referencia.
La tabla de tiers queda:
| Tier | Valor | Contrato | Entra por |
|---|---|---|---|
Canon (src/uix/) |
densidad de contrato (eventos, ARIA, teclado, tokens que otros consumen) | morfo + matriz de aceptación | ruta de 9 fases (docs/building-a-component.md) |
Packs (src/packs/) |
decoración parametrizada de hoja | contrato P | packs-check |
Blocks (src/uix/blocks/) |
composición de función de página | contrato B (§3) | blocks-check |
Regla de admisión (espejo de la de packs): si al construir un block hace
falta comportamiento nuevo con superficie de contrato — un evento real, una
máquina de estados, un data-attr que el CSS necesita seleccionar, una
obligación a11y de widget — esa pieza se construye ANTES como componente
canónico por la ruta de 9 fases, y el block la compone. Nunca se le crece
el privilegio al block. (Es exactamente la regla "compose existing
components; flag gaps" de docs/guides/component-guide.md §4, elevada a
frontera de tier.) La F1 de este plan existe porque ese triage ya está hecho:
los 7 gaps detectados se construyen primero.
Lo que un block SÍ posee semánticamente: landmarks y estructura de
documento (header/nav/main/aside/footer, jerarquía h1–h6, skip-link,
aria-label de región). Los componentes no pueden saber el contexto de
página; los blocks sí. Es su única superficie a11y propia.
Un block PUEDE tener estado de vista local (pestaña activa, toggle mensual/anual) usando los props/eventos públicos de los componentes que compone. Lo que NO puede es materializar ese estado con DOM/CSS propio que requiera contrato (→ regla de admisión).
2. Decisiones D-BLK — FIRMADAS 2026-07-21
Repasadas y firmadas por el usuario en el kickoff (F0.1 HECHA). D-BLK.1
quedó enmendada respecto a la propuesta original del plan (que proponía
src/blocks/); el resto se firmó tal como estaba propuesto. D-BLK.3/4/5
derivan de doctrina ya vigente y se firmaron por no-objeción.
| # | Decisión | FIRMADO |
|---|---|---|
| D-BLK.1 | Ubicación y alias | src/uix/blocks/ + alias $blocks (decisión de usuario: el tier es UI y vive junto a las capas). La prueba de encapsulación se conserva íntegra: borrar src/uix/blocks/ deja npm run check verde y NADA del canon (morfo/soma/sema/eidos/active-uix/langs) lo importa — blocks es un tier bajo src/uix/, no una quinta capa. |
| D-BLK.2 | Estilos | Layout-components-first: el layout se hace componiendo Container/Section/Stack/Flex/Grid/AutoGrid/Wrap/Group/Separator/AspectRatio/Surface y sus props. Un block NO trae .css propio; un <style> scoped puntual exige justificación en su README y nunca selecciona internals de componentes compuestos. |
| D-BLK.3 | API | Componente compuesto con partes anidadas: <SiteHeader> / <SiteHeader.Nav> / <SiteHeader.Actions>; contenido SIEMPRE por children, nunca árboles de datos (items={...} solo donde el componente canónico compuesto ya es data-driven). (Derivada de la regla compositional-not-data-driven ya vigente.) |
| D-BLK.4 | Demos | web/routes/blocks/{kebab}/+page.svelte + galería índice en web/routes/blocks/. (La ruta web/routes/alpha/ sigue TERMINADA y prohibida — no tocarla.) |
| D-BLK.5 | Idiomas/strings | Un block no posee NINGÚN string visible: todo texto llega del app como children/props. Si un string parece inevitable, es superficie de contrato → lo posee el componente canónico subyacente (vía texts: del morfo + langs). (Consecuencia mecánica de B-1: sin morfo no hay texts:.) |
| D-BLK.6 | Servicios | v1 sin servicios: los blocks NO consumen uix.prefs/langs/eidos directamente; el cableado (tema/idioma, submit de auth, transporte) llega como handlers/props del app. Revisable si ≥2 blocks demuestran necesidad real (misma vara que la 2-de-3). |
| D-BLK.7 | Naming F1 | sticky · anchor-nav · empty-state · result · callout · prose · sidebar — confirmados. (Los matices de naming siguen revisables en la fase 0 de cada uno, como toda fase 0.) |
Cualquier enmienda futura a una D-BLK se registra aquí con fecha ANTES de seguir construyendo (regla dura: los desvíos de alcance se declaran, nunca en silencio).
3. El contrato B (suelo de calidad de un block — guard: blocks-check)
| B | Obligación |
|---|---|
| B-1 | Sin morfo, sin pack sema, sin fila en component:audit. Un block es composición; el comportamiento con contrato se promociona al canon ANTES (regla de admisión §1). |
| B-2 | Todo elemento interactivo es un componente eidos del catálogo (Button, Link, Field, …). Elementos nativos interactivos crudos (button/input/select/textarea/a) = error de blocks-check. (Excepción única: el HTML que Prose recibe ya renderizado — ese contenido es del app.) |
| B-3 | Los componentes compuestos se consumen AS-IS por sus props públicos (variant/size/color/…). Prohibido re-estilizar sus internals desde el block (ni CSS ni style=). Los colores son siempre roles/tokens vía props — un block no decide color fuera del sistema. |
| B-4 | Dependencia unidireccional: src/uix/blocks/* importa $uix, $adom y arts públicos; nada del canon (src/uix/{morfo,soma,sema,eidos,active-uix,langs}) ni de src/{arts,libs,packs} importa de src/uix/blocks/. Borrar el tier deja check verde — la prueba de encapsulación se mantiene aunque viva bajo src/uix/. Entre blocks tampoco se importa (B-10). |
| B-5 | Contenido por composición (children/snippets). Nunca root={tree} ni props-árbol propias. |
| B-6 | Responsive con los mecanismos del framework (props responsive de los componentes de layout, breakpoints canónicos). Cero matchMedia/listeners propios — si hiciera falta observar algo, es señal de componente canónico (→ admisión). |
| B-7 | Cero strings propios (D-BLK.5). |
| B-8 | Landmarks correctos: elemento sectioning + aria-label/aria-labelledby cuando hay más de un landmark del mismo tipo; jerarquía de headings coherente y documentada en el README del block (qué nivel emite y cómo se ajusta). |
| B-9 | Cada block: README.md (secciones: Función · Mapa de composición — qué componentes canónicos usa y con qué props — · Decisiones · Gaps-con-disposición) + demo con profundidad de testbed (cada prop pública = control vivo; guía: docs/guides/demo-authoring.md, adaptada — sin las 9 tabs completas de componente, mínimo: escena realista + panel de props + código copiable). |
| B-10 | Un block no importa otro block. Si dos blocks comparten estructura, la pieza compartida o es un componente canónico o se duplica conscientemente (anotado en Gaps). Excepción declarada: los shells (app-shell, docs-shell) SÍ componen blocks/componentes de F1 por diseño — se lista explícitamente en su README. |
| B-11 | Motion: solo vía los props motion/presets de los componentes compuestos o Cascade para coreografía de entrada. Cero @keyframes/transitions propias (la regla R-4.5 del canon aplica moralmente aunque el audit no corra aquí). |
blocks-check (F0.5) verifica mecánicamente: B-2 (AST/regex de elementos
nativos interactivos), B-4 (dirección de imports), B-1 (no hay ficheros bajo
src/uix/morfo/components/ reclamados por blocks; no imports de
sema/components), D-BLK.2 (no .css bajo src/uix/blocks/; <style> solo
con /* justified: … */), B-9 (README + ruta demo existen), B-10 (imports
entre blocks solo en la allowlist de shells).
4. Reglas de trabajo (TODAS las sesiones de este plan)
Lectura obligatoria antes de tocar nada (leer los docs directamente — nunca delegar la lectura a agentes):
- Siempre:
CLAUDE.md(llega solo) · este plan ·docs/README.md(mapa). - F1 (componentes canon):
docs/building-a-component.md— LA puerta; cada fase de la ruta nombra su doc y su guard. No saltarse la fase 0 (tabla comparativa vs ≥3 referencias; cada ❌/⚠️ del scope recibe decisión del usuario ANTES de construir). - F2–F4 (blocks):
docs/architecture/blocks.md(existirá tras F0) + los README de CADA componente que el block compone (el mapa de composición se escribe leyendo, no de memoria) + referencias de blocks equivalentes (shadcn blocks · Tailwind UI/Plus · Flowbite blocks · PrimeBlocks · Relume) — comparativa ANTES de diseñar y al declarar done. - Dossier de referencia (2026-07-21): TODA fase 0 (F1–F4) contrasta
contra
docs/process/RESEARCH-blocks-references.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
- Compone:
Section+Surface(tratamiento de fondo del sistema — un CTA se distingue por acabado, y eso ya es vocabulario del framework) +Heading+Text+Group(Buttons). - API:
<Cta>+.Title+.Description+.Actions.
F2.9 newsletter
- Compone: como F2.8 +
Form+Field(email) +Button. - API:
<Newsletter>+.Title+.Description+.Form(submit handler del app; validación por el sistema deForm— leer su README: el block no inventa validación). - Nota: separado de
ctaporque el form introduce a11y y estados (invalid/submitting) que el CTA puro no tiene.
F2.10 site-footer
- Compone:
<footer>+Container+Grid(columnas:Headingsm +StackdeLink) +Separator+Group(IconButtonsociales) +Textlegal. - API:
<SiteFooter>+.Column+.Social+.Legal+.Extra(slot libre: theme/lang pickers del app — D-BLK.6). - v1: 3–5 columnas responsive → apiladas en móvil.
F2.11 banner (E-3)
- Compone: el componente
Bannerexistente +Container+Link+Button(dismiss lo posee Banner si ya lo trae — leer su README). - API:
<SiteBanner>+ children; posición top pareja desite-header. - Nota dossier (P1): 13 TW · 16 Untitled · 5 Flowbite — la categoría convergente más barata de cubrir (el componente ya existe).
F2.12 team (E-3)
- Compone:
Section+AutoGrid+Avatar+Heading+Text+Group(IconButtonsociales por miembro). - API:
<Team>+.Header+.Member(+.MemberAvatar/.MemberName/.MemberRole/.MemberLinks).
F2.13 contact (E-3)
- Compone:
Section+Grid(info + form) +Form+Field+Textarea+Button+ filas de datos de contacto (Icon+Text+Link). - API:
<Contact>+.Info+.Form; validación y submit = sistemaFormcon handlers del app (misma regla quenewsletter).
F2.14 content-section (E-3)
- Compone:
Section+Container(medida estrecha) +Prose(F1.5) + slots de media (Image/Figurefuera del flujo prose). - API:
<ContentSection>+ children (el HTML renderizado va al Prose). - Depende de: F1.5.
(logo-cloud sigue en F5: su versión honesta pide Marquee o queda en un
Wrap trivial que no justifica block todavía — decisión anotada, revisable.
El resto de la unión del dossier — bento, gallery, cookie-consent, popups,
careers, events, comparison, timeline — queda en F5 con disparador, E-3.)
Cierre F2: blocks-check verde; galería web/routes/blocks/ con los 14;
una página compuesta de PRUEBA (header + hero + features + pricing + faq +
cta + footer juntos) que valide que los blocks ensamblan sin fricción — esa
página ES el test de integración del tier y se mira en claro/oscuro/375/1280.
F3 — Blocks de aplicación (10)
Mismas reglas y ficha que F2. Depende de: F1.2/F1.3/F1.4 (estados),
F1.7 (sidebar) para F3.1.
F3.1 app-shell
- Función: esqueleto de aplicación: sidebar + topbar + contenido (+ aside opcional).
- Compone:
Sidebar(F1.7) +Sticky+Container/Grid+ScrollArea+Group+ slots. - API:
<AppShell>+.Sidebar+.Topbar+.Content+.Aside. - Landmark:
header/nav/main/aside+ skip-link (según decisión F2.1); jerarquía documentada. - v1: sidebar colapsable + topbar afijada + main con scroll propio; móvil = sidebar en drawer (lo trae F1.7).
- Excepción B-10 declarada: los shells componen otros elementos de F1 y
blocks — allowlist en
blocks-check.
F3.2 auth
- Función: familia de formularios de identidad. Sub-blocks:
sign-in·sign-up·recover·otp. - Compone:
Card+Form+Field+PasswordField+PinInput(otp) +ProofOfHuman+Button+Separator+Link+ slot de proveedores sociales (Buttons del app). - API:
<AuthCard kind='sign-in'|…>con partes (.Title·.Fields·.Actions·.Footer), o cuatro exports finos — decidir en su fase 0 mirando el API real 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 en next-features.md §7.)
Cierre F3: blocks-check verde; demo de app-shell montando dentro
data-table + dashboard + notifications + user-menu como página de
integración; navegador claro/oscuro/375/1280 + teclado (tab por toda la
página de integración sin trampas de foco).
F4 — Blocks de docs (3)
Depende de: F1 completo (prose, anchor-nav, nav-tree, callout) + F2.1. Conecta con la iniciativa docs-corpus=site (el corpus es la semilla del sitio); estos blocks son su vehículo de UI.
F4.1 docs-shell
- Compone:
AppShell(F3.1, excepción B-10) especializado:NavTree(F1.8) como árbol de docs +Prose(main) +AnchorNav(TOC derecha) +Breadcrumb+ trigger de búsqueda (Command— ya existe) + prev/next (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) · logo-cloud
(bloqueado por decisión marquee) · billing · file-manager ·
profile-card (cuando haya consumidor).
6. Verificación de cierre del plan (definition of done global)
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).