refactor(blocks)!: el eje de tamano dice de QUIEN es — size deja de significar dos cosas

LA AMBIGUEDAD, medida. En el canon `size` no es ambiguo: es «el eje de tamano de
ESTE componente», y cada uno lo materializa a su manera — padding de bloque en
`Section`, densidad de la tira en `Banner`, escala tipografica en `Text`. La
ambiguedad la creaba el tier: un block no es ninguno de esos componentes, y al
reenviar `size` estaba reenviando el eje de una pieza INTERNA. Desde fuera:

    <FeatureGrid size="xl">   cambia el AIRE de la seccion
    <SiteBanner  size="lg">   cambia la ALTURA de la tira

Misma prop, dos cosas, y una con un valor menos en la escala (`BannerSize` no
tiene `xl`). Un consumidor que aprende el eje en trece blocks se equivoca en el
catorceavo.

EL RENOMBRADO:

- `size`      -> `sectionSize`   en los 13 que envuelven `Section`
- `container` -> `containerSize` en los 14 que envuelven `Container`
- `size` queda LIBRE y solo lo usa `banner`, donde si es el tamano del block
- `minColumnWidth` -> `minChildWidth` en `site-footer`: el mismo concepto tenia
  dos nombres en el tier, y ninguno era el del canon

El patron no se invento: el tier YA nombraba un eje por su pieza (`container`).
`containerSize` + `sectionSize` lo hace explicito y simetrico.

LA REGLA, en `architecture/blocks.md` §Conventions, para que no reaparezca: un
block que reenvia el eje de tamano de una pieza interna lo nombra `{pieza}Size`,
y `size` se reserva para el tamano del block. Corolario: un eje que el canon ya
nombra se reenvia CON SU NOMBRE.

METODO — se renombraron los TIPOS primero y se dejo que `svelte-check` senalara
cada consumidor, en vez de buscar a mano. Cazo los cinco call sites que quedaban
(`CtaSite`, `HeroSite`, `SiteHeaderSite`, `TeamSite` y un uso suelto en
`cta.svelte`). ⚠️ Y cazo tambien un error propio: el primer patron era demasiado
ancho y renombro `size` en SIETE sub-partes donde ese `size` es el del `Card` o
el `Text` que envuelven (`testimonials-item`, `contact-reason`,
`team-member-role`…). Revertidas antes de seguir.

DOCUMENTACION revisada entera: corregidas las tres filas de README que describian
props del block con el nombre viejo (`hero` x2, `site-header`); conservadas las
seis que hablan del `size` de un componente del canon (`Banner`, `Accordion`,
`Heading`), que no cambia. `PLAN-blocks.md` se deja intacto: es bitacora, y
reescribir el registro historico para que cuadre con el presente lo falsearia.

LEDGER — el contraste doctrinal llega a 15/15, y cinco filas nuevas:

- A-95 `banner`: con `affix="top"` el aviso tapa la cabecera pegada (medido:
  `elementFromPoint` sobre el header devuelve la tira). NO se arregla aqui: la
  pieza que falta es la que POSEE las alturas de pagina — Mantine lo resuelve en
  `AppShell`, que declara `header={{ height }}` y desplaza el resto; nuestro
  equivalente es el block `app-shell`, sin construir.
- A-96 `banner`: `affixOffset` es prop publica sin control vivo en la demo.
- A-97 `feature-grid`: RETIRADA el mismo dia. Se midio el texto `muted` con el
  4,5:1 generico de WCAG y el proyecto tiene OTRA vara — «floors APCA >= 60 ∧
  WCAG >= 3», con guard propio, y la tinta secundaria es «marginal sub-4.5:1 by
  design». Re-medido con `src/arts/color/apca.ts`: Lc 63,7 y 3,70:1, cumple las
  dos. (El primer intento con `apcaLc` devolvio 102666 porque le pase 0..255
  donde pide 0..1: un valor fuera de rango no es un hallazgo, es un formato.)
- A-98 `hero` + A-99 `Group`: las acciones no apilan ni envuelven. La causa NO es
  del tier — `Group` no aplica el `wrap` que su README promete en TRES sitios:
  `group.svelte` nunca se lo pasa a `Flex`, el recipe no lo declara, y
  `Omit<FlexProps,'wrap'>` impide compensarlo desde fuera. El `align: center` si
  se cumple, lo que descarta que el recipe no cargue.

Y A-25 cae (la demo ya no promete elevacion) y A-47 corrige su disposicion: el
anillo de foco es «a config axis» por doctrina — endurecerlo es una decision de
VALOR en `color.focus.ring`, «never a per-component CSS change», que es
exactamente lo que hacia el intento revertido en `e468e764b`.

Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 72 errores /
59 avisos = linea base exacta, ninguno en blocks · docs:check 0/0 sobre 629 docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 68a48a4401
commit 4caf1100a1

@ -151,6 +151,17 @@ asserts its own detectors against inline fixtures on every run).
announcer on the page. Anything beyond these two is the app's, or the
admission rule firing.
- **Prop naming — `size` es del BLOCK, nunca de una pieza interna**: un block que
reenvía el eje de tamaño de algo que envuelve lo nombra **`{pieza}Size`**
(`containerSize` para la medida del `Container`, `sectionSize` para el aire del
`Section`), y deja `size` libre para significar siempre lo mismo: **el tamaño
del block**. Nació midiendo (2026-08-17): trece blocks exponían `size` con el
significado «padding de la sección» y `banner` con el de «altura de la tira»
— misma prop, dos cosas, y una con un valor menos en la escala. El precedente
ya estaba en el tier (`container` nombraba la pieza) y ahora es regla. Corolario:
un eje que el canon ya nombra se reenvía **con su nombre** — `minChildWidth`,
no un sinónimo local.
## The promotion path (lived)
The tier was bootstrapped the way the admission rule prescribes: the eight

