docs(blocks): cada block declara su posicion bajo la doctrina de coordinacion

Y `content-section.Media` pasa a seguir al texto por defecto.

DEFAULT: `.Media` era `width='wide'`, asi que una figura se salia de la columna
SALVO que dijeras lo contrario. Al reves de lo que hacen todas las referencias
(en `@tailwindcss/typography` una imagen dentro de `prose` queda capada a la
medida y salirse exige el truco del `translate`; en `markdown-body` de GitHub,
`max-width: 100%` de la columna). Ahora el default es `measure` — la figura
sigue al texto y el escape se PIDE. Cambiado en los cuatro sitios que decian lo
contrario: la parte, la ruta `preview`, el estado inicial del control de la demo
(una demo que pisa el default en silencio ensena algo que el block no hace solo)
y la doc. Verificado: 528 = ancho del texto exacto a 1011/1280/1920, y `wide` /
`full` siguen funcionando cuando se piden.

DOCUMENTACION: los 15 READMEs declaran ahora su posicion bajo §«Coordination»,
y el mapa real no es uniforme —

- Coordinan: `contact` (maquina + esquema + palabras) y `pricing` (el periodo
  por contexto). Pero en pricing nada se puede BLOQUEAR, asi que no necesita
  maquina ni palabras: coordinar no es siempre una maquina.
- Comparten CONFIGURACION, no estado: `team` (`align`) y `content-section` (la
  medida).
- Costura bindable hacia el canon: `faq` (`value` → `Accordion`) y
  `site-header` (`mobileOpen` → `Drawer`). Reenviar no es poseer.
- No poseen nada: banner · cta · feature-grid · feature-split · hero ·
  site-footer · stats-band · testimonials. Decirlo explicitamente es el punto:
  inventarle estado a un block que no coordina nada es el error contrario.
- `newsletter` queda como CANDIDATO ABIERTO: coloca un envio que puede quedar
  bloqueado sin que nada diga por que — el `disabled` lo monta el app en el
  punto de uso — que es exactamente el hueco que cerro `contact`. Es anterior a
  la doctrina (block del 30-07, doctrina del 31-07). Registrado, no asumido: se
  toca con decision del usuario.

El README del tier apunta a la doctrina y el handoff recoge el mapa completo.

Gates: blocks:check verde (15) · docs:check 0 · svelte-check sin errores en lo
tocado · prettier limpio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 2 months ago
parent 378457f46c
commit 96393e4305

@ -1,4 +1,4 @@
# CONTINUE — F2 blocks de sitio (handoff, act. 2026-07-31)
# CONTINUE — F2 blocks de sitio (handoff, act. 2026-08-01)
**Estado: F1 CERRADA (8/8) · F2 CERRADA (15/15).**
Hechos: site-header · hero · feature-grid · **feature-split** · pricing ·
@ -6,10 +6,9 @@ testimonials · faq · stats-band · cta · newsletter · site-footer · banner
**contact** (§F2.13, el primero que COORDINA) · **content-section** (§F2.14).
**Siguiente**: F3 (blocks de aplicación, 10) — o antes la página compuesta de
prueba que el plan pide para cerrar F2 (header + hero + features + pricing + faq
- cta + footer juntos, mirada en claro/oscuro/375/1280): es el test de
integración del tier y todavía no existe.
prueba que el plan pide para cerrar F2: header, hero, features, pricing, faq, cta
y footer juntos, mirada en claro/oscuro/375/1280. Es el test de integración del
tier y todavía no existe.
> ⚠️ **CAMBIO DE DOCTRINA (decisión del usuario, 2026-07-31).** Lee la sección
> «La doctrina cambió» antes de tocar nada: un block ya no es solo colocación.
@ -57,10 +56,24 @@ enmendado (posee las palabras de SUS estados, como idlangref con fallback
inglés) y la convención de servicios se acota: **el traductor es el único
servicio sancionado**. La sección dice también a quién NO aplica.
**Alcance acordado**: `contact` primero como prueba de la forma — **HECHO**;
queda revisar los que tienen estado real — `pricing` (periodo), `newsletter`
(alta), `site-footer` (idioma), `banner` (descarte), `team` (eje). El resto es
layout puro y ahí no hay coordinación que mover.
**Alcance acordado**: `contact` primero como prueba de la forma — **HECHO**.
**Revisión documental de los 15 HECHA** (2026-08-01): cada README declara ahora
su posición bajo la doctrina, y el mapa real salió así —
- **Coordinan**: `contact` (máquina + esquema + palabras) · `pricing` (el periodo
por contexto, pero nada se puede BLOQUEAR ahí, así que no necesita máquina ni
palabras: coordinar no es siempre una máquina).
- **Comparten configuración, no estado**: `team` (`align`) · `content-section`
(la medida).
- **Costura bindable hacia el canon**: `faq` (`value` → `Accordion`) ·
`site-header` (`mobileOpen` → `Drawer`). Reenviar no es poseer.
- **No poseen nada**: banner · cta · feature-grid · feature-split · hero ·
site-footer · stats-band · testimonials.
- ⚠️ **`newsletter` es el candidato ABIERTO**: coloca un envío que puede quedar
bloqueado sin que nada diga por qué (el `disabled` lo monta el app en el punto
de uso) — exactamente el hueco que cerró `contact`. Es anterior a la doctrina
(block del 2026-07-30, doctrina del 2026-07-31). **Siguiente candidato a
tocar**, con decisión tuya antes de moverlo.
⚠️ Al revisarlos, el listón es el que dejó `contact`: **el estado se deriva de
una fuente y las partes lo leen**; si un estado puede bloquear algo, tiene frase

