|
|
# 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) | ✅ **CERRADA 2026-08-18** — los cuatro tramos ejecutados |
|
|
|
| **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). **ENMENDADA por la práctica — la doctrina vigente es `architecture/blocks.md` §Services, no esta celda**: el traductor entró el 2026-07-31 (un block posee las palabras de SUS estados) y el anunciador el 2026-08-06 (poseerlas sin poder decirlas es medio trabajo), los dos por `ActiveEidos.require()`; y el breakpoint del sistema (`dom.isAtLeast`) es consumo obligado por B-6, que prohíbe el `matchMedia` propio — `site-header` lo hace desde su construcción. Lo que la celda sigue diciendo bien, y no se ha movido: **ningún block crea servicios ni los cablea por su cuenta** (tema, sesión, permisos, transporte, persistencia). Reafirmado en la fase 0 de F3.1 (2026-08-18) contra la tentación más fuerte que ha tenido el tier: un shell que dibujara el menú de usuario leyendo `App.session`. |
|
|
|
| **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 |
|
|
|
|
|
|
#### Plan de ejecución de V1 — `Pricing.Compare` (firmado 2026-08-17)
|
|
|
|
|
|
**Fase 0 hecha, y con una corrección propia**: el README de `Table` lista el
|
|
|
pinning de columnas como gap diferido, y **es falso a fecha de hoy** — el engine
|
|
|
tiene `enablePinning` por columna e `initialColumnPinning`, soma escribe
|
|
|
`position: sticky` con su offset (`buildColumnStyle`) y el recipe pinta
|
|
|
`[data-pinned]` con un borde que revela lo que hay detrás. La primera versión de
|
|
|
esta fase 0 se apoyó en el README y dio un dato equivocado; el paso 1 lo corrige.
|
|
|
|
|
|
**Las dos decisiones que la ficha reservaba, resueltas por doctrina:**
|
|
|
|
|
|
- **(a) Quién declara los planes → el `createTable` del app, y `.Compare` lo
|
|
|
COMPONE.** B-5 admite `items={…}` «sólo donde el canon compuesto ya es
|
|
|
data-driven», y `Table` lo es: exige la instancia de `createTable`. Así que
|
|
|
`.Compare` NO inventa un formato de matriz propio ni lo traduce por dentro: el
|
|
|
app crea la tabla como en cualquier otra del ecosistema y `.Compare` la recibe.
|
|
|
`.Plans` y `.Compare` CONVIVEN (las refs muestran ambos), alimentados por el
|
|
|
MISMO array del app — sin registro por contexto (acoplamiento por orden de
|
|
|
montaje) y sin sustitución. Descartadas: matriz propia (viola B-5 y duplica el
|
|
|
engine) · registro de `.Plan` en contexto (acopla dos partes que no se hablan).
|
|
|
- **(b) Móvil → scroll horizontal con la columna de características FIJADA.**
|
|
|
Es lo que hacen las referencias y el canon ya lo da (`enablePinning` +
|
|
|
`initialColumnPinning: { left: [...] }`); cero código nuevo, cero gap.
|
|
|
Descartado el colapso a lista por plan: el mismo contenido dos veces, y una
|
|
|
tabla que deja de ser tabla pierde la navegación por celdas que es la razón de
|
|
|
existir de una comparación.
|
|
|
|
|
|
**Qué es `.Compare` (y qué no):**
|
|
|
|
|
|
- **PARTE** de `pricing` (término 4 de la regla de forma): coordina con el
|
|
|
periodo — la fila de precio lee el contexto y muestra la cifra mensual o anual;
|
|
|
el app entrega ambas por snippet, como en `PlanPrice`.
|
|
|
- **Coloca**: fija la columna de características, y sobre las columnas de plan
|
|
|
aplica `featured` (acento) y `state` (`current` atenuado · `selected` con
|
|
|
anillo) — los mismos ejes que `.Plan`, así que **el estado se declara UNA vez
|
|
|
por plan y viaja a las dos vistas**.
|
|
|
- **Posee la semántica**: la tabla lleva su `label` (nombre accesible; el canon
|
|
|
`Table` lo tiene) y las celdas booleanas (✓ / —) el `aria-label` que sus glifos
|
|
|
no dan — palabras del block por idlangref con fallback inglés, como
|
|
|
`PRICING_PLAN_REASON`.
|
|
|
- **No traduce, no formatea, no inventa**: precios por snippet, palabras del app.
|
|
|
|
|
|
**Pasos, cada uno con su verificación:**
|
|
|
|
|
|
1. **Corregir el README de `Table`** (fila «Column pinning» → HECHO, con las tres
|
|
|
capas nombradas) → `docs:check`.
|
|
|
2. **`compare-langs`**: idlangrefs `#?blocks.pricing.compare.included|Included` ·
|
|
|
`.notIncluded|Not included` (para el `aria-label` de las celdas booleanas) →
|
|
|
fila en `blocks-langs.ts` del arnés y test que afirma prefijo + fallback.
|
|
|
3. **`.Compare`** (`pricing-compare.svelte` + tipos): recibe `table`
|
|
|
(`TableInstance`), `label`, y un `plans` alineado con las columnas — `Record`
|
|
|
por id de columna → `{ featured?, state? }` — para pintar acento/estado; fija
|
|
|
la primera columna si el app no pinneó ninguna (default sensato, opt-out con
|
|
|
`pinFeatures={false}`); la fila de precio compone `PlanPrice`; las celdas
|
|
|
booleanas componen `Icon.Check` + `aria-label` → `svelte-check` 0 en `pricing`
|
|
|
- `blocks:check` verde.
|
|
|
4. **Demo**: `createTable` del app con la matriz real (features × 3 planes) ·
|
|
|
control vivo `comparar` sí/no · `.Plans` y `.Compare` a la vez · el toggle de
|
|
|
periodo mueve la fila de precio de la tabla Y las cards a la vez → medido.
|
|
|
5. **Medir** (Playwright, píxeles y AX): 1280 → la tabla es `<table>` real con
|
|
|
`caption`/`aria-label`, columnas de plan con `data-featured`/estado, precio
|
|
|
cambia con el periodo · **375 → la tabla scrollea en horizontal y la columna
|
|
|
de características queda FIJA** (`position: sticky`, `left: 0`, borde de
|
|
|
`[data-pinned]`) · lectura AT: cada celda anuncia fila + columna, ✓/— con
|
|
|
nombre · claro/oscuro · RTL (la columna fija va a `right`).
|
|
|
6. **README + registro** (§«The comparison table» con las dos decisiones y las
|
|
|
medidas; fila de Gaps → SHIPPED; entrada en §7 del plan; handoff).
|
|
|
|
|
|
**Riesgos que el agente debe saber:**
|
|
|
|
|
|
- La otra sesión está tocando `hero/` y `eidos/generated` (componente
|
|
|
`background`): stage SÓLO `src/uix/blocks/pricing`, `web/routes/blocks/pricing`,
|
|
|
`web/routes/blocks/_lib/blocks-langs.ts`, `src/uix/eidos/components/table/README.md`
|
|
|
y los docs de proceso. Nada de `git add -A`, nada de `stash`.
|
|
|
- `Table` con `maxHeight` envuelve su propio scroll; sin él, pega al ancestro.
|
|
|
Aquí NO hay `maxHeight`: el scroll horizontal lo da el `overflow-x` del
|
|
|
contenedor de la tabla — comprobar qué envuelve el recipe antes de asumir.
|
|
|
- El pinning en RTL: soma escribe `${pinned}:${offset}px` con la posición FÍSICA
|
|
|
(`left`/`right`). El plan pide medir en RTL, no asumir que espeja.
|
|
|
|
|
|
**Fuera de la ola, a F5 con disparador** — el dossier declara `stats-band`
|
|
|
**«Paridad OK»**, así que estas dos son _adoptables_, no suelo, y meterlas aquí
|
|
|
diluiría E-2 (que dice suelo = v1, no «todo lo que exista en una referencia»):
|
|
|
|
|
|
- **V6** `stats-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` — ✅ EJECUTADA 2026-08-18
|
|
|
|
|
|
> **Cerrada.** Lo entregado y las dos desviaciones, antes de la ficha original:
|
|
|
>
|
|
|
> - **Una sola llamada requerida, `onDecision(consent, via)`**, en vez de los
|
|
|
> `onAcceptAll` + `onRejectAll` que la ficha pedía (la propia ficha ya
|
|
|
> contenía las dos formas). Es **más fuerte**, no menos: con dos callbacks el
|
|
|
> consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo
|
|
|
> renderiza el block y el API no expone knob para tocarlo. `via` distingue los
|
|
|
> tres caminos para el log del app.
|
|
|
> - **`Dialog`, no `Drawer`** — la fase 0 lo decidía con la guía delante: el
|
|
|
> panel es una decisión corta con una salida clara, no una superficie que se
|
|
|
> arrastra.
|
|
|
> - **La barra NO es un `Card`**: `data-depth="overlay"`, el plano del sistema.
|
|
|
> El `Card variant="outline"` fue el primer intento y midió 1.00:1 contra la
|
|
|
> página — la barra se disolvía en ella.
|
|
|
> - **Tres gaps de canon** salieron al componerla, todos en el README: `Button`
|
|
|
> acepta `ref` y no lo reenvía (forma de A-94) · `Switch` no tiene parte de
|
|
|
> etiqueta · el `Dialog` restaura el foco a un nodo muerto cuando su
|
|
|
> disparador se ha desmontado.
|
|
|
> - Medido en Chrome real (el pane del navegador no compone frames y daba foco
|
|
|
> ficticio): tres caminos, teclado, anuncio capturado en la región polite,
|
|
|
> claro/oscuro (5.96:1 / 8.79:1 en las dos acciones), 375, RTL.
|
|
|
|
|
|
- **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 — ✅ EJECUTADA 2026-08-18
|
|
|
|
|
|
> **Cerrada.** `web/routes/blocks/landing/` con 14 de los 18 blocks, a sangre.
|
|
|
> Las medidas viven en su README con fecha e instrumento (Playwright headless).
|
|
|
> Lo que salió, resumido:
|
|
|
>
|
|
|
> - **Outline** 1 `h1`, 22 encabezados, 0 saltos · **landmarks** sin duplicado
|
|
|
> anónimo · **ritmo** 0px de hueco entre secciones (el aire es el `padding` de
|
|
|
> cada `Section`; costuras 112–192px) · **motion** 4 de 5 cascadas esperan a su
|
|
|
> sección · **sema** un solo `commit-submit` · **teclado** 70 tabulaciones sin
|
|
|
> trampa y el drawer devuelve el foco.
|
|
|
> - **A-95 RE-CONFIRMADO con cifras**: con `affix="top"` el solape es 49px — la
|
|
|
> altura entera de la cabecera — y el hit-test cae en la tira. `app-shell`
|
|
|
> (F3.1) sigue siendo la pieza que falta; la página compone en flujo.
|
|
|
> - **Un defecto arreglado**: `Newsletter.Reason` medía **2,33:1** sobre el panel
|
|
|
> (la nota a su lado, 8,5:1) — la frase que explica por qué la acción está
|
|
|
> bloqueada no se podía leer. Ahora toma la tinta del panel: **8,83:1**.
|
|
|
> - **Dos gaps nuevos**: los DOS landmarks `banner` (el canon estampa el `role`
|
|
|
> después de los rest props) y **`feature-split` sin parte `.Header`**, el único
|
|
|
> block de sección que no la tiene.
|
|
|
> - **El catálogo aprendió a publicar páginas**: `kind: 'page'` en `BlockEntry`, y
|
|
|
> el guard lo salta en las dos direcciones (self-test propio, probado por
|
|
|
> mutación).
|
|
|
|
|
|
- **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` — fase 0 FIRMADA 2026-08-18
|
|
|
|
|
|
> La ficha original (cinco líneas) se escribió antes de que existieran el canon
|
|
|
> `Sidebar` y la capa `affix`, y no miraba el ecosistema. Ésta la sustituye. Lo
|
|
|
> que la motivó: el autor preguntó si el alcance del shell tenía delante
|
|
|
> `active-app` y `active-uix` enteros. No los tenía.
|
|
|
|
|
|
- **Función**: la geometría de una página de aplicación — el esqueleto que
|
|
|
POSEE las regiones, sus landmarks y sus alturas, y por tanto el único sitio
|
|
|
donde el aviso, la cabecera pegada y el contenido dejan de pelearse.
|
|
|
- **Compone**: `Sidebar` (F1.7, que ya trae `<aside>`+`<nav>`+`<main>`, el
|
|
|
drawer móvil y los tres modos de colapso) + `Box`/`Grid` para la rejilla +
|
|
|
`Sticky` (con `root`, el scroll-host) + el nuevo canon `SkipLink` + el
|
|
|
`Container` de la medida. **NO** `ScrollArea` en v1: el scroll del main es
|
|
|
`overflow` de una caja, y montar barras propias sobre la región que scrollea
|
|
|
toda la app es una decisión visual que nadie ha pedido (Gap).
|
|
|
- **API**:
|
|
|
|
|
|
```svelte
|
|
|
<AppShell scroll="main|body" nav={{ collapsible, side, mobileBreakpoint }}
|
|
|
bind:navOpen aside={{ breakpoint }} bind:asideOpen>
|
|
|
<AppShell.Banner> <!-- aviso EN FLUJO, altura intrínseca -->
|
|
|
<AppShell.Nav> <!-- compone Sidebar; la app rellena las filas -->
|
|
|
<AppShell.Header> <!-- <header> DENTRO del inset (decisión Q3) -->
|
|
|
<AppShell.Main> <!-- <main id> — destino del skip-link -->
|
|
|
<AppShell.Aside> <!-- <aside aria-label>, plegable por breakpoint -->
|
|
|
<AppShell.Footer> <!-- <footer>, opcional -->
|
|
|
</AppShell>
|
|
|
```
|
|
|
|
|
|
- **Landmark**: 1 `banner` (`<header>`) · 1 `main` · el `navigation` del
|
|
|
`Sidebar` · `complementary` ×2 NOMBRADOS (panel del Sidebar + Aside) · 1
|
|
|
`contentinfo`. Sin heading propio: un shell no es una sección.
|
|
|
- **v1**: los dos modelos de scroll, el colapso del nav por las costuras del
|
|
|
`Sidebar`, el aside plegable, y los skip-links generados de las regiones
|
|
|
MONTADAS.
|
|
|
- **Excepción B-10 declarada**: `SHELL_ALLOWLIST` ya existe en
|
|
|
`blocks-check.ts:44` y está probada por fixture — el shell puede componer
|
|
|
`banner`, `site-footer`… sin tocar el guard.
|
|
|
|
|
|
#### Las cuatro decisiones firmadas (2026-08-18)
|
|
|
|
|
|
| # | Decisión | Firmado | Por qué |
|
|
|
| ------ | ------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| **Q1** | Frontera de servicios | **el block NO cablea servicios** | La doctrina queda intacta (`blocks.md` §Services + D-BLK.6). La integración del ecosistema se demuestra donde de verdad vive: en una **app de referencia** en app-land (abajo). Un block que leyera `App.session` sería una raíz de composición disfrazada. |
|
|
|
| **Q2** | Modelo de scroll | **`scroll="main"` y `scroll="body"`, por prop** | `main` (rejilla `100dvh`, sin `fixed`, sin números) es lo que cierra **A-95** por construcción; `body` es lo que `docs-shell` (F4.1) necesita para anclas, TOC pegada y la barra de URL móvil. Es el eje que Mantine expone como `mode` y Atlassian como `isFixed` por slot. |
|
|
|
| **Q3** | Colocación de la cabecera | **`alt`: `<header>` dentro del inset** | El provider del `Sidebar` **ES** la fila flex (`soma/components/sidebar/components/sidebar.svelte` renderiza el `<div data-sidebar>`), así que el `Trigger` sólo vive dentro de ella. Una cabecera a todo lo ancho POR ENCIMA del nav exigiría partir ese provider — cambio de canon que v1 no paga. Es lo que hacen shadcn y Mantine `layout='alt'`. |
|
|
|
| **Q4** | Skip-links | **canon `SkipLink` + uno por región montada** | Técnica **G124** de la WCAG 2.4.1 («links at the top of the page to each area of the content»), que es lo que Atlassian genera. Se emiten de las regiones que EXISTEN, no de una lista escrita: el mismo contrato que estampa el landmark estampa su enlace. |
|
|
|
|
|
|
Consecuencias registradas: el eje `layout` del `Sidebar` (provider ≠ fila)
|
|
|
queda como **gap de canon para v2**, y lo pedirá `docs-shell` si quiere topbar
|
|
|
a todo el ancho. `ScrollArea` sobre el main queda como Gap del block.
|
|
|
|
|
|
#### Comprobado en código antes de escribir esto (2026-08-18)
|
|
|
|
|
|
- `Sidebar.Inset` acepta **`child`** — el shell lo renderiza como caja y mete
|
|
|
dentro `<header>` + `<main>`, así que el `<main>` no acaba envolviendo a la
|
|
|
cabecera. Sin esto, Q3 no era ejecutable.
|
|
|
- `Box` expone `minHeight` / `position` / `overflow` (`LayoutLengthValue =
|
|
|
number | string`, así que `100dvh` pasa crudo) y `Grid` expone
|
|
|
`templateRows` / `templateColumns`: **la rejilla del shell no necesita CSS
|
|
|
propio** — D-BLK.2 se cumple sin excepción, como en `hero`.
|
|
|
- `Sticky` expone `root` (el scroll-host): bajo `scroll="main"` la cabecera se
|
|
|
pega contra el main, no contra el viewport.
|
|
|
- ⚠️ `Sticky.offset` y `AnchorNav.topOffset` son **números px**. Bajo
|
|
|
`scroll="body"` un raíl que deba anclarse a la altura de la cabecera necesita
|
|
|
ese número; el shell publica la altura como token, y **quien la consuma en px
|
|
|
es F4.1** — allí se decide si `offset` gana longitudes CSS (Gap ya declarado
|
|
|
en el README de `sticky`).
|
|
|
|
|
|
#### El suelo de referencia (contrastado 2026-08-18, fuentes leídas)
|
|
|
|
|
|
Mantine `AppShell` · shadcn `Sidebar`+blocks · AntD `Layout`/`ProLayout` ·
|
|
|
Polaris `Frame`/`TopBar` · Atlassian (`page-layout` DEPRECADO +
|
|
|
`navigation-system` actual) · Toolpad `DashboardLayout` · Tailwind Plus
|
|
|
(Stacked 9 · Sidebar 8 · Multi-Column 6).
|
|
|
|
|
|
- **Suelo (≥3 refs)**: propiedad de la altura de cabecera · colapso
|
|
|
offcanvas/icono/none · drawer móvil · breakpoint como prop · aside/panel
|
|
|
derecho · padding/inset · layouts alt/multi-columna · landmarks por
|
|
|
construcción · RTL. De esos, **el `Sidebar` ya nos da cuatro**; el shell debe
|
|
|
los otros cinco.
|
|
|
- **Suelo-2**: skip-links · persistencia · atajo · ranura de ribbon ·
|
|
|
**el modelo de scroll como API** · geometría como CSS vars (Mantine 8
|
|
|
`--app-shell-*`; Atlassian 7 con fallback).
|
|
|
- **Superación posible**: los landmarks y sus skip-links salen del MISMO
|
|
|
contrato (nadie lo hace: Polaris pide un `ref`, Atlassian pide `id` +
|
|
|
`skipLinkTitle` a mano); dos `complementary` nombrados; RTL lógico; tokens,
|
|
|
densidad y modo de serie.
|
|
|
- ⚠️ **Riesgo, y hay que mirarlo de frente**: **Skeleton RETIRÓ su `AppShell`
|
|
|
en v3** — «the number of issues introduced by it far outweigh its potential
|
|
|
gains» (a11y en navegadores móviles con `100vh`, historia de sticky header
|
|
|
confusa). Por eso `scroll` es explícito, la altura va en `100dvh` y en móvil
|
|
|
no se fuerza el scroll propio del main.
|
|
|
|
|
|
#### Correcciones al dossier que esta fase 0 obliga
|
|
|
|
|
|
El dossier (`RESEARCH-blocks-references.md`) se corrige en el mismo pase: su
|
|
|
fila de `app-shell` afirmaba que **ninguna referencia trae skip-link** (Polaris
|
|
|
y Atlassian sí) y atribuía el modo icon-rail a Mantine (no lo tiene). Detalle y
|
|
|
fecha, en el propio dossier.
|
|
|
|
|
|
#### La app de referencia (donde SÍ se integra el ecosistema)
|
|
|
|
|
|
`web/routes/blocks/app/`, publicada en el catálogo con `kind: 'page'` como la
|
|
|
landing. Es el «Cierre F3» adelantado y, sobre todo, **la primera raíz de
|
|
|
composición en modo attach del repo**: `createActiveApp` → `attachActiveUix` →
|
|
|
`<Uix>` → `Soma.create()` → `ActiveEidos.create({ modeSource, densitySource })`
|
|
|
→ `createActivePrefsDomProjection` (en attach NO se proyecta sola) →
|
|
|
`createPrefsStorageBridge` → `setActiveApp` · `setBus` · `setPermsContext` ·
|
|
|
`applyStandardOrca`.
|
|
|
|
|
|
Hoy **ningún** shell del repo hace nada de eso: los siete arrancan
|
|
|
`createActiveUix` standalone, ninguno usa el canon `Sidebar`, ninguno tiene
|
|
|
skip-link, y cada uno reescribe a mano su `localStorage` y su `Set<listener>`
|
|
|
de modo. La app de referencia existe para que el cableado se escriba UNA vez y
|
|
|
para que cada punto que duela quede documentado como candidato (un helper de
|
|
|
fuentes visuales, una guía de raíz de app), nunca resuelto en silencio.
|
|
|
|
|
|
### 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.
|
|
|
- **⚠️ Abierto para su fase 0 (anotado 2026-08-18)**: la ficha dice CUATRO
|
|
|
vistas y el dominio tiene SEIS. El `ViewType` de Supabase —la enumeración
|
|
|
más limpia del oficio— es `sign_in · sign_up · magic_link ·
|
|
|
forgotten_password · update_password · verify_otp`: nos faltan **magic-link**
|
|
|
y **update-password**, y esta última no es un lujo (es la pantalla a la que
|
|
|
aterriza el enlace de recuperación; sin ella el camino de `recover` no
|
|
|
termina). Se decide en su fase 0, no aquí. Dos hilos de canon la esperan y
|
|
|
hay que mirarlos antes: `Form`/SIUM con `progressive` no expone mensajes en
|
|
|
un envío inválido, y el provider de `ProofOfHuman` escribe su propio
|
|
|
`status`, así que el veredicto del app no se sostiene.
|
|
|
|
|
|
### 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). ⚠️ **DESFASADA (visto en la fase 0
|
|
|
de F3.1, 2026-08-18)**: `Command.Dialog` **ya trae el atajo**
|
|
|
(`shortcut`, por defecto `'mod+k'`, con `open` bindable y `onOpenChange`;
|
|
|
`eidos/components/command/types.ts:41-52,96-99`). El listener a mano no hay
|
|
|
que escribirlo: se pasa el prop, o se pone a `null` para apagarlo. Lo que
|
|
|
sigue vigente de E-5 es lo otro: el art `shortcuts` no nace por esto.
|
|
|
- **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.
|
|
|
- 2026-08-17 — **F2b · V5 `cta` split-with-media HECHA**. Tercer `layout` +
|
|
|
slot `media`, con dos decisiones que la ficha no traía: la copia **conserva
|
|
|
sus acciones en la MISMA columna** (separarlas dejaría el botón bajo la
|
|
|
captura, lejos de la frase que lo pide) y **`split` sin `media` cae a
|
|
|
`justified`**, no a una rejilla con una pista vacía — la regla que ya sigue el
|
|
|
`split` del `hero`. Medido: a 1280 dos pistas de 436px en un panel de 992; a
|
|
|
375 una pista, con el CTA POR ENCIMA de la media al apilar; idéntico en claro
|
|
|
y oscuro. Demo: `split` en el control de layout + un `Mockup` en el slot.
|
|
|
- 2026-08-17 — **`Card` gana el eje `lift`, y `pricing` lo reenvía** (petición del
|
|
|
usuario: «¿por qué no hay hover en la card del precio? debería estar al menos
|
|
|
como opción»). Empezó con una afirmación mía FALSA —«`Card` no expone estado de
|
|
|
puntero»—: el realce existía desde siempre, con sus dos tokens en la fundación,
|
|
|
pero **colgaba entero de `[data-interactive]`**, que promueve la tarjeta a
|
|
|
`<button>` y emite `commit-select`. Una tarjeta de plan lleva su CTA dentro, así
|
|
|
que ese paquete es inaplicable: `<button>` en `<button>`. La elevación pasa a su
|
|
|
propio attr **visual-wrapper** (`data-lift`, fuera del morfo por la doctrina del
|
|
|
2026-08-15, con fila en `component-visual-attrs.test.ts` — la primera de `card`);
|
|
|
`interactive` lo implica, así que nada cambia para una tarjeta clicable. Sin
|
|
|
tokens nuevos. `PricingPlanProps` gana `lift` y la demo su control vivo. Detalle
|
|
|
y medidas: `theming/changelog.md` §49.
|
|
|
**Y destapó un hueco del propio guard**: el comentario que explica por qué NO se
|
|
|
usa un nativo nombra `` `<button>` ``, y **B-2 leía comentarios como código** —
|
|
|
su barrido quitaba sólo `<!-- … -->` de UNA línea, mientras `stripComments`, que
|
|
|
maneja todas las formas y conserva los saltos, ya existía tres líneas más abajo
|
|
|
y se usaba sólo para los imports. Es la lección de la fase 5 aplicada a una regla
|
|
|
y no a la otra. Arreglado, con **dos** fixtures nuevos (nativo nombrado en
|
|
|
comentario multilínea = prosa · nativo REAL debajo de un comentario multilínea =
|
|
|
error) y **probado en rojo sobre el árbol real**: un `<button>` inyectado en
|
|
|
`hero.svelte` sale con su línea exacta (158) y el árbol se restaura después.
|
|
|
- 2026-08-17 — **`pricing` gana el estado del plan y, con él, PALABRAS** (firma del
|
|
|
usuario: la decoración del elegido y la propiedad de «no disponible» para
|
|
|
upgrade / downgrade). Su README decía lo contrario desde julio —«nada en una
|
|
|
tabla de precios puede ser bloqueado, así que no hay nada que explicar»— y eso
|
|
|
se cae en cuanto el visitante ya está EN un plan: el actual no se puede elegir.
|
|
|
Ahí manda la lección de `contact`: **una acción bloqueada debe su frase**.
|
|
|
Forma: **`.Plan state`** = `available | current | selected`, un enum y no tres
|
|
|
booleanos (permitirían «elegido y no disponible» a la vez), **ortogonal a
|
|
|
`featured`** —que es la recomendación de quien vende, no dónde está quien
|
|
|
compra—; el estado se deriva UNA vez en `.Plan` y viaja por contexto; `.PlanAction`
|
|
|
se lo entrega al `Button` del app por parámetro del snippet, así que nadie
|
|
|
re-deduce un `disabled` en el punto de uso y las palabras del botón siguen
|
|
|
siendo del app (B-7); **`.PlanReason`** (parte nueva) dice la frase y posee el id
|
|
|
al que apunta el `aria-describedby` del botón bloqueado — y no renderiza nada
|
|
|
cuando no hay nada que decir. Las palabras son del block, idlangref con fallback
|
|
|
inglés en un `Record` EXHAUSTIVO (`plan-state.ts` + `plan-state.test.ts`, que
|
|
|
afirma en las dos direcciones: todo estado que bloquea tiene frase, y todo
|
|
|
estado que no bloquea calla). **La decoración es del CANON**: `selected` →
|
|
|
el anillo de `Card`, `current` → su tratamiento deshabilitado. Nada de
|
|
|
`data-pricing-selected`: sería un attr que nadie lee, porque un block no pinta
|
|
|
CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale
|
|
|
con `cursor: not-allowed`, botón deshabilitado y `aria-describedby` que CASA con
|
|
|
el id de la frase traducida; al elegir otro, su tarjeta gana el anillo primary
|
|
|
de 2px y el resto no.
|
|
|
⚠️ **Gap de canon medido aquí, sin tocar: `Card disabled` NUNCA atenúa.** La
|
|
|
regla existe y su `cursor: not-allowed` sí aplica, pero la animación de montaje
|
|
|
(`card-emerge`, `fill-mode: both`) fija `opacity: 1` con prioridad de animación
|
|
|
y se come la del estado — probado aislándolo: con `data-no-emerge` en el mismo
|
|
|
nodo, atenúa a 0.4. Es el patrón KNOWN-FRAGILE de la doctrina de motion (dos
|
|
|
dueños, una propiedad, un nodo) y alcanza a TODA `Card` deshabilitada del
|
|
|
ecosistema. Reparación natural: atenuar por `filter: opacity(...)`, que la
|
|
|
animación no posee, sin tokens nuevos. Espera firma.
|
|
|
- 2026-08-17 — **F2b · V2 `testimonials` spotlight HECHA**. La brecha del dossier
|
|
|
para este block: la cita única es la variante MODAL de las referencias y el grid
|
|
|
la minoría (2 de 8 en TW). Entra como **`layout="grid" | "spotlight"`**, no como
|
|
|
hermano — la anatomía no cambia (cita · autor · avatar), sólo la disposición, y
|
|
|
el precedente es el `background` del `hero`, que también redefine lo que hacen
|
|
|
sus partes; el hermano se reabre SÓLO si un día pide rotación o carrusel, que es
|
|
|
otro componente. **Viaja por contexto de CONFIGURACIÓN** (el patrón del `align`
|
|
|
de `feature-grid`/`team`, no la coordinación de `pricing`) porque `spotlight` no
|
|
|
es una piel: `Items` deja de ser rejilla **y de estampar `data-stagger`** —el
|
|
|
ritmo estructural es para una FILA—, `Item` **pierde la `Card`** —enmarcar una
|
|
|
cita sola la hace parecer un ítem de una lista que no existe—, `Quote` sube a
|
|
|
`xl` y se centra **con `as="p"`** (F17: sin él el centrado sería mentira) y
|
|
|
`Author` pasa debajo. La medida de la sección se estrecha a `md`. Slot nuevo
|
|
|
**`logo`** en `.Item`, en las dos disposiciones: la marca de quien firma es lo
|
|
|
que hace que la cita se lea como evidencia. Medido: grid → 5 tarjetas, 20px,
|
|
|
container 1024, stagger; spotlight → **0 tarjetas**, una cita de 28px centrada en
|
|
|
`<p>`, container 768, autor centrado, sin stagger; a 375 la cita cae a 24px. El
|
|
|
grid queda idéntico.
|
|
|
⚠️ **Aviso de árbol compartido**: `svelte-check` marcó 73 durante esta tanda y
|
|
|
ninguno es del tier — otra sesión está a medias con un componente `background`
|
|
|
(morfo, langs y `eidos/generated` tocados). Medido con todo en stash: la base
|
|
|
real sin nadie es 60. **No volver a hacer `git stash --include-untracked` en este
|
|
|
árbol**: arrastra el trabajo sin commitear de la otra sesión.
|
|
|
- 2026-08-17 — **F2b · V3 `faq` lista estática HECHA**. La brecha del dossier y la
|
|
|
mayoría del formato: **6 de 7 en TW no son acordeón**. Entra como
|
|
|
**`.List layout="accordion" | "list"`**, y el sitio del eje importa: vive en la
|
|
|
LISTA, no en la raíz, porque la lista es lo único que cambia —el header se lee
|
|
|
igual— y porque esa colocación es la que permite que **el TIPO diga la verdad**.
|
|
|
`FaqListProps` pasa a ser **unión discriminada**: bajo `accordion` atraviesa
|
|
|
toda la superficie del `Accordion` del canon; bajo `list` **no se ofrece
|
|
|
ninguna**, porque una rejilla estática no tiene estado abierto que enlazar, nada
|
|
|
que colapsar ni modo simple/múltiple. Es A-94 aplicado donde iba a morder
|
|
|
después. El `Item` lee la disposición del CONTEXTO —no puede discriminar sobre
|
|
|
la unión de la lista sin obligar a la app a repetir el arreglo en cada
|
|
|
pregunta— y sus `value`/`disabled` quedan documentados como del arreglo
|
|
|
`accordion`, la misma convención que el `backdrop` del `hero` y el `media` del
|
|
|
`cta`. **Bajo `list` la pregunta es un `h3` de verdad**: en una lista estática ES
|
|
|
el encabezado de su respuesta, mientras que bajo `accordion` el canon la mete en
|
|
|
el disparador y esa semántica es suya — el block no inventa una segunda. Medido
|
|
|
a 1280: accordion → 5 disparadores, 0 encabezados, medida 768, respuestas
|
|
|
ocultas; list → 0 disparadores, **5 `h3`**, tres pistas de 309px, medida 1024 y
|
|
|
**todas las respuestas visibles sin pulsar**; a 375 cae a una columna
|
|
|
conservando ambas cosas. El acordeón queda idéntico.
|
|
|
- 2026-08-17 — **F2b · V1 `Pricing.Compare` HECHA — tramo A CERRADO (6/6)**. La
|
|
|
brecha recurrente del dossier en pricing. **PARTE y no hermano**: tiene que
|
|
|
obedecer al periodo de la sección y sólo una parte lee ese contexto (B-10 le
|
|
|
prohíbe a un hermano importar el block que lo posee) — vivir dentro de
|
|
|
`<Pricing>` es también lo que permite que la app meta un `PlanPrice` en una
|
|
|
celda y cambie con el conmutador, sin cablear nada. **La app construye la tabla
|
|
|
y el block la COMPONE**: B-5 admite datos justo donde el canon compuesto ya es
|
|
|
data-driven, y `Table` lo es (exige la instancia de `createTable`), así que
|
|
|
`.Compare` no inventa formato de matriz ni traduce. `.Plans` y `.Compare`
|
|
|
CONVIVEN, alimentados por el mismo array.
|
|
|
⚠️ **Dos cosas que el plan sketchó y construirlas demostró equivocadas**,
|
|
|
registradas y no calladas: (1) **sin `Record` por plan** — acentuar es componer
|
|
|
canon, que es lo que ya permite el snippet `columnHeader`; el `Record` habría
|
|
|
sido una segunda fuente de lo que la app declara en su `.Plan`; (2) **el block
|
|
|
NO pinnea**: el motor es del app y mutarlo desde el block sería decidir cómo se
|
|
|
comporta su tabla. La app declara `enablePinning` + `initialColumnPinning`.
|
|
|
**Posee** el nombre accesible de la tabla y las palabras que una marca ✓/— no
|
|
|
dice (`PRICING_COMPARE_VALUE`, `Record` exhaustivo con test en las dos
|
|
|
direcciones; el glifo va `aria-hidden`). **Y absorbe el cast del canon** siendo
|
|
|
genérico: `Table` tipa `TableInstance<unknown>` mientras `createTable<T>`
|
|
|
devuelve `TableInstance<T>` — todo consumidor castea, su propia demo incluida —,
|
|
|
así que `.Compare` castea UNA vez a la entrada y lo deshace a la salida para que
|
|
|
el snippet `cell` devuelva la fila en la forma del app.
|
|
|
**Dos defectos de CANON salieron de aquí** (`theming/changelog.md` §51): la
|
|
|
tabla **no tenía escape horizontal** —a 375 medía 420 en un contenedor de 327 y
|
|
|
93px eran inalcanzables, con `overflow: hidden` en los dos ejes— **ARREGLADO**
|
|
|
(`overflow-x: auto` + `overflow-y: hidden`, guard probado por mutación); y **la
|
|
|
columna fijada no aguanta en RTL** —se ancla con `left` FÍSICO y al desplazar
|
|
|
(`scrollLeft` −94) su borde pasó de 350 a 444 y salió de pantalla— **FILADO**,
|
|
|
porque es el eje de dirección y tiene doctrina propia. Además, `Table` **no
|
|
|
exportaba los tipos que su API exige**: el guard B-4 rechazó `$libs/datagrid`
|
|
|
por nombre y tenía razón — se corrige exponiéndolos desde el canon, no
|
|
|
ensanchando la frontera.
|
|
|
⚠️ **Nota de proceso, segunda reincidencia**: pasé prettier por
|
|
|
`docs/theming/changelog.md` otra vez y reformateó 400 líneas ajenas. Restaurado
|
|
|
y reaplicado a mano. **Ese fichero no se formatea.**
|
|
|
- 2026-08-17 — **F2b · TRAMO B CERRADO: las 15 matrices de equivalencia + el guard
|
|
|
encendido**. El entregable central de la ola: por cada block, una sección
|
|
|
`## Equivalencias` con cinco columnas (variante de la referencia · quién la
|
|
|
shippea · **la receta exacta** · dónde se ha visto · estado), y la regla dura
|
|
|
operando — **una receta que no se ha visto en el navegador NO entra en la
|
|
|
tabla**. Saldo: ~95 filas, de las que **4 son superación** con nombre (el plan
|
|
|
actual no elegible de `pricing`, la explicación del envío bloqueado de
|
|
|
`newsletter` y de `contact`, y las cifras que cuentan de `stats-band`: ninguna
|
|
|
referencia puede expresarlas porque shippean markup estático), **~20 gaps con
|
|
|
nombre** —la mitad son «la receta existe y falta enseñarla», que es trabajo de
|
|
|
demo, y la otra mitad candidatos de canon ya conocidos (`visually-hidden`,
|
|
|
`layout-primitive-element`, `card-static-elevation`, `scroll-hide`)— y **8 «no
|
|
|
se ofrece» con su motivo escrito**, que es una postura del sistema y no un
|
|
|
hueco: el carrusel en el hero (movimiento automático sin control de pausa), el
|
|
|
mapa en `contact` (dependencia externa), el buscador de `faq` (estado de datos
|
|
|
del app)…
|
|
|
⚠️ **La primera tabla ya justificó el tramo**: el `hero` declaraba en sus Gaps
|
|
|
desde julio que el mockup de móvil estaba HECHO, y **la demo sólo había
|
|
|
enseñado el cromo de navegador**. `Mockup` ofrece tres; se añadió el control, se
|
|
|
midió (`data-chrome="phone"`, 320px) y sólo entonces se escribió la fila. Una
|
|
|
receta declarada no es una receta demostrada: eso es lo que este tramo separa.
|
|
|
**El guard exige ya la sección** (`README_SECTIONS` gana `## Equivalencias`),
|
|
|
encendido AL FINAL como el plan mandaba —encenderlo antes habría dejado 15
|
|
|
READMEs en rojo toda la ola—, con fixture negativo propio, los tres fixtures
|
|
|
antiguos actualizados (el self-test los cazó él solo al cambiar la regla) y
|
|
|
**probado en rojo sobre el árbol real**: quitándole la sección a `team` sale su
|
|
|
línea exacta, y restaurado vuelve a verde.
|
|
|
- 2026-08-18 — **F2b · C · N1 `logo-cloud` HECHO, y el eje de tinta con él**. El
|
|
|
block se difirió en F2 sobre una premisa que las referencias desmienten («su
|
|
|
versión honesta pide `Marquee`»): TW (6), Untitled y Flowbite lo shippean
|
|
|
ESTÁTICO. Reabierto — pero el argumento para construirlo no es la paridad: **la
|
|
|
parte difícil de un muro de logos es la TINTA, no la disposición**.
|
|
|
**Ese tratamiento fue al CANON, a la fundación, no al block ni a `Image`.**
|
|
|
`data-ink` es el hermano de `data-on`: uno re-entinta porque cambió el LIENZO,
|
|
|
el otro porque el CONTENIDO son marcas ajenas. Medido antes de elegir sitio:
|
|
|
`Image` **no tiene eje de tinta** (ni prop ni regla) y tampoco era su casa — es
|
|
|
un componente con máquina de carga para fotos, mientras que un logo suele ser un
|
|
|
`<svg>` en línea, y el mecanismo es DOBLE (`currentColor` para vectorial,
|
|
|
`filter` para raster). Un primitivo `Logo` nuevo se descartó por lo mismo: no
|
|
|
habría cargado más que el contexto. **Cero tokens nuevos** (`--opacity-muted` y
|
|
|
`--opacity-full` ya estaban en el tier semántico de la escala). Detalle y
|
|
|
medidas: `theming/changelog.md` §52.
|
|
|
**El block queda FINO**, que es la forma correcta cuando lo difícil es un
|
|
|
tratamiento: estampa `data-ink` en su sección y coloca un `Wrap`. `.Items` es
|
|
|
`Wrap` y **no rejilla** —medido: proporciones dispares dejan huecos irregulares
|
|
|
en pistas iguales— y `.Item` se gana la parte **DEFAULTEANDO** el eje de bloque,
|
|
|
lo único que un logo nunca trae consigo. Verificado con 8 marcas de anchos y
|
|
|
colores distintos: `mono` → gris al 0.65 con la tinta del tema (y otra en
|
|
|
oscuro), `brand` → su color, puntero → color entero; **una sola altura (28px)
|
|
|
con 64px de diferencia de ancho**, que es cada marca guardando su proporción.
|
|
|
⚠️ **Dos lecciones**: (1) `renderBlock` del generador **no añade `;`** entre
|
|
|
declaraciones —cada una trae el suyo salvo la última—, y sin ellos las reglas
|
|
|
LLEGAN a la hoja y no pintan nada; anotado en el propio generador. (2) El hueco
|
|
|
**`visually-hidden`** sale aquí por TERCERA vez en la ola (`newsletter`,
|
|
|
`site-footer`, `logo-cloud`): es candidato de canon con tres consumidores reales.
|
|
|
- 2026-08-18 — **F2b · C · N2 `article-grid` HECHO** (lo que las refs llaman
|
|
|
«blog»). Sus **dos decisiones de fase 0, resueltas**: (1) **el nombre** — «blog»
|
|
|
nombra una sección de SITIO y esto es la fila de artículos recientes DENTRO de
|
|
|
otra página; el tier nombra por función de página y el elemento que renderiza es
|
|
|
un `<article>`, así que `article-grid` hace coincidir nombre, función y markup.
|
|
|
«Blog» sobrevive donde se busca: primera línea del README y etiqueta del
|
|
|
catálogo. (2) **`<article>` vs F21 — sin tocar canon**: el plan reservaba elegir
|
|
|
entre añadir `as` a `Card` o declarar gap, y construirlo enseñó una tercera vía
|
|
|
sin coste: **el block renderiza el `<article>` y compone el `Card` DENTRO**. El
|
|
|
tier ya renderiza sus propios elementos de seccionado (`<footer>`, `<header>`,
|
|
|
`<section>`), así que el elemento es del block y la superficie del canon. F21
|
|
|
amuralla al primitivo, no a la composición.
|
|
|
Además: **la tarjeta entera NO es un enlace** —el teaser lleva chip y firma, y
|
|
|
anidar interactivos en un enlace es marcado inválido, que es donde acaban las
|
|
|
refs que hacen clicable toda la tarjeta—, el enlace vive en el título (1 por
|
|
|
teaser, medido); **`lift` viene ENCENDIDO** al revés que en `pricing`, porque un
|
|
|
teaser SÍ es algo a lo que se va; y **el block no formatea fechas** (la app
|
|
|
compone `RelativeTime`, verificado: «anteayer», «hace 6 días»).
|
|
|
⚠️ **Dos errores míos en esta tanda, los dos cazados MIRANDO la captura y no la
|
|
|
sonda**: el `minChildWidth` por defecto (`20rem`) sólo cabía DOS veces en la
|
|
|
medida `lg` —`(992+32)/(320+32) = 2.9`— cuando las refs lideran con tres; a
|
|
|
`18rem` mide 3/2/1 pistas en 1280/768/375. Y el segundo intento de arreglarlo
|
|
|
**no se aplicó y no me enteré**: lancé el `replace` sin `assert` y prettier había
|
|
|
juntado el destructuring en una línea. Culpé a la caché del dev server antes de
|
|
|
mirar el fichero. **Todo `replace` lleva `assert`.**
|