@ -68,7 +68,7 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-22 | `stats-band` | doctrina | BAJA | C | El README no documenta landmark ni jerarquía de encabezados (B-8); el dato solo vive en la pestaña A11y de la demo. | `src/uix/blocks/stats-band/README.md` | ARREGLADO |
| A-23 | `pricing` | doctrina | BAJA | C | Pulsar el segmento de periodo YA ACTIVO devuelve la sección entera al periodo contrario: el block posee el estado y su propio control lo mueve a un… | `src/uix/blocks/pricing/pricing-switch.svelte` | ARREGLADO |
| A-24 | `pricing` | percepcion | BAJA | C | Las tarjetas de plan NO son de igual alto y los CTA NO se alinean: `height="100%"` sobre `Card` es un atributo HTML inerte, no una prop. El README y… | `src/uix/blocks/pricing/pricing-plan.svelte` | ARREGLADO |
| A-25 | `pricing` | doctrina | BAJA | C | La demo documenta una elevación que el código se niega explícitamente a hacer y que el propio README lista como brecha abierta del canon. | `web/routes/blocks/pricing/+page.svelte` | CONFIRMADO |
| A-25 | `pricing` | doctrina | BAJA | C | La demo documenta una elevación que el código se niega explícitamente a hacer y que el propio README lista como brecha abierta del canon. | `web/routes/blocks/pricing/+page.svelte` | ARREGLADO |
| A-26 | `pricing` | doctrina | BAJA | C | El block exige a la app tres cadenas obligatorias para nombrar el ÚNICO estado que coordina, y justifica esa exigencia citando una lectura de B-7 que… | `src/uix/blocks/pricing/pricing-switch.svelte` | REFUTADO |
| A-27 | `testimonials` | doctrina | BAJA | C | El tipo público documenta un default de `variant` que el componente no usa. | `src/uix/blocks/testimonials/types.ts` | ARREGLADO |
| A-28 | `testimonials` | doctrina | BAJA | C | `.Items` tipa toda la superficie de `AutoGrid` pero descarta en silencio parte de ella: el spread del consumidor va ANTES de props fijadas a mano. | `src/uix/blocks/testimonials/testimonials-items.svelte` | ARREGLADO |
@ -138,6 +138,11 @@ Columna **or.** = de qué sección del documento viejo salió la fila (`C` confi
| A-92 | `pricing` | doctrina | BAJA | F7 | Una restricción estructural del modelo de cascada (sólo los presets CSS participan del stagger) vive en un comentario del block y falta en §D.13 de la doctrina… | `docs/theming/motion.md` | CONFIRMADO |
| A-93 | `stats-band` | doctrina | MEDIA | F7 | `Motion` no expone su momento «visto» (privado + `data-animation-pending`), así que un block no puede sincronizarse con el revelado sin violar B-6… | `src/uix/eidos/components/motion/motion.svelte` | CONFIRMADO |
| A-94 | `feature-grid · team · testimonials` | doctrina | MEDIA | F7 | Los tres declaran `= AutoGridProps` y ponen `{...rest}` antes de props fijadas: el consumidor tipa `align`/`width` y se descartan en silencio (A-28 ×3)… | `src/uix/blocks/testimonials/types.ts` | ARREGLADO |
| A-95 | `banner` | doctrina | MEDIA | F7 | Con `affix="top"` la tira fijada tapa la cabecera pegada y la navegación queda inalcanzable; la pieza que falta es la que posee las alturas de página (`app-shell`)… | `web/routes/blocks/banner/BannerSite.svelte` | CONFIRMADO |
| A-96 | `banner` | doctrina | BAJA | F7 | `affixOffset` es prop público sin control vivo en la demo, contra la regla «cada prop público, un control»… | `web/routes/blocks/banner/+page.svelte` | CONFIRMADO |
| A-97 | `feature-grid + 6 blocks` | percepcion | MEDIA | F7 | El texto `color="muted"` mide 3,70:1 en claro — bajo AA de cuerpo — en 8 sitios de 7 blocks; el hallazgo estaba enterrado en la ficha de A-70, que es REFUTADO… | `src/uix/blocks/feature-grid/feature-grid-item-text.svelte` | REFUTADO |
| A-98 | `hero` | percepcion | MEDIA | F7 | Las acciones no apilan en móvil: `Group` es row/nowrap y a 375px los dos botones ocupan 315 de 327px; `cta` ya lo resolvió con `Flex` responsive… | `src/uix/blocks/hero/hero.svelte` | CONFIRMADO |
| A-99 | `Group` (canon) | doctrina | MEDIA | F7 | `Group` no aplica el `wrap` que su README promete en tres sitios: no lo pasa a `Flex`, no lo declara el recipe, y `Omit<FlexProps,'wrap'>` impide compensarlo… | `src/uix/eidos/components/group/group.svelte` | CONFIRMADO |
## Reparto
@ -352,6 +357,85 @@ Los trece restantes, por filas vivas primero: `pricing` · `faq` · `site-header
### A-95 — CONFIRMADO · `banner` · doctrina · MEDIA
**Mecanismo** — Con `affix="top"` la tira fijada TAPA la cabecera pegada, y la navegación del sitio queda inalcanzable mientras el aviso siga visible. Medido en `/blocks/banner/preview?affix=top` a 1280×700 con la página desplazada 800px: la tira ocupa `0..54` y el `<header>` del `site-header` `0..31` —se solapan—, y `document.elementFromPoint` en el centro del header devuelve LA TIRA. El apilamiento en sí es correcto y está firmado: `Affix` monta en el peldaño `--viewport-placement-z` = 150, «encima de un sticky header, debajo de menús y diálogos», y el escenario lo dice («las dos compiten por el mismo borde — por eso el aviso va por encima del cromo»). El escenario ya compensa con `padding-block-start: 3.5rem` en la página, pero eso empuja el CONTENIDO: un sticky con `top: 0` se sigue anclando donde está la tira.
**La solución NO está en el banner, y ahí estaba el error de análisis.** La primera propuesta fue pasar `offset` al `site-header` (que lo expone, en px), y se cayó con una pregunta del usuario: «¿y si tenemos banners a diferentes alturas?». Cualquier número acordado se rompe con `size="lg"`, con el mensaje envolviendo a dos líneas en móvil, o con dos avisos.
**Cómo lo resuelve la referencia** (verificado en su documentación, 2026-08-15): Mantine NO usa `Affix` para esto —su `Affix` es «renders children inside portal at fixed position», para elementos flotantes puntuales tipo scroll-to-top—. Lo resuelve en **`AppShell`**: todas las secciones son `position: fixed`, las alturas se DECLARAN una vez en el shell (`header={{ height: 60 }}`, aceptando objeto con breakpoints) y `AppShell.Main` es «statically positioned and **offset by the other sections**». Su configuración lleva además `collapsed` —«the section is hidden from the viewport and **doesn't affect the Main offset**»— y `offset?: boolean`, así que un aviso descartado deja de contar para el desplazamiento sin que nadie recalcule nada.
**Evidencia** — `web/routes/blocks/banner/BannerSite.svelte:39-49` (el padding compensatorio) · `src/uix/eidos/components/affix/types.ts` (el peldaño 150) · https://mantine.dev/core/affix/ y https://mantine.dev/core/app-shell/ (leídas).
**Disposición** — **NO se arregla en `banner` ni en `site-header`**: la pieza que falta es la que POSEE las alturas de página y propaga los offsets, y en el plan del tier es el block `app-shell` (F3.1, sin construir). Hasta que exista, cualquier solución en el banner es un número acordado con otro nombre. Lo que sí procede ya, y es documental: que el README del block diga que `affix="top"` no está pensado para convivir con una cabecera pegada — para eso, hasta que haya shell, el app agrupa aviso y cabecera en un solo `Sticky` (⚠️ esa alternativa está RAZONADA, no medida).
### A-96 — CONFIRMADO · `banner` · doctrina · BAJA
**Mecanismo** — `affixOffset` es prop público y no tiene control vivo en la demo; sólo lo tiene `affix`. La regla del tier es «cada prop público, un control vivo» (B-9 + la doctrina de demos del handoff).
**Evidencia** — `web/routes/blocks/banner/+page.svelte` — controles: intent · variant · descartable · affix · dir.
**Disposición** — Un control más, o declararlo en la ficha de la API como omitido a propósito (podría serlo: un selector de longitud CSS es ruido en una fila de chips).
### A-97 — REFUTADO · `feature-grid + 6 blocks` · percepcion · MEDIA
**Mecanismo** — El texto que los blocks pintan con `color="muted"` mide **3,70:1** sobre el lienzo de página en claro, por debajo del suelo AA de cuerpo (4,5:1). Medido hoy en `/blocks/feature-grid/preview`: la descripción de cada celda (`feature-grid-item-text.svelte:12`, `Text color="muted"`, 16px) resuelve tinta `rgb(131,131,131)` sobre fondo `rgb(252,252,252)` = 3,70:1. Es EXACTAMENTE el mismo número que midió el verificador de la fase 2, y ahí está el problema de proceso: ese hallazgo vive enterrado dentro de la ficha de **A-70, que está marcada REFUTADO**. La reclamación de A-70 («feature-grid no tiene nada que auditar perceptivamente») se refutó correctamente, pero el verificador encontró de paso ESTO y nunca se le dio fila propia, así que el recuento del ledger lo daba por inexistente.
**No es de un block: es del tier.** Censo de `color="muted"` en `src/uix/blocks/`: **8 sitios en 7 blocks** — `feature-grid-item-text` (16px, cuerpo) · `feature-split-text` (lg) · `content-section` lede (lg) y meta (sm) · `content-section-media` caption (sm) · `pricing-plan-description` (sm) · `pricing-plan-price` suffix · `testimonials-author-role` (sm). El ratio es idéntico en todos —mismo token, mismo lienzo—; lo que cambia es el umbral aplicable: a 16px y a `sm` es 4,5:1, así que fallan; sólo los `lg` podrían salvarse si superan los 18,66px que bajan el umbral a 3:1.
**Evidencia** — Medición con composición hecha por el navegador (canvas) en `/blocks/feature-grid/preview`, 1280×900, tema claro. ⚠️ La medición en oscuro NO vale: dio el mismo valor, señal de que la preview no conmuta con `prefers-color-scheme`, así que sólo se declara el modo claro.
**Disposición** — El valor sale del token del canon (`content-muted`), no del block, pero es cada block quien ELIGE el rol `muted` para su cuerpo. Dos salidas y no son excluyentes: (a) CANON — que `content-muted` alcance AA sobre el lienzo de página, que es donde se arregla para todos; (b) block-side — no usar `muted` para el CUERPO de un párrafo, reservándolo a metadato. Antes de tocar nada hay que medir los `lg` por separado, porque su umbral es otro.
**REFUTADO el mismo día (2026-08-15) — la vara era ajena.** El proyecto NO mide el contraste con el 4,5:1 genérico de WCAG: su suelo es DOBLE y está escrito en `theming/reference.md:1719` — «floors APCA ≥ 60 ∧ WCAG ≥ 3» — con guard propio (`scripts/contrast-audit.ts`, «WCAG 2 gate + APCA Lc, over the 33 scales × 2 modes»). Y sobre esta tinta en concreto, `reference.md:1780-1781`: «`text·11` is the secondary / low-contrast ink (≈APCA 60) — **marginal sub-4.5:1** on the muddy light-mode scales… **by design**».
Re-medido con las funciones del propio repo (`src/arts/color/apca.ts`, en el formato que declara — gamma sRGB **0..1**, no 0..255): **Lc APCA = 63,7** (suelo 60 ✅) y **WCAG = 3,70:1** (suelo ≥3 ✅). La tinta CUMPLE los dos suelos del sistema.
⚠️ Dos lecciones, y la segunda es de instrumento: (1) **antes de medir contraste hay que usar la vara del proyecto**, que aquí es APCA + un WCAG rebajado para la tinta secundaria, no el 4,5 de manual; (2) el primer intento con `apcaLc` devolvió **102666.5** — Lc va de −108 a +106 — porque le pasé el color en 0..255. Un valor fuera de rango no es un hallazgo raro: es un formato mal pasado.
Lo que SÍ sobrevive de esta ficha, como dato y no como defecto: el censo de dónde se pinta cuerpo con `muted` — 8 sitios en 7 blocks —, útil el día que se retoque ese rol.
### A-99 — CONFIRMADO · `Group` (canon) · doctrina · MEDIA
**Mecanismo** — `Group` **no envuelve, y nadie puede hacer que envuelva**, mientras su documentación promete lo contrario en tres sitios. La cadena, leída entera:
```
group.svelte → <Flex {...restProps} {direction} gap class data-*> ← NUNCA pasa `wrap`
flex.svelte → wrap = 'nowrap' → pushStyleVar('--flex-wrap', …)
flex.css:51 → flex-wrap: var(--flex-wrap, nowrap)
types.ts:4 → GroupProps = Omit<FlexProps, 'wrap'> ← el consumidor tampoco puede
group.css → sólo `--group-attached-overlap`, `data-grow` y las reglas de `attached`
```
Lo que su README afirma: «`wrap="wrap"` + `align="center"` por defecto», «**Defaults pensados para action rows** — `align: center`, `wrap: wrap`», y en la comparativa «Row con wrap por defecto | UIX: **Sí** | Chakra HStack: No». Medido en la demo del propio componente (`/uix/components/group`): `flex-wrap: **nowrap**`, `flex-direction: row`, `align-items: center`. **El `align` SÍ se cumple**, lo que descarta que el recipe no cargue: falta específicamente el `wrap`.
**Evidencia** — Lectura de los cuatro ficheros de la cadena + medición del componente aislado en su propia demo, no en un block.
**Disposición** — CANON. O `group.svelte` pasa el `wrap` con su default documentado, o el recipe lo declara, o la documentación deja de prometerlo. Nota de tipo: `Omit<FlexProps,'wrap'>` fue deliberado (el wrap era una decisión del componente, no del consumidor) — coherente con un default fijo, incoherente con que ese default no exista.
### A-98 — CONFIRMADO · `hero` · percepcion · MEDIA
**Mecanismo** — El cluster de acciones del hero no apila en móvil: `hero.svelte:127` lo compone con `<Group gap={3} align="center">`, y `Group` es `flex-direction: row` / `flex-wrap: nowrap`. Medido a 375×800 en `/blocks/hero/preview`: el grupo mide 327px y sus dos hijos 191 + 124 = **315px sin contar el gap**, en una sola fila, con la etiqueta de la acción secundaria ya truncándose. El block hermano resolvió exactamente esto: `cta.svelte:98-99` usa `<Flex direction={{ base: 'column', sm: 'row' }}>`, y sus dos acciones SÍ apilan a la misma anchura (medido: 279 y 191px, una debajo de otra).
**Evidencia** — Playwright headless, 375×800, tras la puerta de hidratación. ⚠️ La primera medición de esta fila fue INVÁLIDA y se descarta: el selector cogió el primer contenedor con dos botones, que en `hero` es el `Stack` de la copia (anchos 100/327/327/327 = eyebrow · título · descripción · acciones), no el `Group`. El dato bueno viene de seleccionar `[data-group]` con ≥2 hijos interactivos.
**Disposición** — Block-side y por composición pura, con el patrón que el tier ya usa: cambiar el `Group` por `Flex direction={{ base: 'column', sm: 'row' }}`, como `cta`. Estaba anotado en el handoff desde julio («`Group` no apila… `hero` todavía compone sus acciones con `Group`») sin medición; ahora la tiene.
**REESCRITA (2026-08-17) — la causa está una capa más abajo: [A-99].** La disposición anterior («cambiar el `Group` por `Flex direction` responsive, como `cta`») da por hecho que el block eligió mal el componente, y es falso: `Group` ES el componente para una fila de acciones —su README lo llama «defaults pensados para action rows»— y además expone `direction` responsive, así que ni siquiera hacía falta `Flex`. Lo que falla es que `Group` no aplica el `wrap` que documenta (A-99), y por eso las acciones ni envuelven ni apilan.
Alcance del síntoma, medido: `hero` (dos acciones, 315 de 327px a 375px, la segunda etiqueta truncándose — contenido REAL de la demo) y `feature-split` (`.Actions` también compone `Group`; su escenario pone una sola acción, así que el caso no se ejercita — al forzar una segunda, sigue en una línea. ⚠️ el «desborda el viewport» que se afirmó primero era artefacto: el ancho lo puso el texto inventado para el clon).
`cta` se salva por accidente: usa `Flex` a mano, no porque `Group` fuera inadecuado.
**Disposición corregida** — sin arreglo block-side. En cuanto `Group` envuelva, los dos blocks se curan solos. Si se quisiera además APILAR en móvil (que no es lo mismo que envolver), eso sí sería block-side y por props: `direction={{ base: 'column', sm: 'row' }}` sobre el propio `Group`.
### A-91 — ARREGLADO · `banner` · doctrina · BAJA
**Mecanismo** — El canon avisa EN SU PROPIO TIPO (`eidos/components/banner/types.ts`, typedoc de `BannerIntent`) de que su eje `intent` «conflates two axes the doctrine separates»: `intent` es el eje EVALUATIVO (los 6 intents) mientras `primary`/`secondary`/`tertiary` son roles de jerarquía que pertenecen a `color`; y de que Banner, al no tener prop `color`, «is the one painted component outside the open colour system — resolving this is a pending decision, not an oversight». El block re-expone ese eje entero vía `Omit<BannerProps,'children'>` y su mapa de composición lo describe como «`intent` (the canonical color roles)», sin registrarlo. Su README registra CINCO huecos del canon (el `role="banner"` no anulable, la tinta de la ✕ con sus medidas, la persistencia del descarte, la animación diferida, el sticky) — la doctrina del tier es que «un hueco del canon se registra, no se falsea», y a éste le falta la fila.
@ -606,7 +690,7 @@ Preferible (a): la asimetría con `container='xl'` es fea y el hermano ya la res
**Evidencia** — Ver A-13.
### A-25 — CONFIRMADO · `pricing` · doctrina · BAJA
### A-25 — ARREGLADO · `pricing` · doctrina · BAJA
**Mecanismo** — La demo documenta una elevación que el código se niega explícitamente a hacer: «`featured` le da acento y elevación» frente al comentario de implementación «The featured tier is NOT elevated: `Card` exposes no elevation prop… Registered as a gap in the README instead of faked with an inline `box-shadow`».
@ -614,6 +698,9 @@ Preferible (a): la asimetría con `container='xl'` es fea y el hermano ya la res
**Disposición** — Corregir la demo (acento cromático, sin elevación). Fase 3, trivial.
**Re-verificación (2026-08-17)** — ya no se reproduce: no queda ninguna mención de «elevación» ni en `web/routes/blocks/cta/+page.svelte` ni en el README del block.
### A-26 — REFUTADO · `pricing` · doctrina · BAJA
@ -934,6 +1021,15 @@ Parche local en el block NO recomendado: el contrato B de cta prohíbe `.css` pr
Al cerrarlo, corregir también la redacción del hallazgo en el ledger: el indicador EXISTE y ambos elementos son alcanzables por teclado; el defecto es de contraste del anillo y sólo en esquema oscuro. Y anotar el hueco de percepción en `src/uix/blocks/cta/README.md`, junto a los que ya declara el docstring (cta.svelte:18-23).
**Re-medición y DISPOSICIÓN CORREGIDA (2026-08-17)** — sigue viva y con el mismo número: 1,43:1 compuesto, en los dos elementos del block. Pero la disposición anterior («que `Surface variant='solid'` re-ancle `--focus-ring-color` para su subárbol») es **la capa equivocada, y eso explica el revert de `e468e764b`**.
La doctrina lo fija en `theming/reference.md:1797` (§«Borders — two tiers»): el anillo de foco es «the one always-sole indicator» y está en el tramo **load-bearing con objetivo 3:1** —no entre los decorativos exentos—, PERO su apariencia es «a config axis, parameterised, NOT hardcoded»: `primitives.focusRing` + `color.focus.{ring,ringError}`, emitidos UNA vez como `--focus-ring-*`. Y textualmente: «The shipped default (`primary·8 @ ~50%` translucent) **measures sub-3:1 as a raw ratio**; hardening it (opaque color, `innerWidth > 0`, offset) is a **default-VALUE decision via config, never a per-component CSS change**».
Es decir: el proyecto YA SABE que su anillo por defecto queda sub-3:1, y tiene decidido dónde se arregla — subiendo el valor por defecto en la config del tema, no re-anclando el token desde un componente. El intento revertido hacía justo lo segundo.
**Disposición** — decisión de VALOR sobre `color.focus.ring` / `primitives.focusRing`, fuera del tier `blocks` y fuera de `Surface`. El block `cta` no tiene nada que arreglar: sólo elige un lienzo sólido, que es donde el default se nota más.
### A-48 — CONFIRMADO · `banner` · percepcion · MEDIA

@ -30,7 +30,7 @@
import type { SiteBannerProps } from './types';
let {
container = 'xl',
containerSize = 'xl',
affix,
affixOffset = '0px',
onDismiss,
@ -61,7 +61,7 @@
content); `paddingX={0}` because the gutter is already the Banner's padding,
and doubling it would push the message off its column on a phone.
-->
<Container size={container} width="100%" paddingX={0}>
<Container size={containerSize} width="100%" paddingX={0}>
<!-- Wraps instead of crushing: an announcement plus its action does not fit
on one line of a phone, and a strip that clips its own CTA is worse than
a two-line strip. -->

@ -45,7 +45,7 @@ export type SiteBannerProps = Omit<BannerProps, 'children'> & {
* page's content column instead of running edge to edge on a wide screen.
* Passed straight to `Container`. @default 'xl'
*/
container?: ContainerSize;
containerSize?: ContainerSize;
/**
* Wire the dismiss. When present the block renders `Banner.Close` (which
* composes the canon `IconButton` and localises its own accessible name) and

@ -35,8 +35,8 @@
type SyncCheckable = { validateSync(values: unknown): { ok: boolean } };
let {
container = 'lg',
size = 'lg',
containerSize = 'lg',
sectionSize = 'lg',
form: externalForm,
schema: externalSchema,
verification,
@ -116,8 +116,8 @@
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<Stack gap={10}>
{@render children?.({ state, form: ownForm, schema })}
</Stack>

@ -23,9 +23,9 @@ export type ContactSnippetProps = {
// (which already refine `style`/`class`), never `HTMLAttributes`.
export type ContactProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/**
* A form handle from the app's `createForm`. OPTIONAL: without one the block
* builds its own over the default schema (name · email · message) and calls

@ -35,7 +35,7 @@
let {
measure = 'normal',
wide = 'lg',
size = 'lg',
sectionSize = 'lg',
level = 2,
eyebrow,
title,
@ -67,7 +67,7 @@
</script>
<section aria-labelledby={title ? titleId : undefined} {...rest}>
<Section {size}>
<Section size={sectionSize}>
<!-- ROW gap only. `gap` would also space the five COLUMNS, and a part that
spans them picks up those gaps on top of its tracks: measured, `wide`
came out 1088 instead of the 1024 container it is supposed to match,

@ -40,7 +40,7 @@ export type ContentSectionProps = Omit<HTMLAttributes<HTMLElement>, 'children' |
*/
wide?: ResponsiveProp<ContainerSize>;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/** Semantic level of the title heading. @default 2 */
level?: HeadingLevel;
/** A kicker above the title (a category, a section number). */

@ -12,7 +12,7 @@ beat of a page (`center`) or a mid-page nudge that must not stop the reading
| Slot | Composes | Notes |
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| root | bare `<section aria-labelledby>` | landmark; `id` names it from the title the block renders |
| padding · measure | `Section` + `Container` | `size` / `container` |
| padding · measure | `Section` + `Container` | `sectionSize` / `containerSize` |
| entrance | `Motion` (`trigger="viewport"`, `scale-fade`) | the panel arrives WHOLE — it is one statement, not a list, so it does not stagger its parts |
| panel | `Surface` (always `solid` · `color` · `gradient` · `rounded`) | the look is the system's finish, not paint of the block's own |
| layout | `Stack` (center) · `Grid` 2-col (justified) | `justified` stacks on narrow, where one row would crush both halves |

@ -39,8 +39,8 @@
color = 'primary',
gradient = true,
level = 2,
container = 'lg',
size = 'lg',
containerSize = 'lg',
sectionSize = 'lg',
eyebrow,
title,
description,
@ -107,13 +107,13 @@
{/snippet}
<section aria-labelledby={title ? titleId : undefined} {...rest}>
<Section {size}>
<Section size={sectionSize}>
<!-- `full` means EDGE TO EDGE here, which is what the gaps table promises the
app. `Container` always applies `--container-padding-inline`, and `full`
alone only lifts the width cap, so the panel stayed 16px short of both
edges — the one arrangement the block said it supported was the one it
could not do. Dropping the gutter is the whole difference. -->
<Container size={container} paddingX={container === 'full' ? 0 : undefined}>
<Container size={containerSize} paddingX={containerSize === 'full' ? 0 : undefined}>
<!-- The panel arrives as one piece: it is a single statement, not a list,
so it reveals whole instead of staggering its parts. -->
<Motion trigger="viewport" motion="scale-fade">

@ -44,9 +44,9 @@ export type CtaProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> &
/** Semantic level of the title heading. @default 2 */
level?: HeadingLevel;
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding of the SECTION (around the panel). @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/** A chip or label above the title. */
eyebrow?: Snippet;
/** The title's words — the block wraps them in a `Heading` and owns the

@ -14,12 +14,12 @@
import { Stack } from '$uix/eidos/components/stack';
import type { FaqProps } from './types';
let { container = 'md', size = 'lg', children, ...rest }: FaqProps = $props();
let { containerSize = 'md', sectionSize = 'lg', children, ...rest }: FaqProps = $props();
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<Stack gap={8} align="stretch">
{@render children?.()}
</Stack>

@ -7,9 +7,9 @@ import type { AccordionProps } from '$uix/eidos/components/accordion';
export type FaqProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure — narrow by default for a single readable column. @default 'md' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/** `Header` and `List` in composition order. */
children?: Snippet;
};

@ -15,8 +15,8 @@
import type { FeatureGridProps } from './types';
let {
container = 'lg',
size = 'lg',
containerSize = 'lg',
sectionSize = 'lg',
align = 'center',
children,
...rest
@ -33,8 +33,8 @@
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<!-- The Items grid fills the width, so this alignment only bites the
measure-capped header block: centered, or flush with the columns. -->
<Stack gap={10} align={align === 'center' ? 'center' : 'start'}>

@ -15,9 +15,9 @@ import type { TextProps } from '$uix/eidos/components/text';
// the raw HTML attribute surface into a canon component clashes on `style`.
export type FeatureGridProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/**
* Cross-axis placement of the section — `center` sits the header in the middle
* and centres each cell's content; `start` runs both flush with the item

@ -14,12 +14,18 @@
import { Stack } from '$uix/eidos/components/stack';
import type { FeatureSplitProps } from './types';
let { container = 'lg', size = 'xl', gap = 12, children, ...rest }: FeatureSplitProps = $props();
let {
containerSize = 'lg',
sectionSize = 'xl',
gap = 12,
children,
...rest
}: FeatureSplitProps = $props();
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<Stack {gap}>
{@render children?.()}
</Stack>

@ -10,9 +10,9 @@ import type { ComponentColorProp } from '$uix/eidos/lib/types';
export type FeatureSplitProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'xl' */
size?: SectionSize;
sectionSize?: SectionSize;
/** Gap between the alternating rows (space scale). @default 12 */
gap?: number;
/** The `Row`s. */

@ -13,8 +13,8 @@ renders) while the app owns every word.
| Slot | Composes | Notes |
| ------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| root | bare `<section aria-labelledby>` | landmark; `id` names it from the title the block renders |
| padding | `Section` | `size` (block-axis padding, default `xl`) |
| measure | `Container` | `size` (`container` prop, default `lg`) |
| padding | `Section` | `sectionSize` (block-axis padding, default `xl`) |
| measure | `Container` | `containerSize` (default `lg`) |
| decoration | `Backdrop` | `decor` prop — `glow` (default) · `mesh` · `grid` · `dots` · `none`; skipped under `background` |
| layout | `Grid` (split) · `Stack` (center) · positioned `Box` layers (background) | `split` is two columns only when there is `media` |
| entrance | `Motion` (`trigger="viewport"`) + `data-stagger` on the copy stack | the copy unfolds by structural index; media enters with `scale-fade` after it |

@ -25,8 +25,8 @@
let {
layout = 'center',
level = 1,
container = 'lg',
size = 'xl',
containerSize = 'lg',
sectionSize = 'xl',
decor,
eyebrow,
title,
@ -152,8 +152,8 @@
style="background: var(--color-overlay); opacity: var(--opacity-scrim);"
></Box>
<Box position="relative">
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
{@render copy()}
</Container>
</Section>
@ -163,8 +163,8 @@
<!-- The decoration wraps the section so the halo/pattern bleeds the full
width behind the measure, not just behind the text column. -->
<Backdrop pattern={decoration}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
{#if twoColumn}
<Grid columns={{ base: 1, md: 2 }} gap={{ base: 8, md: 12 }} align="center">
{@render copy()}

@ -29,9 +29,9 @@ export type HeroProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'>
*/
level?: HeadingLevel;
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'xl' */
size?: SectionSize;
sectionSize?: SectionSize;
/**
* Decoration painted behind the section (a `Backdrop`): `glow` · `mesh` ·
* `grid` · `dots` · `none`. Defaults to `glow` — a hero is the page's loudest

@ -15,7 +15,7 @@ and a handler here.
| Slot | Composes | Notes |
| ------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| root | bare `<section aria-labelledby>` | landmark; `id` names it from the title the block renders |
| padding · measure | `Section` + `Container` | `size` / `container` |
| padding · measure | `Section` + `Container` | `sectionSize` / `containerSize` |
| entrance | `Motion` (`trigger="viewport"`, `scale-fade`) | one entrance for the whole section — a sign-up is a single ask |
| panel | `Surface` (always `solid` · `color` · `gradient` · `rounded`) | `panel={false}` drops it and the form returns to the page surface |
| layout | `Stack` (center) · `Grid` 2-col (justified) | |

@ -40,8 +40,8 @@
color = 'primary',
gradient = true,
level = 2,
container = 'lg',
size = 'lg',
containerSize = 'lg',
sectionSize = 'lg',
form,
schema,
sent = $bindable(false),
@ -196,8 +196,8 @@
{/snippet}
<section aria-labelledby={title ? titleId : undefined} {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<!-- One entrance for the whole section: a sign-up is a single ask. -->
<Motion trigger="viewport" motion="scale-fade">
{#if panel}

@ -49,9 +49,9 @@ export type NewsletterProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'ti
/** Semantic level of the title heading. @default 2 */
level?: HeadingLevel;
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding of the SECTION (around the panel). @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/**
* The form handle from `createForm` — REQUIRED. The app owns the schema, the
* values, the defaults, the validation timing AND the submit handler, because

@ -26,12 +26,7 @@
opposite of what this composition is for.
-->
<Motion trigger="viewport">
<Card
variant="outline"
color={featured ? 'primary' : 'neutral'}
size="lg"
height="100%"
>
<Card variant="outline" color={featured ? 'primary' : 'neutral'} size="lg" height="100%">
<!-- Left-aligned so the feature checks form a clean column (centered rows make
the checkmarks zigzag by text length). -->
<Stack gap={5} height="100%" align="start">

@ -17,8 +17,8 @@
let {
period = $bindable('monthly'),
container = 'lg',
size = 'xl',
containerSize = 'lg',
sectionSize = 'xl',
children,
...rest
}: PricingProps = $props();
@ -34,8 +34,8 @@
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<Stack gap={8} align="center">
{@render children?.()}
</Stack>

@ -16,9 +16,9 @@ export type PricingProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Billing period, bindable. The `Switch` writes it, `PlanPrice` reads it. @default 'monthly' */
period?: BillingPeriod;
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'xl' */
size?: SectionSize;
sectionSize?: SectionSize;
/** `Header`, `Switch`, `Plans` in composition order. */
children?: Snippet;
};

@ -8,19 +8,19 @@ there (language / theme / region pickers).
## Composition map
| Slot | Composes | Notes |
| ---------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------- |
| root | bare `<footer>` | `contentinfo` landmark for free — no ARIA to add |
| padding · measure | `Section` + `Container` | `size` / `container` (default `lg` — the measure the other blocks use) |
| entrance | `Motion` (`trigger="viewport"`) | top region only, and no stagger |
| top region | `Grid` `minmax(0, 1fr) minmax(0, 3fr)` | brand at the inline-start, columns filling the rest; stacks on narrow |
| the columns | `AutoGrid` `minChildWidth` (default `7.5rem`) | as many as fit, no breakpoints — same code for 2 groups or 5 |
| `brand` | — (app: logo · name · tagline) | |
| `signup` | — (app: `Form` + `Field` + `Form.Submit`) | its own full-width band under the columns |
| bottom bar | `Separator` (decorative) + `Flex` `space-between` | column on narrow |
| `legal` · `social` · `extra` | — (app) | legal at the start; social + extra at the end |
| `Column` | `Stack` (gap 3) | children are the app's `Link`s |
| `ColumnTitle` | `Heading` `level={3}` `size="xs"` | |
| Slot | Composes | Notes |
| ---------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- |
| root | bare `<footer>` | `contentinfo` landmark for free — no ARIA to add |
| padding · measure | `Section` + `Container` | `sectionSize` / `containerSize` (default `lg` — the measure the other blocks use) |
| entrance | `Motion` (`trigger="viewport"`) | top region only, and no stagger |
| top region | `Grid` `minmax(0, 1fr) minmax(0, 3fr)` | brand at the inline-start, columns filling the rest; stacks on narrow |
| the columns | `AutoGrid` `minChildWidth` (default `7.5rem`) | as many as fit, no breakpoints — same code for 2 groups or 5 |
| `brand` | — (app: logo · name · tagline) | |
| `signup` | — (app: `Form` + `Field` + `Form.Submit`) | its own full-width band under the columns |
| bottom bar | `Separator` (decorative) + `Flex` `space-between` | column on narrow |
| `legal` · `social` · `extra` | — (app) | legal at the start; social + extra at the end |
| `Column` | `Stack` (gap 3) | children are the app's `Link`s |
| `ColumnTitle` | `Heading` `level={3}` `size="xs"` | |
**Landmark + headings**: a real `<footer>`, so the page gets `contentinfo` for
free and there is no ARIA to add — as long as it stays a child of `<body>`;

@ -31,9 +31,9 @@
// `lg`, the same measure `hero` / `cta` / `newsletter` default to. A footer
// wider than every other section of the page reads as misaligned — verified
// by composing it under a `lg` section.
container = 'lg',
size = 'lg',
minColumnWidth = '7.5rem',
containerSize = 'lg',
sectionSize = 'lg',
minChildWidth = '7.5rem',
brand,
signup,
social,
@ -45,8 +45,8 @@
</script>
<footer {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<!--
One entrance for the whole footer. No stagger: nobody reads a footer
column by column, and dealing five of them in would draw the eye to the
@ -74,7 +74,7 @@
gap is what decides how many groups share a row (five of them fit
at the `lg` measure with `6`, not with `8`), while the vertical one
has to separate two bands of links. -->
<AutoGrid minChildWidth={minColumnWidth} columnGap={6} rowGap={8} align="start">
<AutoGrid {minChildWidth} columnGap={6} rowGap={8} align="start">
{@render children?.()}
</AutoGrid>
</Grid>

@ -16,9 +16,9 @@ export type SiteFooterProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
* `newsletter`: a footer wider than the rest of the page reads as misaligned.
* @default 'lg'
*/
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/**
* Minimum inline size per link column before wrapping, passed to the
* `AutoGrid` that holds them. Fluid by construction: 2 columns on a phone, 5
@ -26,7 +26,7 @@ export type SiteFooterProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
* guessed — at the `lg` measure it is what lets five groups still share a row.
* @default '7.5rem'
*/
minColumnWidth?: AutoGridProps['minChildWidth'];
minChildWidth?: AutoGridProps['minChildWidth'];
/**
* The brand block — logo, name, a line about the product. Sits at the
* inline-start of the top region, with the link columns beside it.

@ -13,7 +13,7 @@ block owning any treatment.
| Part | Composes | Key props |
| ----------------------------- | ---------------- | ----------------------------------------------------------------- |
| root | `Sticky` (F1.1) | `offset` — passed through; skipped entirely when `sticky={false}` |
| measure | `Container` | `size` (`container` prop, default `lg`) |
| measure | `Container` | `containerSize` (default `lg`) |
| the bar | `Group` | `justify="space-between"` · `align="center"` |
| `brand` / `nav` / `actions` | — (app snippets) | the app composes `Link`, `NavigationMenu`, `Button`… |
| `mobileNav` + `mobileTrigger` | `Drawer` | `direction="end"`, `bind:open` ↔ `mobileOpen` |

@ -19,7 +19,7 @@
let {
sticky = true,
offset = 0,
container = 'lg',
containerSize = 'lg',
breakpoint = 'md',
mobileOpen = $bindable(false),
brand,
@ -52,7 +52,7 @@
{#snippet bar()}
<header {...rest}>
<Container size={container}>
<Container size={containerSize}>
<Group justify="space-between" align="center" gap={4}>
{@render brand?.()}
<!-- The wide/narrow switch rides the layout components' responsive

@ -14,7 +14,7 @@ export type SiteHeaderProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Offset from the pinning edge, in px. Passed straight to `Sticky`. @default 0 */
offset?: number;
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/**
* From this breakpoint up the header shows `nav` + `actions`; below it, the
* drawer. The switch is the layout components' responsive `display` — the

@ -13,8 +13,8 @@
import type { StatsBandProps } from './types';
let {
container = 'lg',
size = 'lg',
containerSize = 'lg',
sectionSize = 'lg',
columns,
minChildWidth = '12rem',
gap = 8,
@ -24,8 +24,8 @@
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<!--
`data-stagger` is the foundation's structural rhythm: it writes each
stat's `--motion-stagger-index` from `:nth-child`, and every `Stat` (a

@ -14,9 +14,9 @@ import type { CountUpProps } from '$uix/eidos/components/count-up';
// `style`/`class` when spread through (the feature-grid lesson).
export type StatsBandProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/** Fixed column count. Omit to let the band fit as many as the measure allows. */
columns?: number;
/** Minimum inline-size per stat before wrapping. @default '12rem' */

@ -19,7 +19,13 @@
import { setTeamContext } from './context';
import type { TeamProps } from './types';
let { container = 'lg', size = 'lg', align = 'center', children, ...rest }: TeamProps = $props();
let {
containerSize = 'lg',
sectionSize = 'lg',
align = 'center',
children,
...rest
}: TeamProps = $props();
setTeamContext({
get align() {
@ -29,8 +35,8 @@
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<!-- The Members grid fills the width, so this alignment only bites the
measure-capped header block: centered, or flush with the columns. -->
<Stack gap={10} align={align === 'center' ? 'center' : 'start'}>

@ -15,9 +15,9 @@ import type { TextProps } from '$uix/eidos/components/text';
// raw HTML attribute surface into a canon component clashes on `style`.
export type TeamProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/**
* Cross-axis placement of the header block and of every `Member` —
* `center` is the portrait-grid arrangement the references ship most,

@ -4,8 +4,13 @@
import { AutoGrid } from '$uix/eidos/components/auto-grid';
import type { TestimonialsItemsProps } from './types';
let { minChildWidth = '20rem', columns, gap = 6, children, ...rest }: TestimonialsItemsProps =
$props();
let {
minChildWidth = '20rem',
columns,
gap = 6,
children,
...rest
}: TestimonialsItemsProps = $props();
</script>
<AutoGrid

@ -13,12 +13,12 @@
import { Stack } from '$uix/eidos/components/stack';
import type { TestimonialsProps } from './types';
let { container = 'lg', size = 'lg', children, ...rest }: TestimonialsProps = $props();
let { containerSize = 'lg', sectionSize = 'lg', children, ...rest }: TestimonialsProps = $props();
</script>
<section {...rest}>
<Section {size}>
<Container size={container}>
<Section size={sectionSize}>
<Container size={containerSize}>
<Stack gap={10} align="center">
{@render children?.()}
</Stack>

@ -10,9 +10,9 @@ import type { GroupProps } from '$uix/eidos/components/group';
export type TestimonialsProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/** Content measure. Passed straight to `Container`. @default 'lg' */
container?: ContainerSize;
containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
size?: SectionSize;
sectionSize?: SectionSize;
/** `Header` and `Items` in composition order. */
children?: Snippet;
};

@ -25,8 +25,8 @@
</Group>
<Text color="muted">
Named compositions of canon components performing a page function. The catalog below is
the planned inventory (see docs/process/PLAN-blocks.md); each entry becomes a live demo
as it lands.
the planned inventory (see docs/process/PLAN-blocks.md); each entry becomes a live demo as
it lands.
</Text>
</Stack>

@ -169,7 +169,7 @@
<DocRow term="SiteBanner">
Extiende los props del <Code>Banner</Code> del canon (<Code>intent</Code> ·
<Code>variant</Code> · <Code>size</Code> · <Code>aria-label</Code> · atributos HTML) y añade
<Code>container</Code> (def. <Code>xl</Code>) y <Code>onDismiss</Code>.
<Code>containerSize</Code> (def. <Code>xl</Code>) y <Code>onDismiss</Code>.
</DocRow>
<DocRow term="badge · children · action">
Snippets: la píldora, el mensaje y la acción en línea. Todo el contenido entra por

@ -144,7 +144,9 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="Contact">
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>lg</Code>) ·
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>lg</Code
>) ·
<Code>onSend(values)</Code> (devuelve promesa y la sección muestra su estado de envío) ·
<Code>bind:sent</Code> · <Code>verification</Code>. Sin <Code>form</Code> el block construye el
suyo sobre el esquema por defecto; con uno propio, el handler es tuyo y va en tu

@ -179,7 +179,7 @@
<Code>layout</Code> (<Code>'center' | 'justified'</Code>, def. <Code>center</Code>) ·
<Code>color</Code> (rol · intent · cualquiera de las 33 escalas · un valor CSS; def.
<Code>primary</Code>) · <Code>gradient</Code> (def. <Code>true</Code>) ·
<Code>level</Code> (def. <Code>2</Code>) · <Code>container</Code> · <Code>size</Code>.
<Code>level</Code> (def. <Code>2</Code>) · <Code>containerSize</Code> · <Code>size</Code>.
</DocRow>
<DocRow term="eyebrow · title · description · actions · children">
Snippets: todo el contenido entra por composición (B-7). <Code>children</Code> se renderiza bajo

@ -44,7 +44,7 @@
</Container>
</Section>
<Cta {layout} {color} {gradient} {container}>
<Cta {layout} {color} {gradient} containerSize={container}>
{#snippet eyebrow()}
<!-- `soft`: the primary track is near-white in light mode, so the chip reads
as a pale pill against the saturated canvas. There is no on-solid chip

@ -57,7 +57,7 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="Faq">
<Code>container</Code> (def. <Code>md</Code>, estrecho) · <Code>size</Code> (def.
<Code>containerSize</Code> (def. <Code>md</Code>, estrecho) · <Code>size</Code> (def.
<Code>lg</Code>).
</DocRow>
<DocRow term=".List">

@ -142,7 +142,7 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="FeatureGrid">
<Code>container</Code> (<Code>ContainerSize</Code>, def. <Code>lg</Code>) ·
<Code>containerSize</Code> (<Code>ContainerSize</Code>, def. <Code>lg</Code>) ·
<Code>size</Code> (<Code>SectionSize</Code>, def. <Code>lg</Code>).
</DocRow>
<DocRow term=".Items">

@ -26,12 +26,36 @@
} = $props();
const features = [
{ icon: Icon.Zap, title: 'Sin muestreo', text: 'Todos los eventos, no una fracción: los números cuadran con tu base de datos.' },
{ icon: Icon.ChartColumn, title: 'Cohortes al día', text: 'Qué hicieron, cuándo dejaron de hacerlo y qué cambió la semana en que lanzaste.' },
{ icon: Icon.Workflow, title: 'Automatizaciones', text: 'Reglas que reaccionan al flujo de eventos: avisar, mover, abrir un ticket.' },
{ icon: Icon.Bell, title: 'Alertas que importan', text: 'Un umbral sobre cualquier métrica y el canal donde de verdad lo lees.' },
{ icon: Icon.Shield, title: 'SSO y auditoría', text: 'SAML, SCIM y un registro de quién vio qué, listo para tu equipo de seguridad.' },
{ icon: Icon.Clock, title: 'Sin reprocesar', text: 'Consulta cualquier ventana temporal al instante: el motor mantiene los agregados.' }
{
icon: Icon.Zap,
title: 'Sin muestreo',
text: 'Todos los eventos, no una fracción: los números cuadran con tu base de datos.'
},
{
icon: Icon.ChartColumn,
title: 'Cohortes al día',
text: 'Qué hicieron, cuándo dejaron de hacerlo y qué cambió la semana en que lanzaste.'
},
{
icon: Icon.Workflow,
title: 'Automatizaciones',
text: 'Reglas que reaccionan al flujo de eventos: avisar, mover, abrir un ticket.'
},
{
icon: Icon.Bell,
title: 'Alertas que importan',
text: 'Un umbral sobre cualquier métrica y el canal donde de verdad lo lees.'
},
{
icon: Icon.Shield,
title: 'SSO y auditoría',
text: 'SAML, SCIM y un registro de quién vio qué, listo para tu equipo de seguridad.'
},
{
icon: Icon.Clock,
title: 'Sin reprocesar',
text: 'Consulta cualquier ventana temporal al instante: el motor mantiene los agregados.'
}
];
const shown = $derived(features.slice(0, count));
@ -43,7 +67,8 @@
Todo lo que necesitas para medir de verdad
</Heading>
<Text size="lg" color="muted" align={align === 'center' ? 'center' : 'start'}>
Sin configurar pipelines ni esperar a mañana. Conectas la fuente de eventos y ya estás midiendo.
Sin configurar pipelines ni esperar a mañana. Conectas la fuente de eventos y ya estás
midiendo.
</Text>
</FeatureGrid.Header>

@ -39,8 +39,8 @@
{#snippet lede()}
La <strong>brecha nº1 del dossier</strong>: una afirmación de producto junto a un screenshot,
alternando lados fila a fila. La media se compone con <Code>Mockup</Code> (cromo de navegador o
teléfono), el primitivo de media que cierra ese hueco de raíz.
alternando lados fila a fila. La media se compone con <Code>Mockup</Code> (cromo de navegador o teléfono),
el primitivo de media que cierra ese hueco de raíz.
{/snippet}
{#snippet controls()}
@ -109,12 +109,12 @@
<Code>"phone"</Code>—, el primitivo de media del canon.
</DocRow>
<DocRow term=".Eyebrow · .Title · .Text">
<Code>Text</Code> primary + <Code>Heading</Code> (h2) + <Code>Text</Code> apagado. La app
pone las palabras.
<Code>Text</Code> primary + <Code>Heading</Code> (h2) + <Code>Text</Code> apagado. La app pone
las palabras.
</DocRow>
<DocRow term=".Features · .Feature">
<Code>Stack</Code> de líneas; cada <Code>.Feature</Code> es un check (decorativo,
aria-hidden) + <Code>Text</Code>.
<Code>Stack</Code> de líneas; cada <Code>.Feature</Code> es un check (decorativo, aria-hidden)
+ <Code>Text</Code>.
</DocRow>
<DocRow term=".Actions">
<Code>Group</Code> de <Code>Button</Code>/<Code>Link</Code>.
@ -126,17 +126,18 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="FeatureSplit">
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>xl</Code>) ·
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>xl</Code
>) ·
<Code>gap</Code> entre filas (def. <Code>12</Code>).
</DocRow>
<DocRow term=".Row">
<Code>reversed</Code> (bool) · <Code>media</Code> (snippet) · <Code>gap</Code> de la copy
(def. <Code>5</Code>).
<Code>reversed</Code> (bool) · <Code>media</Code> (snippet) · <Code>gap</Code> de la copy (def.
<Code>5</Code>).
</DocRow>
<DocRow term="Mockup (canon)">
<Code>chrome</Code>: <Code>'plain' | 'browser' | 'phone'</Code> · <Code>url</Code> para la
barra del navegador. Vive en <Code>$uix/eidos/components/mockup</Code> y lo componen varios
blocks.
<Code>chrome</Code>: <Code>'plain' | 'browser' | 'phone'</Code> · <Code>url</Code> para la barra
del navegador. Vive en <Code>$uix/eidos/components/mockup</Code> y lo componen varios blocks.
</DocRow>
</Stack>
{/snippet}
@ -161,8 +162,8 @@
{#snippet gaps()}
<Stack gap={3}>
<DocRow term="Banda de fondo por fila">
<strong>Diferido</strong> — la app envuelve una <Code>.Row</Code> en un <Code>Surface</Code>;
una prop sería azúcar.
<strong>Diferido</strong> — la app envuelve una <Code>.Row</Code> en un <Code>Surface</Code
>; una prop sería azúcar.
</DocRow>
<DocRow term="Media pegajosa">
<strong>Diferido</strong> — el screenshot se ancla mientras la copy hace scroll; compondría

@ -34,7 +34,11 @@
eyebrow: 'Analítica',
title: 'Cohortes al día, sin configurar pipelines',
text: 'Los eventos llegan crudos y salen en cohortes: qué hicieron, cuándo dejaron de hacerlo y qué cambió la semana en que lanzaste.',
features: ['Sin muestreo — los números cuadran', 'Ventanas sin reprocesar', 'Embudos y retención al vuelo'],
features: [
'Sin muestreo — los números cuadran',
'Ventanas sin reprocesar',
'Embudos y retención al vuelo'
],
cta: 'Ver la analítica',
media: 'browser' as const
},
@ -128,8 +132,8 @@
<Stack gap={4}>
<Heading level={2}>Todo desde un solo SDK</Heading>
<Text color="muted" measure="wide">
Un evento entra una vez y alimenta cohortes, alertas y automatizaciones — sin pegamento entre
servicios.
Un evento entra una vez y alimenta cohortes, alertas y automatizaciones — sin pegamento
entre servicios.
</Text>
</Stack>
</Container>

@ -49,8 +49,8 @@
{#snippet lede()}
La sección de apertura de una página: <Code>eyebrow</Code>, título, subtítulo, acciones y media,
en disposición <Code>center</Code> o <Code>split</Code>. El block envuelve las palabras del
título en un <Code>Heading</Code> y nombra con él el <Code>section</Code> — la app pone las
palabras, el block pone el sitio.
título en un <Code>Heading</Code> y nombra con él el <Code>section</Code> — la app pone las palabras,
el block pone el sitio.
{/snippet}
{#snippet controls()}
@ -205,18 +205,18 @@
</Text>
<Stack gap={3}>
<DocRow term="raíz">
<Code>&lt;section aria-labelledby&gt;</Code> — el <Code>id</Code> lo pone el título que el
block renderiza, así que la sección queda nombrada sin que la app cablee nada.
<Code>&lt;section aria-labelledby&gt;</Code> — el <Code>id</Code> lo pone el título que el block
renderiza, así que la sección queda nombrada sin que la app cablee nada.
</DocRow>
<DocRow term="padding · medida">
<Code>Section</Code> (prop <Code>size</Code>) sobre <Code>Container</Code> (prop
<Code>container</Code>).
<Code>containerSize</Code>).
</DocRow>
<DocRow term="disposición">
<Code>center</Code> = <Code>Stack</Code> centrado con la media debajo; <Code>split</Code> =
<Code>Grid</Code> de dos columnas (una sola cuando no hay media), que apila en estrecho;
<Code>background</Code> = capas de <Code>Box</Code> posicionadas (media · velo · copy) que
se ordenan por orden de fuente, sin <Code>z-index</Code>.
<Code>background</Code> = capas de <Code>Box</Code> posicionadas (media · velo · copy) que se
ordenan por orden de fuente, sin <Code>z-index</Code>.
</DocRow>
<DocRow term="título · subtítulo">
El block los envuelve en <Code>Heading</Code> (a <Code>level</Code>) y <Code>Text</Code>
@ -224,7 +224,8 @@
</DocRow>
<DocRow term="eyebrow · acciones · media">
Contenido libre: un <Code>Badge</Code>, unos <Code>Button</Code>/<Code>Link</Code>, una
<Code>Image</Code>/<Code>AspectRatio</Code>. Ahí los propios componentes del canon SON la API.
<Code>Image</Code>/<Code>AspectRatio</Code>. Ahí los propios componentes del canon SON la
API.
</DocRow>
</Stack>
</Stack>
@ -234,9 +235,9 @@
<Stack gap={3}>
<DocRow term="layout">
<Code>'center' | 'split' | 'background'</Code>, por defecto <Code>center</Code>.
<Code>background</Code> corre la media a sangre detrás de la copy con velo de contraste y
texto on-solid que el block gestiona. La alineación del texto va con el layout; no hay una
segunda prop que se contradiga con él.
<Code>background</Code> corre la media a sangre detrás de la copy con velo de contraste y texto
on-solid que el block gestiona. La alineación del texto va con el layout; no hay una segunda prop
que se contradiga con él.
</DocRow>
<DocRow term="level">
<Code>HeadingLevel</Code>, por defecto <Code>1</Code>. Un hero suele llevar el
@ -249,10 +250,10 @@
<Code>SectionSize</Code>, por defecto <Code>xl</Code>. Padding de bloque de la sección.
</DocRow>
<DocRow term="eyebrow · title · description · actions · media · backdrop · children">
Snippets: todo el contenido entra por composición (B-7). <Code>backdrop</Code> es la media
a sangre del layout <Code>background</Code> (un <Code>&lt;img&gt;</Code>/<Code>&lt;video&gt;</Code>
que el block cover-fitea); <Code>children</Code> se renderiza al final de la copy (un
logo-strip, una nota).
Snippets: todo el contenido entra por composición (B-7). <Code>backdrop</Code> es la media a sangre
del layout <Code>background</Code> (un <Code>&lt;img&gt;</Code>/<Code>&lt;video&gt;</Code>
que el block cover-fitea); <Code>children</Code> se renderiza al final de la copy (un logo-strip,
una nota).
</DocRow>
</Stack>
{/snippet}
@ -268,8 +269,8 @@
<Code>h1</Code> por página es responsabilidad de la app.
</DocRow>
<DocRow term="Media">
Decorativa por defecto: la aporta la app, que le pone <Code>alt</Code> si es informativa. El
placeholder de esta demo es puramente visual.
Decorativa por defecto: la aporta la app, que le pone <Code>alt</Code> si es informativa. El placeholder
de esta demo es puramente visual.
</DocRow>
</Stack>
{/snippet}
@ -282,16 +283,17 @@
el block. Cover-fit vía un <Code>&lt;style&gt;</Code> justificado (D-BLK.2).
</DocRow>
<DocRow term="Marcos de media en columna">
<strong>App-land</strong> — screenshot enmarcado o phone-mockup: la app ya mete lo que quiera
en <Code>media</Code>; un preset de cromo es solo azúcar, se hace cuando una demo lo pida.
<strong>App-land</strong> — screenshot enmarcado o phone-mockup: la app ya mete lo que
quiera en <Code>media</Code>; un preset de cromo es solo azúcar, se hace cuando una demo lo
pida.
</DocRow>
<DocRow term="form-in-hero">
<strong>Scope-approval pendiente</strong> — recurre en las refs; sería un slot
<Code>form</Code> componiendo <Code>Form</Code> + <Code>Field</Code>. Diferido de v1.
</DocRow>
<DocRow term="logo-strip">
<strong>App-land por ahora</strong> — la app lo mete en <Code>children</Code> (se renderiza
bajo las acciones); un slot propio solo si una demo real lo pide.
<strong>App-land por ahora</strong> — la app lo mete en <Code>children</Code> (se renderiza bajo
las acciones); un slot propio solo si una demo real lo pide.
</DocRow>
</Stack>
{/snippet}
@ -299,8 +301,8 @@
{#snippet notes()}
<Stack gap={3}>
<DocRow term="El block envuelve título y subtítulo">
A diferencia de <Code>eyebrow</Code>/<Code>actions</Code>/<Code>media</Code> (contenido
libre), el título y el subtítulo los envuelve el block en <Code>Heading</Code>/<Code>Text</Code>:
A diferencia de <Code>eyebrow</Code>/<Code>actions</Code>/<Code>media</Code> (contenido libre),
el título y el subtítulo los envuelve el block en <Code>Heading</Code>/<Code>Text</Code>:
así posee el <Code>id</Code> del landmark y el nivel, mientras la app conserva las palabras.
</DocRow>
<DocRow term="split sin media">

@ -59,7 +59,7 @@
const centered = $derived(layout !== 'split');
</script>
<Hero {layout} {level} {container} {size} {decor}>
<Hero {layout} {level} containerSize={container} sectionSize={size} {decor}>
{#snippet backdrop()}
<!-- The backdrop is a real Surface with the aurora gradient finish: the
identity's `primary` solid painted by `--gradient-aurora` (derived from

@ -10,7 +10,9 @@
const params = $derived(page.url.searchParams);
const layout = $derived((params.get('layout') ?? 'split') as 'center' | 'split' | 'background');
const level = $derived((Number(params.get('level') ?? 1) || 1) as 1 | 2 | 3 | 4 | 5 | 6);
const container = $derived((params.get('container') ?? 'lg') as 'sm' | 'md' | 'lg' | 'xl' | 'full');
const container = $derived(
(params.get('container') ?? 'lg') as 'sm' | 'md' | 'lg' | 'xl' | 'full'
);
const size = $derived((params.get('size') ?? 'xl') as 'sm' | 'md' | 'lg' | 'xl');
const withMedia = $derived(params.get('media') !== 'false');
const withEyebrow = $derived(params.get('eyebrow') !== 'false');

@ -171,7 +171,7 @@
<Code>layout</Code> (<Code>'center' | 'justified'</Code>, def. <Code>center</Code>) ·
<Code>panel</Code> (def. <Code>true</Code>) · <Code>color</Code> (def.
<Code>primary</Code>) · <Code>gradient</Code> (def. <Code>true</Code>) ·
<Code>level</Code> (def. <Code>2</Code>) · <Code>container</Code> · <Code>size</Code>.
<Code>level</Code> (def. <Code>2</Code>) · <Code>containerSize</Code> · <Code>size</Code>.
</DocRow>
<DocRow term="Newsletter — formulario">
<Code>form</Code> (obligatorio) y <Code>schema</Code> (opcional): se reenvían tal cual al

@ -31,10 +31,10 @@
{/snippet}
{#snippet lede()}
La sección de planes: un <Code>Switch</Code> de periodo sobre una fila de planes, uno destacado.
Es el primer block <strong>compound con contexto</strong> del tier — el <Code>Switch</Code>
escribe el periodo y cada <Code>PlanPrice</Code> lo lee, así que las partes se COORDINAN. Prueba
el toggle mensual/anual: todos los precios cambian a la vez.
La sección de planes: un <Code>Switch</Code> de periodo sobre una fila de planes, uno destacado. Es
el primer block <strong>compound con contexto</strong> del tier — el <Code>Switch</Code>
escribe el periodo y cada <Code>PlanPrice</Code> lo lee, así que las partes se COORDINAN. Prueba el
toggle mensual/anual: todos los precios cambian a la vez.
{/snippet}
{#snippet composition()}
@ -54,8 +54,8 @@
<Code>badge</Code> encima.
</DocRow>
<DocRow term=".PlanPrice">
Muestra la cifra activa según el periodo. <strong>El block no formatea moneda</strong>:
la app compone <Code>FormatNumber</Code> dentro de los snippets (B-7).
Muestra la cifra activa según el periodo. <strong>El block no formatea moneda</strong>: la
app compone <Code>FormatNumber</Code> dentro de los snippets (B-7).
</DocRow>
<DocRow term=".PlanAction">
El CTA, fijado al borde inferior de la tarjeta (<Code>margin-block-start: auto</Code>),
@ -69,7 +69,9 @@
<Stack gap={3}>
<DocRow term="Pricing">
<Code>period</Code> (<Code>'monthly' | 'annual'</Code>, bindable, def.
<Code>monthly</Code>) · <Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code>
<Code>monthly</Code>) · <Code>containerSize</Code> (def. <Code>lg</Code>) · <Code
>sectionSize</Code
>
(def. <Code>xl</Code>).
</DocRow>
<DocRow term=".Switch">
@ -83,7 +85,9 @@
<Code>annual</Code>, el <Code>monthly</Code> vale para ambos periodos.
</DocRow>
<DocRow term=".PlanName · .PlanDescription · .PlanFeatures · .PlanFeature · .PlanAction">
Partes de conveniencia sobre <Code>Heading</Code>/<Code>Text</Code>/<Code>Stack</Code>/<Code>Group</Code>.
Partes de conveniencia sobre <Code>Heading</Code>/<Code>Text</Code>/<Code>Stack</Code>/<Code
>Group</Code
>.
</DocRow>
</Stack>
{/snippet}
@ -91,8 +95,8 @@
{#snippet a11y()}
<Stack gap={3}>
<DocRow term="Toggle">
<Code>Switch</Code> compone <Code>ToggleGroup</Code> (radiogroup): teclado y ARIA de
selección salen del componente del canon.
<Code>Switch</Code> compone <Code>ToggleGroup</Code> (radiogroup): teclado y ARIA de selección
salen del componente del canon.
</DocRow>
<DocRow term="Encabezados">
La sección un <Code>h2</Code>; cada plan un <Code>h3</Code>. El check de cada feature es
@ -121,14 +125,14 @@
{#snippet notes()}
<Stack gap={3}>
<DocRow term="Por qué contexto, no solo repetición">
<Code>feature-grid</Code> y <Code>feature-split</Code> son compound porque sus partes se
REPITEN. <Code>Pricing</Code> es el primero cuyas partes se HABLAN: el <Code>Switch</Code>
escribe el periodo y los <Code>PlanPrice</Code> lo leen. Esa coordinación es la que gana el
contexto — la forma más fuerte de compound.
<Code>feature-grid</Code> y <Code>feature-split</Code> son compound porque sus partes se REPITEN.
<Code>Pricing</Code> es el primero cuyas partes se HABLAN: el <Code>Switch</Code>
escribe el periodo y los <Code>PlanPrice</Code> lo leen. Esa coordinación es la que gana el contexto
— la forma más fuerte de compound.
</DocRow>
<DocRow term="El dinero es de la app">
<Code>period</Code> es bindable por si la app quiere observarlo (analítica, recordar la
elección), y el importe lo formatea la app con <Code>FormatNumber</Code>.
<Code>period</Code> es bindable por si la app quiere observarlo (analítica, recordar la elección),
y el importe lo formatea la app con <Code>FormatNumber</Code>.
</DocRow>
</Stack>
{/snippet}

@ -87,7 +87,13 @@
yr: '79 €',
cta: 'Hablar con ventas',
featured: false,
features: ['Todo lo de Pro', 'SSO/SAML + SCIM', 'Residencia de datos', 'SLA 99,9 %', 'Soporte dedicado']
features: [
'Todo lo de Pro',
'SSO/SAML + SCIM',
'Residencia de datos',
'SLA 99,9 %',
'Soporte dedicado'
]
}
];
</script>

@ -132,8 +132,10 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="SiteFooter">
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>lg</Code>) ·
<Code>minColumnWidth</Code> (def. <Code>7.5rem</Code>).
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>lg</Code
>) ·
<Code>minChildWidth</Code> (def. <Code>7.5rem</Code>).
</DocRow>
<DocRow term="brand · signup · social · legal · extra">
Snippets. <Code>extra</Code> es el slot libre de D-BLK.6: los selectores de idioma / tema / región

@ -28,7 +28,6 @@
const previewSrc = $derived(
`/blocks/site-header/preview?sticky=${sticky}&offset=${offset}&container=${container}&breakpoint=${breakpoint}&dir=${dir}`
);
</script>
<BlockDemo
@ -147,13 +146,13 @@
<Code>Sticky</Code> — solo cuando <Code>sticky</Code>; el estado llega como
<Code>[data-stuck]</Code> y el tratamiento lo pone la app.
</DocRow>
<DocRow term="medida"><Code>Container</Code> — prop <Code>container</Code>.</DocRow>
<DocRow term="medida"><Code>Container</Code> — prop <Code>containerSize</Code>.</DocRow>
<DocRow term="la barra">
<Code>Group</Code> con <Code>justify="space-between"</Code> y <Code>align="center"</Code>.
</DocRow>
<DocRow term="ancho ↔ estrecho">
<Code>Box</Code> con <Code>display</Code> responsive: el block no observa nada por su
cuenta (B-6) y las dos navegaciones nunca coexisten.
<Code>Box</Code> con <Code>display</Code> responsive: el block no observa nada por su cuenta
(B-6) y las dos navegaciones nunca coexisten.
</DocRow>
<DocRow term="cajón móvil">
<Code>Drawer</Code> con <Code>direction="end"</Code> y <Code>bind:open</Code>.
@ -186,8 +185,8 @@
boolean bindable — el estado del cajón, por si la app quiere cerrarlo al navegar.
</DocRow>
<DocRow term="brand · nav · actions · mobileNav · mobileTrigger · children">
Snippets: todo el contenido entra por composición (B-5). <Code>children</Code> se renderiza
bajo la barra, que es donde va un banner o una segunda fila.
Snippets: todo el contenido entra por composición (B-5). <Code>children</Code> se renderiza bajo
la barra, que es donde va un banner o una segunda fila.
</DocRow>
</Stack>
{/snippet}
@ -195,8 +194,8 @@
{#snippet a11y()}
<Stack gap={3}>
<DocRow term="Landmark">
Un solo <Code>header</Code>; el <Code>aria-label</Code> pasa tal cual, para distinguirlo si
la página tiene varias cabeceras.
Un solo <Code>header</Code>; el <Code>aria-label</Code> pasa tal cual, para distinguirlo si la
página tiene varias cabeceras.
</DocRow>
<DocRow term="Encabezados">
Ninguno: una cabecera de sitio es un landmark, no una sección. El <Code>h1</Code> pertenece a
@ -219,9 +218,9 @@
{#snippet gaps()}
<Stack gap={3}>
<DocRow term="CTA que navega y parece botón">
<strong>Candidato a canon</strong> — salió al componer este block: <Code>Button</Code> no
tiene <Code>href</Code> por decisión propia y <Code>Link</Code> no tiene variante prominente.
Se decide en el canon, no se falsea aquí.
<strong>Candidato a canon</strong> — salió al componer este block: <Code>Button</Code> no tiene
<Code>href</Code> por decisión propia y <Code>Link</Code> no tiene variante prominente. Se decide
en el canon, no se falsea aquí.
</DocRow>
<DocRow term="Scroll-hide">
<strong>Candidato a canon</strong> — es comportamiento con superficie de contrato, así que
@ -232,8 +231,8 @@
<Code>actions</Code>; el block no necesita saberlo.
</DocRow>
<DocRow term="Banner de anuncio">
<strong>Otro block</strong> — F2.11 <Code>banner</Code>, o un <Code>Banner</Code> del canon
dentro de <Code>children</Code>.
<strong>Otro block</strong> — F2.11 <Code>banner</Code>, o un <Code>Banner</Code> del canon dentro
de <Code>children</Code>.
</DocRow>
</Stack>
{/snippet}

@ -40,8 +40,16 @@
let mobileOpen = $state(false);
const product = [
{ href: '#analytics', title: 'Analytics', blurb: 'Cohortes, embudos y retención sobre tus eventos.' },
{ href: '#automations', title: 'Automations', blurb: 'Reglas que reaccionan a lo que hacen tus clientes.' },
{
href: '#analytics',
title: 'Analytics',
blurb: 'Cohortes, embudos y retención sobre tus eventos.'
},
{
href: '#automations',
title: 'Automations',
blurb: 'Reglas que reaccionan a lo que hacen tus clientes.'
},
{ href: '#analytics', title: 'Changelog', blurb: 'Lo que cambió cada semana.' }
];
@ -55,13 +63,15 @@
id: 'analytics',
title: 'Mide lo que de verdad pasa',
body: 'Los eventos llegan crudos y salen en cohortes: qué hicieron, cuándo dejaron de hacerlo y qué cambió la semana en que lanzaste. Sin muestreo y sin esperar a mañana.',
extra: 'Consulta cualquier ventana temporal sin reprocesar: el motor mantiene los agregados al día.'
extra:
'Consulta cualquier ventana temporal sin reprocesar: el motor mantiene los agregados al día.'
},
{
id: 'automations',
title: 'Reglas que se disparan solas',
body: 'Una automatización es una condición sobre el flujo de eventos y una acción: avisar a un canal, mover a un segmento, abrir un ticket. Se escribe una vez y se ejecuta en cuanto la condición se cumple.',
extra: 'Cada ejecución deja traza, así que puedes explicar por qué un cliente recibió lo que recibió.'
extra:
'Cada ejecución deja traza, así que puedes explicar por qué un cliente recibió lo que recibió.'
},
{
id: 'startups',
@ -84,11 +94,10 @@
];
</script>
<SiteHeader
{sticky}
{offset}
{container}
containerSize={container}
{breakpoint}
bind:mobileOpen
aria-label="Acme"

@ -10,7 +10,9 @@
const params = $derived(page.url.searchParams);
const sticky = $derived(params.get('sticky') !== 'false');
const offset = $derived(Number(params.get('offset') ?? 0));
const container = $derived((params.get('container') ?? 'lg') as 'sm' | 'md' | 'lg' | 'xl' | 'full');
const container = $derived(
(params.get('container') ?? 'lg') as 'sm' | 'md' | 'lg' | 'xl' | 'full'
);
const breakpoint = $derived((params.get('breakpoint') ?? 'md') as 'sm' | 'md' | 'lg');
</script>

@ -38,9 +38,10 @@
{/snippet}
{#snippet lede()}
La fila de cifras que respalda lo que la página acaba de afirmar. Compone el <Code>Metrics</Code>
del canon para la semántica de KPI y <Code>CountUp</Code> para la cifra que se cuenta sola al
entrar en pantalla — <strong>lo único de este tier que ninguna referencia puede shipear</strong>,
La fila de cifras que respalda lo que la página acaba de afirmar. Compone el <Code>Metrics</Code
>
del canon para la semántica de KPI y <Code>CountUp</Code> para la cifra que se cuenta sola al entrar
en pantalla — <strong>lo único de este tier que ninguna referencia puede shipear</strong>,
porque todas entregan markup estático.
{/snippet}
@ -98,8 +99,8 @@
{#snippet composition()}
<Stack gap={4}>
<Text color="muted">
El block coloca y escalona; la semántica de KPI es del <Code>Metrics</Code> del canon y el
conteo, del <Code>CountUp</Code>. Cifras y palabras son de la app.
El block coloca y escalona; la semántica de KPI es del <Code>Metrics</Code> del canon y el conteo,
del <Code>CountUp</Code>. Cifras y palabras son de la app.
</Text>
<Stack gap={3}>
<DocRow term="raíz">
@ -124,13 +125,15 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="StatsBand">
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>lg</Code>) ·
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>lg</Code
>) ·
<Code>columns</Code> o <Code>minChildWidth</Code> (def. <Code>12rem</Code>) ·
<Code>gap</Code> (def. <Code>8</Code>).
</DocRow>
<DocRow term=".Stat">
Todo lo de <Code>Metrics</Code>: <Code>align</Code> (def. <Code>center</Code> — una banda
son cifras centradas, no tarjetas de panel), <Code>size</Code> (def. <Code>lg</Code>),
Todo lo de <Code>Metrics</Code>: <Code>align</Code> (def. <Code>center</Code> — una banda son
cifras centradas, no tarjetas de panel), <Code>sectionSize</Code> (def. <Code>lg</Code>),
<Code>live</Code>…
</DocRow>
<DocRow term=".Value">
@ -148,8 +151,8 @@
animar. Lo resuelve el <Code>CountUp</Code>, no el block.
</DocRow>
<DocRow term="Cifras">
El formato numérico sale de <Code>uix.format</Code> (separadores por idioma), así que la
cifra se lee correcta en cada locale sin que la app la reescriba.
El formato numérico sale de <Code>uix.format</Code> (separadores por idioma), así que la cifra
se lee correcta en cada locale sin que la app la reescriba.
</DocRow>
<DocRow term="Estructura">
Sin encabezados: una banda son datos, no una sección con título. Si la página necesita uno,
@ -165,8 +168,8 @@
recurre en las refs. Entra como <Code>layout</Code> o hermano cuando una demo lo pida.
</DocRow>
<DocRow term="Timeline / stepped">
<strong>Diferido</strong> — el <Code>Timeline</Code> del canon existe; es otra sección, no
una banda.
<strong>Diferido</strong> — el <Code>Timeline</Code> del canon existe; es otra sección, no una
banda.
</DocRow>
<DocRow term="Delta / tendencia por cifra">
<strong>App-land</strong> — <Code>Metrics.Delta</Code> se compone dentro del

@ -146,7 +146,9 @@
<Stack gap={3}>
<DocRow term="Team">
<Code>align</Code> (<Code>'center' | 'start'</Code>, def. <Code>center</Code>) ·
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>lg</Code>).
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>lg</Code
>).
</DocRow>
<DocRow term="Team.Header · .Members">
<Code>Header</Code> envuelve un <Code>Box</Code> con medida (def. <Code>48rem</Code>);

@ -77,7 +77,7 @@
const people = $derived(PEOPLE.slice(0, columns === 3 ? 6 : columns === 2 ? 4 : columns + 2));
</script>
<Team {align} container="lg">
<Team {align} containerSize="lg">
<Team.Header>
<Heading level={2} align={align === 'center' ? 'center' : 'start'}>Quién lo construye</Heading>
<Text color="muted" align={align === 'center' ? 'center' : 'start'} wrap="pretty">

@ -30,13 +30,15 @@
{#snippet lede()}
Prueba social: una rejilla de citas, cada una con su autor (avatar · nombre · cargo). Compound
—la <Code>.Item</Code> se repite— como <Code>feature-grid</Code>; las partes no coordinan, así que
sin contexto.
—la <Code>.Item</Code> se repite— como <Code>feature-grid</Code>; las partes no coordinan, así
que sin contexto.
{/snippet}
{#snippet composition()}
<Stack gap={4}>
<Text color="muted">El block coloca las tarjetas; las palabras y las caras son de la app.</Text>
<Text color="muted"
>El block coloca las tarjetas; las palabras y las caras son de la app.</Text
>
<Stack gap={3}>
<DocRow term=".Items · .Item">
<Code>AutoGrid</Code> de tarjetas de igual alto; cada <Code>.Item</Code> es una
@ -46,9 +48,10 @@
<Code>Text</Code> (lg) con la cita. La app pone las comillas y las palabras.
</DocRow>
<DocRow term=".Author">
<Code>Group</Code> con el slot <Code>avatar</Code> + <Code>.AuthorName</Code>/<Code>.AuthorRole</Code>,
fijado al borde inferior de la tarjeta (<Code>margin-block-start: auto</Code>) para alinear
las caras aunque las citas midan distinto.
<Code>Group</Code> con el slot <Code>avatar</Code> + <Code>.AuthorName</Code>/<Code
>.AuthorRole</Code
>, fijado al borde inferior de la tarjeta (<Code>margin-block-start: auto</Code>) para
alinear las caras aunque las citas midan distinto.
</DocRow>
<DocRow term="avatar">
La cara es del app: un <Code>&lt;Avatar&gt;</Code> con imagen o un
@ -61,14 +64,19 @@
{#snippet api()}
<Stack gap={3}>
<DocRow term="Testimonials">
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>lg</Code>).
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>lg</Code
>).
</DocRow>
<DocRow term=".Items">
<Code>minChildWidth</Code> (def. <Code>20rem</Code>) o <Code>columns</Code> · <Code>gap</Code>
<Code>minChildWidth</Code> (def. <Code>20rem</Code>) o <Code>columns</Code> · <Code
>gap</Code
>
(def. <Code>6</Code>).
</DocRow>
<DocRow term=".Author">
<Code>avatar</Code> (snippet) + children (<Code>.AuthorName</Code>/<Code>.AuthorRole</Code>).
<Code>avatar</Code> (snippet) + children (<Code>.AuthorName</Code>/<Code>.AuthorRole</Code
>).
</DocRow>
</Stack>
{/snippet}
@ -90,8 +98,8 @@
minoría. Es otra disposición → candidato a hermano <Code>testimonial-spotlight</Code>.
</DocRow>
<DocRow term="Carrusel">
<strong>Diferido</strong> — el <Code>Carousel</Code> del canon existe; entra cuando una demo
lo pida.
<strong>Diferido</strong> — el <Code>Carousel</Code> del canon existe; entra cuando una demo lo
pida.
</DocRow>
</Stack>
{/snippet}
@ -99,8 +107,8 @@
{#snippet notes()}
<Stack gap={3}>
<DocRow term="Compound sin contexto">
Como <Code>feature-grid</Code>/<Code>feature-split</Code>: la <Code>.Item</Code> se repite
pero las partes no se hablan, así que no hay contexto (a diferencia de <Code>pricing</Code>).
Como <Code>feature-grid</Code>/<Code>feature-split</Code>: la <Code>.Item</Code> se repite pero
las partes no se hablan, así que no hay contexto (a diferencia de <Code>pricing</Code>).
</DocRow>
</Stack>
{/snippet}

Loading…
Cancel
Save

Powered by TurnKey Linux.