@ -4,6 +4,13 @@ Named compositions of canon components performing a **page function** (sticky
site header, hero, app shell, docs shell, …). This tree IS the live
inventory — one folder per block, no list in any doc.
Every block's README states its **position under the coordination doctrine**
(2026-07-31): whether it owns its section's state, the shape of its data and the
words for its states — or owns nothing because it is layout. Most are layout, and
saying so explicitly is the point: inventing state for a block that coordinates
nothing is the opposite mistake. Worked example: `contact`. Open candidate:
`newsletter`.
- **Doctrine** (what a block is, the admission rule, the B contract):
[`docs/architecture/blocks.md`](../../../docs/architecture/blocks.md)
- **Guard**: `npm run blocks:check` (B contract, self-testing)
@ -16,7 +23,7 @@ inventory — one folder per block, no list in any doc.
```text
src/uix/blocks/{kebab}/
├── README.md # Function · Composition map · Decisions · Gaps (B-9)
├── README.md # Function · Composition map · Coordination · Decisions · Gaps
├── index.ts # public export (compound component)
└── {kebab}.svelte # composition — canon components only, layout via
# layout components, no .css file (B contract)
@ -59,21 +66,25 @@ web/routes/blocks/{kebab}/
# {Name}
## Function
One paragraph: the page function this block performs.
## Composition map
| Part | Composes | Key props |
| --- | --- | --- |
| `.Nav` | `NavigationMenu` | … |
| Part | Composes | Key props |
| ------ | ---------------- | --------- |
| `.Nav` | `NavigationMenu` | … |
Landmark + heading hierarchy: which sectioning element / heading level the
block emits and how the app adjusts it.
## Decisions
Dated, with the reference comparison (which block catalogs were studied,
what was adopted/discarded).
## Gaps
Every deferred feature with a disposition (canon candidate · future variant
· rejected-with-reason).
```

@ -39,6 +39,14 @@ A banner's parts neither repeat nor coordinate, so there is no compound API —
the tier's admission rule. _(The plan sketched `<SiteBanner>` + children; the
`badge` and `action` slots are the two positions the references converge on.)_
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The dismissal is the app's — a `{#if}` around the block plus
`onDismiss` — and the canon `Banner` owns `dismissed` where it belongs. Nothing
here can be blocked, so there is no state to derive and no state to explain.
## Decisions
**2026-07-30 — reference floor** (dossier: Tailwind Plus «Banners» 13 · Untitled

@ -48,12 +48,12 @@ edge.
## Composition map
| Slot | Composes | Notes |
| ------- | --------------------------------------- | --------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Grid` | owns the landmark `id`; the grid replaces the usual `Container` |
| header | `Motion` + `Stack` + `Heading` + `Text` | snippets: `eyebrow` · `title` · `lede` · `meta` |
| `Body` | `Box` + `Prose` (`measure={false}`) | a run of the article's copy |
| `Media` | `Box` + `Motion` + `<figure>` | a figure outside the prose flow, with a real `<figcaption>` |
| Slot | Composes | Notes |
| ------- | --------------------------------------- | ---------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Grid` | owns the landmark `id`; the grid replaces the usual `Container` |
| header | `Motion` + `Stack` + `Heading` + `Text` | snippets: `eyebrow` · `title` · `lede` · `meta` |
| `Body` | `Box` + `Prose` (`measure={false}`) | a run of the article's copy |
| `Media` | `Box` + `Motion` + `<figure>` | a figure with a real `<figcaption>`; follows the text by default |
The root shares the measure by context (`context.ts`) — **configuration, not
state**: the caption of a broken-out figure needs the section's reading column,
@ -82,6 +82,12 @@ entry; Tailwind Plus, GitHub `.markdown-body`, Starlight):
- **Adopted — the breakout.** Every editorial reference lets media exceed the
text measure; it is the single thing that distinguishes an article layout from
a paragraph in a box.
- **Adopted — a figure follows the text by default.** In every reference an
image inside the reading column stays in it (`@tailwindcss/typography` caps it
at the measure; escaping takes the `translate` hack or a hand-rolled grid), so
`Media` defaults to `width='measure'` and breaking out is opt-in. A figure
that widens itself unasked is a surprise, and the block's own demo was the
proof: the first question it got was «why is the blue wider than the text?».
- **Adopted — the caption stays narrow.** A `<figcaption>` set to the width of a
full-bleed image is unreadable, so the caption is capped at the reading
measure even when its figure is not. Several references do not do this.

@ -8,6 +8,10 @@
* interactive, so this is document structure — the one a11y surface a block
* owns (B-8) — not the admission rule firing.
*
* It follows the TEXT by default, and breaking out is opt-in — the way every
* reference behaves (an image inside `prose` is capped at the measure; escaping
* it takes a hack). A figure that widens itself unasked is a surprise.
*
* The caption stays in the READING column even when the figure breaks out: a
* caption set to the width of a full-bleed image is unreadable.
*/
@ -19,14 +23,14 @@
import { getContentSectionContext, captionWidth } from './context';
import type { ContentSectionMediaProps } from './types';
let { width = 'wide', caption, children, ...rest }: ContentSectionMediaProps = $props();
let { width = 'measure', caption, children, ...rest }: ContentSectionMediaProps = $props();
const eidos = ActiveEidos.require();
const section = getContentSectionContext();
// Resolved the way every eidos primitive resolves a responsive prop, so
// `{ base: 'full', md: 'wide' }` works here exactly as it does on `Box`.
const reach = $derived(eidos.resolve(width, 'wide'));
const reach = $derived(eidos.resolve(width, 'measure'));
const capWidth = $derived(captionWidth(section?.measure ?? 'normal', reach));
</script>

