|
|
# PLAN — Tier `blocks`: composición reutilizable (infraestructura + componentes base + catálogo)
|
|
|
|
|
|
> **Kickoff para sesión nueva**: _"Lee `docs/process/PLAN-blocks.md` y continúa
|
|
|
> la fase que toque."_ Decisión de usuario (2026-07-21): existe un tier nuevo
|
|
|
> **`blocks`** — conjuntos de componentes desempeñando una función (cabecera
|
|
|
> sticky, hero, footer, app-shell…). Este plan es autosuficiente: cada fase
|
|
|
> lista QUÉ leer, QUÉ producir y CON QUÉ guard se verifica. Un agente no debe
|
|
|
> descubrir la doctrina por arqueología — este documento la enlaza toda.
|
|
|
>
|
|
|
> **Antes de escribir código de F0**: presentar al usuario las decisiones
|
|
|
> D-BLK de §2 (AskUserQuestion o tabla en chat) y obtener firma. Las
|
|
|
> propuestas de este plan son eso — propuestas razonadas, no decisiones
|
|
|
> tomadas.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 0. Contexto y estado
|
|
|
|
|
|
- **Origen**: análisis del ecosistema (sesión 2026-07-21). Diagnóstico: el
|
|
|
catálogo de primitivas es excepcional (~140 componentes eidos, ~95 con soma);
|
|
|
la brecha está en (a) piezas de contenido/estado de página y (b) el nivel de
|
|
|
composición. Iniciativa registrada en `docs/next-features.md` §8.
|
|
|
- **Precedentes en el repo**: la familia `chat-*` es un "bloque" construido
|
|
|
como componentes canónicos (siguió la ruta de 9 fases porque cada pieza
|
|
|
tiene contrato real); `picker-shell` es un chasis compartido; el tier
|
|
|
`packs` (`docs/architecture/packs.md`) ya resolvió la pregunta "¿cómo vive
|
|
|
un tier fuera del canon?" — este plan lo usa de espejo.
|
|
|
- **Estado**: F0/F1/F2 cerradas. Actualizar esta tabla al cerrar
|
|
|
cada tanda, estilo `PLAN-component-coherence.md`.
|
|
|
|
|
|
| Fase | Contenido | Estado |
|
|
|
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| **F0** | Infraestructura del tier: doctrina + alias + guard + rutas demo | **HECHA 2026-07-21** (F0.1–F0.7; cross-ref en `comparison.md` omitido a propósito — sin aporte hasta que exista catálogo) |
|
|
|
| **F1** | **8** componentes base del CANON que los blocks necesitan (7 + `nav-tree` por E-1) | **CERRADA — 8/8 HECHAS** (empty-state · result · callout · **sticky** · **prose** · **anchor-nav** · **nav-tree** · **sidebar**, PASS las ocho; ver registro) |
|
|
|
| **F2** | Blocks de sitio (**15**: 10 + banner·team·contact·content-section por E-3 + feature-split) | **CERRADA EN UNIDADES — 15/15** (+ saneamiento, guard endurecido y contraste doctrinal 15/15; ledger `AUDIT-blocks-ledger.md`). La página compuesta que su cierre exigía NO se construyó: la integración vive en F2b |
|
|
|
| **F2b** | **Ola de paridad de variedad**: variantes de suelo (A) + matriz de equivalencia (B) + `logo-cloud`·`blog`·`cookie-consent` (C) + página compuesta (D) | **FIRMADA 2026-08-17** — pendiente de ejecutar |
|
|
|
| **F3** | Blocks de aplicación (10) | pendiente |
|
|
|
| **F4** | Blocks de docs (3) | pendiente |
|
|
|
| **F5** | Backlog condicionado (componentes media/mobile + blocks diferidos) | pendiente |
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. Qué es un block (doctrina propuesta — aterriza en `docs/architecture/blocks.md` en F0)
|
|
|
|
|
|
Un **block** es una composición nombrada de componentes del canon que
|
|
|
desempeña una función de página: no aporta primitivas nuevas, aporta
|
|
|
_ensamblaje correcto_ (layout, landmarks, jerarquía de headings, responsive,
|
|
|
puntos de contenido). Consume el framework; el framework nunca lo referencia.
|
|
|
|
|
|
La tabla de tiers queda:
|
|
|
|
|
|
| Tier | Valor | Contrato | Entra por |
|
|
|
| ------------------------------ | ------------------------------------------------------------------------ | ---------------------------- | ------------------------------------------------ |
|
|
|
| **Canon** (`src/uix/`) | densidad de contrato (eventos, ARIA, teclado, tokens que otros consumen) | morfo + matriz de aceptación | ruta de 9 fases (`docs/building-a-component.md`) |
|
|
|
| **Packs** (`src/packs/`) | decoración parametrizada de hoja | contrato P | `packs-check` |
|
|
|
| **Blocks** (`src/uix/blocks/`) | composición de función de página | contrato B (§3) | `blocks-check` |
|
|
|
|
|
|
**Regla de admisión (espejo de la de packs)**: si al construir un block hace
|
|
|
falta comportamiento nuevo con superficie de contrato — un evento real, una
|
|
|
máquina de estados, un `data-attr` que el CSS necesita seleccionar, una
|
|
|
obligación a11y de widget — esa pieza se construye ANTES como componente
|
|
|
canónico por la ruta de 9 fases, y el block la compone. **Nunca se le crece
|
|
|
el privilegio al block**. (Es exactamente la regla "compose existing
|
|
|
components; flag gaps" de `docs/guides/component-guide.md` §4, elevada a
|
|
|
frontera de tier.) La F1 de este plan existe porque ese triage ya está hecho:
|
|
|
los 7 gaps detectados se construyen primero.
|
|
|
|
|
|
Lo que un block SÍ posee semánticamente: **landmarks y estructura de
|
|
|
documento** (`header/nav/main/aside/footer`, jerarquía h1–h6, skip-link,
|
|
|
`aria-label` de región). Los componentes no pueden saber el contexto de
|
|
|
página; los blocks sí. Es su única superficie a11y propia.
|
|
|
|
|
|
Un block PUEDE tener estado de vista local (pestaña activa, toggle
|
|
|
mensual/anual) usando los props/eventos públicos de los componentes que
|
|
|
compone. Lo que NO puede es materializar ese estado con DOM/CSS propio que
|
|
|
requiera contrato (→ regla de admisión).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 2. Decisiones D-BLK — FIRMADAS 2026-07-21
|
|
|
|
|
|
Repasadas y firmadas por el usuario en el kickoff (F0.1 HECHA). **D-BLK.1
|
|
|
quedó enmendada** respecto a la propuesta original del plan (que proponía
|
|
|
`src/blocks/`); el resto se firmó tal como estaba propuesto. D-BLK.3/4/5
|
|
|
derivan de doctrina ya vigente y se firmaron por no-objeción.
|
|
|
|
|
|
| # | Decisión | FIRMADO |
|
|
|
| ----------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
|
| **D-BLK.1** | Ubicación y alias | **`src/uix/blocks/`** + alias `$blocks` (decisión de usuario: el tier es UI y vive junto a las capas). La prueba de encapsulación se conserva íntegra: borrar `src/uix/blocks/` deja `npm run check` verde y NADA del canon (`morfo/soma/sema/eidos/active-uix/langs`) lo importa — blocks es un tier bajo `src/uix/`, no una quinta capa. |
|
|
|
| **D-BLK.2** | Estilos | **Layout-components-first**: el layout se hace componiendo `Container/Section/Stack/Flex/Grid/AutoGrid/Wrap/Group/Separator/AspectRatio/Surface` y sus props. Un block NO trae `.css` propio; un `<style>` scoped puntual exige justificación en su README y nunca selecciona internals de componentes compuestos. |
|
|
|
| **D-BLK.3** | API | Componente compuesto con partes anidadas: `<SiteHeader>` / `<SiteHeader.Nav>` / `<SiteHeader.Actions>`; contenido SIEMPRE por children, nunca árboles de datos (`items={...}` solo donde el componente canónico compuesto ya es data-driven). (Derivada de la regla compositional-not-data-driven ya vigente.) |
|
|
|
| **D-BLK.4** | Demos | `web/routes/blocks/{kebab}/+page.svelte` + galería índice en `web/routes/blocks/`. (La ruta `web/routes/alpha/` sigue TERMINADA y prohibida — no tocarla.) |
|
|
|
| **D-BLK.5** | Idiomas/strings | Un block no posee NINGÚN string visible: todo texto llega del app como children/props. Si un string parece inevitable, es superficie de contrato → lo posee el componente canónico subyacente (vía `texts:` del morfo + langs). (Consecuencia mecánica de B-1: sin morfo no hay `texts:`.) |
|
|
|
| **D-BLK.6** | Servicios | **v1 sin servicios**: los blocks NO consumen `uix.prefs`/langs/eidos directamente; el cableado (tema/idioma, submit de auth, transporte) llega como handlers/props del app. Revisable si ≥2 blocks demuestran necesidad real (misma vara que la 2-de-3). |
|
|
|
| **D-BLK.7** | Naming F1 | `sticky` · `anchor-nav` · `empty-state` · `result` · `callout` · `prose` · `sidebar` — confirmados. (Los matices de naming siguen revisables en la fase 0 de cada uno, como toda fase 0.) |
|
|
|
|
|
|
Cualquier enmienda futura a una D-BLK se registra aquí con fecha ANTES de
|
|
|
seguir construyendo (regla dura: los desvíos de alcance se declaran, nunca en
|
|
|
silencio).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 3. El contrato B (suelo de calidad de un block — guard: `blocks-check`)
|
|
|
|
|
|
| B | Obligación |
|
|
|
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| **B-1** | Sin morfo, sin pack sema, sin fila en `component:audit`. Un block es composición; el comportamiento con contrato se promociona al canon ANTES (regla de admisión §1). |
|
|
|
| **B-2** | Todo elemento interactivo es un componente eidos del catálogo (`Button`, `Link`, `Field`, …). Elementos nativos interactivos crudos (`button/input/select/textarea/a`) = **error** de `blocks-check`. (Excepción única: el HTML que `Prose` recibe ya renderizado — ese contenido es del app.) |
|
|
|
| **B-3** | Los componentes compuestos se consumen AS-IS por sus props públicos (`variant/size/color/…`). Prohibido re-estilizar sus internals desde el block (ni CSS ni `style=`). Los colores son siempre roles/tokens vía props — un block no decide color fuera del sistema. |
|
|
|
| **B-4** | Dependencia unidireccional: `src/uix/blocks/*` importa `$uix`, `$adom` y arts públicos; nada del canon (`src/uix/{morfo,soma,sema,eidos,active-uix,langs}`) ni de `src/{arts,libs,packs}` importa de `src/uix/blocks/`. Borrar el tier deja `check` verde — la prueba de encapsulación se mantiene aunque viva bajo `src/uix/`. Entre blocks tampoco se importa (B-10). |
|
|
|
| **B-5** | Contenido por composición (children/snippets). Nunca `root={tree}` ni props-árbol propias. |
|
|
|
| **B-6** | Responsive con los mecanismos del framework (props responsive de los componentes de layout, breakpoints canónicos). Cero `matchMedia`/listeners propios — si hiciera falta observar algo, es señal de componente canónico (→ admisión). |
|
|
|
| **B-7** | Cero strings propios (D-BLK.5). |
|
|
|
| **B-8** | Landmarks correctos: elemento sectioning + `aria-label`/`aria-labelledby` cuando hay más de un landmark del mismo tipo; jerarquía de headings coherente y documentada en el README del block (qué nivel emite y cómo se ajusta). |
|
|
|
| **B-9** | Cada block: `README.md` (secciones: **Función · Mapa de composición** — qué componentes canónicos usa y con qué props — **· Decisiones · Gaps-con-disposición**) + demo con profundidad de testbed (cada prop pública = control vivo; guía: `docs/guides/demo-authoring.md`, adaptada — sin las 9 tabs completas de componente, mínimo: escena realista + panel de props + código copiable). |
|
|
|
| **B-10** | Un block no importa otro block. Si dos blocks comparten estructura, la pieza compartida o es un componente canónico o se duplica conscientemente (anotado en Gaps). Excepción declarada: los shells (`app-shell`, `docs-shell`) SÍ componen blocks/componentes de F1 por diseño — se lista explícitamente en su README. |
|
|
|
| **B-11** | Motion: solo vía los props `motion`/presets de los componentes compuestos o `Cascade` para coreografía de entrada. Cero `@keyframes`/transitions propias (la regla R-4.5 del canon aplica moralmente aunque el audit no corra aquí). |
|
|
|
|
|
|
`blocks-check` (F0.5) verifica mecánicamente: B-2 (AST/regex de elementos
|
|
|
nativos interactivos), B-4 (dirección de imports), B-1 (no hay ficheros bajo
|
|
|
`src/uix/morfo/components/` reclamados por blocks; no imports de
|
|
|
`sema/components`), D-BLK.2 (no `.css` bajo `src/uix/blocks/`; `<style>` solo
|
|
|
con `/* justified: … */`), B-9 (README + ruta demo existen), B-10 (imports
|
|
|
entre blocks solo en la allowlist de shells).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 4. Reglas de trabajo (TODAS las sesiones de este plan)
|
|
|
|
|
|
**Lectura obligatoria antes de tocar nada** (leer los docs directamente —
|
|
|
nunca delegar la lectura a agentes):
|
|
|
|
|
|
- Siempre: `CLAUDE.md` (llega solo) · este plan · `docs/README.md` (mapa).
|
|
|
- F1 (componentes canon): `docs/building-a-component.md` — LA puerta; cada
|
|
|
fase de la ruta nombra su doc y su guard. No saltarse la fase 0 (tabla
|
|
|
comparativa vs ≥3 referencias; cada ❌/⚠️ del scope recibe decisión del
|
|
|
usuario ANTES de construir).
|
|
|
- F2–F4 (blocks): `docs/architecture/blocks.md` (existirá tras F0) + los
|
|
|
README de CADA componente que el block compone (el mapa de composición se
|
|
|
escribe leyendo, no de memoria) + referencias de blocks equivalentes
|
|
|
(shadcn blocks · Tailwind UI/Plus · Flowbite blocks · PrimeBlocks · Relume)
|
|
|
— comparativa ANTES de diseñar y al declarar done.
|
|
|
- **Dossier de referencia (2026-07-21)**: TODA fase 0 (F1–F4) contrasta
|
|
|
contra `docs/process/RESEARCH-blocks-references.md` ANTES de diseñar — 6
|
|
|
pistas de investigación con suelos de paridad a nivel de prop, pitfalls y
|
|
|
ángulos de superación por ítem. La fase 0 verifica contra el dossier (y
|
|
|
solo investiga de cero lo que el dossier no cubra); las brechas de suelo
|
|
|
que el dossier señala para el ítem se resuelven en su scope-approval.
|
|
|
|
|
|
**Proceso por tanda** (una tanda = un componente o un block):
|
|
|
|
|
|
1. `git reset -q` + verificar HEAD (sesiones concurrentes; NUNCA amend).
|
|
|
2. Fase 0 del ítem: comparativa + scope al usuario si hay decisiones.
|
|
|
3. Construir. UN fichero → verificar → resto (no-cascade). Componer, jamás
|
|
|
re-implementar; gap detectado = FLAG al usuario, no workaround inline.
|
|
|
4. Verificar: `npm run check` + scope vitest del ítem + guard del tier
|
|
|
(`component:audit --only {kebab}` para F1 · `blocks-check` para F2+) +
|
|
|
**navegador de verdad**: screenshot y MIRARLO, claro Y oscuro
|
|
|
(`colorScheme:'dark'`), móvil y desktop para blocks (resize 375/1280).
|
|
|
5. Demo con profundidad de testbed (B-9); docs del framework en el MISMO pase
|
|
|
(README del ítem + mapa si procede).
|
|
|
6. Commit (convención viva del repo): `uix({kebab}): …` para F1,
|
|
|
`blocks({kebab}): …` para F2+, `docs(blocks): …` para doctrina. Stage
|
|
|
SOLO los paths propios (nunca `git add -A`; excluir `words/`, `palabras/`,
|
|
|
`web/routes/alpha/`).
|
|
|
7. Actualizar la tabla de estado de este plan (y `next-features.md` §8 al
|
|
|
cerrar cada fase).
|
|
|
|
|
|
**Prohibiciones**: no tocar `palabras/`, `chronos/`, `media-player`
|
|
|
(foráneos/WIP — componerlos solo cuando estén landed; hoy chronos NO lo
|
|
|
está); no crear servicios/mocks falsos en tests (instancias reales vía
|
|
|
`createActiveUix`); no `--no-verify`; no borrar nada sin instrucción
|
|
|
explícita; responder en castellano, código y docs en inglés.
|
|
|
|
|
|
---
|
|
|
|
|
|
## F0 — Infraestructura del tier
|
|
|
|
|
|
**Objetivo**: que exista el tier con doctrina, guard y sitio donde vivir —
|
|
|
vacío pero verde.
|
|
|
|
|
|
| Paso | Producir | Verificación |
|
|
|
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
|
| F0.1 | **Firma D-BLK** (§2) con el usuario; enmendar el plan si procede | **HECHA 2026-07-21** — firmas en §2 (D-BLK.1 enmendada: `src/uix/blocks/`) |
|
|
|
| F0.2 | `docs/architecture/blocks.md` — la doctrina de §1 + §3 en formato espejo de `packs.md` (frontmatter E1, regla de admisión, hard boundaries, contrato B, "promotion path" = la F1 como ejemplo vivido). Enlazar sin copiar: canon → `CANON.md`, ruta → `building-a-component.md` | `npm run docs:check` (links) |
|
|
|
| F0.3 | Alias `$blocks` → `src/uix/blocks` en el const `aliases` de `vite.config.ts` (fuente de verdad) + `svelte.config.js` en sync + fila en la tabla de aliases de `CLAUDE.md` | `npm run check` |
|
|
|
| F0.4 | `src/uix/blocks/README.md` — mapa del tier (inventario vivo = el árbol, como packs) + template de README de block (B-9) | — |
|
|
|
| F0.5 | `scripts/blocks-check.ts` + npm script `blocks:check` — los checks mecánicos listados en §3. Espejo estructural de `packs-check`. Test negativo: un fixture con `<button>` crudo debe fallar | `npm run blocks:check` verde en tier vacío + test negativo rojo |
|
|
|
| F0.6 | `web/routes/blocks/+page.svelte` — galería índice (dogfooding: componer `Container/Section/Card/…` del propio catálogo para la galería) | navegador claro/oscuro |
|
|
|
| F0.7 | Cablear docs: fila E1 en el mapa de `docs/README.md` (architecture/blocks.md) + nota del tier en el diagrama de arquitectura de `CLAUDE.md` (línea `blocks/ → …`) + cross-ref en `docs/comparison.md` si aporta | `npm run docs:check` |
|
|
|
|
|
|
**Cierre F0**: `check` + `docs:check` + `blocks:check` verdes; galería
|
|
|
renderiza vacía con mensaje de "en construcción" compuesto con el catálogo.
|
|
|
|
|
|
**CERRADA 2026-07-21** — evidencia: `docs:check` 0/0 (511 docs);
|
|
|
`svelte-check` 76E/51W = baseline exacto (los archivos nuevos compilan
|
|
|
limpios; los 76 son deuda foránea preexistente del árbol sin commitear);
|
|
|
`blocks:check` verde (self-test 10 fixtures OK — el `<button>` crudo SE
|
|
|
detecta — y 5 532 archivos de canon/arts/libs/packs escaneados sin
|
|
|
violaciones de dirección). Galería verificada en navegador vía árbol de
|
|
|
accesibilidad + estilos computados en AMBOS modos (light: `base-light`, h1
|
|
|
oklch(0.24…); dark: `base-dark`, h1 oklch(0.95…)); la captura de píxeles
|
|
|
del panel embebido expiró (renderer suspendido en segundo plano — clase
|
|
|
conocida) — pendiente de un vistazo humano o Playwright en la primera
|
|
|
sesión F1. El listener `prefers-color-scheme` del layout funciona al boot;
|
|
|
el evento `change` en vivo no dispara con el panel suspendido (peculiaridad
|
|
|
del entorno, mismo patrón raw-matchMedia que el layout de `/uix`).
|
|
|
|
|
|
---
|
|
|
|
|
|
## F1 — Componentes base del CANON (8)
|
|
|
|
|
|
Estos NO son blocks: entran por `src/uix/{morfo,soma,sema,eidos}` siguiendo
|
|
|
**la ruta completa de 9 fases** de `docs/building-a-component.md` (fase 0
|
|
|
Decide → 8 Acceptance), con `component:audit --only {kebab}` como oráculo.
|
|
|
Las fichas siguientes NO sustituyen la ruta — la parametrizan: dan el gap, la
|
|
|
membresía esperada, el boceto de morfo, las referencias mínimas de la fase 0
|
|
|
y las trampas conocidas del ecosistema que aplican. El vocabulario cerrado
|
|
|
(archetypes, familias, verbos, intents, holds) se toma SIEMPRE de
|
|
|
`docs/canon/vocabularies.md` (generado del código) — las fichas nombran
|
|
|
candidatos, el builder valida contra la lista real.
|
|
|
|
|
|
**Orden recomendado**: F1.2 → F1.3 → F1.4 (display, baratos, establecen
|
|
|
ritmo) → F1.1 (desbloquea F2) → F1.5 → F1.6 → F1.8 → F1.7 (desbloquean
|
|
|
F4/F3).
|
|
|
|
|
|
**E-2 firmada (2026-07-21)**: los v1 de F1–F4 se dimensionan al **suelo de
|
|
|
paridad** del dossier (lo que ≥2 referencias convergen) — cada fase 0 trae
|
|
|
la subida concreta al scope-approval; quedarse bajo suelo exige decisión
|
|
|
registrada, nunca omisión.
|
|
|
|
|
|
### F1.1 `sticky` — afijado con estado
|
|
|
|
|
|
- **Gap**: wrapper `position:sticky` que SABE cuándo está afijado
|
|
|
(`data-stuck`) para que la cabecera cambie elevación/fondo al pegarse.
|
|
|
- **Membresía**: soma + eidos (comportamiento real: observación + estado).
|
|
|
- **Fase 0, comparar**: AntD `Affix` · Mantine `Affix` · la técnica sentinel
|
|
|
con IntersectionObserver (CSS-Tricks/web.dev) · shadcn (no lo tiene —
|
|
|
anotarlo como diferencial).
|
|
|
- **Morfo (boceto)**: parts `provider` (el contenedor sticky) + `sentinel`
|
|
|
(1px observado, `aria-hidden`); data: `data-stuck` (presente/ausente),
|
|
|
`data-edge` (`top | bottom`). **Sin eventos v1** (afijarse es hecho de
|
|
|
layout, no ocurrencia perceptiva del usuario — si algún consumidor pide
|
|
|
señal sema, se revisa con el criterio D.4). Sin keyboard, sin `texts`.
|
|
|
Partes display → **sin `archetype`** (un archetype interactivo arrastra
|
|
|
estilos de item; y el archetype `content` pisa `position` — justo lo que
|
|
|
sticky no puede permitirse).
|
|
|
- **Soma**: provider observa el sentinel vía `dom.observe`
|
|
|
(IntersectionObserver por adom) — **JAMÁS** scroll listener +
|
|
|
`getBoundingClientRect` síncrono (regla de reflow: lecturas de layout solo
|
|
|
post-layout vía `dom.measure`/rAF). `data-stuck` lo escriben los effects
|
|
|
(`dom.apply`), único escritor.
|
|
|
- **Eidos**: wrapper fino; tokens `--sticky-top-offset` /
|
|
|
`--sticky-z-index` (alias de la escala `--z-index-*` semántica, no número
|
|
|
crudo). La receta NO decide sombra/fondo del contenido — eso lo hace el
|
|
|
consumidor seleccionando su propio estado visual con `data-stuck` presente
|
|
|
(documentar el patrón en el README).
|
|
|
- **Demo**: página larga con header/toolbar afijable, chip mostrando el
|
|
|
estado, edge top y bottom.
|
|
|
- **Trampas**: `mergeProps` clobberea stamps — attrs visuales del wrapper
|
|
|
fuera del morfo salvo que crucen a soma (aquí `data-stuck` SÍ es soma).
|
|
|
|
|
|
### F1.2 `empty-state` — estado vacío
|
|
|
|
|
|
- **Gap**: patrón universal icono/título/descripción/acción para listas y
|
|
|
paneles sin datos. Hoy no existe nada.
|
|
|
- **Membresía**: morfo + eidos, **sin provider soma** (display puro; hay
|
|
|
precedente sancionado: `metrics` es `scope: ['sema','eidos']` sin soma).
|
|
|
Morfo-first aplica igual: hasta las hojas llevan morfo.
|
|
|
- **Fase 0, comparar**: Chakra `EmptyState` · AntD `Empty` · HeroUI ·
|
|
|
patrones de empty state de Material.
|
|
|
- **Morfo (boceto)**: parts `provider` + `media` (icon o ilustración) +
|
|
|
`title` + `description` + `actions`. Sin eventos, sin keyboard, sin
|
|
|
archetype en partes display. `texts:` NO — los strings los trae el app
|
|
|
(es contenido, no chrome del componente).
|
|
|
- **Eidos**: receta pequeña espejo de la más cercana ya enviada (estudiar
|
|
|
`banner`/`card` antes de escribir una línea); centrado, spacing tokenizado
|
|
|
`--empty-state-*`, tamaño vía canon `size` si aporta (probablemente solo
|
|
|
`sm/md`). `actions` compone `Button` del catálogo vía children.
|
|
|
- **Demo**: en contexto real — un `table`/`grid-list` sin filas mostrando el
|
|
|
empty-state, más la escena aislada con controles.
|
|
|
|
|
|
### F1.3 `result` — página de resultado
|
|
|
|
|
|
- **Gap**: estado terminal de página/flujo (éxito, error, 403/404/500).
|
|
|
Pareja de `empty-state`, distinto rol: cierra un flujo, no describe
|
|
|
ausencia de datos.
|
|
|
- **Membresía**: como F1.2 (morfo + eidos display).
|
|
|
- **Fase 0, comparar**: AntD `Result` · patrones de error page de
|
|
|
Tailwind UI · HeroUI.
|
|
|
- **Morfo (boceto)**: parts `provider` + `media` + `title` + `description` +
|
|
|
`actions` + `extra`. Prop `status` → `data-status`
|
|
|
(`success | error | info | forbidden | not-found | server-error`).
|
|
|
**Cuidado doctrinal**: `data-status` NO es el intent perceptivo — si en
|
|
|
algún momento gana eventos con evaluación, el intent viaja por
|
|
|
`fromProp:intent` del morfo, no reciclando status (regla data-color ≠
|
|
|
perceptual-intent). v1 sin eventos.
|
|
|
- **Eidos**: color del media por rol canónico según status (roles, no
|
|
|
hex); iconografía por status con override por children.
|
|
|
- **Demo**: los 6 status + composición con `Button` home/back.
|
|
|
|
|
|
### F1.4 `callout` — admonición inline
|
|
|
|
|
|
- **Gap**: aviso DENTRO del contenido (info/tip/aviso/peligro). `Banner` es
|
|
|
el anuncio de página; esto es la nota de documento — imprescindible para
|
|
|
`prose` y el docs-shell.
|
|
|
- **Membresía**: morfo + eidos display (sin soma; variante dismissible se
|
|
|
DIFIERE — si se pidiera, el dismiss es evento real → soma + sema en esa
|
|
|
pasada, no antes).
|
|
|
- **Fase 0, comparar**: Radix Themes `Callout` · shadcn `Alert` ·
|
|
|
admonitions de Docusaurus/Starlight · `banner` propio (leer su README:
|
|
|
qué decisiones ya están tomadas para avisos y cuáles NO trasladan).
|
|
|
- **Morfo (boceto)**: parts `provider` (role `note`) + `icon` + `title` +
|
|
|
`content`. La evaluación ES semántica aquí: prop `intent` restringido a
|
|
|
un subset canónico (candidatos: `neutral | affirm | risk | threat`;
|
|
|
validar contra `vocabularies.md`) estampado vía `fromProp:intent` — así
|
|
|
eidos tiñe con la MISMA mecánica evaluativa del sistema, no con un enum
|
|
|
paralelo inventado.
|
|
|
- **Eidos**: tinte por intent usando roles/escalas (fondo suave + borde +
|
|
|
icono saturado — estudiar cómo tiñe `banner` e imitar la mecánica);
|
|
|
tipografía del canon; `--callout-*` para spacing/radius.
|
|
|
- **Demo**: los 4 intents × con/sin título × con contenido multilínea, e
|
|
|
incrustado en un texto largo (anticipo de `prose`).
|
|
|
|
|
|
### F1.5 `prose` — contenido largo estilizado
|
|
|
|
|
|
- **Gap**: contenedor que estiliza HTML/markdown renderizado (h1–h6, p,
|
|
|
listas, blockquote, tabla, código, img, hr) con los tokens del tema. Sin
|
|
|
esto no hay docs ni blog.
|
|
|
- **Membresía**: morfo mínimo + eidos (sin soma). El morfo declara UNA part
|
|
|
`provider`; el trabajo vive en la receta.
|
|
|
- **Fase 0, comparar**: Tailwind Typography (`prose`) — el patrón de
|
|
|
referencia — · Mantine `TypographyStylesProvider` · Radix Themes.
|
|
|
- **Norma que lo hace legal**: los selectores de elemento descendientes
|
|
|
(`[data-prose] h2`, `[data-prose] ul`…) son **selectores estructurales
|
|
|
bajo una parte del morfo** — sancionados por la norma S1 del eidos-lint
|
|
|
(hook = data-attr del morfo o estructural bajo parte; clases NO).
|
|
|
- **Eidos**: LA receta grande del grupo. Reglas: tipografía SOLO vía
|
|
|
primitivos del canon (`--font-*`, escala tipográfica; cero literales — y
|
|
|
los proporcionales que hagan falta con `/* literal: */` justificado);
|
|
|
medida de lectura `--prose-max-width` (~65ch) tokenizada; `code`/`pre`
|
|
|
alineados con los tokens de `code`/`code-block` (leer sus recetas ANTES;
|
|
|
si hay que duplicar valores, es un gap a flag, no un copy-paste);
|
|
|
imágenes `max-width:100%`; tablas con overflow propio. El HTML interno es
|
|
|
del app — aquí la regla B-2 no aplica (es la excepción documentada).
|
|
|
- **Demo**: documento markdown real renderizado (headings, listas anidadas,
|
|
|
tabla, código, blockquote, callout incrustado), claro/oscuro, densidades.
|
|
|
|
|
|
### F1.6 `anchor-nav` — índice con scrollspy
|
|
|
|
|
|
- **Gap**: TOC lateral con sección activa según scroll (docs, settings
|
|
|
largos, landing largas).
|
|
|
- **Membresía**: soma + eidos (observación + estado activo + navegación).
|
|
|
- **Fase 0, comparar**: AntD `Anchor` · Mantine `TableOfContents` ·
|
|
|
Starlight/Docusaurus TOC. APG: no hay patrón de widget — es un `nav`
|
|
|
landmark con `aria-current`; anotarlo en el README (válvula A-1.4 con
|
|
|
nota, como pagination/stepper).
|
|
|
- **Morfo (boceto)**: parts `provider` (nav, `aria-label` vía `texts:` —
|
|
|
aquí SÍ hay string de chrome: "On this page"/"En esta página" → catálogo
|
|
|
langs) + `list` + `item` + `link` (¿archetype de item interactivo? —
|
|
|
validar contra vocabularies; el link compone `Link` del catálogo).
|
|
|
Data: `data-active` en item; `aria-current`. Eventos: click de link =
|
|
|
navegación (familia/verbo a decidir en fase 1 contra `SEMA_VERBS` —
|
|
|
candidato familia `shift`; validar). `expression:` según criterio D.4.
|
|
|
- **Soma**: registro de secciones objetivo (por id o por `attachPart`),
|
|
|
observación vía `dom.observe` (IntersectionObserver, umbrales al gusto
|
|
|
del provider) — mismas reglas anti-reflow que F1.1. El activo es estado
|
|
|
del provider; los effects estampan `data-active`. Scroll programático al
|
|
|
click vía `dom` (scrollIntoView por ActiveDom), no window crudo.
|
|
|
- **Eidos**: raíl vertical con indicador de activo (¡leer la memoria
|
|
|
NavMenu Indicator: soma posiciona, eidos da forma, nunca transition de
|
|
|
transform!); niveles h2/h3 con indentación tokenizada.
|
|
|
- **Demo**: página larga real con `prose` (F1.5) + anchor-nav vivo.
|
|
|
|
|
|
### F1.7 `sidebar` — navegación vertical de app
|
|
|
|
|
|
> ✅ **E-1 FIRMADA (2026-07-21)**: la colisión sidebar-app vs árbol-docs
|
|
|
> (P5/B-5) se resuelve con el componente canónico **`nav-tree`** (F1.8) —
|
|
|
> este sidebar queda app-céntrico. Además: el dossier (P4) fija el suelo
|
|
|
> shadcn (23 partes, dos ejes estado/modo, costuras open/onOpenChange/
|
|
|
> toggle expuestas desde v1) — la fase 0 dimensiona contra él por E-2.
|
|
|
|
|
|
- **Membresía**: soma + eidos. Es el componente más pesado de F1 —
|
|
|
reservarle tanda propia.
|
|
|
- **Fase 0, comparar**: shadcn `Sidebar` (el patrón de referencia actual) ·
|
|
|
Mantine `AppShell.Navbar` · Ark/Radix (no lo tienen — anotar). Scope al
|
|
|
usuario ANTES de construir: qué features de shadcn entran en v1
|
|
|
(propuesta v1: grupos + colapsable a raíl con tooltips + activo +
|
|
|
slots header/footer; FUERA v1: keyboard shortcut global, persistencia —
|
|
|
llega del app por D-BLK.6, submenús flotantes en raíl).
|
|
|
- **Morfo (boceto)**: parts `provider` + `header` + `content` + `group` +
|
|
|
`group-label` + `item` + `footer` + `trigger` (botón colapso — compone
|
|
|
`Button` con `asChild`/child pattern). Data: `data-collapsed`,
|
|
|
`data-rail`, `data-active` (item). Eventos: toggle de colapso (verbo
|
|
|
candidato del set real; evaluación neutral), activación de item si el
|
|
|
item es más que un `Link` — decidir en fase 1 (si item = `Link` puro, la
|
|
|
navegación no necesita evento propio del sidebar).
|
|
|
- **Soma**: estado collapsed/rail; grupos colapsables PUEDEN componer el
|
|
|
provider de `collapsible` (precedente compose-compound-in-component:
|
|
|
NumberField dentro de Knob, con aislamiento de eventos); variante móvil
|
|
|
COMPONE `Drawer` (no lo reimplementa) — el provider decide qué montar por
|
|
|
breakpoint responsive del sistema, no matchMedia propio.
|
|
|
- **Eidos**: raíl con `will-change` cuidado (memoria: `will-change:
|
|
|
transform` produce jitter en raíles finos con DPR≠1 — override a `auto`);
|
|
|
tooltips de item en modo raíl componen `Tooltip`; tokens `--sidebar-*`
|
|
|
(width, rail-width, paddings).
|
|
|
- **Demo**: shell de app simulada, toggle colapso, grupos, móvil (375px) con
|
|
|
drawer, RTL.
|
|
|
|
|
|
### F1.8 `nav-tree` — árbol de navegación data-driven (E-1, 2026-07-21)
|
|
|
|
|
|
- **Gap**: navegación jerárquica profunda (docs-shell, árboles de páginas)
|
|
|
con active-trail — lo que el dossier (P5) demostró que NO cabe en el
|
|
|
sidebar-app: ≥3 niveles, active-trail auto-expandido, auto-colapso de
|
|
|
hermanos, profundidad default-open, badges, scroll-active-into-view,
|
|
|
presentación móvil. **Data-driven por diseño** (el app pasa el árbol como
|
|
|
datos — precedente sancionado: Menubar; B-5 no se viola porque el
|
|
|
componente canónico ES data-driven).
|
|
|
- **Membresía — fase 0 OBLIGADA contra nuestro propio catálogo**: leer
|
|
|
`tree-view` (ya existe: selección de nodos) ANTES de diseñar y delimitar
|
|
|
la frontera tree-view (selección/widget tree) vs nav-tree (navegación por
|
|
|
links, landmark nav) — o justificar extender tree-view. La decisión es
|
|
|
del scope-approval.
|
|
|
- **Fase 0, comparar**: sidebars de Starlight/Docusaurus/Fumadocs (dossier
|
|
|
P5 — comportamientos y anatomía) + APG Disclosure Navigation (dossier
|
|
|
P4/P3: botones `aria-expanded`/`aria-controls`, Esc devuelve foco,
|
|
|
flechas opcionales) + `tree-view` propio.
|
|
|
- **Morfo (boceto)**: parts `provider` (nav landmark, `aria-label` vía
|
|
|
`texts:`) + `list` + `item` + `trigger` (grupo colapsable) + `link`
|
|
|
(compone `Link`; `aria-current="page"` del MISMO estado que `data-active`)
|
|
|
- slot badge (compone `Badge`). Data: `data-active`, `data-expanded`,
|
|
|
profundidad como CSS var tokenizada (no attr por nivel). Keyboard: patrón
|
|
|
disclosure; roving opcional opt-in.
|
|
|
- **Soma**: árbol como datos; el activo llega por costura del app (matcher
|
|
|
de URL — el componente NO conoce el router); active-trail expande
|
|
|
ancestros; colapso de grupos evalúa componer `collapsible`
|
|
|
(compose-first); scroll-active-into-view vía `dom`.
|
|
|
- **Eidos**: indent por nivel con token (`--nav-tree-depth-offset`, estilo
|
|
|
Mantine pero tokenizado), línea/raíl de nivel, estados active/expanded.
|
|
|
- **Demo**: árbol real de ~40 nodos y 3 niveles (p. ej. el propio mapa de
|
|
|
docs), active-trail vivo, móvil 375px.
|
|
|
|
|
|
**Cierre F1**: los 8 con `component:audit --only` PASS (o NEEDS-WORK
|
|
|
únicamente por reglas D-\* de demos v3 si esa fase global sigue abierta —
|
|
|
anotar en la tabla), `morfo:check`, `eidos-lint` por componente, suite
|
|
|
`npx vitest run src/uix/eidos` verde, `npm run check` sin regresión sobre
|
|
|
baseline.
|
|
|
|
|
|
---
|
|
|
|
|
|
## F2 — Blocks de sitio (14)
|
|
|
|
|
|
Todos entran por el contrato B. Ficha = **Función · Compone · API (partes) ·
|
|
|
Layout/landmark · v1 · Demo**. Regla transversal: fase 0 ligera SIEMPRE
|
|
|
(mirar el block equivalente en ≥2 catálogos de referencia de §4 y anotar en
|
|
|
el README qué se adopta/descarta). Variantes: v1 = LA variante (una);
|
|
|
ampliaciones = Gaps con disposición, no código especulativo.
|
|
|
|
|
|
**Depende de**: F1.1 (`sticky`) para F2.1; el resto de F2 no depende de F1.
|
|
|
|
|
|
### F2.1 `site-header`
|
|
|
|
|
|
- **Función**: cabecera de sitio con afijado y cambio de elevación al pegarse;
|
|
|
colapso a menú móvil.
|
|
|
- **Compone**: `Sticky` (F1.1) + `Container` + `NavigationMenu` + `Button` +
|
|
|
`Drawer` (móvil) + `Link` + `Separator`.
|
|
|
- **API**: `<SiteHeader>` (props: `sticky?`, `container?`) + `.Brand` +
|
|
|
`.Nav` + `.Actions` + `.MobileNav` (children del drawer).
|
|
|
- **Layout/landmark**: `<header>` + `<nav aria-label>`; skip-link como primer
|
|
|
foco (decidir en su fase 0 si el skip-link vive aquí o en los shells — una
|
|
|
sola respuesta, documentada).
|
|
|
- **v1**: brand izquierda · nav centro · actions derecha · drawer móvil;
|
|
|
estilización del estado pegado vía `data-stuck` (elevación/fondo con tokens
|
|
|
de los componentes compuestos, no CSS nuevo).
|
|
|
- **Demo**: página con scroll largo, claro/oscuro, 375/1280, RTL.
|
|
|
|
|
|
### F2.2 `hero`
|
|
|
|
|
|
- **Función**: sección de apertura con titular, subtítulo, acciones y media.
|
|
|
- **Compone**: `Section` + `Container` + `Stack`/`Grid` + `Heading`
|
|
|
(nivel configurable, default h1) + `Text` + `Group` (acciones con
|
|
|
`Button`) + `Badge` (chip anuncio) + `Image`/`AspectRatio`.
|
|
|
- **API**: `<Hero>` (prop `layout: 'center' | 'split'`) + `.Eyebrow` +
|
|
|
`.Title` + `.Description` + `.Actions` + `.Media`.
|
|
|
- **Layout/landmark**: `<section aria-labelledby={title.id}>`; un solo h1
|
|
|
por página es responsabilidad del app — el README lo dice.
|
|
|
- **v1**: `center` y `split` (la segunda existe porque discrimina el layout,
|
|
|
no por lujo — es el criterio "casos que discriminen").
|
|
|
- **Demo**: ambos layouts, con/sin media, con badge, dark.
|
|
|
|
|
|
### F2.3 `feature-grid`
|
|
|
|
|
|
- **Compone**: `Section` + `Container` + `AutoGrid` + `Stack` + `Icon` +
|
|
|
`Heading` + `Text`.
|
|
|
- **API**: `<FeatureGrid>` + `.Header` (title+description de sección) +
|
|
|
`.Item` (con `.ItemIcon`/`.ItemTitle`/`.ItemText` o children libres).
|
|
|
- **v1**: items planos (sin Card — la variante card es Gap).
|
|
|
- **Demo**: 3/6 items, columnas responsive del AutoGrid.
|
|
|
|
|
|
### F2.4 `pricing`
|
|
|
|
|
|
- **Compone**: `Section` + `CardGroup`/`Card` + `Heading` + `Text` + `Badge`
|
|
|
(plan destacado) + `Button` + `ToggleGroup` (mensual/anual) + filas de
|
|
|
features (`Stack` + `Group` + `Icon` check + `Text`).
|
|
|
- **API**: `<Pricing>` + `.Switch` (billing toggle; estado de vista local
|
|
|
permitido §1) + `.Plan` (prop `featured?`) + `.PlanPrice` + `.PlanFeatures`
|
|
|
- `.PlanAction`.
|
|
|
- **v1**: 2–4 planes en fila responsive; el precio mostrado por periodo lo
|
|
|
resuelve el app con el valor del toggle (block emite el cambio vía prop
|
|
|
callback del ToggleGroup — sin formatear moneda: eso es `FormatNumber`
|
|
|
del app).
|
|
|
- **Demo**: 3 planes, featured al centro, toggle vivo.
|
|
|
|
|
|
### F2.5 `testimonials`
|
|
|
|
|
|
- **Compone**: `Section` + `AutoGrid` + `Card` + `Avatar` + `Text` +
|
|
|
`Group`.
|
|
|
- **API**: `<Testimonials>` + `.Header` + `.Item` (+`.ItemAuthor` con
|
|
|
Avatar/nombre/cargo).
|
|
|
- **v1**: grid; variante `Carousel` = Gap (el componente existe; entra
|
|
|
cuando una demo real la pida).
|
|
|
|
|
|
### F2.6 `faq`
|
|
|
|
|
|
- **Compone**: `Section` + `Container` (medida estrecha) + `Heading` +
|
|
|
`Accordion`.
|
|
|
- **API**: `<Faq>` + `.Header` + `.Item` (proxy fino de Accordion.Item con
|
|
|
children pregunta/respuesta — respetando el API real del Accordion, que
|
|
|
se lee antes).
|
|
|
- **v1**: una columna; `type` del accordion expuesto tal cual.
|
|
|
|
|
|
### F2.7 `stats-band`
|
|
|
|
|
|
- **Compone**: `Section` + `Group`/`AutoGrid` + `Metrics` + `CountUp` +
|
|
|
`Text`.
|
|
|
- **API**: `<StatsBand>` + `.Stat` (valor + etiqueta; `CountUp` opt-in por
|
|
|
prop).
|
|
|
- **v1**: banda horizontal 2–4 stats; los formatos numéricos llegan ya
|
|
|
formateados o vía `FormatNumber` compuesto por el app.
|
|
|
|
|
|
### F2.8 `cta` — HECHO (2026-07-30)
|
|
|
|
|
|
- **Compone**: `Section` + `Container` + `Motion` + `Surface` (tratamiento de
|
|
|
fondo del sistema — un CTA se distingue por acabado, y eso ya es vocabulario
|
|
|
del framework) + `Heading` + `Text` + `Flex` (`Button`s).
|
|
|
- **API**: `<Cta layout color gradient level container size>` + slots de snippet
|
|
|
`eyebrow` · `title` · `description` · `actions` · `children`. **Desviación del
|
|
|
plan**: no `.Title`/`.Description`/`.Actions` — las partes no repiten ni
|
|
|
coordinan, así que son slots posicionales (misma forma que `hero`).
|
|
|
`variant` NO existe: medido, el canvas `soft` no acota panel (bitácora).
|
|
|
|
|
|
### F2.9 `newsletter` — HECHO (2026-07-30)
|
|
|
|
|
|
- **Compone**: como F2.8 + `Form` (`variant="plain"`) + `Grid` de fila
|
|
|
`1fr auto`; el `Field` de correo y el `Form.Submit` los compone el app en
|
|
|
sus slots.
|
|
|
- **API**: `<Newsletter layout panel color gradient level container size>` +
|
|
|
`form` (OBLIGATORIO, de `createForm`) + `schema` (opcional) + slots
|
|
|
`eyebrow` · `title` · `description` · `field` · `submit` · `note` ·
|
|
|
`children`. **Desviación del plan**: slots, no `.Title`/`.Description`/
|
|
|
`.Form` (las partes no repiten ni coordinan). **No expone
|
|
|
`onValidSubmit`**: `Form.Provider` lo ignora cuando recibe un `form` ya
|
|
|
construido, así que el handler va en el `createForm` del app.
|
|
|
- **Nota**: separado de `cta` porque el form introduce a11y y estados
|
|
|
(invalid/submitting) que el CTA puro no tiene.
|
|
|
- El block **no inventa validación**: `Form` posee runtime, esquema, dirty/
|
|
|
touched, agregación de errores y foco al primer error; `Field` posee el
|
|
|
cableado ARIA. Tampoco emite sema: `commit-submit` / `signal-invalid` ya
|
|
|
los declara el morfo del `Form`.
|
|
|
|
|
|
### F2.10 `site-footer` — HECHO (2026-07-30)
|
|
|
|
|
|
- **Compone**: `<footer>` + `Section` + `Container` + `Motion` + `Grid`
|
|
|
(`1fr 3fr`: marca | columnas) + `AutoGrid` (las columnas) + `Separator` +
|
|
|
`Flex` (barra inferior).
|
|
|
- **API**: `<SiteFooter container size minColumnWidth>` + slots `brand` ·
|
|
|
`signup` · `social` · `legal` · `extra` + partes compound `.Column` y
|
|
|
`.ColumnTitle`. **Desviación del plan**: `.Social`/`.Legal`/`.Extra` son
|
|
|
SLOTS, no partes — no se repiten ni coordinan; `.Column` sí se repite, así
|
|
|
que es compound. `extra` es el slot libre de D-BLK.6.
|
|
|
- **v1**: 2–5 columnas fluidas (`minChildWidth`, sin breakpoints) → 2 en móvil.
|
|
|
|
|
|
### F2.11 `banner` — HECHO (2026-07-31)
|
|
|
|
|
|
- **Compone**: el componente `Banner` del canon (reenviando su superficie
|
|
|
entera vía `Omit<BannerProps, 'children'>`) + `Container` + `Wrap` +
|
|
|
`Banner.Close`.
|
|
|
- **API**: `<SiteBanner container onDismiss>` + slots `badge` · `children` ·
|
|
|
`action`. El dismiss lo posee el `Banner` (composición, no booleano), así que
|
|
|
el block solo cablea el clic y **la visibilidad es del app**.
|
|
|
- **Nota dossier (P1)**: 13 TW · 16 Untitled · 5 Flowbite — la categoría
|
|
|
convergente más barata de cubrir (el componente ya existe).
|
|
|
|
|
|
### F2.12 `team` — HECHO (2026-07-31)
|
|
|
|
|
|
- **Compone**: `Section` + `Container` + `AutoGrid` + `Stack` + `Heading` +
|
|
|
`Text` + `Group` + `Motion`. El `Avatar` lo compone el APP dentro del
|
|
|
`Member`.
|
|
|
- **API**: `<Team align container size>` + `.Header` + `.Members` + `.Member` +
|
|
|
`.MemberName` + `.MemberRole` + `.MemberLinks`. **Desviación del plan**: NO
|
|
|
hay `.MemberAvatar` — no tendría nada que añadir sobre `<Avatar size radius>`
|
|
|
y escondería su API (`Image`/`Fallback`/estados de carga). Se envuelve lo que
|
|
|
el block DEFAULTEA, no lo que solo reenvía.
|
|
|
|
|
|
### F2.13 `contact` — HECHO (2026-07-31) · el primer block que COORDINA
|
|
|
|
|
|
- **Compone**: `Section` + `Container` + `Grid` + `Form` + `Field` +
|
|
|
`TextArea` + `Stack` + `Group` + `Motion`; las filas de contacto las llena
|
|
|
el app (`Icon` + `Text` + `Link`).
|
|
|
- **API**: `<Contact container size form? schema? verification? bind:sent
|
|
|
onSend>` + `.Header` + `.Body` + `.Form` + `.Fields` + `.Submit` +
|
|
|
`.Reason` + `.Details` + `.Detail` + `.DetailLabel` + `.DetailValue`.
|
|
|
- **Desviación del plan**: el plan dibujaba `.Info` + `.Form` y dejaba
|
|
|
«validación y submit = handlers del app». Se cumple lo segundo (el envío
|
|
|
sigue siendo del app por `onSend`), pero el reparto cambió con la doctrina
|
|
|
de coordinación: el block posee el ESTADO de la sección, la FORMA de sus
|
|
|
datos y las PALABRAS de cada estado. `.Info` se partió en
|
|
|
`.Details`/`.Detail` porque la fila se repite, y aparecieron `.Fields`,
|
|
|
`.Submit` y `.Reason` porque coordinan (leen el estado por contexto).
|
|
|
- **La máquina** (`state.ts`): `incomplete · unverified · verifying ·
|
|
|
rejected · ready · sending · sent`, con `CONTACT_REASON` y `CONTACT_ACTION`
|
|
|
como `Record` exhaustivos — un estado bloqueado sin frase no se puede
|
|
|
escribir. Cubierta por `state.test.ts` (9 casos), primera lógica pura del
|
|
|
tier y por tanto su primer test unitario.
|
|
|
- **Defecto encontrado al verificar**: la máquina leía `form.isValid`, que
|
|
|
con `progressive` significa «aún no se ha encontrado nada mal» — un
|
|
|
formulario vacío se declaraba válido y `incomplete` era INALCANZABLE al
|
|
|
cargar: la sección pedía resolver la verificación con los tres campos
|
|
|
vacíos. Arreglado preguntando al esquema (`validateSync` de SIUM: síncrono
|
|
|
y sin escribir errores en el formulario).
|
|
|
- **i18n**: fallbacks ingleses en el block; el arnés registra el namespace
|
|
|
`blocks` (`web/routes/blocks/_lib/blocks-langs.ts`) en las DOS cadenas de
|
|
|
arranque (galería y previews) y la demo se lee en castellano.
|
|
|
- **Contrato**: la doctrina está escrita en
|
|
|
[`architecture/blocks.md`](../architecture/blocks.md) §«Coordination» +
|
|
|
B-5/B-7 enmendados + la convención de servicios (el traductor es el único
|
|
|
servicio sancionado).
|
|
|
|
|
|
### F2.14 `content-section` — HECHO (2026-07-31) · cierra F2
|
|
|
|
|
|
- **Compone**: `Section` + `Grid` + `Box` + **`Prose` (F1.5)** + `Heading` +
|
|
|
`Text` + `Motion`.
|
|
|
- **API**: `<ContentSection measure wide size level>` + snippets de cabecera
|
|
|
(`eyebrow`/`title`/`lede`/`meta`) + `.Body` + `.Media`.
|
|
|
- **Desviación del plan — SIN `Container`**: el plan decía `Section` +
|
|
|
`Container` + `Prose`, pero **dentro de un `Container` nada puede ser más
|
|
|
ancho que él**, así que la propia meta del plan (media FUERA del flujo del
|
|
|
prose) sería imposible: una figura a sangre habría que sacarla del artículo
|
|
|
y ponerla de hermana, rompiendo el orden de lectura. La sección es una
|
|
|
REJILLA de 5 pistas cuya central es la medida, y cada parte dice hasta dónde
|
|
|
llega (`measure` col 3 · `wide` col 2/5 · `full` col 1/-1). Ese escape es lo
|
|
|
único que este block añade sobre un `<Prose>` en una caja — sin él sería un
|
|
|
envoltorio de tres capas.
|
|
|
- **Todas las longitudes son tokens**: medidas `--measure-*` (54/66/78ch),
|
|
|
pista ancha `--container-width-*`, canalones con suelo en
|
|
|
`--container-padding-inline`.
|
|
|
- **Semántica propia**: renderiza `<figure>`/`<figcaption>` de verdad (no son
|
|
|
interactivos → estructura de documento, B-8, no la regla de admisión). Hueco
|
|
|
señalado: el canon no tiene primitiva `Figure`.
|
|
|
- **Contexto de CONFIGURACIÓN** (no estado): la raíz comparte la medida para
|
|
|
que el pie de una figura desbordada se alinee con la columna de lectura —
|
|
|
mismo uso que el `align` de `team`. Es un block de LAYOUT y no coordina nada,
|
|
|
como dice `architecture/blocks.md` §«Coordination».
|
|
|
|
|
|
_(`logo-cloud` **REABIERTO el 2026-08-17** y movido a F2b: la premisa con la que
|
|
|
se difirió —«su versión honesta pide `Marquee`»— no casa con las referencias,
|
|
|
que lo shippean como **grid estático** (TW 6 · Untitled · Flowbite). `blog` y
|
|
|
`cookie-consent` entran también por F2b. El resto de la unión del dossier —
|
|
|
bento, gallery, popups, careers, events, comparison, timeline — sigue en F5 con
|
|
|
disparador, E-3.)_
|
|
|
|
|
|
**Cierre F2**: `blocks-check` verde; galería `web/routes/blocks/` con los 15.
|
|
|
|
|
|
⚠️ **Enmendado el 2026-08-17**: este cierre exigía además una **página compuesta
|
|
|
de PRUEBA** (header + hero + features + pricing + faq + cta + footer juntos) y
|
|
|
esa página NO se construyó — quedó fuera de alcance por decisión del usuario.
|
|
|
F2 está cerrada **en unidades**: cada block verificado contra sí mismo. La
|
|
|
integración —lo único que puede probar que los blocks ensamblan sin fricción—
|
|
|
es la puerta de cierre de F2b (tramo D). Mientras esa página no exista, el
|
|
|
claim del tier está sin demostrar, y aquí se dice así en vez de dar por probado
|
|
|
lo que no se probó.
|
|
|
|
|
|
---
|
|
|
|
|
|
## F2b — Ola de paridad de variedad (firmada 2026-08-17)
|
|
|
|
|
|
**De dónde sale**: el saldo de la tabla §P1 del dossier
|
|
|
(`RESEARCH-blocks-references.md`) contra los 15 blocks ya construidos, medido
|
|
|
el 2026-08-17 leyendo el `types.ts` y la tabla de Gaps del README de cada uno.
|
|
|
De las 11 filas del dossier, **6 están hechas** (frames de media vía el canon
|
|
|
`Mockup` · `hero layout="background"` · `feature-split`, que ERA la brecha #1 ·
|
|
|
`cta layout="justified"` · `newsletter note` · `site-footer signup` + `extra`)
|
|
|
y **4 se resolvieron por composición o app-land**, declaradas en su README
|
|
|
(flyout del header por `NavigationMenu`, announcement por el block `banner`,
|
|
|
logo-strip y vídeo por los slots del `hero`, cola de soporte del `faq`).
|
|
|
|
|
|
⚠️ **Ninguna fila de variante de esta ola es un hallazgo nuevo**: estaban ya
|
|
|
registradas con disposición en el README de su block desde julio. Lo que
|
|
|
faltaba no era medir — era decidir.
|
|
|
|
|
|
⚠️ **Esta sección se reescribió entera el mismo día en que se escribió.** La
|
|
|
primera versión atacaba sólo el suelo de §P1 — ocho variantes — y con eso NO se
|
|
|
cierra la brecha que motivó la revisión, que es de **cardinalidad percibida**
|
|
|
(TW: Heroes 12, Features 15; nosotros 1–3 arreglos por block). El propio
|
|
|
dossier fija el método en P6.1 y la primera versión no lo trajo: **no competir
|
|
|
en número de dumps, sino demostrar la equivalencia variante-a-variante**. De
|
|
|
ahí que la ola tenga cuatro tramos y no uno.
|
|
|
|
|
|
### La regla de forma de la ola — se firma ANTES que las filas
|
|
|
|
|
|
No es doctrina nueva: es el precedente que F2 ya vivió. Son **cuatro**
|
|
|
términos, y el primero es el que más trabajo hace:
|
|
|
|
|
|
1. **Variante alcanzable componiendo lo que ya hay → RECETA demostrada, CERO
|
|
|
código.** Se prueba en la demo y se escribe en el README del block. Sin este
|
|
|
término, cada fila de las referencias parece pedir un `layout` nuevo o un
|
|
|
hermano, y el tier engorda por imitación en vez de por función.
|
|
|
2. **Misma anatomía, otro ARREGLO → prop `layout`** en el mismo block —
|
|
|
`hero` (`center|split|background`), `cta` y `newsletter`
|
|
|
(`center|justified`).
|
|
|
3. **Otra anatomía → block hermano** — `feature-split` frente a
|
|
|
`feature-grid`: filas alternantes con lista de features y acciones no son
|
|
|
celdas de icono.
|
|
|
4. **Variante que debe OBEDECER al estado de su sección → PARTE** del block que
|
|
|
lo posee, nunca hermano: un hermano no puede leer ese contexto y B-10 le
|
|
|
prohíbe importarlo. (Gobierna V1.)
|
|
|
|
|
|
### Tramo A — las variantes de suelo
|
|
|
|
|
|
| # | Block | Qué falta | Forma firmada | Coste |
|
|
|
| ------ | -------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
|
|
|
| **V1** | `pricing` | tabla comparativa features×planes | **`Pricing.Compare`** (parte, por el término 4). **Tanda propia con fase 0**: dos decisiones de diseño que la ficha no puede prejuzgar — (a) **quién declara las columnas** si `.Plans` y `.Compare` conviven (doble declaración de planes) y (b) **qué pasa en móvil** (las refs colapsan a lista-por-plan o a scroll) | **alto** |
|
|
|
| **V2** | `testimonials` | cita única en spotlight | **`layout="grid" \| "spotlight"`**. ⚠️ No es «misma anatomía» exacta: gana un slot `logo`, pierde la `Card` y `Items` pasa a un solo protagonista. Aun así `layout`, con el precedente de `hero layout="background"`, que también redefine partes. El hermano se reabre SÓLO si un día pide rotación / carousel | medio |
|
|
|
| **V3** | `faq` | lista estática 2/3 columnas | **`layout="accordion" \| "list"`**, con `.List` conmutando entre `Accordion` y una rejilla de Q&A siempre abiertas. **El punto fino es el TIPO**: hoy `FaqListProps = AccordionProps`, y bajo `list` no aplican ni esas props ni `value`/`disabled` de `.Item` → unión discriminada, jamás superficie descartada en silencio (A-94) | medio |
|
|
|
| **V4** | `hero` | `form-in-hero` | ✅ **HECHA 2026-08-17.** Slot `form`, el app compone `Form` + `Field` + `Form.Submit` entero (el hero NO toma el handle: tomarlo lo convertiría en un block que coordina). Aterriza en el sitio del CTA — tras la descripción, antes de `actions` — con medida propia `sm`; los dos conviven si llegan los dos. Detalle y medidas: README del block §«The sign-up slot» | bajo |
|
|
|
| **V5** | `cta` | split-with-media | tercer **`layout="split"`** + slot `media` (el nombre que ya usa `hero`); el `Mockup` existe. Es suelo: el dossier la lista | bajo |
|
|
|
| **V8** | `pricing` | single-price | ✅ **HECHA 2026-08-17 — y NO era gratis.** Medido: la fila es `auto-fill`, así que RESERVA las pistas que caben aunque nadie las ocupe → un plan solo daba 315px pegados al borde con 677px de vacío, y `columns={1}` lo cambiaba por una card de 992px. Resuelto con **`.Plans maxWidth`** (tope de fila, centrado, reenviado nombrado por A-94); el número lo pone el app. Detalle: README del block §«The row cap» | ~0 → bajo |
|
|
|
|
|
|
**Fuera de la ola, a F5 con disparador** — el dossier declara `stats-band`
|
|
|
**«Paridad OK»**, así que estas dos son _adoptables_, no suelo, y meterlas aquí
|
|
|
diluiría E-2 (que dice suelo = v1, no «todo lo que exista en una referencia»):
|
|
|
|
|
|
- **V6** `stats-band` split-with-image — arreglo de 2 columnas.
|
|
|
- **V7** `stats-band` timeline / stepped — el canon `Timeline` existe y esto es
|
|
|
otra sección, no una banda.
|
|
|
|
|
|
### Tramo B — la matriz de equivalencia (el entregable central)
|
|
|
|
|
|
**Es el tramo que responde a la brecha percibida**, y sale del dossier P6.1:
|
|
|
«cubrir la matriz con 1 block parametrizado + presets, **documentando la
|
|
|
equivalencia variante-a-variante**; no competir en cardinalidad copy-paste».
|
|
|
|
|
|
Por cada block de F2, una tabla en su README: **variante de la referencia →
|
|
|
composición nuestra que la logra** (con la receta real: props + qué se pasa por
|
|
|
qué slot), y lo que NO se alcance sale como **gap con nombre** en vez de
|
|
|
quedarse en la sensación de que «tienen más». Cada receta se demuestra en la
|
|
|
demo del block — una receta no verificada en navegador no entra en la tabla.
|
|
|
|
|
|
Tres cosas que esto produce y las ocho variantes no producían:
|
|
|
|
|
|
1. La prueba de la tesis del tier («variantes como props tipadas vs N dumps»),
|
|
|
que hoy es una afirmación de la doctrina sin evidencia enseñable.
|
|
|
2. La lista **honesta** de lo que de verdad no podemos hacer — que es la
|
|
|
entrada legítima a nuevas filas, en vez de imitar catálogos.
|
|
|
3. Material directo para la galería pública: la matriz ES la respuesta a «¿por
|
|
|
qué 15 y no 133?».
|
|
|
|
|
|
**La plantilla de la matriz** — se fija aquí para no re-decidirla block a
|
|
|
block:
|
|
|
|
|
|
| Columna | Qué lleva |
|
|
|
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| Variante | el nombre que la referencia le da («Hero with app screenshot», «Split with image») |
|
|
|
| Ref | TW · Flowbite · Untitled · PrimeBlocks · sv-blocks — las que la ofrecen (≥2 = era suelo) |
|
|
|
| Receta | la composición nuestra EXACTA: props + slots (`layout="split"` + `media` con `<Mockup chrome="phone">`), o el término 2/3/4 aplicado |
|
|
|
| Demostrada en | el control vivo de la demo o la página compuesta donde se ve — **una receta sin demostración en navegador NO entra en la tabla** |
|
|
|
| Estado | `cubierta` · `gap: <nombre>` (candidata a variante o a canon, con disposición) · `no se ofrece` (con el motivo — p. ej. dark pattern) |
|
|
|
|
|
|
- **Dónde vive**: sección `## Equivalencias` en el README de cada block. La
|
|
|
plantilla del tier (`src/uix/blocks/README.md`) la gana, y **`blocks:check`
|
|
|
la exige como ÚLTIMA tanda del tramo** — encenderla el primer día pondría 18
|
|
|
READMEs en rojo durante toda la ola; encenderla al final se verifica EN ROJO
|
|
|
quitando la sección de un block (la regla de toda regla nueva del guard: un
|
|
|
guard que inspecciona el conjunto vacío pasa).
|
|
|
- **Regla dura**: una fila `gap` sin disposición no cierra el tramo; las filas
|
|
|
«no se ofrece» llevan el motivo escrito, porque son una postura del sistema,
|
|
|
no un hueco.
|
|
|
- La fuente de variantes por block es la tabla §P1 del dossier + el catálogo
|
|
|
vivo de cada referencia mirado al escribir la tabla (los conteos del dossier
|
|
|
son de 2026-07-21; si una ref añadió una variante convergente nueva, se
|
|
|
anota con fecha).
|
|
|
|
|
|
### Tramo C — el catálogo que se reabre (tres decisiones firmadas)
|
|
|
|
|
|
| # | Block | Decisión |
|
|
|
| ------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| **N1** | `logo-cloud` | **ENTRA, v1 estático.** Se difirió sobre una premisa que las referencias desmienten (TW 6 · Untitled · Flowbite lo shippean como grid estático), y una decisión cuya premisa cae se corrige. El argumento positivo es más fuerte que el de paridad: el problema real de un logo-cloud es **la tinta** — logos de marcas dispares que gritan entre sí y desaparecen en dark, que las refs venden como segundo artefacto. Tratamiento monocromo derivado de los tokens del tema = la tesis del sistema donde nadie más puede. `Marquee` queda como Gap con disparador: si entra, es un **preset de motion sobre este block**, no un block nuevo |
|
|
|
| **N2** | `blog` | **ENTRA.** Misma naturaleza que `team` — grid de entidades con metadatos (`Card` + `Image` + `Badge` + fecha + autor) — y `team` entró: negarle la entrada sería arbitrario. Es la categoría de marketing más visible que queda fuera. Fase 0 normal; **el nombre se decide ahí** (`blog` es el término de las referencias pero nombra ambiguo — la función es «grid de artículos») |
|
|
|
| **N3** | `cookie-consent` | **ENTRA, con doctrina propia y tanda propia — NO es una sección**, es un overlay legal con estado. El proyecto ya firmó la regla que le aplica en la familia commerce: **la ley europea vive en el TIPO**. Un cookie-consent de referencia hace lo que ningún dump hace: el tipo OBLIGA a que rechazar exista al mismo nivel que aceptar y a que las categorías no esenciales nazcan apagadas — legalidad por construcción, no por disciplina del consumidor. Fase 0 profunda (ePrivacy/GDPR + refs) |
|
|
|
| — | `onboarding` | **NO entra como block.** Es un `wizard` (F3.8) con contenido de bienvenida; Flowbite lo clasifica en marketing por accidente de catálogo. Crear `onboarding` sería duplicar una función con otro nombre — exactamente la inflación (los 16 sidebars) que nuestro modelo existe para evitar. Se resuelve como **receta** de `wizard` en el tramo B (+ `tour` de F5 si algún día hay spotlight) |
|
|
|
|
|
|
Las fichas siguen el formato de F2 (Función · Compone · API · Landmark · v1 ·
|
|
|
Demo). Como toda ficha del plan, **parametrizan la tanda, no la sustituyen**:
|
|
|
la fase 0 de cada una lee los README reales de lo que compone.
|
|
|
|
|
|
#### Ficha N1 `logo-cloud`
|
|
|
|
|
|
- **Función**: banda de prueba social — logos de clientes/partners bajo un
|
|
|
encabezado corto («Trusted by…» — palabras del app, B-7).
|
|
|
- **Compone**: `Section` + `Container` + `Box`/`Stack` (header) + `Wrap` o
|
|
|
`AutoGrid` (la fila; fase 0 decide midiendo con logos reales) + `Motion`.
|
|
|
Los logos son contenido del app: `Image` del canon o `<svg>` inline.
|
|
|
- **⚠️ La tinta vive en el CANON, no en el block.** Un block no posee CSS
|
|
|
(D-BLK.2), así que el tratamiento monocromo — la razón de reabrir este
|
|
|
block — se promociona ANTES por la regla de admisión del tier. Fase 0
|
|
|
decide el hueco: prop del canon `Image` (candidato `tone`) o primitivo
|
|
|
nuevo; si es primitivo, entra por la ruta de 9 fases antes que el block.
|
|
|
Para `<svg>` inline el camino natural es `currentColor` + la tinta del
|
|
|
tema; para raster, filtro. El opt-out por logo existe (marcas que exigen
|
|
|
su color) y lo decide el consumidor, no el block.
|
|
|
- **API**: `<LogoCloud>` (props `containerSize` · `sectionSize` + el eje de
|
|
|
tinta) + slots `eyebrow`/`title` + `.Items` + `.Item` (compound: el logo SE
|
|
|
REPITE). Alturas normalizadas por token — un logo no trae su tamaño.
|
|
|
- **Landmark**: con título, `<section aria-labelledby>`; sin título es una
|
|
|
banda decorativa y el README declara qué significa eso para la AT (los
|
|
|
logos siguen necesitando nombre accesible — `alt` del app).
|
|
|
- **v1**: grid/wrap estático. `Marquee` = Gap con disparador (preset de
|
|
|
motion sobre este block, no block nuevo).
|
|
|
- **Demo**: 6–8 logos reales de tamaños dispares (la normalización se VE),
|
|
|
control del eje de tinta, claro/oscuro (la razón del block se ve ahí),
|
|
|
RTL, 375/1280.
|
|
|
|
|
|
#### Ficha N2 `blog` (nombre a validar en su fase 0)
|
|
|
|
|
|
- **Función**: rejilla de artículos recientes — la puerta editorial del sitio
|
|
|
(posts, noticias, casos de estudio).
|
|
|
- **Compone**: `Section` + `Container` + `Box` (header) + `AutoGrid` +
|
|
|
`Card` + `AspectRatio`/`Image` (media) + `Badge` (categoría) + `Heading` +
|
|
|
`Text` + `Link` + `Group`/`Avatar` (autor — el patrón de
|
|
|
`testimonials.Author`). **Fechas: el app compone `FormatDate` /
|
|
|
`RelativeTime`** — el block no formatea fechas, como `pricing` no formatea
|
|
|
moneda.
|
|
|
- **API**: `<Blog>` + `.Header` + `.Posts` + `.Post` (compound: SE REPITE) +
|
|
|
`.PostMedia` + `.PostMeta` (categoría + fecha) + `.PostTitle` (el `Link`
|
|
|
del app dentro — la card entera NO es un link: anidaría interactivos) +
|
|
|
`.PostExcerpt` + `.PostAuthor`.
|
|
|
- **Landmark**: `<section aria-labelledby>`. ⚠️ La semántica de `<article>`
|
|
|
choca con F21 (los primitivos de layout no cambian de elemento; `Card`
|
|
|
renderiza `div`): fase 0 decide entre `as` en `Card` (decisión de CANON,
|
|
|
se presenta antes) o gap declarado en el README. No se finge con roles.
|
|
|
- **v1**: UNA variante — el grid de 3. Featured-post y lista horizontal =
|
|
|
Gaps con disposición (candidatos a `layout` si el dossier de su fase 0
|
|
|
los da como convergentes).
|
|
|
- **Demo**: 3–6 posts con contenido real, con/sin media, autor on/off,
|
|
|
claro/oscuro, 375/1280.
|
|
|
|
|
|
#### 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ó):
|
|
|
`onAcceptAll` Y `onRejectAll` REQUERIDOS — el block no se puede montar sin
|
|
|
camino de rechazo; las dos acciones se renderizan con la MISMA prominencia
|
|
|
por construcción y el API no expone ningún knob para degradar «rechazar»;
|
|
|
las categorías no esenciales nacen apagadas (el tipo no admite
|
|
|
pre-marcado); `essential` es fija y no desactivable.
|
|
|
- **Coordina** (doctrina de coordinación): máquina de la sección
|
|
|
(`unset · preferences · decided`), la FORMA de sus datos (B-5: el esquema
|
|
|
de categorías por defecto, sustituible) y las palabras de sus estados por
|
|
|
idlangref `#?blocks.cookie-consent.*` con fallback inglés (B-7). La
|
|
|
decisión se anuncia a la AT (`uix.announce` — el segundo servicio
|
|
|
sancionado).
|
|
|
- **Persistencia y ejecución = del APP** (D-BLK.6): el block emite
|
|
|
`onDecision(consent)`; jamás escribe cookies ni bloquea scripts.
|
|
|
- **Compone**: `Card`/`Surface` (la barra) + la capa de colocación anclada
|
|
|
(el eje affix canonizado 2026-08-15 — su fase 0 lee ese README y NO
|
|
|
re-implementa colocación) + `Dialog` o `Drawer` (preferencias; fase 0
|
|
|
decide con la guía delante) + `Switch` por categoría + `Button` + `Group` +
|
|
|
`Text`/`Link` (política del app).
|
|
|
- **Foco y descarte**: la barra NO roba el foco al aparecer (no es modal);
|
|
|
`Escape` no decide — cerrar sin elegir no es consentir, y eso también es
|
|
|
estructura, no estilo.
|
|
|
- **Landmark**: `role="region"` nombrada; el panel de preferencias hereda la
|
|
|
semántica del `Dialog`/`Drawer` que compone.
|
|
|
- **v1**: barra inferior no bloqueante + panel de preferencias + esquema de
|
|
|
4 categorías por defecto (essential · functional · analytics · marketing).
|
|
|
- **Demo**: los tres caminos (aceptar todo · rechazar todo · granular) con la
|
|
|
decisión emitida VISIBLE, teclado completo, AT (anuncio verificado),
|
|
|
claro/oscuro, 375/1280, RTL.
|
|
|
|
|
|
### Tramo D — la página compuesta (la puerta de cierre)
|
|
|
|
|
|
Es la condición de cierre que F2 escribió y no cumplió, y **entra aquí**. El
|
|
|
claim del tier es «los blocks ensamblan sin fricción», y ese claim sólo se
|
|
|
prueba compuesto: la jerarquía de headings CRUZANDO blocks, los landmarks
|
|
|
únicos, el ritmo vertical entre `sectionSize` consecutivos, la cascada de
|
|
|
motion sección a sección. Todo lo que vendemos como superación es
|
|
|
**indemostrable block a block**.
|
|
|
|
|
|
No es teórico: el defecto de los 16px de `content-section` sólo apareció al
|
|
|
poner otra sección debajo, y **A-95** (el `banner affix="top"` tapando la
|
|
|
cabecera pegada) es un defecto que sólo existe cruzado.
|
|
|
|
|
|
Va **al final** — componer antes de cambiar los blocks sería componer dos
|
|
|
veces — es el vehículo natural de la matriz del tramo B, y se promociona a
|
|
|
demo pública (P1: todas las referencias shippean páginas compuestas como
|
|
|
artefacto de prueba; TW 10 · Untitled 105).
|
|
|
|
|
|
#### Ficha — la página compuesta
|
|
|
|
|
|
- **Ruta**: `web/routes/blocks/landing/` con el patrón de las previews
|
|
|
(`+layout@.svelte` que resetea: la página se ve A SANGRE, sin marco de
|
|
|
demo) + ficha en `_lib/catalog.ts` (B-9) para que el raíl la publique.
|
|
|
- **Qué es**: una landing VEROSÍMIL de un producto — contenido real de
|
|
|
principio a fin (el patrón `{Name}Site`), no un desfile de blocks. El
|
|
|
criterio de inclusión es la naturalidad: los blocks que una landing real
|
|
|
lleva (site-header + banner + hero + logo-cloud + feature-grid +
|
|
|
feature-split + stats-band + testimonials + pricing + faq + cta +
|
|
|
newsletter + site-footer + cookie-consent encima) entran; los que no le
|
|
|
caben con naturalidad (team · contact · content-section · blog) quedan
|
|
|
REGISTRADOS en el README de la página como no-compuestos — una segunda
|
|
|
página compuesta (about/blog) es Gap con disparador, no alcance de esta.
|
|
|
- **Qué verifica** (cada punto con su medida, no a ojo):
|
|
|
- **Outline de headings**: UN `h1` (el hero), `h2` por sección — la
|
|
|
política de heading-level que vendemos como superación, medida sobre el
|
|
|
árbol de accesibilidad.
|
|
|
- **Landmarks únicos y nombrados** — aquí se re-mide el hallazgo de los
|
|
|
DOS `role="banner"` (block `banner` + `<header>` del site-header).
|
|
|
- **Ritmo vertical**: los `sectionSize` consecutivos comparados en las
|
|
|
costuras (el método que destapó los 16px de `content-section`: bordes
|
|
|
medidos entre secciones vecinas, no cada block contra sí mismo).
|
|
|
- **Motion sección a sección**: las cascadas disparan al entrar cada una
|
|
|
en viewport, no todas a la vez (§D.13; Playwright headless — el pane
|
|
|
suspendido congela IO y rAF).
|
|
|
- **A-95** (`banner affix="top"` + header pegado): si `app-shell` (F3.1)
|
|
|
existe ya, se arregla y se cierra; si no, se re-confirma y queda.
|
|
|
- **La sonda de sema** sobre el formulario de la página (el cabo de los
|
|
|
hermanos del `Field`: ¿doble `commit-submit`?).
|
|
|
- Claro/oscuro · 375/1280 · RTL · **teclado de punta a punta** (sin
|
|
|
trampas de foco; el drawer del header devuelve el foco).
|
|
|
- **Gates**: `blocks:check` verde · la página en el catálogo · las medidas
|
|
|
de arriba anotadas en el README de la página con fecha e instrumento.
|
|
|
|
|
|
### Orden
|
|
|
|
|
|
**A** V4 → V8 (medir primero) → V5 → V2 → V3 → **V1 en tanda propia** ·
|
|
|
**B** la matriz por tandas, un block por vez, según se tocan ·
|
|
|
**C** N1 `logo-cloud` → N2 `blog` → N3 `cookie-consent` (tanda propia) ·
|
|
|
**D** la página compuesta cierra.
|
|
|
|
|
|
Cada fila entra por el proceso por tanda de §4. La fase 0 ligera de las
|
|
|
variantes ya está hecha y vive en el README de su block; N1/N2/N3 traen la
|
|
|
suya, y la de N3 es profunda.
|
|
|
|
|
|
### Cierre F2b
|
|
|
|
|
|
`blocks:check` verde; **cada variante nueva con su control vivo en la demo**
|
|
|
(B-9, «cada prop público un control»); cada fila de Gaps del README pasada de
|
|
|
«scope-approval pending» a la decisión firmada; **la matriz del tramo B escrita
|
|
|
y demostrada en los 15 + los 3 nuevos**; la página compuesta verificada en
|
|
|
claro/oscuro y 375/1280, con recorrido de teclado completo.
|
|
|
|
|
|
La ola **no reabre ninguna fila CONFIRMADO del ledger** — son ejes distintos y
|
|
|
se miden aparte — con una excepción esperada: la página compuesta es donde
|
|
|
**A-95** deja de ser inarreglable, si para entonces existe `app-shell` (F3.1).
|
|
|
|
|
|
---
|
|
|
|
|
|
## F3 — Blocks de aplicación (10)
|
|
|
|
|
|
Mismas reglas y ficha que F2. **Depende de**: F1.2/F1.3/F1.4 (estados),
|
|
|
F1.7 (`sidebar`) para F3.1. Las cuatro dependencias están CERRADAS, así que F3
|
|
|
no está bloqueada por nada del canon.
|
|
|
|
|
|
**Orden recomendado** (2026-08-17): **F3.1 `app-shell` PRIMERO** — es la pieza
|
|
|
que desbloquea dos cosas ajenas a su propia ficha: la fila **A-95** del ledger
|
|
|
(`banner affix="top"` tapa la cabecera pegada; no se arregla en ninguno de los
|
|
|
dos blocks porque falta quien POSEA las alturas de página) y **F4.1
|
|
|
`docs-shell`**, que lo especializa. Después, por coste creciente y con las
|
|
|
fases 0 profundas al final: F3.9 `error-page` (compone `Result`, ya hecho) →
|
|
|
F3.6 `user-menu` → F3.7 `notifications` → F3.5 `settings` → F3.2 `auth` →
|
|
|
F3.4 `dashboard` → F3.8 `wizard` → **F3.3 `data-table`** y **F3.10 `kanban`**,
|
|
|
que son las dos con fase 0 OBLIGADA y más profunda (leer `soma/components/table`
|
|
|
|
|
|
- `$libs/datagrid` y `soma/components/drag-drop` antes de diseñar el API).
|
|
|
|
|
|
### F3.1 `app-shell`
|
|
|
|
|
|
- **Función**: esqueleto de aplicación: sidebar + topbar + contenido (+ aside
|
|
|
opcional).
|
|
|
- **Compone**: `Sidebar` (F1.7) + `Sticky` + `Container`/`Grid` +
|
|
|
`ScrollArea` + `Group` + slots.
|
|
|
- **API**: `<AppShell>` + `.Sidebar` + `.Topbar` + `.Content` + `.Aside`.
|
|
|
- **Landmark**: `header/nav/main/aside` + skip-link (según decisión F2.1);
|
|
|
jerarquía documentada.
|
|
|
- **v1**: sidebar colapsable + topbar afijada + main con scroll propio;
|
|
|
móvil = sidebar en drawer (lo trae F1.7).
|
|
|
- **Excepción B-10 declarada**: los shells componen otros elementos de F1 y
|
|
|
blocks — allowlist en `blocks-check`.
|
|
|
|
|
|
### F3.2 `auth`
|
|
|
|
|
|
- **Función**: familia de formularios de identidad. Sub-blocks: `sign-in` ·
|
|
|
`sign-up` · `recover` · `otp`.
|
|
|
- **Compone**: `Card` + `Form` + `Field` + `PasswordField` + `PinInput`
|
|
|
(otp) + `ProofOfHuman` + `Button` + `Separator` + `Link` + slot de
|
|
|
proveedores sociales (`Button`s del app).
|
|
|
- **API**: `<AuthCard kind='sign-in'|…>` con partes (`.Title` · `.Fields` ·
|
|
|
`.Actions` · `.Footer`), o cuatro exports finos — decidir en su fase 0
|
|
|
mirando el API real de `Form` (lo que menos indirection genere gana:
|
|
|
no-premature-abstraction).
|
|
|
- **Regla dura**: TODO handler (submit, oauth, resend) llega del app; el
|
|
|
block no conoce transporte ni credenciales. La validación es del sistema
|
|
|
`Form` con schemas del app.
|
|
|
- **Demo**: los 4 kinds, estados invalid/submitting, claro/oscuro.
|
|
|
|
|
|
### F3.3 `data-table`
|
|
|
|
|
|
- **Función**: la tabla de trabajo completa: toolbar (búsqueda + filtros +
|
|
|
visibilidad de columnas + bulk actions con selección) + `Table` +
|
|
|
`Pagination` + `EmptyState` integrado.
|
|
|
- **Compone**: `Table` + `SearchField` + `DropdownMenu` + `Button` +
|
|
|
`Pagination` + `EmptyState` (F1.2) + `Group`/`Toolbar`.
|
|
|
- **⚠️ Fase 0 OBLIGADA y más profunda**: leer `soma/components/table` +
|
|
|
`$libs/datagrid` ANTES de diseñar el API — el block debe montar sobre las
|
|
|
capacidades reales (sorting/selection/paging que ya existan). Cada
|
|
|
capacidad que falte = FLAG con disposición (candidata a canon), nunca
|
|
|
lógica de datos dentro del block. El estado de datos es del app; el block
|
|
|
orquesta la UI.
|
|
|
- **API**: `<DataTable>` + `.Toolbar` (+`.Search`/`.Filters`/`.Columns`) +
|
|
|
`.Content` (la Table compuesta por el app con su API real) + `.Selection`
|
|
|
(barra bulk) + `.Footer` (Pagination) + `.Empty`.
|
|
|
- **Demo**: dataset realista (≥100 filas), búsqueda+filtro+selección+bulk,
|
|
|
estado vacío tras filtrar.
|
|
|
|
|
|
### F3.4 `dashboard`
|
|
|
|
|
|
- **Compone**: `AutoGrid`/`Grid` + `Card` + `Metrics`/`CountUp` + `Chart`
|
|
|
(tipos ya existentes) + `Feed` + `EmptyState`.
|
|
|
- **API**: `<Dashboard>` + `.Stat` + `.Panel` (card con `.PanelHeader` +
|
|
|
children libres para chart/feed).
|
|
|
- **v1**: composición fija de ejemplo con slots; nada de layout persistible
|
|
|
ni drag (Gap explícito; si un día entra, el drag es de `drag-drop`).
|
|
|
|
|
|
### F3.5 `settings`
|
|
|
|
|
|
- **Compone**: `Container` (medida estrecha) + `Section` + `Heading` +
|
|
|
`Separator` + filas `Field`/controles + `Callout` (F1.4, intent `threat`)
|
|
|
- `AlertDialog` (confirmación destructiva) + `Button`.
|
|
|
- **API**: `<Settings>` + `.Section` (+`.SectionTitle`/`.SectionDescription`)
|
|
|
- `.Row` (label+control+ayuda) + `.DangerZone`.
|
|
|
- **Regla**: cada control es el componente del ecosistema (nunca nativos —
|
|
|
B-2); el guardado (por fila o global) lo decide el app vía `Form`.
|
|
|
|
|
|
### F3.6 `user-menu`
|
|
|
|
|
|
- **Compone**: `Avatar` + `DropdownMenu` (+ items con `Icon`, `Separator`,
|
|
|
`Kbd` opcional).
|
|
|
- **API**: `<UserMenu>` + `.Trigger` (Avatar + nombre) + `.Items` (children
|
|
|
= items del DropdownMenu real).
|
|
|
- **Nota D-BLK.6**: tema/idioma/logout = items que el APP cablea; el block
|
|
|
da la estructura y los puntos de montaje.
|
|
|
|
|
|
### F3.7 `notifications`
|
|
|
|
|
|
- **Compone**: `Popover` (desktop) / `Drawer` (móvil) + `Badge` (contador en
|
|
|
el trigger `IconButton`) + `Feed` + `EmptyState` + `Button` («marcar
|
|
|
leídas» — string del app).
|
|
|
- **API**: `<Notifications>` + `.Trigger` + `.Panel` (+`.Header`/`.List`/
|
|
|
`.Empty`/`.Footer`).
|
|
|
- **Trampa conocida**: panel más ancho que el min-width del Popover →
|
|
|
recorte; pasar `width` por el prop tokenizado del `PopoverContent` (la
|
|
|
lección ya está aprendida — no re-descubrirla).
|
|
|
|
|
|
### F3.8 `wizard`
|
|
|
|
|
|
- **Compone**: `Stepper` + `Form` + `Group` de `Button`s (atrás/siguiente/
|
|
|
finalizar) + `Progress` opcional.
|
|
|
- **API**: `<Wizard>` + `.Steps` (Stepper compuesto) + `.Step` (contenido) +
|
|
|
`.Nav`.
|
|
|
- **Fase 0**: leer el API real de `Stepper` y de `Form` (validación por
|
|
|
paso) — el block solo coordina visibilidad de paso + navegación; el
|
|
|
estado de validez lo dicta `Form`.
|
|
|
|
|
|
### F3.9 `error-page`
|
|
|
|
|
|
- **Compone**: `Result` (F1.3) + `Group` de `Button`/`Link` + `SearchField`
|
|
|
opcional (404).
|
|
|
- **API**: `<ErrorPage status=…>` + children de acciones.
|
|
|
- **v1**: 404 · 403 · 500 · offline.
|
|
|
|
|
|
### F3.10 `kanban`
|
|
|
|
|
|
- **Compone**: `DragDrop` + `Grid`/`Group` de columnas + `Card` +
|
|
|
`VirtualList` (columnas largas) + `Badge` + `EmptyState` por columna.
|
|
|
- **⚠️ Fase 0 OBLIGADA**: leer `soma/components/drag-drop` a fondo; el
|
|
|
block mapea sus primitivas reales (zonas, handles, anuncios a11y del
|
|
|
provider). Toda carencia = FLAG (candidata a canon), no workaround.
|
|
|
- **API**: `<Kanban>` + `.Column` (+`.ColumnHeader`) + `.Card` (children
|
|
|
libres). El modelo de datos y la persistencia del orden son del app.
|
|
|
- **Demo**: 3 columnas, drag entre columnas, columna vacía, teclado.
|
|
|
|
|
|
_(Diferidos de F3, registrados en F5: `scheduler` — bloqueado hasta que
|
|
|
`chronos` aterrice; `chat-room` — NO va aquí: la familia `chat-_`es canon y
|
|
|
su roadmap vive en`next-features.md` §7.)\*
|
|
|
|
|
|
**Cierre F3**: `blocks-check` verde; demo de `app-shell` montando dentro
|
|
|
`data-table` + `dashboard` + `notifications` + `user-menu` como página de
|
|
|
integración; navegador claro/oscuro/375/1280 + teclado (tab por toda la
|
|
|
página de integración sin trampas de foco).
|
|
|
|
|
|
---
|
|
|
|
|
|
## F4 — Blocks de docs (3)
|
|
|
|
|
|
**Depende de**: F1 completo (prose, anchor-nav, **nav-tree**, callout) +
|
|
|
F2.1. Conecta con la iniciativa docs-corpus=site (el corpus es la semilla
|
|
|
del sitio); estos blocks son su vehículo de UI.
|
|
|
|
|
|
### F4.1 `docs-shell`
|
|
|
|
|
|
- **Compone**: `AppShell` (F3.1, excepción B-10) especializado: **`NavTree`
|
|
|
(F1.8)** como árbol de docs + `Prose` (main) + `AnchorNav` (TOC derecha) +
|
|
|
`Breadcrumb` + trigger de búsqueda (`Command` — ya existe) + prev/next
|
|
|
(`Group` de dos `Card`/`Link`).
|
|
|
- **E-5 firmada**: el atajo ⌘K del trigger de búsqueda = listener app-land
|
|
|
documentado en el README del block; el art `shortcuts` conserva su
|
|
|
disparador F5 (≥2 consumidores reales).
|
|
|
- **API**: `<DocsShell>` + `.Nav` + `.Article` (prose) + `.Toc` + `.Search`
|
|
|
- `.PrevNext`.
|
|
|
- **v1**: 3 columnas desktop → TOC colapsada y sidebar-drawer en móvil.
|
|
|
|
|
|
### F4.2 `code-showcase`
|
|
|
|
|
|
- **Compone**: `Tabs` (Preview/Code) + `Surface` (lienzo de preview) +
|
|
|
`CodeBlock` + `Clipboard` + `Toolbar` opcional.
|
|
|
- **API**: `<CodeShowcase>` + `.Preview` (children vivos) + `.Code`
|
|
|
(children CodeBlock).
|
|
|
- **v1**: preview + código + copiar. (Chips de tema/dirección/densidad del
|
|
|
lienzo = Gap, notado — pediría servicios, D-BLK.6.)
|
|
|
|
|
|
### F4.3 `props-table`
|
|
|
|
|
|
- **Función**: tabla de referencia de un componente **generada del morfo** —
|
|
|
parts, data-attrs, ARIA, keyboard y eventos salen de `compileMorfo`
|
|
|
(ventaja estructural única de este framework: el contrato ES
|
|
|
introspectable).
|
|
|
- **Compone**: `Table` + `Code`/`Kbd` + `Badge`.
|
|
|
- **API**: `<PropsTable morfo={…}>` (aquí el input data-driven es legítimo:
|
|
|
el "árbol" es el contrato canónico, no contenido inventado).
|
|
|
- **v1**: tablas morfo-backed (parts/attrs/keyboard/eventos con
|
|
|
familia·verbo·intent). Tabla de props TS = **Gap tracked** (requiere
|
|
|
extracción de tipos; no improvisar — flag para decidir tooling).
|
|
|
|
|
|
**Cierre F4**: una página de docs REAL montada con los 3 (un doc del corpus
|
|
|
renderizado en `docs-shell` con showcase y props-table de un componente
|
|
|
PASS), verificada en navegador.
|
|
|
|
|
|
---
|
|
|
|
|
|
## F5 — Backlog condicionado (no construir sin disparador)
|
|
|
|
|
|
Componentes canon candidatos (cada uno entraría por la ruta de 9 fases):
|
|
|
|
|
|
| Pieza | Disparador |
|
|
|
| ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
|
| `lightbox` (visor imagen zoom/galería) | cuando un block de media/galería real lo pida |
|
|
|
| `tour` (onboarding spotlight) | cuando el app-shell tenga consumidor real con onboarding |
|
|
|
| `hover-card` genérico | cuando un tercer caso no-URL aparezca (hoy `link-preview` cubre) |
|
|
|
| `description-list` | primer detail-view real (pareja natural de `data-table`) |
|
|
|
| `loading-overlay` | primera pantalla con carga bloqueante real |
|
|
|
| `transfer-list` | primer admin real con asignación dual |
|
|
|
| `mention` | ya registrado en `next-features.md` §7 (chat) — no duplicar aquí |
|
|
|
| `masonry` · `bottom-nav` · `swipe-actions` · `pull-to-refresh` · `image-compare` · `marquee` (¿pack?) · `watermark` · `signature-pad` | demanda real; varios son candidatos a pack, no a canon — decidir con la regla de admisión de packs |
|
|
|
| art `shortcuts` (registro global de atajos + cheat-sheet con `Kbd`) | cuando ≥2 consumidores reales (command palette global + docs) lo pidan |
|
|
|
|
|
|
Blocks diferidos: `scheduler` (bloqueado por `chronos`) · `billing` ·
|
|
|
`file-manager` · `profile-card` (cuando haya consumidor). **`logo-cloud` salió
|
|
|
de esta lista el 2026-08-17**: su premisa («pide `Marquee`») no casaba con las
|
|
|
referencias — entra por F2b tramo C.
|
|
|
|
|
|
Diferidos de F2b (2026-08-17), con disparador: **stats-band split-with-image** y
|
|
|
**stats-band timeline/stepped** — el dossier declara ese block «Paridad OK», así
|
|
|
que son adoptables, no suelo; entran cuando una demo real las pida.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 6. Verificación de cierre del plan (definition of done global)
|
|
|
|
|
|
1. `npm run check` sin regresión; `npm run blocks:check` verde; `npm run
|
|
|
component:audit` → los 7 de F1 PASS (o NEEDS-WORK solo por D-\* de la fase
|
|
|
demos global); `docs:check` verde.
|
|
|
2. Las tres páginas de integración (F2 landing · F3 app · F4 docs) renderizan
|
|
|
compuestas SOLO de canon+blocks, verificadas visualmente en claro/oscuro,
|
|
|
375/1280 y teclado.
|
|
|
3. Doctrina publicada y enlazada (architecture/blocks.md en el mapa; CLAUDE.md
|
|
|
al día); cada block con README B-9 completo.
|
|
|
4. Ningún gap silenciado: todo ❌/⚠️ de las fases 0 tiene decisión firmada o
|
|
|
fila en F5/next-features.
|
|
|
|
|
|
## 7. Registro
|
|
|
|
|
|
- 2026-07-21 — Plan creado (análisis de ecosistema + decisión de usuario:
|
|
|
tier `blocks`). Iniciativa en `next-features.md` §8.
|
|
|
- 2026-07-21 — **F0.1 HECHA**: las 7 D-BLK firmadas (repaso en sesión).
|
|
|
D-BLK.1 **enmendada** frente a la propuesta: el tier vive en
|
|
|
`src/uix/blocks/` (no `src/blocks/`); referencias del plan actualizadas
|
|
|
(tabla de tiers, B-4, blocks-check, F0.3/F0.4). Resto firmado como
|
|
|
propuesto. Siguiente: F0.2 (doctrina en `architecture/blocks.md`).
|
|
|
- 2026-07-21 — **F0 CERRADA** (misma sesión): doctrina
|
|
|
`docs/architecture/blocks.md` · alias `$blocks` (vite + svelte.config +
|
|
|
CLAUDE.md) · `src/uix/blocks/README.md` (mapa + template B-9) ·
|
|
|
`scripts/blocks-check.ts` + `npm run blocks:check` (self-testing) ·
|
|
|
galería `web/routes/blocks/` con layout de bootstrap propio
|
|
|
(`+layout@.svelte`, espejo mínimo del de `/uix`, sin packs sema) · fila
|
|
|
E1 en `docs/README.md`. Nota de commit: `docs/README.md` y
|
|
|
`docs/next-features.md` quedaron FUERA del commit de F0 — ambos traían
|
|
|
diff foráneo de otra sesión imposible de separar por staging de archivo
|
|
|
completo; sus hunks propios (fila E1 + §8) viajan con el árbol de trabajo
|
|
|
hasta que su dueño commitee. Siguiente: **F1** (orden recomendado:
|
|
|
`empty-state` → `result` → `callout` → `sticky` → `prose` → `anchor-nav`
|
|
|
→ `sidebar`).
|
|
|
- 2026-07-21 — **Estudio de referencia COMPLETO** (encargo del usuario:
|
|
|
"a la par y cuanto menos superarlo"): 6 pistas de investigación web en
|
|
|
paralelo → `docs/process/RESEARCH-blocks-references.md` (suelos de
|
|
|
paridad a nivel de prop por ítem, pitfalls, superación). Regla nueva en
|
|
|
§4: toda fase 0 contrasta contra el dossier. Puntero de colisión añadido
|
|
|
a F1.7. **Enmiendas E-1…E-5 presentadas al usuario** (E-1 nav-tree ·
|
|
|
E-2 filosofía de alcance v1 · E-3 categorías nuevas F2 · E-4
|
|
|
registry/llms/MCP · E-5 ⌘K) — firmas pendientes; se registran aquí.
|
|
|
- 2026-07-21 — **E-1…E-5 FIRMADAS** (todas según recomendación): **E-1**
|
|
|
componente canónico `nav-tree` data-driven (F1.8; F1 pasa de 7 a 8; el
|
|
|
naming se valida en su fase 0 como extensión de D-BLK.7); **E-2** suelo
|
|
|
de paridad = v1 (regla en el intro de F1); **E-3** F2 pasa de 10 a 14
|
|
|
(banner · team · contact · content-section; resto de la unión a F5);
|
|
|
**E-4** distribución registry/llms.txt/MCP registrada como iniciativa
|
|
|
propia en `next-features.md` §9; **E-5** ⌘K = listener app-land (nota en
|
|
|
F4.1). Doctrina (`architecture/blocks.md` promotion path) y galería
|
|
|
actualizadas.
|
|
|
- 2026-07-21 — **F1.2 `empty-state` HECHA** (ruta de 9 fases completa,
|
|
|
suelo E-2 del dossier §P3): morfo display 5 partes (`scope: ['eidos']`,
|
|
|
0 eventos justificados, Title `role:'heading'`) + langs `label` + eidos
|
|
|
compound (`Media kind=icon|media` con placa 2× glifo · `Title level`
|
|
|
2–6 default h3, modelo Atlaskit · `Description` measure 45ch ·
|
|
|
`Actions label` → role=group, buttonGroupLabel) + recipe sobre el bundle
|
|
|
`--size-*` (título un paso discreto arriba) + demo v2 de 9 tabs + README
|
|
|
(Comparativa 5 refs · Passive justification · Gaps con disposición).
|
|
|
Verificado: `component:audit` **PASS** · eidos-lint 5 morfo-backed + 4
|
|
|
eidos-only sancionados · `svelte-check` 76E/51W = baseline exacto ·
|
|
|
navegador claro Y oscuro por estilos computados (título 18→24px, placa
|
|
|
40→64px, chips vivos). Siguiente: **F1.3 `result`** (comparte esqueleto;
|
|
|
fase 0 contra dossier §P3).
|
|
|
- 2026-07-21 — **F1.3 `result` HECHA** (ruta completa, suelo E-2 §P3):
|
|
|
morfo 6 partes (`extra` sin archetype — parte genuinamente propia) ·
|
|
|
status enum de 7 CON `warning` (decisión del dossier firme), default
|
|
|
`info`, HTTP renombrados semánticos y renderizados NEUTROS (código mono
|
|
|
grande, jamás rojo — doctrina AntD) · media default compone el mapa
|
|
|
doctrinal `IntentIcon` (fulfill/threat/risk) + `Info` de catálogo,
|
|
|
children lo reemplazan entero (context local eidos-only con getter
|
|
|
reactivo) · Title default h2 (vs h3 de empty-state — deliberado) ·
|
|
|
esqueleto duplicado conscientemente (README lo registra; revisable al
|
|
|
3er consumidor) · sin eje size (paridad AntD). Verificado:
|
|
|
`component:audit` **PASS 0E/0W** · eidos-lint 6 morfo-backed + 5
|
|
|
eidos-only · guards recipe 37/37 · navegador: success=fulfill verde
|
|
|
84px · error=threat rojo · not-found=«404» mono NEUTRO aria-hidden,
|
|
|
copy conmutando, cero errores consola. Siguiente: **F1.4 `callout`**
|
|
|
(decisión IMPORTANT/5º hueco + role=note; dossier §P3).
|
|
|
- 2026-07-21 — **F1.4 `callout` HECHA** (ruta completa, suelo E-2 §P3):
|
|
|
morfo 4 partes + `texts` con **títulos default localizados por intent**
|
|
|
(note/tip/warning/caution — el patrón GitHub de label visible; la
|
|
|
semántica nunca viaja solo en color) · **el hueco IMPORTANT resuelto con
|
|
|
el modelo Radix**: `intent` (4, mapea el vocabulario de facto) + `color`
|
|
|
override SOLO bajo neutral (doctrina §4 — intent evaluativo gana),
|
|
|
montado sobre la maquinaria C6/THM-2 de badge (forwarders + `_palette-*`
|
|
|
→ 8 roles + 33 escalas + custom con CERO CSS extra; el forward
|
|
|
presence-guarded se emitió solo) · `role="note"` nombrado por el Title
|
|
|
vía `aria-labelledby` SOLO mientras está montado (registro reactivo por
|
|
|
context; **bug cazado en navegador**: el `+=` del registro leía el
|
|
|
estado dentro del tracking del `$effect` del hijo → effect_update_depth
|
|
|
y contador a −997 — fix `untrack`, lección para la memoria) · Title NO
|
|
|
heading (protege el outline de anchor-nav) · icono = mapa doctrinal
|
|
|
`IntentIcon` · escalación tipada `role: status|alert|none` (el
|
|
|
`role="alert"` estático de shadcn NO se copia) · dismissible DIFERIDO a
|
|
|
pasada soma+sema (regla de admisión). Verificado: audit **PASS 0E/0W** ·
|
|
|
lint 8 morfo-backed/0 invalid · guards 37/37 · navegador: «Nota»/«
|
|
|
Atención» localizados, risk=ámbar hue 45-60, **IMPORTANT=plum hue 326
|
|
|
por shared layer**, labelledby=titleId. Siguiente: **F1.1 `sticky`**
|
|
|
(orden del plan: desbloquea F2; dossier §P3 — offsets, scroll-host,
|
|
|
centinela por borde, espejo scroll-state).
|
|
|
- 2026-07-21 — **F1.1 `sticky` HECHA** (commit `998b11d9b`, PASS 0E/0W). El
|
|
|
primer componente BEHAVIORAL (soma+eidos). Precedido de un fan-out de
|
|
|
investigación (workflow, 6 lectores) que fijó el mapa de construcción:
|
|
|
template = FeedSentinelProvider; motor = `$adom.observeIntersection` (nunca
|
|
|
scroll+getBoundingClientRect); escritura de attrs por `syncAttrs` (nunca
|
|
|
`dom.apply` crudo). Diseño: un StickyProvider registra AMBAS partes
|
|
|
(box+sentinel) en un runtime; `$effect` retorna el observer como teardown;
|
|
|
callback async voltea `stuck=$state`; **rootMargin DERIVADO del offset** (la
|
|
|
línea de disparo del centinela = la línea de pin — acoplamiento
|
|
|
matemático); centinela = HERMANO de flujo del box (dos nodos raíz), no hijo;
|
|
|
offset viaja como `--_sticky-offset` (dato A8); recipe SOLO posiciona (el
|
|
|
consumidor decora `[data-sticky][data-stuck]`); `data-stuck`/`data-edge`
|
|
|
espejan `@container scroll-state(stuck: top/bottom)` (polyfill del contrato
|
|
|
CSS futuro). Verificación clave: **5 tests, incl. 1 real-IntersectionObserver
|
|
|
en chromium foreground** (no-stuck→scroll→stuck→back, prueba end-to-end de la
|
|
|
geometría — lo que el Browser pane suspendido NO puede). Decisión declarada:
|
|
|
el test usa el `installSomaHarness` compartido (los 86 tests lo hacen) — en
|
|
|
tensión con la regla CLAUDE.md «never fake translators»; flaggeada al usuario
|
|
|
para ratificar. Notas de commit: nav sidebar fuera (flip CRLF ajeno de 2125
|
|
|
líneas); índices langs/soma reconstruidos sticky-only (entangle con `aura`
|
|
|
sin commitear de sesión paralela). **Consola:** detectado un warning
|
|
|
reactivo residual en `callout` (registerTitle en $effect) — a investigar.
|
|
|
Siguiente: **F1.5 `prose`** (o F1.6 anchor-nav / F1.8 nav-tree; los 3
|
|
|
restantes que quedan de F1 tras prose son anchor-nav, nav-tree, sidebar).
|
|
|
- 2026-07-21 — **Review adversarial de sticky** (workflow `wrssrn7tx`, 3
|
|
|
lentes + verify): 1 hallazgo CONFIRMADO (minor) — pin en eje lógico vs
|
|
|
observer físico, diverge en writing-modes verticales. **Corregido**
|
|
|
(`6cba9a0b1`): recipe a `top`/`bottom` físico + margins físicos; 3 claims
|
|
|
«logical/RTL» del demo corregidos. Re-verificado PASS 0E/0W + test real-IO.
|
|
|
(1 lente teardown-ssr falló por API stall; caminos cubiertos por tests.)
|
|
|
**F1.1 sticky CERRADO.**
|
|
|
- 2026-07-21 — **F1.5 `prose` HECHA** (commit `5b84749ee`, PASS 0E/0W). Quinto
|
|
|
F1 (eidos-only). EL DIFERENCIADOR verificado en navegador: reglas de elemento
|
|
|
a especificidad CERO (`:where([data-prose] EL)`) → un `<Callout>` embebido
|
|
|
conserva su tinte/borde/grid AUTOMÁTICAMENTE (su `[data-callout]` gana a
|
|
|
`:where`), en claro Y oscuro — sin `not-prose`, cosa que ninguna ref puede.
|
|
|
Escala EM-relativa (un font-size raíz re-deriva todo, no la escala 5× de
|
|
|
tailwind); overflow de tabla estilo GitHub; dark gratis por roles (no
|
|
|
`-invert`); propiedades lógicas. Decisión `:where` (no `@scope`, diferido a
|
|
|
Gaps). Lección: recipe token defs (base.ts) admiten literales sin tripar
|
|
|
R-2.7; los literales em en el .css se justifican con `/* literal */` (21
|
|
|
añadidos). **F1 = 5/8.** Restan: anchor-nav (F1.6) · nav-tree (F1.8) ·
|
|
|
sidebar (F1.7).
|
|
|
- 2026-07-22 — **F1.6 `anchor-nav` HECHA** (commit `fe2d43b7a`, PASS 0E/0W).
|
|
|
Sexto F1, segundo BEHAVIORAL (soma+eidos) tras sticky. TOC scrollspy:
|
|
|
landmark `<nav>` + anchors nativos; detección por IntersectionObserver de
|
|
|
banda (`rootMargin -{topOffset}px 0px -70% 0px`), NUNCA scroll-listener +
|
|
|
`getBoundingClientRect` (doctrina anti-reflow — Mantine/Docusaurus lo
|
|
|
violan). `aria-current="location"` (spec-preciso; solo 1 de 5 refs pone
|
|
|
aria-current). Rail por-link con acento en `data-active`; indent por
|
|
|
`data-level`. Verificación: **test real-IO chromium** (`anchor-nav-io`) —
|
|
|
banda + zona muerta al fondo + fallback inicial; el pane suspendido daba un
|
|
|
`getComputedStyle` de color OBSOLETO (falsa alarma, la regla `[data-active]`
|
|
|
sí aplica — el `font-weight:500` lo probaba). **Review adversarial** (4
|
|
|
dimensiones × verificador escéptico, 9/16 confirmadas): arregladas →
|
|
|
ref-count en register/unregister (hrefs duplicados / churn ya no tiran un
|
|
|
target vivo, +test); transición y `outline-offset` tokenizados
|
|
|
(`--duration-fast`/`--ease-default`, `calc(--focus-ring-width * -1)`).
|
|
|
Documentadas como límite v1 (Gaps) → salto instantáneo al fondo (Ctrl+Fin)
|
|
|
en zona muerta + orden-enlaces==orden-secciones (ambas piden cambio de
|
|
|
contrato de observación v2). `data-level` fuera de rango degrada a flush
|
|
|
(no arreglado, simplicidad). **Además** (`37e35d7a2`): `npm run check`
|
|
|
global destapó 2 errores de tipo en F1.1 sticky (sentinelRef debía ser
|
|
|
`State` no `Active`; eidos types importaba `StickyProps` en vez del export
|
|
|
renombrado `ProviderProps`) — míos, arreglados. Mis archivos suman 0
|
|
|
errores de check (80→73; los 73 son deuda foránea de sesiones paralelas).
|
|
|
**F1 = 6/8.** Restan: nav-tree (F1.8) · sidebar (F1.7).
|
|
|
- 2026-07-22 — **F1.8 `nav-tree` EN CURSO** (WIP commiteado, sin cerrar).
|
|
|
Componente COMPLETO en verde en gates estáticos (audit PASS · eidos-lint 20/0
|
|
|
· svelte-check 0 · contracts mis-partes limpias); morfo+soma+eidos+pack de
|
|
|
sema+4 READMEs+todos los registros escritos. Falta demo + verificación en
|
|
|
NAVEGADOR + review adversarial + cierre. Data-driven (E-1); disclosure propio
|
|
|
(getter de estado + eventos `emerge`, pack espeja collapsible); filas propias
|
|
|
(no `Link`/`Badge` compuestos — Badge diferido a Gap); `activeHref` como
|
|
|
costura. Corregido de paso: README de soma de anchor-nav faltaba (contracts
|
|
|
lo destapó). **Handoff detallado: `docs/process/CONTINUE-nav-tree.md`.** ⚠️ 2
|
|
|
fallos contracts AJENOS (menubar/radio-group, sesiones paralelas).
|
|
|
- 2026-07-22 — **F1.8 `nav-tree` HECHA** (cierre de la ruta: demo v2 de 9 tabs
|
|
|
con el mapa real de docs —43 nodos, 3 niveles—, verificación en navegador
|
|
|
REAL y review adversarial de 5 dimensiones × 3 verificadores escépticos,
|
|
|
22 hallazgos brutos → **10 confirmados**, 12 rechazados). Arreglado del
|
|
|
review: (1) **el emit de sema nunca ocurría** — la parte `group` es el
|
|
|
target de los eventos y se registraba SIN `ref`, así que `runtime.trigger`
|
|
|
lanzaba `SomaRuntimeTargetError` en silencio (lo cazó el navegador: cero
|
|
|
`data-event-*` en el grupo frente a los de collapsible); (2) **el chevron se
|
|
|
comía media fila** (`inline-size:100%` resolvía como flex-basis → 101px de
|
|
|
231 en «Soma»: pulsar el hueco tras la etiqueta plegaba en vez de navegar) →
|
|
|
toggle compacto de 24px con suelo de diana WCAG 2.5.8; (3) **ciclo potencial
|
|
|
en `parentByKey`** (clave duplicada = nodo padre de sí mismo → `trailKeys`
|
|
|
giraba para siempre y congelaba la pestaña) → clave sufijada + aviso del
|
|
|
logger + guarda de ciclo en el paseo; (4) **colapso ahora CONTEXTUAL**
|
|
|
(recuerda el `activeKey` bajo el que se hizo: cerrar la sección que lees se
|
|
|
respeta, pero caduca al navegar DENTRO — si no, la página actual se quedaba
|
|
|
sin fila visible y el trail auto-expandido quedaba anulado; sigue siendo
|
|
|
query pura); (5) **`child` ya no se comía el árbol** (recibe `children`
|
|
|
además de `props` — un árbol data-driven no lo puede reautorar el consumidor);
|
|
|
(6) **nombre accesible del chevron** declarado en morfo + localizado
|
|
|
(«Alternar sección {label}») en vez de duplicar el nombre del enlace;
|
|
|
(7) paridad de snippet en la demo + token fantasma `--nav-tree-rail-width`
|
|
|
fuera del docblock. **RTL completo** (el glyph se dibuja con bordes lógicos:
|
|
|
espeja solo; lo que no espeja es el GIRO → bajo `[dir='rtl']` las dos
|
|
|
rotaciones se intercambian) — el Gap «dirección del chevron en RTL» queda
|
|
|
RESUELTO, no diferido. **Decisión de usuario (delegada)**: `badge` SÍ entra
|
|
|
en v1 (está en el suelo del dossier §P5, los 6 refs lo llevan) resuelto con
|
|
|
**snippet**, no con recursión de eidos — soma declara la parte `badge` y
|
|
|
renderiza el snippet recibido (sin él, el valor crudo: sigue headless), el
|
|
|
wrapper de eidos pasa el `Badge` canónico; va DENTRO del control de la fila
|
|
|
para que su texto entre en el nombre accesible («TSC, New, enlace»).
|
|
|
`disabled` **también entra**, por decisión explícita del usuario tras
|
|
|
presentarle el criterio: el nodo conserva la fila pero pierde el `href` (no
|
|
|
hay nada que activar con Enter, clic ni «abrir en pestaña nueva» — un
|
|
|
`pointer-events:none` de CSS solo tapa el ratón) y añade `aria-disabled` +
|
|
|
`data-disabled`; el toggle del grupo NUNCA se deshabilita (disclosure es
|
|
|
control de vista, no destino) y un nodo sin `href` lo ignora.
|
|
|
Verificado: `component:audit` **PASS** · eidos-lint **26 morfo-backed / 0
|
|
|
invalid** · `svelte-check` 0 errores en mis archivos · `vitest src/uix/eidos`
|
|
|
353/353 · navegador real (Playwright, el pane suspendido NO sirve: congela
|
|
|
rAF y el scroll-into-view parecía roto) → trail auto-expandido, sema
|
|
|
`emerge-expand/collapse` estampando en `group`, teclado nativo (Tab/Enter/
|
|
|
Espacio), foco visible, claro+oscuro, RTL, 375px sin desbordes, 0 errores de
|
|
|
consola. **F1 = 7/8.** Resta: sidebar (F1.7).
|
|
|
- 2026-07-22 — **F1.7 `sidebar` — fase 0 + morfo + soma** (commit `2bc5fe6bd`).
|
|
|
Fase 0 presentada al usuario ANTES de construir, con 4 decisiones firmadas:
|
|
|
**17 partes** (13 estructurales + `menu-action` · `menu-badge` · `menu-sub` ·
|
|
|
`menu-sub-button`), **submenú FLOTANTE en modo icono ya en v1** (superación:
|
|
|
shadcn los oculta y deja ramas inalcanzables), **sema
|
|
|
`emerge-expand/collapse`** sobre `panel`, y **ritmo por fases con parada tras
|
|
|
morfo+soma**. Contrato: DOS ejes (`data-state` + `data-collapsible` SIEMPRE
|
|
|
estampado) + `data-side` + `data-mobile`; `panel` = `<aside>` nombrado y
|
|
|
`content` = `<nav>` nombrado con header/footer FUERA (ninguna ref trae
|
|
|
landmark); `rail` = `<button>` real en el orden de tabulación (shadcn:
|
|
|
`tabIndex=-1`); `aria-current="page"` del MISMO prop que `data-active`;
|
|
|
`menu-badge` dentro del control de la fila; `separator`/`input` NO son partes
|
|
|
(se componen `Separator` y `Field`). Soma: costuras
|
|
|
`open`/`defaultOpen`/`onOpenChange`/`toggle()` desde v1 (sin ellas la
|
|
|
persistencia y el atajo de app-land serían rediseño posterior), `mobile` del
|
|
|
breakpoint del SISTEMA (`uix.dom.isAtLeast`) y no de un `matchMedia` propio,
|
|
|
submenú flotante sobre `createFloatingShellRoot` con Escape que devuelve el
|
|
|
foco, e ids per-instancia para los dos `partRef` que son last-write-wins
|
|
|
(precedente `navigation-menu`). Verde: `morfo:check` carga el contrato ·
|
|
|
`component:audit --only sidebar` PASS 0E/0W · `svelte-check` 0 errores
|
|
|
propios · `contracts.test` solo los 2 fallos ajenos. **Siguiente**: eidos
|
|
|
(raíl, anchos tokenizados, modo icono, Drawer móvil) + demo + review.
|
|
|
- 2026-07-23 — **F1.7 `sidebar` HECHA · F1 CERRADA (8/8)**. Eidos + demo +
|
|
|
navegador + review adversarial (commits `2bc5fe6bd` morfo+soma ·
|
|
|
`21bbf9f50` eidos+demo · `293593307` alto completo · `dcce75cc2` foco del
|
|
|
flyout · `245c22471` controles de demo · `75e1fe32a` review). El review
|
|
|
(6 dimensiones × 3 verificadores, 135 agentes) dio **43 brutos → 31
|
|
|
confirmados**, todos arreglados. Los gordos: (1) `collapsible='none'`
|
|
|
dejaba el sidebar INALCANZABLE en móvil (toggle inerte + raíl oculto +
|
|
|
drawer sin trigger) → la inercia del modo se limita al escritorio; (2) ese
|
|
|
mismo modo hacía MENTIR a `data-state`/`aria-expanded` sobre un panel
|
|
|
visible → el modo pinta el estado; (3) el flyout de modo icono solo cerraba
|
|
|
desde el propio flyout → puntero, foco y Escape se resuelven ahora en el
|
|
|
`menu-item`, que contiene fila y sub, y el flag se limpia al cambiar de
|
|
|
presentación; (4) las filas del raíl no tenían NOMBRE (un tooltip solo
|
|
|
describe) → el texto de `tooltip` pasa también a `aria-label`; (5) el panel
|
|
|
off-canvas colapsado seguía en el tab order → `visibility: hidden` con la
|
|
|
transición retrasada; (6) el signo del off-canvas era físico → en RTL
|
|
|
barría el panel por encima de la página; (7) el flyout era la única
|
|
|
superficie flotante sin `z-index`. Verificado en navegador real todo lo
|
|
|
anterior. Gates: audit PASS 0E/0W · eidos-lint 47 morfo-backed / 0 invalid ·
|
|
|
svelte-check 0 errores propios · vitest src/uix/eidos 353/353. **F1 = 8/8:
|
|
|
la fase queda CERRADA.** Siguiente: F2 (blocks de sitio, 14).
|
|
|
- 2026-07-23 — **F2.1 `site-header` HECHA · primer block del tier** (commits
|
|
|
`3cfc030b3` block+landmark · `016658a0c` controles · `f98c844ed` fix de Box ·
|
|
|
`50574806d` harness · `33328df7a` cromo de sección · `d574982ab` escenario a
|
|
|
ras · `ae7b4f8c3` vista previa honesta). Composición pura: `Sticky` (solo si
|
|
|
`sticky`, sin wrapper inútil) + `Container` + `Group` + `Box` (el interruptor
|
|
|
ancho/estrecho por `display` responsive, B-6) + `Drawer`; marca, nav,
|
|
|
acciones y navegación móvil entran como snippets del app (B-7: cero cadenas
|
|
|
propias). Fase 0 contra el dossier §P1: adoptado el FLYOUT (la brecha de
|
|
|
paridad) sin reimplementarlo — el app pasa un `NavigationMenu`.
|
|
|
**Encontrado al componer, arreglado en el canon**: `NavigationMenu` no emitía
|
|
|
landmark (su morfo declara `defaultElement: 'nav'` y soma pintaba un `div`).
|
|
|
**Hueco registrado, no falseado → CERRADO el mismo día**: el CTA que NAVEGA
|
|
|
y parece botón. Se sirve por COMPOSICIÓN, no por prop nueva: el `child`
|
|
|
(asChild) de `Button` entrega ahora un snippet `content` con el cuerpo ya
|
|
|
decorado, así que `<a href {...props}>{@render content()}</a>` recibe la
|
|
|
pintura sólida completa y conserva icono/etiqueta/spinner (antes el `child`
|
|
|
los sustituía: por eso la flecha del sitio alpha estaba escrita a mano); y
|
|
|
soma deja de estampar `type` en un elemento que no es suyo (`<a
|
|
|
type="button">` es una pista de MIME falsa) — el morfo lo declara
|
|
|
condicional. `Button` sigue sin `href` y `Link` sigue poseyendo la
|
|
|
navegación: las dos decisiones firmadas se mantienen. Doctrina en el README
|
|
|
de eidos Button §«CTA que navega»; en uso en la demo del block.
|
|
|
**Defecto de framework destapado por la demo**: las 44 variables de cascada
|
|
|
de `Box` heredaban, así que un `Section` regalaba su padding a cada
|
|
|
descendiente (la galería arrastraba ~300px de aire desde F0) →
|
|
|
`@property { inherits: false }`, radio verificado sin regresiones
|
|
|
(`docs/theming/changelog.md` §46).
|
|
|
**Superficie de demo del tier (doctrina nueva, B-9 ampliada)**: harness
|
|
|
compartido en `web/routes/blocks/_lib/` + cromo de sección con los ejes que
|
|
|
mueven el framework (tema, idioma, dirección, densidad) + catálogo único.
|
|
|
El block se enseña **a sangre en la página**, nunca dentro de un marco con
|
|
|
relleno ni de una caja con scroll: medido, un `Card` desplazaba 21px un
|
|
|
header con `offset: 0`, y un `sticky` dentro de un div con scroll es un
|
|
|
comportamiento que nadie vive. Los anchos de dispositivo se sirven desde una
|
|
|
ruta `preview` propia (iframe opt-in: en dev, dos documentos sin empaquetar
|
|
|
agotan las conexiones del navegador).
|
|
|
**Siguiente**: F2.2 `hero` — handoff en `docs/process/CONTINUE-blocks.md`.
|
|
|
|
|
|
- 2026-07-23 — **F2.2 `hero` HECHA** (mismo día). API por **slots de snippet**
|
|
|
(no compound): `eyebrow`/`title`/`description`/`actions`/`media`/`background` +
|
|
|
`layout: 'center' | 'split' | 'background'` + `level`/`container`/`size`. La
|
|
|
decisión de forma se razonó con el usuario: compound se reserva para partes que
|
|
|
COORDINAN (Accordion, Dialog); un hero son slots de layout que el root arregla
|
|
|
→ snippets. El campo entero shippea marketing como copy-paste plano; nadie
|
|
|
aplica compound a una sección → los slots por zona SON la historia de
|
|
|
personalización del tier. El block **envuelve** título/subtítulo en
|
|
|
`Heading`/`Text` (posee el `id` del landmark y el nivel; la app pone las
|
|
|
palabras); `eyebrow`/`actions`/`media` son contenido libre. Landmark:
|
|
|
`<section aria-labelledby>` nombrado por el título (verificado: región con
|
|
|
nombre accesible). **Layout `background` (cover)** añadido por scope-approval
|
|
|
del usuario: media a sangre detrás, velo `--color-overlay` a `--opacity-scrim`,
|
|
|
texto `on-solid` y `object-fit: cover` vía `<style>` justificado (D-BLK.2) —
|
|
|
las ÚNICAS reglas que el block posee, todas sobre tokens del ecosistema, cero
|
|
|
color a mano. La demo dogfooda el sistema de color: el backdrop es un `Surface`
|
|
|
`color="primary" gradient` (finish aurora, `--gradient-aurora` derivado de los
|
|
|
roles del tema). **Registrado, no resuelto**: `Box`/`Surface` `flex`/`grow` no
|
|
|
hicieron crecer un hijo flex (bars a 0-width, `flex: 0 1 auto`; prop sin usar
|
|
|
en shipped) → la demo usó `Grid` (tracks `1fr`); flag para revisar el cableado
|
|
|
de `--box-flex`. Gates: `blocks:check` verde (2 blocks) · `svelte-check` sin
|
|
|
errores propios. **Siguiente**: F2.3 `feature-grid` (primer compound del tier:
|
|
|
`.Item` repetido — LA brecha #1 del dossier, split/alternante texto-screenshot).
|
|
|
|
|
|
- 2026-07-23 — **F2.3 `feature-grid` HECHA** (mismo día). **Primer block COMPOUND
|
|
|
del tier**: `<FeatureGrid>` + `.Header` + `.Items` + `.Item` + `.ItemIcon`/
|
|
|
`.ItemTitle`/`.ItemText`. Aplica la regla de forma: `.Item` se REPITE (el app
|
|
|
mapea sobre N features) → gana compound; las partes no coordinan (sin
|
|
|
contexto/estado entre ellas), la rejilla es del padre. `.Items` es el envoltorio
|
|
|
honesto de la rejilla (`AutoGrid`) — deja la cabecera fuera sin un
|
|
|
`grid-column: 1/-1` a pelo. El block coloca (Section·Container·AutoGrid·Surface·
|
|
|
Heading·Text), el app pone contenido (B-7). `align` de sección (center/start)
|
|
|
coloca la cabecera coherente con las columnas. **Sub-componentes de bloque
|
|
|
extienden los props del componente canon que envuelven, NO `HTMLAttributes`**:
|
|
|
el `style: string|null` del atributo HTML crudo choca con el `style: string` del
|
|
|
canon (+ "union too complex") al hacer spread. **ItemIcon default `solid`**: el
|
|
|
soft-primary en claro es casi blanco (oklch 0.99) → invisible; solid da chip con
|
|
|
glifo on-solid (la tinta de contraste la pone `Surface`). Demo: control de
|
|
|
columnas (fluido/2/3/4), align, nº items, dir; 6 features con iconos del canon.
|
|
|
Gates: `blocks:check` verde (3 blocks) · `svelte-check` sin errores propios.
|
|
|
**Hueco a decisión del usuario**: el **feature-split/alternante** (texto junto a
|
|
|
screenshot, lados alternos) — la brecha nº1 del dossier — es otra disposición
|
|
|
(filas de 2 columnas, no rejilla de iconos): probablemente block hermano
|
|
|
`feature-split`. Presentado como scope-approval, no resuelto aquí. **Siguiente**:
|
|
|
F2.4 `pricing`.
|
|
|
|
|
|
- 2026-07-23 — **Primitivo canon `Mockup` + F2.3b `feature-split` HECHOS** (mismo
|
|
|
día; scope-approval del usuario: «split + primitivo de media reutilizable»).
|
|
|
La clave del dossier: _la brecha del hero/features es el TRATAMIENTO DE MEDIA,
|
|
|
no el conteo de layouts_. En vez de falsear un screenshot por demo, se cierra
|
|
|
de raíz con un componente del canon.
|
|
|
- **`Mockup`** (`src/uix/eidos/components/mockup/` + morfo eidos-only, 0-event,
|
|
|
como `aspect-ratio`): enmarca media en cromo de dispositivo —
|
|
|
`chrome: 'plain' | 'browser' | 'phone'` + `url` para la barra. Recipe dibuja
|
|
|
la barra (dots + pill de URL), el bisel del teléfono y el notch; todo sobre
|
|
|
tokens (`--radius-*`, `--color-border-*`, `--shadow-*`, `--primitive-*`).
|
|
|
eidos-lint: 0 invalid / 0 class-hooks (mismo patrón que aspect-ratio, todo
|
|
|
eidos-only). Retrofiteado en la demo del `hero` (dogfood).
|
|
|
- **`feature-split`** (compound: `.Row` repite): `<FeatureSplit>` + `.Row`
|
|
|
(`reversed`, slot `media`) + `.Eyebrow`/`.Title`/`.Text`/`.Features`/
|
|
|
`.Feature`/`.Actions`. `reversed` mueve la media al inicio vía
|
|
|
`grid-column` (Box tiene `gridColumn`/`order`), dejando la copy PRIMERA en
|
|
|
el DOM (orden de lectura intacto). El check de `.Feature` es decorativo
|
|
|
(aria-hidden). La media se compone con `Mockup`.
|
|
|
- **BUG de `hero` arreglado de paso** (lo tapaba mi filtro de svelte-check con
|
|
|
backslashes mal escapados): los snippets `title` y `background` colisionaban
|
|
|
con los atributos HTML homónimos (`title?: string` / `background`) →
|
|
|
`string & Snippet`. Fix: `Omit<HTMLAttributes, 'children' | 'title'>` +
|
|
|
renombrar el slot `background`→`backdrop`. Lección para blocks: **un slot de
|
|
|
snippet cuyo nombre sea un atributo HTML necesita Omit o un nombre distinto**.
|
|
|
Gates: `blocks:check` verde (4 blocks) · `svelte-check` sin errores propios
|
|
|
(73 = deuda ajena) · eidos lint.test + morfo 131/131. **Siguiente**: F2.4
|
|
|
`pricing`.
|
|
|
|
|
|
- 2026-07-24 — **Fix de `Grid` + F2.4 `pricing` HECHOS**.
|
|
|
- **Fix canon `Grid`** (theming/changelog §47): `align`/`justify`/`alignContent`
|
|
|
NO aplicaban — los atajos `place-items`/`place-content` con fallback
|
|
|
`revert-layer` (cuando su prop no está puesto) revertían los longhands a su
|
|
|
inicial, pisándolos. Ahora el fallback COMPONE los vars de los longhands. 353
|
|
|
tests eidos verdes, grids por defecto sin cambio. Encontrado alineando
|
|
|
`feature-split`.
|
|
|
- **`pricing`** (compound **CON CONTEXTO** — el primero del tier cuyas partes se
|
|
|
COORDINAN, no solo se repiten): `<Pricing bind:period>` + `.Header` +
|
|
|
`.Switch` + `.Plans` + `.Plan`(featured, badge) + `.PlanName`/`.PlanDescription`/
|
|
|
`.PlanPrice`/`.PlanFeatures`/`.PlanFeature`/`.PlanAction`. El `Switch`
|
|
|
(`ToggleGroup`) escribe el periodo en un contexto reactivo (`context.ts`,
|
|
|
getter sobre el `$bindable`) y cada `PlanPrice` lo lee → muestra el snippet
|
|
|
`monthly`/`annual`. **El block NO formatea moneda**: la app compone
|
|
|
`FormatNumber` en los snippets (B-7). `.PlanAction` fija el CTA al borde
|
|
|
inferior (`margin-block-start: auto`) para alinear los botones; `.Plan` con
|
|
|
`align="start"` deja los checks en columna. Verificado: el toggle cambia los 3
|
|
|
precios a la vez (0/29/99 → 0/23/79), featured con acento+elevación, claro/
|
|
|
oscuro/RTL. **Hueco a decisión del usuario**: tabla de comparación
|
|
|
(features × planes) — otra disposición, candidato a hermano `pricing-table`.
|
|
|
Gates: `blocks:check` verde (5 blocks) · `svelte-check` sin errores propios.
|
|
|
**Siguiente**: F2.5 `testimonials`.
|
|
|
|
|
|
- 2026-07-24 — **F2.5 `testimonials` HECHO**. Compound (`.Item` repite, SIN
|
|
|
contexto — como feature-grid): `<Testimonials>` + `.Header` + `.Items` +
|
|
|
`.Item` + `.Quote` + `.Author`(slot `avatar` + `.AuthorName`/`.AuthorRole`).
|
|
|
`AutoGrid` de tarjetas de igual alto; `.Author` fijada al borde inferior
|
|
|
(`marginTop="auto"`) para alinear las caras aunque las citas midan distinto. La
|
|
|
cara es del app (`Avatar` con imagen o `Avatar.Fallback` de iniciales). `Card`
|
|
|
para el quote = `variant="outline"` (soft neutral es casi invisible en claro).
|
|
|
**Encontrado**: el tema activa solo un SUBCONJUNTO de escalas donor —
|
|
|
`green/indigo/orange/plum/teal`; una escala no activada (`cyan/ruby/amber/jade`)
|
|
|
en `color` cae en silencio a `primary` (la regla `[data-color]` hace
|
|
|
`var(--scale-…, primary)`). No es bug de componente, es config del tema; usar
|
|
|
escalas activadas. **Hueco a decisión del usuario**: la cita única en spotlight
|
|
|
(grande, centrada, logo+avatar+autor) — la variante modal del dossier, otra
|
|
|
disposición → candidato a hermano `testimonial-spotlight`. Verificado en
|
|
|
claro/oscuro/RTL, caras alineadas, 5 colores distintos. Gates: `blocks:check`
|
|
|
verde (6 blocks) · `svelte-check` sin errores propios. **Siguiente**: F2.6 `faq`.
|
|
|
|
|
|
- 2026-07-24 — **F2.6 `faq` HECHO**. Compound (`.Item` repite) y **proxy fino del
|
|
|
`Accordion` del canon** (regla del handoff: leer su API, no reinventar el
|
|
|
disclosure): `<Faq>` + `.Header` + `.List` + `.Item`. `.List` **ES** el
|
|
|
`Accordion` — toda su API pasa tal cual (`type`, `bind:value`, `collapsible`,
|
|
|
`variant`, `size`; defaults FAQ: single, collapsible, outline). `.Item` proxya
|
|
|
`Accordion.Item > Header > Trigger`(snippet `question`) / `Content`(children), y
|
|
|
autogenera el `value` con `$props.id()` si no se pasa. `Container` estrecho
|
|
|
(`md`). El teclado/ARIA del disclosure salen del `Accordion`, el block no los
|
|
|
toca. **Hueco a decisión del usuario**: la lista estática 2/3 columnas (6 de 7
|
|
|
en TW NO son acordeón) — otra disposición, prop `layout` o hermano. La cola
|
|
|
«¿aún tienes dudas?» es app-land (la demo la pone tras `.List`). Verificado: el
|
|
|
acordeón abre/cierra, claro/oscuro/RTL. Gates: `blocks:check` verde (7 blocks) ·
|
|
|
`svelte-check` sin errores propios. **Siguiente**: F2.7 `stats-band`.
|
|
|
|
|
|
- 2026-07-29 — **F2.7 `stats-band` HECHO**. Compound (`.Stat` repite, sin
|
|
|
contexto): `<StatsBand>` + `.Stat` + `.Value` + `.Label`. Compone el `Metrics`
|
|
|
del canon para la semántica de KPI —surfaceless, que es lo correcto: una banda
|
|
|
no son tarjetas de panel— y `CountUp` para la cifra que se cuenta sola al
|
|
|
entrar en pantalla: **la superación literal del dossier**, porque ninguna
|
|
|
referencia puede shipearla (todas entregan markup estático). `count` es
|
|
|
OPT-IN, nunca default (una cifra que se anima sin que el lector lo pida es
|
|
|
ruido, y «99,98 %» no es contable). El block NO formatea ni escribe: el
|
|
|
separador de millares sale de `uix.format` DENTRO del `CountUp` y las palabras
|
|
|
entran por children (B-7). Escalonado estructural (`data-stagger` en la banda,
|
|
|
cada `.Stat` un `Motion trigger="viewport"`). Verificado en navegador: 4 cifras
|
|
|
con índices 0,1,2,3 contando (9377→11.678 · 255→318 · 36→45), el porcentaje
|
|
|
quieto, separador por locale, cero errores de página. Gates: `blocks:check`
|
|
|
verde (8 blocks) · `svelte-check` sin errores propios. **Siguiente**: F2.8 `cta`.
|
|
|
|
|
|
- 2026-07-30 — **F2.8 `cta` HECHO**. Slots de snippet (`eyebrow` · `title` ·
|
|
|
`description` · `actions` · `children`), misma forma que `hero`: las partes de
|
|
|
un CTA no repiten ni coordinan. Compone `Section` + `Container` + `Motion` +
|
|
|
`Surface` + `Heading` + `Text` + `Flex`. Dos disposiciones: `center` (`Stack`,
|
|
|
cierre de página) y **`justified`** (`Grid` 2 col., el hueco que nombraba el
|
|
|
dossier), que apila en estrecho. Un solo `Motion trigger="viewport"` con
|
|
|
`scale-fade`: el panel llega ENTERO, porque un CTA es una sola afirmación y
|
|
|
repartir sus tres partes se leería como duda. Landmark `section aria-labelledby`
|
|
|
al título que el propio block escribe.
|
|
|
|
|
|
**Tres hallazgos medidos en navegador, no a ojo** (los tres van a los gaps del
|
|
|
README y a §6 de `PLAN-blocks-quality.md`):
|
|
|
1. `variant='soft'` **eliminado de la API**: su track queda a `oklch(0.9932)`
|
|
|
contra un `--color-surface-default` de `oklch(0.9911)` — 0.002 L, o sea
|
|
|
ningún panel en claro — y `Surface` no tiene borde al que caer. Un panel
|
|
|
sosegado _con borde_ no tiene primitivo (`Card outline` acota pero no acepta
|
|
|
acabado de degradado).
|
|
|
2. La ranura `contrast` de la paleta es `#ffffff` en **todo** escalón sólido, así
|
|
|
que un lienzo de luminancia media deja el cuerpo por debajo de AA: medido,
|
|
|
`primary` 5.18 · `indigo` 5.21 · `plum` 4.75 pasan en ambos modos, mientras
|
|
|
`neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 fallan en
|
|
|
claro. El block reenvía cualquier `color`; la demo solo ofrece los que pasan.
|
|
|
3. `Text align` es **inerte** por defecto: `Text` renderiza un `span` y
|
|
|
`text-align` no hace nada sobre una caja inline, así que `align="center"`
|
|
|
dejaba la copia alineada a la izquierda dentro del layout centrado. Resuelto
|
|
|
con `as="p"` (que es lo que ese contenido es).
|
|
|
|
|
|
También: `Group` no apila — las acciones necesitaron
|
|
|
`Flex direction={{ base: 'column', sm: 'row' }}` porque a 420px la etiqueta de
|
|
|
la acción secundaria se parte contra el botón primario (`hero` compone sus
|
|
|
acciones con `Group`, así que arrastra el mismo comportamiento — anotado para su
|
|
|
siguiente pasada). El CTA que navega usa el patrón `Button child` + `content`
|
|
|
con `intent="fulfill"`, igual que `hero`. Verificado en navegador: `center` y
|
|
|
`justified` en claro/oscuro/RTL + 420px, tres colores, entrada disparada
|
|
|
(`data-animation-pending` fuera), descripción en `<p>` centrada, cero errores de
|
|
|
página. Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores
|
|
|
propios. **Siguiente**: F2.9 `newsletter`.
|
|
|
|
|
|
- 2026-07-30 — **F2.9 `newsletter` HECHO**. Slots de snippet; `center` +
|
|
|
`justified`; `panel` es un interruptor real (no un fallback). La fila es
|
|
|
`Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se queda la
|
|
|
pista libre y la acción abraza su contenido, y bajo `sm` pasa a una columna
|
|
|
porque un botón al lado de un campo de correo deja inservibles a los dos. El
|
|
|
block **no valida ni emite sema**: `Form` posee el runtime y ya declara
|
|
|
`commit-submit`/`signal-invalid`; el app posee esquema, valores y handler.
|
|
|
|
|
|
**Superación del dossier**: la nota de privacidad (el único hueco que el
|
|
|
dossier nombraba para este block) **y la etiqueta**. Las referencias shipean la
|
|
|
fila escondiendo la etiqueta con `sr-only` o dejando solo un placeholder;
|
|
|
aquí se compone `Field floatingLabel` —la etiqueta arranca dentro del control y
|
|
|
sube al borde al enfocar o rellenar—, así que la fila queda alineada CON
|
|
|
etiqueta real y asociada. Verificado con pulsaciones reales en Playwright: 10px
|
|
|
dentro en reposo → −11px sobre el borde al enfocar y al rellenar. (Esto corrige
|
|
|
la advertencia de 2026-07-05 de que el flotado no era verificable sin navegador
|
|
|
real: el pane suspendido no valía, Playwright headless con teclas de verdad sí.)
|
|
|
|
|
|
**Tres hallazgos de canon más, medidos** (F18/F19/F20 en
|
|
|
`PLAN-blocks-quality.md` §6):
|
|
|
1. **La fundación de eidos no trae reset de modelo de caja.** Recetas como
|
|
|
`[data-field-control]` declaran `inline-size: 100%` + padding, así que bajo
|
|
|
`content-box` el control mide 30px más que su contenedor y se metía por
|
|
|
debajo del botón: campo 480 / control 510 en la galería frente a 502 / 502
|
|
|
en los docs de componentes, que sí resetean. El framework lo **asume** del
|
|
|
app. Arreglado en app-land (`web/routes/blocks/_lib/reset.css`), con A/B
|
|
|
sobre los 10 previews y 5 páginas de shell: cambia el newsletter y nada más.
|
|
|
Hubo que importarlo DOS veces porque la galería arranca UIX en línea en vez
|
|
|
de pasar por `BootUix` (deuda del arnés, anotada).
|
|
|
2. **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya
|
|
|
construido: el componente solo lo reenvía al `createForm` que hace él mismo.
|
|
|
El envío validaba, limpiaba y no anunciaba nada.
|
|
|
3. **Los mensajes de SIUM son idlangref**; la vía correcta es
|
|
|
`uix.langs.t(issue.message, issue.params)` (verificado: sale «Debe ser una
|
|
|
dirección de correo válida»), pero la demo de docs del propio `Form` parte la
|
|
|
cadena a mano tras el `|` y por eso siempre muestra inglés.
|
|
|
|
|
|
Verificado en navegador el arco completo: correo inválido → error TRADUCIDO con
|
|
|
`role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error;
|
|
|
correo válido → confirmación del app (`Callout` afirmativo con la dirección) y
|
|
|
error limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no y 420px (fila
|
|
|
apilada). Cero errores de página. Gates: `blocks:check` verde (10 blocks) ·
|
|
|
`svelte-check` sin errores propios · prettier limpio en mis ficheros.
|
|
|
**Siguiente**: F2.10 `site-footer`.
|
|
|
|
|
|
- 2026-07-30 — **F2.10 `site-footer` HECHO**. El block donde la regla de forma del
|
|
|
tier se ve mejor: `.Column` SE REPITE → parte compound; marca, alta, social,
|
|
|
legal y extra no se repiten ni coordinan → slots. Landmark `<footer>` real, así
|
|
|
que es `contentinfo` sin pedir nada. Las columnas van en `AutoGrid`
|
|
|
(`minChildWidth`, **cero breakpoints**): el mismo código sirve para 2 grupos o
|
|
|
para 5, y en móvil caen a dos. Entrada solo en la región superior y **sin
|
|
|
escalonado** — una línea de copyright que aparece con fundido es teatro, y nadie
|
|
|
lee un pie columna por columna.
|
|
|
|
|
|
**Los dos huecos que nombraba el dossier, cerrados**: la banda de alta al
|
|
|
boletín EN el pie (3 de 7 en TW) como slot `signup` —el app compone su propio
|
|
|
`Form`, el block NO importa el block `newsletter` (B-10)— y el **selector de
|
|
|
idioma** como `extra`, vivo sobre el eje real: escribe
|
|
|
`uix.prefs.setIntent('language', …)` y se verificó que mueve
|
|
|
`document.documentElement.lang` a `en`.
|
|
|
|
|
|
**Tres números que salieron de medir, no de suponer**: el `container` bajó de
|
|
|
`xl` a **`lg`** (con `xl` el pie no se alineaba con ninguna sección de la página
|
|
|
compuesta); la rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`** (con `2fr` un
|
|
|
pie de cuatro grupos se partía en 3+1); y el gap de columnas es **6, no 8**
|
|
|
(con 8, cinco grupos no comparten fila al ancho `lg` — 728px justos).
|
|
|
|
|
|
**Hallazgos** (F21/F22 en `PLAN-blocks-quality.md` §6): (1) **ningún primitivo de
|
|
|
layout puede cambiar de elemento** — `Text`/`Heading` aceptan `as`, pero `Box` y
|
|
|
todo lo construido sobre él es un `<div>` fijo, así que una columna de enlaces no
|
|
|
puede ser `<ul>/<li>`; (2) **un `Select` controlado muestra el VALOR crudo** hasta
|
|
|
que se abre una vez: `getDisplayText()` resuelve contra el registro de etiquetas
|
|
|
que llenan los `Select.Item` al montarse, y con el `Content` en un portal cerrado
|
|
|
no hay ninguno montado. La demo lo rodea con el `child` de `Select.Value`.
|
|
|
|
|
|
Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL
|
|
|
y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal,
|
|
|
separador `aria-hidden`, los tres `IconButton` con nombre, y el selector de
|
|
|
idioma cambiando el idioma de verdad. Cero errores de página. Gates:
|
|
|
`blocks:check` verde (11 blocks) · `svelte-check` sin errores propios · prettier
|
|
|
limpio. **Siguiente**: F2.11 `banner`.
|
|
|
|
|
|
- 2026-07-31 — **F2.11 `banner` HECHO**. El block más FINO del tier a propósito:
|
|
|
cuando el canon ya tiene la pieza, el block es colocación y nada más. Reenvía la
|
|
|
superficie del `Banner` con `Omit<BannerProps, 'children'>` (no re-declara
|
|
|
`intent`/`variant`/`size` ni los estrecha), lo mete en columna con `Container`
|
|
|
(`width="100%"` + `paddingX={0}`: a sangre por fuera, en columna por dentro),
|
|
|
envuelve con `Wrap` en vez de una fila rígida, y cablea `Banner.Close` solo si
|
|
|
llega `onDismiss`. La demo lo enseña **encima del `site-header` de verdad**, con
|
|
|
página para desplazarse: un aviso dentro de un recuadro no se parece a un aviso.
|
|
|
|
|
|
**Hallazgo real (fase 0 lo anticipó)**: `Banner` estampa `role="banner"` DESPUÉS
|
|
|
de sus rest props, así que no se puede relajar; con el `site-header` en la misma
|
|
|
página quedan **dos landmarks `banner`** (medido). Único paliativo hoy:
|
|
|
nombrarlos con `aria-label`.
|
|
|
|
|
|
**Y una lección de método que costó una sesión**: reporté tres «defectos del
|
|
|
canon» (`aria-label` ausente en `Banner.Close`, `type` desaparecido de todo
|
|
|
`Button`, `IconButton` tragándose el `onclick`) y **los tres eran falsos**. La
|
|
|
causa: medía demasiado pronto. Los `data-variant`/`data-size` los escribe el
|
|
|
componente de eidos al renderizar —están desde el primer frame—, pero `type`,
|
|
|
`aria-label` y los handlers los aplica la **runtime del morfo en un efecto
|
|
|
posterior**. A ~1s: `type` ausente en 3 de 3 botones y ningún clic disparando; a
|
|
|
~6s: `type="button"`, `aria-label="Descartar"` y el descarte funcionando. La
|
|
|
espera válida es un atributo que solo pueda haber puesto la runtime
|
|
|
(`waitForFunction(() => boton.hasAttribute('type'))`), no que el nodo exista ni
|
|
|
que se vea. Regla afinada en `CONTINUE-blocks.md`.
|
|
|
|
|
|
De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre
|
|
|
accesible en `BlockDemo.svelte` — afectaba a las 12 demos del tier.
|
|
|
|
|
|
Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no
|
|
|
(con `no` el botón **deja de renderizarse**, no se oculta), claro/oscuro/RTL y
|
|
|
420px, la tira siempre por encima de la cabecera, cero desbordamiento
|
|
|
horizontal, y los controles de la demo moviendo la vista previa en línea. Gates:
|
|
|
`blocks:check` verde (12 blocks) · `svelte-check` sin errores propios ·
|
|
|
`docs:check` sin errores míos · prettier limpio. **Siguiente**: F2.12 `team`.
|
|
|
|
|
|
- 2026-07-31 — **F2.12 `team` HECHO**. Compound porque `Member` SE REPITE, y el
|
|
|
primero del tier cuyo **contexto sirve para una sola cosa**: el `align` de la
|
|
|
sección viaja a cada `Member` y a su fila de enlaces, así que la foto, el
|
|
|
nombre, el cargo y los enlaces comparten eje sin que el app repita la
|
|
|
alineación en cada tarjeta. Un `align` explícito en una parte siempre gana. Es
|
|
|
la diferencia con `feature-grid`, cuyos ítems solo se repiten y por eso no
|
|
|
tienen contexto: **la regla se aplica parte por parte, no block por block**.
|
|
|
|
|
|
**Dos decisiones de forma**: (1) el nombre es un `Heading level={3}` apagado
|
|
|
con `size="sm"` —una persona en una retícula tiene nombre y las referencias lo
|
|
|
marcan igual—, nivel = estructura, tamaño = tipografía; (2) la fila de enlaces
|
|
|
va pegada al fondo (`marginTop: auto`), porque con biografías de distinto largo
|
|
|
las filas quedaban a alturas distintas y la retícula se leía descuadrada — sin
|
|
|
biografías las tarjetas ya miden lo mismo y la regla no cambia nada.
|
|
|
|
|
|
**Encontrado al verificar**: un `columns` FIJO no colapsa. A 420px, cuatro
|
|
|
columnas dejan celdas de ~90px con el nombre partido en tres líneas y el avatar
|
|
|
desbordando. El default del block es fluido (`minChildWidth`), así que el fallo
|
|
|
era de la demo: ahora pasa `columns={{ base: 2, md: N }}` — para eso están los
|
|
|
props responsivos. Anotado en el README.
|
|
|
|
|
|
Verificado en navegador (esperando el atributo que pone la runtime, no el
|
|
|
reloj): 2/3/4 columnas, `align` center/start propagándose por contexto al eje de
|
|
|
cada miembro, biografía sí/no, claro/oscuro/RTL y 420px, seis avatares, doce
|
|
|
botones de icono **con nombre propio por persona**, escalonado estructural con
|
|
|
índices 0,1,2…, cero desbordamiento y cero errores de página. Gates:
|
|
|
`blocks:check` verde (13 blocks) · `svelte-check` sin errores propios ·
|
|
|
`docs:check` 0 · prettier limpio. **Siguiente**: F2.13 `contact`.
|
|
|
|
|
|
- 2026-07-31 — **CAMBIO DE DOCTRINA (decisión del usuario)**: «si al final es una
|
|
|
lista de componentes… el bloque es coordinación, estado, y data también». Con
|
|
|
el `contact` delante se vio el motivo: con el estado fuera, cada `disabled` es
|
|
|
una expresión distinta montada en el punto de uso y aparecen **huecos** — un
|
|
|
envío que no se puede hacer y nadie dice por qué. Escrito en
|
|
|
`architecture/blocks.md` §«Coordination», con B-5 (el block puede traer la
|
|
|
FORMA de sus datos) y B-7 (posee las palabras de SUS estados, como idlangref
|
|
|
con fallback inglés) enmendados y la convención de servicios acotada: el
|
|
|
traductor es el único servicio sancionado. **No aplica a todos**: los blocks de
|
|
|
layout no coordinan nada y inventarles estado sería el error contrario.
|
|
|
|
|
|
- 2026-07-31 — **F2.13 `contact` HECHO**, el primero que COORDINA. Posee tres
|
|
|
cosas: la máquina de la sección (`state.ts`, 7 estados derivados de una sola
|
|
|
fuente), la forma por defecto de sus datos (`schema.ts`) y las palabras de cada
|
|
|
estado. `CONTACT_REASON` y `CONTACT_ACTION` son `Record` EXHAUSTIVOS: el mismo
|
|
|
mapa que bloquea el botón escribe la frase, así que un estado mudo no se puede
|
|
|
ni escribir. Las partes leen el estado por contexto — ninguna lo recalcula ni
|
|
|
inventa un `disabled`. A la demo se le cayeron el esquema, las etiquetas, el
|
|
|
`disabled`, el flag de envío y la frase: **249 líneas menos**, y lo que queda es
|
|
|
lo que de verdad es del app.
|
|
|
|
|
|
**Encontrado al verificar — y era real**: la máquina leía `form.isValid`, que
|
|
|
con `validationBehaviour: 'progressive'` significa «aún no se ha encontrado nada
|
|
|
mal». Un formulario vacío e intacto no tiene errores → se declaraba válido →
|
|
|
`incomplete` era INALCANZABLE al cargar y la sección pedía resolver la
|
|
|
verificación con los tres campos vacíos: justo el hueco que la doctrina existe
|
|
|
para cerrar. Arreglado preguntando al ESQUEMA (`validateSync` de SIUM:
|
|
|
síncrono, y sin escribir errores en el formulario — `form.validate()` habría
|
|
|
encendido los tres campos en rojo al cargar). Fijado con `state.test.ts`
|
|
|
(9 casos), primera lógica pura del tier y por tanto su primer test unitario.
|
|
|
|
|
|
**Lección de verificación (cara)**: perseguí un fallo inexistente durante
|
|
|
varias rondas porque mi guion esperaba `button[type="submit"]` con atributo
|
|
|
`type` como puerta de hidratación — y **ese atributo viene ya en el HTML del
|
|
|
servidor**, así que la espera se cumplía sobre el documento estático y yo
|
|
|
tecleaba antes de que Svelte tomara los inputs: los valores entraban en el DOM
|
|
|
y `form.values` seguía vacío. Matiza la regla del handoff: no basta con que el
|
|
|
atributo lo escriba la runtime en cliente, tiene que ser algo que **NO exista
|
|
|
en el SSR**. La puerta honesta aquí fue `<style id="uix-blocks-display">` (lo
|
|
|
escribe un `$effect` de `BootUix`) más un ida y vuelta reactivo real. También
|
|
|
costó una pista falsa: el servidor de dev servía módulos rancios y el `console.log`
|
|
|
del derivado salía por el **stdout del servidor** (SSR), no por la consola del
|
|
|
navegador — que es exactamente lo que delató el diagnóstico.
|
|
|
|
|
|
Verificado en navegador, arco completo y las dos ramas: **sin reto** vacío →
|
|
|
un campo → tres campos (se desbloquea) → «Enviando…» → «Enviado» con la frase
|
|
|
afirmativa; **con reto**, con los tres campos llenos el botón sigue bloqueado y
|
|
|
la razón pasa a «Resuelve la verificación de arriba» — es decir, nombra el
|
|
|
bloqueo REAL y en orden. Cero errores de página. Los estados `verifying` y
|
|
|
`rejected` quedan cubiertos por test unitario, no por el reto en vivo: el
|
|
|
provider de `ProofOfHuman` escribe `status` él mismo y el veredicto del app no
|
|
|
se sostiene (hilo de canon abierto). i18n: el arnés registra el namespace
|
|
|
`blocks` en las DOS cadenas de arranque (galería y previews) y la demo se lee
|
|
|
en castellano. Añadido el control vivo que faltaba para un prop público:
|
|
|
`verification` con reto / sin reto — y omitir el prop NO es lo mismo que pasar
|
|
|
un estado que nunca verifica. Gates: `blocks:check` verde (14 blocks) ·
|
|
|
`svelte-check` sin errores propios · `state.test.ts` 9/9 · prettier limpio en
|
|
|
los ficheros propios. **Siguiente**: F2.14 `content-section`.
|
|
|
|
|
|
- 2026-07-31 — **F2.14 `content-section` HECHO → F2 CERRADA 15/15**. El último
|
|
|
de sitio, y el que más cerca estuvo de ser un envoltorio: `Section` +
|
|
|
`Container` + `Prose` no habría añadido nada, porque `Prose` ya trae su medida
|
|
|
de lectura. Lo que sí falta en el ecosistema es el **escape**: dentro de un
|
|
|
`Container` nada puede ser más ancho que él, así que una figura a sangre hay
|
|
|
que sacarla del artículo y ponerla de hermana — y el orden de lectura se
|
|
|
rompe. La sección es una REJILLA de 5 pistas (canalón · flanco · MEDIDA ·
|
|
|
flanco · canalón) y cada parte dice hasta dónde llega: `measure` col 3,
|
|
|
`wide` col 2/5, `full` col 1/-1. Todas las longitudes son tokens del sistema
|
|
|
(`--measure-*`, `--container-width-*`, `--container-padding-inline`).
|
|
|
`.Body` y `.Media` son compound porque SE REPITEN (un artículo alterna prosa y
|
|
|
figuras en el orden en que se lee); la cabecera son slots de snippet.
|
|
|
`.Body` apaga la medida de `Prose`: dos dueños del mismo ancho se pelean y
|
|
|
ganaría el más estrecho en silencio.
|
|
|
|
|
|
**Tres defectos que solo dijo el navegador**, ninguno visible leyendo el
|
|
|
código: (1) **`gap` separa también las COLUMNAS**, y una parte que las cruza
|
|
|
se lleva esos huecos encima — `wide` medía **1088** en vez de los 1024 del
|
|
|
contenedor con el que debe alinearse, y a 420px `full` llegaba a **532** en
|
|
|
una rejilla de 404 y hacía scrollear la página; las columnas aquí son un
|
|
|
instrumento de medida, no cosas puestas al lado, así que van con `rowGap` y
|
|
|
`columnGap={0}`. (2) una pista `min(medida, 100%)` **ignora sus propios
|
|
|
canalones**: a 420px la central se quedaba con los 404 enteros y los canalones
|
|
|
empujaban la rejilla a 436. (3) **`100%` significa algo distinto dentro de
|
|
|
cada parte** — en `measure`/`wide` ya es una pista, en `full` es la rejilla
|
|
|
entera con canalones incluidos —, así que capar el pie igual en los dos casos
|
|
|
lo dejaba a 340 contra una columna de 372 en uno y a 404 en el otro.
|
|
|
|
|
|
Verificado tras los arreglos en **3 viewports × 3 medidas × 3 anchuras**: el
|
|
|
pie tiene el mismo ancho y la misma x que la prosa en las 18 combinaciones,
|
|
|
cero desbordamiento horizontal. Claro,
|
|
|
oscuro y 420px mirados. Semántica propia `figure`/`figcaption` (no
|
|
|
interactivos → estructura de documento, B-8) con el hueco señalado: **el canon
|
|
|
no tiene primitiva `Figure`**. Contexto de CONFIGURACIÓN, no de estado (la
|
|
|
medida, para el pie) — es un block de layout y no coordina nada. Gates:
|
|
|
`blocks:check` verde (15 blocks) · `svelte-check` sin errores propios ·
|
|
|
`docs:check` 0 · prettier limpio. **F2 CERRADA**; siguiente F3 (blocks de
|
|
|
aplicación) o la página compuesta de prueba del cierre de F2.
|
|
|
|
|
|
- 2026-07-31 — **Cambio de CANON pedido por el usuario: un componente no es
|
|
|
librería de otro.** Al tipar `content-section` importé `TextMeasure` desde
|
|
|
`components/text/types`, y el usuario lo cortó: no se importan librerías de
|
|
|
otros componentes; lo compartido se saca a una librería común. Al mirarlo, la
|
|
|
violación ya existía y era mayor que la mía — **`Heading` importaba CINCO
|
|
|
escalas tipográficas de `Text`** (`TextTracking`, `TextLeading`, `TextWrap`,
|
|
|
`TextNumeric`, `TextMeasure`). Las cinco se han movido a
|
|
|
`src/uix/eidos/lib/types.ts`, que es donde su propio encabezado dice que van
|
|
|
(«only types that would otherwise duplicate across components live here») y
|
|
|
donde ya está el precedente documentado del mismo fallo (`ColorRole`
|
|
|
re-escrito a mano que derivó a 8 miembros contra 9). Sin shim de compat:
|
|
|
`text/types.ts` las importa, `heading/types.ts` también, y el bloque igual.
|
|
|
Arrastraba además un error mío anterior: había declarado
|
|
|
`ContentMeasure = 'narrow' | 'normal' | 'wide'`, copia literal del canónico, y
|
|
|
tipado `wide` como `string` libre con default `'var(--container-width-lg)'` —
|
|
|
lo que empujaba la construcción del nombre del token a un fichero de RUTA.
|
|
|
Ahora `measure` es `TextMeasure`, `wide` es `ContainerSize` y el único `var()`
|
|
|
vive en `grid.ts`. Gates tras el movimiento: `check` sin errores en lo tocado
|
|
|
(74 globales, ninguno mío) · 28 de 29 suites de eidos verdes (la que falla es
|
|
|
`audio-player` sin morfo, del hilo de audio, `c4d64ba98`) · `blocks:check` 15.
|
|
|
|
|
|
- 2026-07-31 — **`content-section`: los tres ejes pasan a RESPONSIVE** (decisión
|
|
|
del usuario: «debe de ser responsive»). `measure`, `wide` y el `width` de cada
|
|
|
parte aceptan `ResponsiveProp`, resueltos con `eidos.resolve()` — el mismo
|
|
|
camino que usan los primitivos de eidos, así que los únicos breakpoints en
|
|
|
juego son los canónicos. Es lo que un valor único no puede decir:
|
|
|
`{ base: 'full', md: 'wide' }` = foto a sangre en móvil y contenida de `md`
|
|
|
arriba. **Verificado contra el breakpoint canónico**: a 767 va a sangre, a 768
|
|
|
se contiene. Expuesto como control vivo en la demo.
|
|
|
|
|
|
- 2026-08-01 — **`content-section`: un cuarto defecto, y lo cazó el usuario
|
|
|
mirando la pantalla.** Yo había verificado la geometría INTERNA del block
|
|
|
(medida, escape, pie, breakpoints, responsive) pero nunca lo había mirado
|
|
|
contra la página: la pista `wide` usaba `--container-width-lg` a pelo, que es
|
|
|
el ancho EXTERIOR del `Container`, así que la figura sobresalía **16px por
|
|
|
cada lado** respecto al texto de la sección hermana de debajo — a 1280, 1440 y
|
|
|
1920, siempre los mismos 16. El README afirmaba justo lo contrario («lines up
|
|
|
with the sections above and below»). Arreglado restando
|
|
|
`2 * --container-padding-inline`: la pista ancha en `lg` son **992**, no 1024,
|
|
|
y el desfase pasa a 0 en los tres anchos. Verificado también en el Chrome del
|
|
|
usuario.
|
|
|
|
|
|
**Lección de método**: medir el block contra SÍ MISMO no basta. Un block vive
|
|
|
en una página, así que la comprobación que faltaba era poner una sección
|
|
|
normal debajo y comparar los bordes izquierdos. Y **generé la captura en
|
|
|
oscuro y nunca la abrí** — la regla dice screenshot + MIRAR, y yo me quedé en
|
|
|
la primera mitad. Ojo también con inspeccionar en una pestaña de fondo:
|
|
|
`visibilityState: 'hidden'` suspende el rAF y deja los `data-animation-pending`
|
|
|
puestos con `opacity: 0`, lo que parece un bug de visibilidad y no lo es (en
|
|
|
primer plano, 0 pendientes).
|
|
|
|
|
|
- 2026-08-01 — **AUDITORÍA del tier (50 agentes)**. Dos dimensiones sobre los 15
|
|
|
blocks —doctrina y percepción— con un verificador escéptico por hallazgo:
|
|
|
**90 en bruto → 40 verificados → 29 CONFIRMADOS y 11 refutados (28% de falsos
|
|
|
positivos)**. Congelada en `AUDIT-blocks-2026-08-01.md`, que es la única copia
|
|
|
(el journal del workflow era de sesión). ⚠️ **50 quedaron SIN VERIFICAR, 18 de
|
|
|
ellos marcados ALTA** — el tope de 40 lo puso mi script, no el trabajo.
|
|
|
|
|
|
Lo gordo confirmado: `contact` importa `$libs/forms` fuera de la frontera dura 1
|
|
|
· el nombre accesible del submit de `contact` está congelado en «Enviar» en los
|
|
|
cinco estados porque `Form.Submit` impone su `aria-label`, así que **las palabras
|
|
|
de estado que el block existe para poseer no llegan al lector de pantalla** ·
|
|
|
`site-header` congela la página si el cajón está abierto al cruzar el breakpoint
|
|
|
(reproducido con swipe táctil real) · `hero` en `background` deja la CTA
|
|
|
secundaria a 1.78:1 · `height="100%"` sobre `Card` es INERTE en `pricing` y
|
|
|
`testimonials` mientras README y demo afirman lo contrario · B-8 sin declarar en
|
|
|
seis READMEs.
|
|
|
|
|
|
La dimensión CRUZADA (los 15 en una página compuesta) NO se ejecutó: sigue
|
|
|
pendiente, y es además la condición de cierre de F2 del plan.
|
|
|
|
|
|
**Confirmó la premisa de la auditoría**: la capa perceptiva no se había ejercido
|
|
|
NUNCA (la galería arrancaba sin packs de sema desde F0), y ahí está lo más feo
|
|
|
sin verificar — el nav del header emitiendo `commit-select` + `affirm` con earcon
|
|
|
audible al pasar el RATÓN por encima, un `Enter` en `newsletter` emitiendo dos
|
|
|
`commit-submit` con intents contradictorios, y 4 de 7 elementos del header mudos.
|
|
|
|
|
|
- 2026-08-09 — **Fase 5 del saneamiento: `blocks:check` endurecido** (el último
|
|
|
punto del plan de saneamiento sin empezar; el detalle de las fases 1–4 vive en
|
|
|
`AUDIT-blocks-ledger.md`, que es la fuente viva desde el 2026-08-05). Tres
|
|
|
reglas nuevas, cada una con su fixture negativo en el `selfTest()` que el guard
|
|
|
corre antes de escanear:
|
|
|
1. **Lista blanca de importaciones (B-4)** — la frontera dura 1 deja de ser sólo
|
|
|
prosa: `$uix`, los arts públicos, `$libs/forms`, `svelte` y los relativos del
|
|
|
propio block; lo demás es error con el especificador por delante. **Los arts
|
|
|
se DERIVAN de `src/arts/*`**, no se listan a mano — una lista escrita se
|
|
|
queda atrás el día que aterriza un art. El tier ya cumplía: cero violaciones
|
|
|
al encenderla (cierra la disposición de A-05).
|
|
|
2. **README completo (B-9) + declaración de landmark (B-8)** — las cuatro
|
|
|
secciones de la plantilla y un párrafo `**Landmark + headings**`. Al
|
|
|
encenderla salieron **7 de 15** sin declaración; escritas leyendo la fuente
|
|
|
de cada block (cierra A-14, que sólo pedía tres).
|
|
|
3. **Ficha en el catálogo de demos (B-9)** — el árbol y
|
|
|
`web/routes/blocks/_lib/catalog.ts` tienen que coincidir en los DOS
|
|
|
sentidos: un block que el catálogo no publica es invisible para el raíl, y
|
|
|
un slug publicado sin block detrás es un enlace muerto.
|
|
|
|
|
|
De paso, el escáner **deja de leer los comentarios como código**: los `index.ts`
|
|
|
documentan su uso con un `// import { Cta } from '$blocks/cta'` y una lista
|
|
|
blanca que lee prosa habría empezado a acusar a los ejemplos.
|
|
|
|
|
|
Verificado **en rojo sobre el árbol real**, no sólo contra los fixtures: un
|
|
|
`zod` y un `$libs/days` metidos en `hero/types.ts` salen señalados con su línea
|
|
|
exacta mientras el mismo import dentro de un comentario NO salta; el catálogo
|
|
|
falla en las dos direcciones; y al romper `isAllowedSpec` a propósito, el
|
|
|
self-test aborta el guard en vez de dar verde. Gates: `blocks:check` verde
|
|
|
(15 blocks / 114 ficheros) · `vitest src/uix/blocks` 20/20 · `docs:check` 0/0 ·
|
|
|
`svelte-check` 74E/54W = la línea base medida justo antes.
|
|
|
|
|
|
- 2026-08-17 — **F2b establecida (firma pendiente)**. Contraste fila a fila de
|
|
|
la tabla §P1 del dossier contra los 15 blocks construidos, leyendo el
|
|
|
`types.ts` y los Gaps del README de cada uno: de sus 11 filas, 6 hechas y 4
|
|
|
resueltas por composición / app-land. El resto es la ola **V1…V8**, con su
|
|
|
regla de forma (misma anatomía → `layout`; otra anatomía → hermano; una
|
|
|
variante que obedece al estado de su sección → PARTE, por B-10) y su orden
|
|
|
(baratos primero, `Pricing.Compare` el último). Ninguna fila es un hallazgo
|
|
|
nuevo: las ocho estaban registradas en el README de su block desde julio —
|
|
|
lo que faltaba era la decisión, no la medida. Añadido también el **orden
|
|
|
recomendado de F3**, con `app-shell` primero porque desbloquea A-95 del
|
|
|
ledger y F4.1. La tabla de estado de §0 decía «F0 pendiente (nada
|
|
|
construido)» y F2 «pendiente»: corregidas al estado real.
|
|
|
- 2026-08-17 — **F2b REESCRITA y FIRMADA** el mismo día. La primera versión
|
|
|
(entrada anterior) atacaba sólo el suelo de §P1 con ocho variantes, y eso NO
|
|
|
cierra la brecha que motivó la revisión: la de **cardinalidad percibida**. El
|
|
|
método para esa brecha ya estaba en el dossier (P6.1 — demostrar la
|
|
|
equivalencia variante-a-variante en vez de competir en número de dumps) y no
|
|
|
se trajo. La ola pasa a cuatro tramos: **A** variantes de suelo · **B** la
|
|
|
matriz de equivalencia por block (el entregable central) · **C** el catálogo
|
|
|
que se reabre · **D** la página compuesta. La regla de forma gana su término
|
|
|
que faltaba y que más trabajo hace: **variante alcanzable componiendo →
|
|
|
RECETA demostrada, cero código** — sin él el tier engorda por imitación.
|
|
|
Fichas corregidas: **V1** con sus dos decisiones de diseño destapadas (quién
|
|
|
declara las columnas si `.Plans` y `.Compare` conviven; el colapso móvil) y
|
|
|
tanda propia; **V2** deja de venderse como «misma anatomía» (gana `logo`,
|
|
|
pierde la `Card`) aunque sigue siendo `layout`; **V3** la unión discriminada
|
|
|
alcanza también a `value`/`disabled` de `.Item`; **V4** tiene que decidir si
|
|
|
el formulario REEMPLAZA a `actions`, como en las referencias; **V8** se MIDE
|
|
|
antes de declararse gratis (`PricingPlansProps` es un `Pick` cerrado);
|
|
|
**V6/V7 salen de la ola** a F5 — el dossier declara `stats-band` «Paridad
|
|
|
OK», son adoptables y no suelo, y meterlas diluía E-2.
|
|
|
**Tres decisiones firmadas**: **`logo-cloud` REABIERTO** como block estático
|
|
|
(su premisa de diferimiento no casa con las refs; el valor propio es la tinta
|
|
|
monocroma derivada de tokens, y `Marquee` queda como preset de motion, no
|
|
|
como block) · **`blog` ENTRA** (misma naturaleza que `team`, que ya entró) ·
|
|
|
**`cookie-consent` ENTRA con doctrina propia** (no es una sección: overlay
|
|
|
legal con estado, y la ley europea vive en el TIPO — rechazar al mismo nivel
|
|
|
que aceptar, no esenciales apagadas por construcción) · **`onboarding` NO
|
|
|
entra**: es un `wizard` con contenido de bienvenida, y duplicar una función
|
|
|
con otro nombre es la inflación que este modelo existe para evitar.
|
|
|
**La página compuesta entra como puerta de cierre de F2b** y el cierre de F2
|
|
|
queda enmendado: cerrada EN UNIDADES, con el claim de integración declarado
|
|
|
sin demostrar en vez de dado por probado.
|
|
|
|
|
|
- 2026-08-17 — **F2b: fichas de ejecución escritas** (las cinco que faltaban
|
|
|
para que la ola fuera plan de implementación al nivel de F2): plantilla de
|
|
|
la matriz en el tramo B (5 columnas; vive como `## Equivalencias` en el
|
|
|
README de cada block; `blocks:check` la exige como ÚLTIMA tanda del tramo,
|
|
|
verificada en rojo) · fichas N1 `logo-cloud` (⚠️ la tinta monocroma se
|
|
|
promociona al CANON antes del block — un block no posee CSS), N2 `blog`
|
|
|
(fechas por `FormatDate`/`RelativeTime`; `<article>` choca con F21 → fase 0
|
|
|
decide `as` en `Card` o gap declarado), N3 `cookie-consent` (la ley en el
|
|
|
TIPO: `onRejectAll` requerido, misma prominencia por construcción, no
|
|
|
esenciales apagadas; `Escape` no decide) · ficha de la página compuesta
|
|
|
(`web/routes/blocks/landing/`, criterio de NATURALIDAD — los que no caben
|
|
|
quedan registrados como no-compuestos, la segunda página es Gap con
|
|
|
disparador; verificación con medida por punto: outline, landmarks —los dos
|
|
|
`role="banner"`—, costuras de `sectionSize`, cascadas por viewport, A-95,
|
|
|
sonda de sema, teclado entero).
|
|
|
- 2026-08-17 — **F2b · V4 `form-in-hero` HECHA** (primera fila de la ola). Slot
|
|
|
`form` en `hero`: el app compone `Form` + `Field` + `Form.Submit` enteros y el
|
|
|
block sólo da sitio y medida — **no toma el handle del formulario**, porque
|
|
|
leer la validez de la sección lo convertiría en un block que COORDINA y
|
|
|
tendría que poseer máquina y palabras; un hero no coordina nada. Medido en
|
|
|
Playwright headless sobre píxeles pintados: tinta escrita 15.88:1 claro /
|
|
|
16.28:1 oscuro · relleno del campo contra el lienzo de `background` 9.61:1 ·
|
|
|
la fila colapsa a 375 (campo 325 / botón 325) y comparte a 1280 (335 / 131,
|
|
|
misma fila) · medida propia 480px en `center` contra sección de 1264 y 472px
|
|
|
en `split` (= la columna: el cap `sm` es inerte ahí) · Enter llega al handler
|
|
|
del app y un correo inválido lo para con el error traducido y `aria-invalid` ·
|
|
|
RTL espeja la fila. **Dos gaps de canon registrados en el README del block**:
|
|
|
`Form.Submit` no reenvía al `Button` ni `intent` ni eje de anchura (el botón
|
|
|
no llena su celda al colapsar; `newsletter` mide lo mismo, y el `justify` de
|
|
|
`Grid` es `justify-content`, así que no se arregla desde fuera) — la demo usa
|
|
|
lo que hay (`style`, que sí se reenvía). **Y el contexto de inversión mordió
|
|
|
por segunda vez**: un `Text` sin `color` explícito bajo `background` mide
|
|
|
1.62:1 contra 10:1 con `on-solid`. ⚠️ Anotada además la lección de
|
|
|
INSTRUMENTO: dos sondas dieron números seguros y falsos antes de la buena
|
|
|
(parser rgb sobre un sistema que emite `oklch`, y canvas `fillStyle` que
|
|
|
tampoco normaliza oklch aquí → medía NEGRO contra todo); la señal de que la
|
|
|
sonda es honesta es que la etiqueta del submit sale a 5.18:1, la cifra que el
|
|
|
repo ya documenta para `primary`.
|
|
|
- 2026-08-17 — **F2b · V8 `single-price` HECHA, y la ficha acertó al exigir que
|
|
|
se midiera ANTES**: «un solo `.Plan` y el layout ya lo sostiene» —la
|
|
|
disposición que el README del block llevaba desde julio— era falsa. La fila es
|
|
|
`repeat(auto-fill, minmax(17rem, 1fr))` y **`auto-fill` reserva toda pista que
|
|
|
quepa**, la ocupe alguien o no: medido a 1280 sobre 992px de fila, un plan solo
|
|
|
daba una card de 315px pegada al borde con 677px de vacío al lado, y
|
|
|
`columns={1}` sólo cambiaba eso por una card de 992px. Ninguna de las dos es la
|
|
|
card centrada de las referencias. Resuelto abriendo `.Plans` a **`maxWidth`**
|
|
|
(tope de la FILA, con `marginX="auto"` incondicional para centrarla),
|
|
|
reenviado NOMBRADO al `AutoGrid` — nunca por `{...rest}`, que es justo lo que
|
|
|
A-94 dejó dicho. El número lo pone el app: la demo pasa `28rem` para un plan y
|
|
|
`44rem` para dos. Medido después: 448px centrados con uno, 704px (dos pistas de 340) con dos, y **992px idénticos con tres** — el tope es opt-in y no toca el
|
|
|
caso que ya existía; a 375 es inerte. Control vivo nuevo en la demo (nº de
|
|
|
planes) y en la URL del preview. ⚠️ Lección que se queda: **una fila
|
|
|
«diferida» puede ser una medición equivocada, no un aplazamiento** — nadie
|
|
|
había mirado este block con menos de tres planes.
|