@ -81,10 +81,13 @@ export type ContentSectionBodyProps = Omit<ProseProps, 'measure'> & {
// so Box's own is not something a caller should be setting anyway.
export type ContentSectionMediaProps = Omit<BoxProps, 'children' | 'width'> & {
/**
* How far it escapes the reading column. **Responsive** — the reason the prop
* exists at all: a photo that runs edge to edge on a phone and sits contained
* on a desktop is `{ base: 'full', md: 'wide' }`, which one value cannot say.
* @default 'wide'
* How far it escapes the reading column. **Responsive** — a photo that runs
* edge to edge on a phone and sits contained on a desktop is
* `{ base: 'full', md: 'wide' }`, which one value cannot say.
*
* @default 'measure' — the figure FOLLOWS THE TEXT unless asked otherwise,
* the way every reference behaves. Breaking out is the exception you request,
* not something a figure does to you.
*/
width?: ResponsiveProp<ContentWidth>;
/** The caption — rendered as a `<figcaption>` under the media. */

@ -9,25 +9,33 @@ beat of a page (`center`) or a mid-page nudge that must not stop the reading
## Composition map
| Slot | Composes | Notes |
| --- | --- | --- |
| root | bare `<section aria-labelledby>` | landmark; `id` names it from the title the block renders |
| padding · measure | `Section` + `Container` | `size` / `container` |
| 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 |
| `eyebrow` | — (app: `Badge` / text) | above the title |
| `title` | `Heading` at `level` (default 2) | the block wraps the app's words and owns the landmark `id`; on-solid ink |
| `description` | `Text` `as="p"` (`60ch` measure) | on-solid ink. A `span` would ignore `align` — `text-align` is inert on an inline box |
| `actions` | `Flex` (column → row at `sm`) | the app drops `Button`s / `Link`s |
| Slot | Composes | Notes |
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| root | bare `<section aria-labelledby>` | landmark; `id` names it from the title the block renders |
| padding · measure | `Section` + `Container` | `size` / `container` |
| 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 |
| `eyebrow` | — (app: `Badge` / text) | above the title |
| `title` | `Heading` at `level` (default 2) | the block wraps the app's words and owns the landmark `id`; on-solid ink |
| `description` | `Text` `as="p"` (`60ch` measure) | on-solid ink. A `span` would ignore `align` — `text-align` is inert on an inline box |
| `actions` | `Flex` (column → row at `sm`) | the app drops `Button`s / `Link`s |
## Form: snippet slots, not sub-components
The tier's rule is that a compound API is earned when parts **repeat**
(`feature-grid.Item`) or **coordinate** (`pricing.Switch` ↔ `PlanPrice`). A CTA's
parts do neither: they are fixed positional slots the root arranges. So it takes
the same shape as `hero`. *(The plan sketched `.Title`/`.Description`/`.Actions`;
this is the same registered deviation as `hero`.)*
the same shape as `hero`. _(The plan sketched `.Title`/`.Description`/`.Actions`;
this is the same registered deviation as `hero`.)_
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** A closing panel places copy and actions; there is no state, no
data shape and no word of its own. Layout blocks do not coordinate, and giving
one a machine would be the opposite mistake.
## Decisions
@ -37,7 +45,7 @@ PrimeBlocks «CTA 12» · Untitled UI):
- **Adopted**: the centred panel (parity) **and the `justified` arrangement** —
copy at the inline-start, actions at the end — which the dossier names as the
gap to close.
- **The finish is the system's**: a CTA is distinguished by *treatment*, and
- **The finish is the system's**: a CTA is distinguished by _treatment_, and
treatment is already framework vocabulary. `Surface` with `gradient` on by
default (the one section that earns it), so a theme or palette swap carries the
panel along. The block paints nothing of its own.
@ -67,14 +75,14 @@ layout, colour, finish and direction, and the device widths served from
## Gaps
| Gap | Disposition |
| --- | --- |
| **Split with media** (panel with a screenshot beside the copy) | **deferred** — recurs in the refs; it is a third arrangement, and `Mockup` already exists for the media. Enters when a demo asks |
| **Full-bleed panel** (edge to edge, no container) | **app-land** — the app sets `container="full"` |
| **Dismissible / sticky CTA** | **out** — that is the `banner` block's job (F2.11), not this one |
| **A quiet BOUNDED panel** | **canon** — `Surface soft` does not bound (0.002 L from the page in light) and has no border; `Card outline` bounds but takes no gradient finish. No primitive covers "quiet CTA panel with an edge" |
| **`contrast` slot is white on every solid step** | **canon** — so a mid-L solid canvas (`neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 in light) puts body copy below AA. The pairing guarantee holds for the dark canvases only. Measured with a luminance probe, not eyeballed |
| **On-solid chip treatment** | **canon** — `Badge` has no on-solid variant; the demo uses `variant="soft"`, whose near-white track happens to read against a saturated canvas. A `Badge` that knows it sits on a solid panel would be the real answer |
| Gap | Disposition |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Split with media** (panel with a screenshot beside the copy) | **deferred** — recurs in the refs; it is a third arrangement, and `Mockup` already exists for the media. Enters when a demo asks |
| **Full-bleed panel** (edge to edge, no container) | **app-land** — the app sets `container="full"` |
| **Dismissible / sticky CTA** | **out** — that is the `banner` block's job (F2.11), not this one |
| **A quiet BOUNDED panel** | **canon** — `Surface soft` does not bound (0.002 L from the page in light) and has no border; `Card outline` bounds but takes no gradient finish. No primitive covers "quiet CTA panel with an edge" |
| **`contrast` slot is white on every solid step** | **canon** — so a mid-L solid canvas (`neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 in light) puts body copy below AA. The pairing guarantee holds for the dark canvases only. Measured with a luminance probe, not eyeballed |
| **On-solid chip treatment** | **canon** — `Badge` has no on-solid variant; the demo uses `variant="soft"`, whose near-white track happens to read against a saturated canvas. A `Badge` that knows it sits on a solid panel would be the real answer |
## Found while composing
@ -85,6 +93,6 @@ layout, colour, finish and direction, and the device widths served from
until the consumer changes the element. Canon candidate.
- **`Surface soft` cannot bound a panel** — measured above; see Gaps.
- **`Group` does not stack.** The actions row needed `Flex direction={{ base:
'column', sm: 'row' }}`: in a `Group` at 420px the secondary action's label
'column', sm: 'row' }}`: in a `Group` at 420px the secondary action's label
breaks mid-phrase against the primary button. `hero` composes its actions with
`Group` too, so it has the same narrow behaviour — noted for its next pass.

@ -7,12 +7,12 @@ narrow readable column. Usually near the end of a page, often after pricing.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` (narrow) + `Stack` | places the header and the list |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.List` | `Motion` (`trigger="viewport"`) wrapping the `Accordion` | **is** the accordion — its whole API passes through |
| `.Item` | `Accordion.Item` + `.Header` + `.Trigger` + `.Content` | thin proxy: `question` snippet → trigger, children → content |
| Part | Composes | Notes |
| --------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
| root | bare `<section>` + `Section` + `Container` (narrow) + `Stack` | places the header and the list |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.List` | `Motion` (`trigger="viewport"`) wrapping the `Accordion` | **is** the accordion — its whole API passes through |
| `.Item` | `Accordion.Item` + `.Header` + `.Trigger` + `.Content` | thin proxy: `question` snippet → trigger, children → content |
## Why compound (a thin proxy)
@ -23,6 +23,15 @@ API and passes it straight through** (`type`, `bind:value`, `collapsible`,
one convenience: it spares the app the `Accordion.Item > Header > Trigger` /
`Content` scaffolding and auto-generates the item `value` when omitted.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing of its own.** `value` is a bindable SEAM forwarded straight to the
canon `Accordion`, which owns the disclosure state, its keyboard and its ARIA. A
seam is not ownership: the block never derives from it and never gates anything
on it.
## Decisions
**2026-07-24 — reference floor** (dossier §P1: Tailwind Plus «FAQs 7» ·
@ -42,10 +51,10 @@ questions. Mini-page in `FaqSite.svelte`, shared by both surfaces.
## Gaps
| Gap | Disposition |
| --- | --- |
| Gap | Disposition |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Static 2/3-column list** (questions + answers laid out, no disclosure) | **scope-approval pending** — the dossier's FAQ gap: 6 of 7 in TW are NOT accordions. It is a different arrangement (a grid of Q&A, always open), so likely a `layout` prop or a sibling — a user decision, not a silent add |
| **"Still have questions?" tail** (a CTA to support below the list) | **app-land** — the app puts a `Text` + `Link`/`Button` after `.List`; a dedicated slot is only worth it if a demo asks |
| **"Still have questions?" tail** (a CTA to support below the list) | **app-land** — the app puts a `Text` + `Link`/`Button` after `.List`; a dedicated slot is only worth it if a demo asks |
## Found while composing

@ -8,15 +8,15 @@ after the hero.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
| `.Header` | `Box` (measure-cap) + `Stack` | the app supplies the section `Heading` (h2) + `Text` |
| `.Items` | `AutoGrid` + `data-stagger` | fluid (`minChildWidth`, default `16rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Stack` | IS the reveal, so it must stay the grid's direct child to get its structural index; `align="start"` (default) or `center` |
| `.ItemIcon` | `Surface` (solid, accent) | the coloured chip — solid so it reads in light mode, where the soft tint is near-white; the app drops an `<Icon.X>` inside |
| `.ItemTitle` | `Heading` (level 3) | the feature name |
| `.ItemText` | `Text` (muted) | the feature description |
| Part | Composes | Notes |
| ------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
| `.Header` | `Box` (measure-cap) + `Stack` | the app supplies the section `Heading` (h2) + `Text` |
| `.Items` | `AutoGrid` + `data-stagger` | fluid (`minChildWidth`, default `16rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Stack` | IS the reveal, so it must stay the grid's direct child to get its structural index; `align="start"` (default) or `center` |
| `.ItemIcon` | `Surface` (solid, accent) | the coloured chip — solid so it reads in light mode, where the soft tint is near-white; the app drops an `<Icon.X>` inside |
| `.ItemTitle` | `Heading` (level 3) | the feature name |
| `.ItemText` | `Text` (muted) | the feature description |
## Why compound (and not snippet slots)
@ -31,6 +31,14 @@ parent's, the cells are the app's.
`.Items` is an explicit grid wrapper (like a list's `List`): it lets the header
sit outside the grid without a full-row `grid-column: 1 / -1` span hack.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The items repeat but do not talk to each other — no context, no
state, no words. That is exactly why they are a compound without a context, and
why the rule is applied part by part, not block by block.
## Decisions
**2026-07-23 — reference floor** (dossier §P1: Tailwind Plus «Features 15» ·
@ -52,11 +60,11 @@ control of `columns`/`minChildWidth`/`align`/item-count, device widths from
## Gaps
| Gap | Disposition |
| --- | --- |
| Gap | Disposition |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Feature-split / alternating** (a text block beside a screenshot, sides alternating) | **scope-approval pending** — the dossier's **#1 F2 gap**: "lo shippean las 5 refs y es la sección nº1 tras el hero". It is a different arrangement (alternating 2-column rows, not an icon grid), so likely its own block (`feature-split`) or a sibling — a user decision, not a silent add here |
| **Card variant** (each item boxed) | **deferred** — one prop away (wrap the `Item` body in a `Card`); built when a demo asks |
| **Stat / numbered variants** | **deferred** — the number replaces the icon chip; app-composable today |
| **Card variant** (each item boxed) | **deferred** — one prop away (wrap the `Item` body in a `Card`); built when a demo asks |
| **Stat / numbered variants** | **deferred** — the number replaces the icon chip; app-composable today |
## Found while composing

@ -9,16 +9,16 @@ beside a screenshot, repeated down the page.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | stacks the rows |
| `.Row` | `Grid` (2-col) + `Box` (grid placement) | `reversed` flips the sides via `grid-column`; DOM stays copy-first |
| `.Row` `media` slot | — (app: a `Mockup`, `Image`, `AspectRatio`) | the media column |
| `.Eyebrow` | `Text` (primary, semibold, sm) | the small label above the title |
| `.Title` | `Heading` (level 2) | the row's claim |
| `.Text` | `Text` (muted, lg) | the supporting sentence |
| `.Features` / `.Feature` | `Stack` / `Group` + `Icon.Check` + `Text` | the checklist; the check is decorative (aria-hidden) |
| `.Actions` | `Group` | the CTA cluster |
| Part | Composes | Notes |
| ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------ |
| root | bare `<section>` + `Section` + `Container` + `Stack` | stacks the rows |
| `.Row` | `Grid` (2-col) + `Box` (grid placement) | `reversed` flips the sides via `grid-column`; DOM stays copy-first |
| `.Row` `media` slot | — (app: a `Mockup`, `Image`, `AspectRatio`) | the media column |
| `.Eyebrow` | `Text` (primary, semibold, sm) | the small label above the title |
| `.Title` | `Heading` (level 2) | the row's claim |
| `.Text` | `Text` (muted, lg) | the supporting sentence |
| `.Features` / `.Feature` | `Stack` / `Group` + `Icon.Check` + `Text` | the checklist; the check is decorative (aria-hidden) |
| `.Actions` | `Group` | the CTA cluster |
**Reversal keeps reading order**: the copy is always first in the DOM; `reversed`
only sets `grid-column` so the media moves to the inline-start side. Screen
@ -30,11 +30,19 @@ Like `feature-grid`, the `Row` **repeats** — the app maps over N feature rows
so it earns a compound API (the tier's admission rule). The media is composed,
not baked: the app drops a **`Mockup`** (the canon media-frame primitive:
browser chrome, phone, or plain) into the `media` slot. That is the point of
this block's fase-0 — the dossier said the real gap is *media treatment*, so we
this block's fase-0 — the dossier said the real gap is _media treatment_, so we
closed it with a reusable primitive (`$uix/eidos/components/mockup`) that this
block, `hero`, `testimonials`… all share, rather than faking a screenshot per
demo.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The rows repeat and are independent; `reversed` is a per-row
prop, not shared state. Nothing here can be blocked, so there is nothing to
explain.
## Decisions
**2026-07-23 — reference floor** (dossier §P1: Tailwind Plus «Features 15» ·
@ -56,10 +64,10 @@ Mini-page in `FeatureSplitSite.svelte`, shared by both surfaces.
## Gaps
| Gap | Disposition |
| --- | --- |
| Feature-split with a **background band** per row (tinted section behind alternating rows) | **deferred** — the app wraps a `Row` in a `Surface`; a prop is sugar |
| **Sticky media** (the screenshot pins while the copy scrolls) | **deferred** — behaviour, would compose `Sticky`; built when a demo asks |
| Gap | Disposition |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Feature-split with a **background band** per row (tinted section behind alternating rows) | **deferred** — the app wraps a `Row` in a `Surface`; a prop is sugar |
| **Sticky media** (the screenshot pins while the copy scrolls) | **deferred** — behaviour, would compose `Sticky`; built when a demo asks |
## Found while composing

@ -10,25 +10,33 @@ renders) while the app owns every word.
## Composition map
| 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`) |
| 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 |
| `eyebrow` | — (app: `Badge` / text) | placed above the title |
| `title` | `Display` (`as={h{level}}`) | the hero typography primitive; the block wraps the app's words and owns the `id`; one size step down in `split`; on-solid ink under `background` |
| `description` | `Text` (`60ch` measure) | muted normally; on-solid ink under `background` |
| `actions` | `Group` | the app drops `Button`s / `Link`s |
| `media` | — (app: `Image` / `AspectRatio` / `Surface`) | second column (split) or below (center) |
| `backdrop` | positioned `Box` + scrim + cover `<style>` | `background` layout only: full-bleed behind the copy, `--color-overlay` scrim at `--opacity-scrim`, media cover-fit |
| 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`) |
| 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 |
| `eyebrow` | — (app: `Badge` / text) | placed above the title |
| `title` | `Display` (`as={h{level}}`) | the hero typography primitive; the block wraps the app's words and owns the `id`; one size step down in `split`; on-solid ink under `background` |
| `description` | `Text` (`60ch` measure) | muted normally; on-solid ink under `background` |
| `actions` | `Group` | the app drops `Button`s / `Link`s |
| `media` | — (app: `Image` / `AspectRatio` / `Surface`) | second column (split) or below (center) |
| `backdrop` | positioned `Box` + scrim + cover `<style>` | `background` layout only: full-bleed behind the copy, `--color-overlay` scrim at `--opacity-scrim`, media cover-fit |
**Landmark + heading**: one `<section>` named by its title. The title is an
`<h{level}>` (default `h1`) — a single `h1` per page is the app's call: it drops
`level` to `2` when another block already took the `h1`.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** Positional snippet slots and a layout; the state of a hero
lives in whatever the app puts in its actions. No machine, no data shape, no
words.
## Decisions
**2026-07-23 — reference floor** (dossier §P1: Tailwind Plus «Heroes 12» ·
@ -64,12 +72,12 @@ surfaces.
## Gaps
| Gap | Disposition |
| --- | --- |
| **Background-image / video cover** | **SHIPPED 2026-07-23** — the `background` layout (user scope-approval). Media full-bleed behind the copy, block-owned scrim + on-solid ink + cover-fit |
| **In-column media frames** (framed screenshot chrome · phone mockup) | **DONE 2026-07-24** — shipped as the canon `Mockup` (`chrome="browser" | "phone" | "plain"`), composed by the app into the `media` slot. The block still names no chrome of its own |
| **`form-in-hero`** (email sign-up in the hero) | **scope-approval pending** — recurs across refs; would be a `form` slot composing `Form` + `Field`. Deferred from v1 |
| **`logo-strip`** under the actions | **app-land for now** — the app can drop a row of logos into `children` (renders under the actions); a dedicated slot is only worth it if a demo asks |
| Gap | Disposition |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------ |
| **Background-image / video cover** | **SHIPPED 2026-07-23** — the `background` layout (user scope-approval). Media full-bleed behind the copy, block-owned scrim + on-solid ink + cover-fit |
| **In-column media frames** (framed screenshot chrome · phone mockup) | **DONE 2026-07-24** — shipped as the canon `Mockup` (`chrome="browser" | "phone" | "plain"`), composed by the app into the `media` slot. The block still names no chrome of its own |
| **`form-in-hero`** (email sign-up in the hero) | **scope-approval pending** — recurs across refs; would be a `form` slot composing `Form` + `Field`. Deferred from v1 |
| **`logo-strip`** under the actions | **app-land for now** — the app can drop a row of logos into `children` (renders under the actions); a dedicated slot is only worth it if a demo asks |
## Found while composing

@ -51,6 +51,21 @@ this is the same registered deviation as `hero` and `cta`.)_
`field` and `submit` are two slots rather than one, because the block's whole
layout job is the relationship between them.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
⚠️ **The tier's open candidate.** This block arranges a form and forwards the
handle, but the submit it places **can be blocked with nothing on screen saying
why** — the app assembles any `disabled` at the point of use, and an invalid or
in-flight signup has no sentence of its own. That is precisely the hole
[`contact`](../contact/README.md) closed by owning the section's state, its
default shape and its words.
It is not done here because this block predates the doctrine (built 2026-07-30,
doctrine signed 2026-07-31). Reviewing it is the agreed next step — recorded in
`docs/process/CONTINUE-blocks.md`, not silently assumed.
## Decisions
**2026-07-30 — reference floor** (dossier: Tailwind Plus «Newsletter» 6 ·

@ -8,18 +8,18 @@ monthly ↔ annual with the prices updating in step.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | `bind:period`; provides the period context |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.Switch` | `ToggleGroup` | writes the period; reads it back to stay in sync |
| `.Plans` | `AutoGrid` + `data-stagger` | fluid row of equal-height plan cards; the stagger rhythm is structural |
| `.Plan` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal (direct child of the staggered row); `featured` → accent colour; `badge` chip |
| `.PlanName` | `Heading` (level 3) | |
| `.PlanDescription` | `Text` (muted, sm) | one-line tagline |
| `.PlanPrice` | `Group` + `Text` | shows the `monthly` / `annual` snippet per the period; `suffix` beside it |
| `.PlanFeatures` / `.PlanFeature` | `Stack` / `Group` + `Icon.Check` + `Text` | the checklist; the check is decorative (aria-hidden) |
| `.PlanAction` | `Box` (`margin-block-start: auto`) | the CTA, pinned to the card's bottom so a row lines up |
| Part | Composes | Notes |
| -------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | `bind:period`; provides the period context |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.Switch` | `ToggleGroup` | writes the period; reads it back to stay in sync |
| `.Plans` | `AutoGrid` + `data-stagger` | fluid row of equal-height plan cards; the stagger rhythm is structural |
| `.Plan` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal (direct child of the staggered row); `featured` → accent colour; `badge` chip |
| `.PlanName` | `Heading` (level 3) | |
| `.PlanDescription` | `Text` (muted, sm) | one-line tagline |
| `.PlanPrice` | `Group` + `Text` | shows the `monthly` / `annual` snippet per the period; `suffix` beside it |
| `.PlanFeatures` / `.PlanFeature` | `Stack` / `Group` + `Icon.Check` + `Text` | the checklist; the check is decorative (aria-hidden) |
| `.PlanAction` | `Box` (`margin-block-start: auto`) | the CTA, pinned to the card's bottom so a row lines up |
## Why compound WITH a context
@ -33,6 +33,18 @@ but do not coordinate. `Pricing` is the tier's first compound whose parts
snippets; the app composes `FormatNumber` (locale, currency, rounding) inside
them. The block owns the switching, not the money.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**It coordinates, and it already did before the doctrine had a name.** The
billing period travels by context from `.Switch` to every `PlanPrice`, so the
app never re-derives which price to show — the case §«Coordination» describes.
What it does NOT need is a state machine or words of its own: nothing in a
pricing table can be _blocked_, so there is no reason to explain and no mute
state to make impossible. Coordination is not always a machine.
## Decisions
**2026-07-24 — reference floor** (dossier §P1: Tailwind Plus «Pricing 12» ·
@ -54,12 +66,12 @@ the middle), the live monthly/annual toggle switching every price. Mini-page in
## Gaps
| Gap | Disposition |
| --- | --- |
| **Feature-comparison table** (features × plans matrix) | **scope-approval pending** — the dossier's recurring pricing gap. It is a table, not a row of cards, so likely a sibling (`pricing-table`) or a `.Compare` sub-block — a user decision, not a silent add |
| **Single-price variant** (one plan, centered) | **deferred** — one `.Plan` in `.Plans`; the layout already handles it |
| **Per-period savings note** ("2 meses gratis") | **app-land** — the app puts it in the `annualLabel` or a `.PlanDescription` |
| **Elevation on the featured plan** | **canon candidate** — `Card` exposes no elevation prop, and a block paints nothing of its own. Marking the recommended tier by depth (not just colour) needs the prop in `Card`; until then `featured` is colour only |
| Gap | Disposition |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Feature-comparison table** (features × plans matrix) | **scope-approval pending** — the dossier's recurring pricing gap. It is a table, not a row of cards, so likely a sibling (`pricing-table`) or a `.Compare` sub-block — a user decision, not a silent add |
| **Single-price variant** (one plan, centered) | **deferred** — one `.Plan` in `.Plans`; the layout already handles it |
| **Per-period savings note** ("2 meses gratis") | **app-land** — the app puts it in the `annualLabel` or a `.PlanDescription` |
| **Elevation on the featured plan** | **canon candidate** — `Card` exposes no elevation prop, and a block paints nothing of its own. Marking the recommended tier by depth (not just colour) needs the prop in `Card`; until then `featured` is colour only |
## Found while composing

@ -30,6 +30,14 @@ neither repeat nor coordinate → snippet slots on the root. _(The plan sketched
`.Social`/`.Legal`/`.Extra` as parts; they are slots for exactly this reason —
same registered deviation as `hero`, `cta` and `newsletter`.)_
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** A footer places columns and slots; the language selector in
`extra` is the app wiring `uix.prefs`, which is app-land by the services rule.
No state, no shape, no words.
## Decisions
**2026-07-30 — reference floor** (dossier: Tailwind Plus «Footers» 7 · Untitled UI

@ -10,13 +10,13 @@ block owning any treatment.
## Composition map
| Part | Composes | Key props |
| --- | --- | --- |
| root | `Sticky` (F1.1) | `offset` — passed through; skipped entirely when `sticky={false}` |
| measure | `Container` | `size` (`container` prop, 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` |
| Part | Composes | Key props |
| ----------------------------- | ---------------- | ----------------------------------------------------------------- |
| root | `Sticky` (F1.1) | `offset` — passed through; skipped entirely when `sticky={false}` |
| measure | `Container` | `size` (`container` prop, 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` |
**Landmark + headings**: emits a single `<header>` and NO heading — a site
header is a landmark, not a section, and the page's `h1` belongs to whatever
@ -24,6 +24,15 @@ follows (usually a `hero`). If the app puts several navigations on the page,
name them: the block passes `aria-label` straight through to the `<header>`,
and the `nav` snippet's own `NavigationMenu` takes its own name.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing of its own.** `mobileOpen` is a bindable SEAM to the canon
`Drawer`, which owns the open state, the focus trap and the scroll lock. The
block forwards it so the app can drive the header from outside; forwarding is
not owning.
## Decisions
**2026-07-23 — reference floor** (dossier §P1: Tailwind Plus «Headers 8 +
@ -55,12 +64,12 @@ las dos superficies.
## Gaps
| Gap | Disposition |
| --- | --- |
| Scroll-hide (header retreats when scrolling down) | **canon candidate** — it is behaviour with contract surface (direction detection), so it goes to `sticky` first, never into this block (admission rule) |
| Skip-link | **shells own it** — it belongs to the page shell (`app-shell` / `docs-shell`, F3/F4), which knows what the main region is; putting it here would emit a second one on every shell page |
| Search field in the bar | **app-land** — it is a `Field`/`Command` the app composes into `actions`; the block does not need to know |
| Sub-navigation row under the bar | **out** — `children` renders under the bar; a second row of links is markup, not new API |
| Gap | Disposition |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scroll-hide (header retreats when scrolling down) | **canon candidate** — it is behaviour with contract surface (direction detection), so it goes to `sticky` first, never into this block (admission rule) |
| Skip-link | **shells own it** — it belongs to the page shell (`app-shell` / `docs-shell`, F3/F4), which knows what the main region is; putting it here would emit a second one on every shell page |
| Search field in the bar | **app-land** — it is a `Field`/`Command` the app composes into `actions`; the block does not need to know |
| Sub-navigation row under the bar | **out** — `children` renders under the bar; a second row of links is markup, not new API |
| A NAVIGATING call-to-action that looks like a button | **CLOSED in the canon 2026-07-23** — surfaced while composing this block. It is served by composition, not by a new prop: `Button`'s `child` (asChild) form now hands over a `content` snippet, so `<a href {...props}>{@render content()}</a>` gets the full solid paint AND keeps the icon / label / spinner slots, and soma stops stamping `type` on an element it does not own. `Button` still has no `href` and `Link` still owns navigation — both signed decisions hold. See the eidos Button README §«CTA que navega» |
## Found while composing

@ -8,12 +8,12 @@ the one section where the number itself is the content.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` + `AutoGrid` + `data-stagger` | fluid by `minChildWidth` (default `12rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Stat` | `Motion` (`trigger="viewport"`) wrapping `Metrics` | IS the reveal, so it must stay the band's direct child to get its index; `align="center"` by default (a band is centred figures, not dashboard tiles) |
| `.Value` | `Metrics.Value` (+ `CountUp` when `count` is set) | the figure |
| `.Label` | `Metrics.Label` | what the figure counts |
| Part | Composes | Notes |
| -------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Container` + `AutoGrid` + `data-stagger` | fluid by `minChildWidth` (default `12rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Stat` | `Motion` (`trigger="viewport"`) wrapping `Metrics` | IS the reveal, so it must stay the band's direct child to get its index; `align="center"` by default (a band is centred figures, not dashboard tiles) |
| `.Value` | `Metrics.Value` (+ `CountUp` when `count` is set) | the figure |
| `.Label` | `Metrics.Label` | what the figure counts |
The KPI semantics (label / value / delta) are the canon `Metrics`'s — the block
adds none of its own. `Metrics` is surfaceless, which is exactly right here: a
@ -37,6 +37,14 @@ Omit `count` and the app renders the value itself through `children` (already
formatted, or composed with `FormatNumber`). **The block never formats a number
and never ships a word** (B-7).
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The stats repeat and are independent, and the numbers are the
app's (`FormatNumber` / `CountUp` in the snippets). Nothing to derive, nothing to
say.
## Decisions
**2026-07-29 — reference floor** (dossier §P1: Tailwind Plus «Stats 8» ·
@ -60,11 +68,11 @@ control of the column count and the count-up, and the device widths served from
## Gaps
| Gap | Disposition |
| --- | --- |
| Gap | Disposition |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Split-with-image** (the band beside a photo) | **deferred** — a different arrangement (2 columns), recurring in the refs; enters as a `layout` or a sibling when a demo asks |
| **Timeline / stepped** variant | **deferred** — the canon `Timeline` exists; it is a different section, not a band |
| **Delta / trend per figure** | **app-land** — `Metrics.Delta` composes into `.Stat` today; the block re-exposing it would only add surface |
| **Timeline / stepped** variant | **deferred** — the canon `Timeline` exists; it is a different section, not a band |
| **Delta / trend per figure** | **app-land** — `Metrics.Delta` composes into `.Stat` today; the block re-exposing it would only add surface |
## Found while composing

@ -30,6 +30,15 @@ share one axis without the app repeating `align` on every card. An explicit
That is why there is a context here and not in `feature-grid` — the rule is
applied part by part, not block by block.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Shares configuration, not state.** The section's `align` travels by context to
every `Member` and to its link row, so the app does not repeat the axis on each
card — the same use `content-section` makes of its measure. There is no machine
here: nothing about a team grid can be blocked.
## Decisions
**2026-07-31 — reference floor** (dossier: Tailwind Plus «Team» 9 · PrimeBlocks

@ -8,16 +8,16 @@ section, usually below features or pricing.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.Items` | `AutoGrid` + `data-stagger` | fluid row of equal-height quote cards; the stagger rhythm is structural |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal, so it must stay the grid's direct child to get its structural index; fills the row height |
| `.Quote` | `Text` (lg) | the quotation |
| `.Author` | `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom |
| `.AuthorName` | `Text` (medium, sm) | |
| `.AuthorRole` | `Text` (muted, sm) | role / company |
| Part | Composes | Notes |
| ------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.Items` | `AutoGrid` + `data-stagger` | fluid row of equal-height quote cards; the stagger rhythm is structural |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal, so it must stay the grid's direct child to get its structural index; fills the row height |
| `.Quote` | `Text` (lg) | the quotation |
| `.Author` | `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom |
| `.AuthorName` | `Text` (medium, sm) | |
| `.AuthorRole` | `Text` (muted, sm) | role / company |
## Why compound
@ -27,6 +27,13 @@ there is no context; the grid is the parent's, the cards the app's. `.Author`
pins to the card's bottom (`margin-block-start: auto`) so a row of uneven quotes
still lines its faces up.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The items repeat without talking to each other — no context, no
state, no words. The face is the app's, in the `avatar` slot.
## Decisions
**2026-07-24 — reference floor** (dossier §P1: Tailwind Plus «Testimonials 8» ·
@ -47,11 +54,11 @@ surfaces.
## Gaps
| Gap | Disposition |
| --- | --- |
| Gap | Disposition |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Single quote in spotlight** (one large quote, logo + avatar + author, centered) | **scope-approval pending** — the dossier's testimonials gap: it is the modal variant across the references, and the grid is the minority (2 of 8 in TW). It is a different arrangement (one big quote, not a grid), so likely a sibling (`testimonial-spotlight`) or a `layout` — a user decision, not a silent add |
| **Logo strip** (customer logos instead of quotes) | **out** — that is the `logo-cloud` category (deferred at F2 scope), not this block |
| **Carousel variant** (quotes in a scroller) | **deferred** — the canon `Carousel` exists; it enters when a demo asks |
| **Logo strip** (customer logos instead of quotes) | **out** — that is the `logo-cloud` category (deferred at F2 scope), not this block |
| **Carousel variant** (quotes in a scroller) | **deferred** — the canon `Carousel` exists; it enters when a demo asks |
## Found while composing

@ -19,7 +19,9 @@
// `responsive` is an axis of the demo, not a value of the prop: it shows what
// only a per-breakpoint value can say — edge to edge on a phone, contained
// from `md` up.
let figure = $state<ContentWidth | 'responsive'>('wide');
// Starts at the block's DEFAULT: a demo that silently overrides it would be
// showing something the block does not do on its own.
let figure = $state<ContentWidth | 'responsive'>('measure');
const figureProp = $derived(
figure === 'responsive' ? ({ base: 'full', md: 'wide' } as const) : figure
);

@ -16,7 +16,7 @@
const figure = $derived(
params.get('figure') === 'responsive'
? ({ base: 'full', md: 'wide' } as const)
: ((params.get('figure') ?? 'wide') as ContentWidth)
: ((params.get('figure') ?? 'measure') as ContentWidth)
);
const wide = $derived((params.get('wide') ?? 'lg') as ContainerSize);
</script>

Loading…
Cancel
Save

Powered by TurnKey Linux.