---
title: Blocks — reusable page-function compositions
type: reference
audience: human + agent
authority: E1 architecture — the blocks tier: what it is, the canon-vs-block admission rule, the B contract, the promotion path
status: current
---
# Blocks
A **block** is a named composition of canon components performing a **page
function** — a sticky site header, a hero, a pricing section, an app shell.
It sits ABOVE the UIX layers and OUTSIDE the component canon, at
`src/uix/blocks/` (alias `$blocks` ). It consumes the framework; the framework
never references it. The live inventory is the `src/uix/blocks/` tree — never
a list in a doc.
Blocks exist so that **assembly knowledge does not dilute the canon** : the
catalog's value is contract density (events, ARIA, keyboard, tokens other
fix(blocks,canon): el ledger dice la verdad y el arco perceptivo vuelve a sonar
La auditoria del tier tenia los veredictos desemparejados de sus hallazgos: un
join por POSICION, y el journal del workflow era de sesion. Reproducido en vivo
al re-verificar — el journal devuelve los resultados en otro orden que la
entrada. Ledger nuevo con 90 ids estables (AUDIT-blocks-ledger.md), union
SIEMPRE por id: 41 arreglados, 34 confirmados, 12 refutados con motivo escrito,
3 dato. La tasa real de refutacion es del 13%, no del 28%.
De las 12 ALTA de percepcion, 8 tenian la causa raiz en el CANON. Los blocks
componen bien; lo que estaba roto era el arco perceptivo.
CANON
- Link: la prop color era inerte en subtle/plain (inherit a 0-2-0 ganaba a la
paleta a 0-1-0), el hover clavaba primary y el active quedaba tapado por el
hover de variante. Medido: el enlace del footer pasa de la tinta del padre a
la suya.
- Card: prometia BoxProps y no los aplicaba — height="100%" era un atributo
inerte. El tipo dice la verdad y el eje de tamano se aplica con un helper
compartido (buildSizeStyle). Tarjetas al fin de igual alto.
- Form.Submit / Form.Reset: componen el Button del canon, asi que la accion
principal de un formulario recupera el contact-activate en el gesto. Y el
aria-label generico deja de pisar el texto propio (WCAG 2.5.3 para todo
consumidor), verificado con el AX tree de Chrome.
- Field: dejaba de duplicar commit-submit dentro de un Form — un Enter emitia
dos commits con intents contradictorios y dos earcons.
- NavigationMenu: commit-select se mueve al Link (navegar es el acto
evaluable), el despliegue habla como emerge-open/close en vez de fingir una
seleccion por hover, y el pack casa por fin con la parte que recibe el
estampado — antes sonaba a la ganancia base, 10x lo disenado.
- Badge: el boton de quitar compone IconButton; la altura del chip pasa a ser
la del control, asi que md significa lo mismo en todo el sistema.
- Fundacion: [data-on] arrastra la propiedad color, no solo las variables.
BLOCKS
- hero: la CTA secundaria pasa de 1.61:1 a 6.61:1.
- contact: separa incomplete de invalid — el camino de error por campo era
inalcanzable por construccion — y su frase llega a la AT.
- newsletter: coordina (fase 4 del plan). Maquina de cinco estados, palabras
propias, Submit y Reason como partes que leen el contexto.
- site-header ya no congela la pagina al cruzar el breakpoint; site-footer
emite su commit; pricing no se vacia; cta llega a sangre de verdad.
DOCTRINA
- La frontera dura 1 nombra $libs/forms como puerta sancionada.
- El contrato B admite un segundo servicio: el anunciador. Un block que posee
las palabras de sus estados tiene que poder decirlas.
Gates: blocks:check 15/0 · vitest 20/20 en blocks y 402/402 en eidos+blocks ·
svelte-check 75/54 (linea base) · docs:check 0/0 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
layers consume); a block's value is _correct assembly_ — layout, landmarks,
heading hierarchy, responsive behavior, content slots. A block brings no new
primitives; it brings the right way to put existing ones together.
The tier map, for orientation:
fix(blocks,canon): el ledger dice la verdad y el arco perceptivo vuelve a sonar
La auditoria del tier tenia los veredictos desemparejados de sus hallazgos: un
join por POSICION, y el journal del workflow era de sesion. Reproducido en vivo
al re-verificar — el journal devuelve los resultados en otro orden que la
entrada. Ledger nuevo con 90 ids estables (AUDIT-blocks-ledger.md), union
SIEMPRE por id: 41 arreglados, 34 confirmados, 12 refutados con motivo escrito,
3 dato. La tasa real de refutacion es del 13%, no del 28%.
De las 12 ALTA de percepcion, 8 tenian la causa raiz en el CANON. Los blocks
componen bien; lo que estaba roto era el arco perceptivo.
CANON
- Link: la prop color era inerte en subtle/plain (inherit a 0-2-0 ganaba a la
paleta a 0-1-0), el hover clavaba primary y el active quedaba tapado por el
hover de variante. Medido: el enlace del footer pasa de la tinta del padre a
la suya.
- Card: prometia BoxProps y no los aplicaba — height="100%" era un atributo
inerte. El tipo dice la verdad y el eje de tamano se aplica con un helper
compartido (buildSizeStyle). Tarjetas al fin de igual alto.
- Form.Submit / Form.Reset: componen el Button del canon, asi que la accion
principal de un formulario recupera el contact-activate en el gesto. Y el
aria-label generico deja de pisar el texto propio (WCAG 2.5.3 para todo
consumidor), verificado con el AX tree de Chrome.
- Field: dejaba de duplicar commit-submit dentro de un Form — un Enter emitia
dos commits con intents contradictorios y dos earcons.
- NavigationMenu: commit-select se mueve al Link (navegar es el acto
evaluable), el despliegue habla como emerge-open/close en vez de fingir una
seleccion por hover, y el pack casa por fin con la parte que recibe el
estampado — antes sonaba a la ganancia base, 10x lo disenado.
- Badge: el boton de quitar compone IconButton; la altura del chip pasa a ser
la del control, asi que md significa lo mismo en todo el sistema.
- Fundacion: [data-on] arrastra la propiedad color, no solo las variables.
BLOCKS
- hero: la CTA secundaria pasa de 1.61:1 a 6.61:1.
- contact: separa incomplete de invalid — el camino de error por campo era
inalcanzable por construccion — y su frase llega a la AT.
- newsletter: coordina (fase 4 del plan). Maquina de cinco estados, palabras
propias, Submit y Reason como partes que leen el contexto.
- site-header ya no congela la pagina al cruzar el breakpoint; site-footer
emite su commit; pricing no se vacia; cta llega a sangre de verdad.
DOCTRINA
- La frontera dura 1 nombra $libs/forms como puerta sancionada.
- El contrato B admite un segundo servicio: el anunciador. Un block que posee
las palabras de sus estados tiene que poder decirlas.
Gates: blocks:check 15/0 · vitest 20/20 en blocks y 402/402 en eidos+blocks ·
svelte-check 75/54 (linea base) · docs:check 0/0 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Tier | Value | Contract | Enters through |
| ------------------------------ | ------------------------------- | ------------------------------------- | --------------------------------------------------------------------------- |
| **Canon** (`src/uix/`) | contract density others consume | morfo + acceptance matrix | the 9-phase route ([`building-a-component.md`](../building-a-component.md)) |
| **Packs** (`src/packs/`) | parameterized leaf decoration | P contract ([`packs.md`](./packs.md)) | `packs:check` |
| **Blocks** (`src/uix/blocks/`) | page-function composition | B contract (below) | `blocks:check` |
## The admission rule (canon vs block)
> **Canon** = anything with contract surface — a real event, a state machine,
> a `data-*` attribute CSS needs to select, a widget a11y obligation. It is
> built FIRST as a canonical component through the 9-phase route; the block
> then composes it.
>
> **Block** = composition only. If building a block reveals a missing
> behavior, that is a **gap to flag and promote** — never a privilege to grow
> inside the block.
What a block DOES own semantically: **landmarks and document structure**
(`header/nav/main/aside/footer`, heading hierarchy, skip links, region
labels). Components cannot know page context; blocks can. That is their only
a11y surface of their own.
A block MAY hold local view-state (the active tab, a monthly/yearly toggle)
through the public props/events of the components it composes. What it may
NOT do is materialize that state with DOM/CSS of its own that would need
contract — that is the admission rule firing.
feat(blocks): contact CERRADO — la coordinacion entra en el contrato B
El estado se le pregunta al ESQUEMA, no al Form. `form.isValid` con
`validationBehaviour: 'progressive'` significa «aun no se ha encontrado nada
mal»: un formulario vacio e intacto no tiene errores, asi que se declaraba
valido y `incomplete` era INALCANZABLE al cargar — la seccion pedia resolver la
verificacion con los tres campos vacios, justo el hueco que la doctrina existe
para cerrar. Ahora la raiz valida los valores con `validateSync` de SIUM:
sincrono y sin escribir errores (`form.validate()` habria encendido los tres
campos en rojo en el primer frame).
`state.test.ts` fija la maquina con 9 casos — primer test unitario del tier,
porque `state.ts` es su primera logica pura: el orden de prioridad, que solo
`ready` deja enviar, y que todo estado bloqueado tiene frase.
Contrato B (architecture/blocks.md): seccion «Coordination» con las tres cosas
que posee un block que coordina (estado, forma de sus datos, palabras), B-5
admite el esquema por defecto, B-7 enmendado (posee las palabras de SUS estados
como idlangref con fallback ingles) y la convencion de servicios acotada: el
traductor es el UNICO servicio sancionado. Dice tambien a quien NO aplica — los
blocks de layout no coordinan nada.
i18n por la via real: el arnes registra el namespace `blocks` en las DOS cadenas
de arranque (galeria y previews, que duplican el boot) y la demo se lee en
castellano en vez de caer al fallback ingles.
Ademas: README y pestanas de doc rehechos al reparto nuevo, ficha F2.13 y
bitacora en PLAN-blocks.md, y el control vivo que faltaba para un prop publico
— `verification` con reto / sin reto, porque omitir el prop no es lo mismo que
pasar un estado que nunca verifica.
Verificado en navegador, arco completo y las dos ramas: sin reto vacio → un
campo → tres campos (se desbloquea) → «Enviando…» → «Enviado»; con reto, con
los tres campos llenos el boton sigue bloqueado y la razon pasa a «Resuelve la
verificacion de arriba». `verifying` y `rejected` quedan cubiertos por test
unitario, no por el reto en vivo: el provider de ProofOfHuman escribe `status`
el mismo y el veredicto del app no se sostiene (hilo de canon abierto).
Gates: blocks:check verde (14 blocks) · vitest 9/9 · docs:check 0 ·
svelte-check sin errores propios · prettier limpio en los ficheros propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## Coordination: a block owns its section's state, shape and words
> Decision of 2026-07-31, from the tier's own evidence: **«if in the end it is
> a list of components… the block is coordination, state, and data too».**
Composition alone is not a floor high enough. When the state of a section lives
OUTSIDE the block, every consumer re-derives it at the point of use — each
`disabled` is its own expression — and the result is **holes** : an action that
cannot be taken and nothing on screen saying why. That is not a block; it is a
grid with instructions.
So a block that has a real state — one where something can be blocked, in
flight, or done — owns three things:
1. **The state of its section** , as ONE derived machine, exhaustive by
construction. Its parts READ it (through context); none recomputes it and
none invents a `disabled` of its own. Exhaustive means a `Record<State, …>`
that forces a sentence for every blocked state — a mute state becomes
impossible to add, instead of merely discouraged.
2. **The default shape of its data** — the schema the section asks for. A
contact section that cannot say what a contact form asks is a grid, not a
block. The app replaces it by passing its own.
3. **The words of the states it owns** , as idlangrefs through the translator
(`#?blocks.< block > .< key > |English fallback`) — the same door the canon uses
for its own `texts:` . Register nothing and the English fallback shows; an
app takes the words over by registering the `blocks` namespace in its langs
schema.
**This does not apply to every block.** Most are layout: `hero` , `cta` ,
`feature-grid` , `feature-split` , `stats-band` , `testimonials` , `faq` ,
`content-section` place content and own nothing. There is nothing to
coordinate there, and inventing state for them would be the opposite mistake.
The rule fires where a section can be blocked or in flight — `contact` is the
worked example (`src/uix/blocks/contact/state.ts`).
The admission rule still governs: coordination is derived state in TypeScript,
not new DOM or CSS. The moment a block wants a `data-*` attribute for CSS to
select, or an event with a semantic family, that is canon surface and it is
promoted — not grown inside the block.
## Hard boundaries
1. **Dependency direction is one-way** : `src/uix/blocks/*` may import `$uix` ,
fix(blocks,canon): el ledger dice la verdad y el arco perceptivo vuelve a sonar
La auditoria del tier tenia los veredictos desemparejados de sus hallazgos: un
join por POSICION, y el journal del workflow era de sesion. Reproducido en vivo
al re-verificar — el journal devuelve los resultados en otro orden que la
entrada. Ledger nuevo con 90 ids estables (AUDIT-blocks-ledger.md), union
SIEMPRE por id: 41 arreglados, 34 confirmados, 12 refutados con motivo escrito,
3 dato. La tasa real de refutacion es del 13%, no del 28%.
De las 12 ALTA de percepcion, 8 tenian la causa raiz en el CANON. Los blocks
componen bien; lo que estaba roto era el arco perceptivo.
CANON
- Link: la prop color era inerte en subtle/plain (inherit a 0-2-0 ganaba a la
paleta a 0-1-0), el hover clavaba primary y el active quedaba tapado por el
hover de variante. Medido: el enlace del footer pasa de la tinta del padre a
la suya.
- Card: prometia BoxProps y no los aplicaba — height="100%" era un atributo
inerte. El tipo dice la verdad y el eje de tamano se aplica con un helper
compartido (buildSizeStyle). Tarjetas al fin de igual alto.
- Form.Submit / Form.Reset: componen el Button del canon, asi que la accion
principal de un formulario recupera el contact-activate en el gesto. Y el
aria-label generico deja de pisar el texto propio (WCAG 2.5.3 para todo
consumidor), verificado con el AX tree de Chrome.
- Field: dejaba de duplicar commit-submit dentro de un Form — un Enter emitia
dos commits con intents contradictorios y dos earcons.
- NavigationMenu: commit-select se mueve al Link (navegar es el acto
evaluable), el despliegue habla como emerge-open/close en vez de fingir una
seleccion por hover, y el pack casa por fin con la parte que recibe el
estampado — antes sonaba a la ganancia base, 10x lo disenado.
- Badge: el boton de quitar compone IconButton; la altura del chip pasa a ser
la del control, asi que md significa lo mismo en todo el sistema.
- Fundacion: [data-on] arrastra la propiedad color, no solo las variables.
BLOCKS
- hero: la CTA secundaria pasa de 1.61:1 a 6.61:1.
- contact: separa incomplete de invalid — el camino de error por campo era
inalcanzable por construccion — y su frase llega a la AT.
- newsletter: coordina (fase 4 del plan). Maquina de cinco estados, palabras
propias, Submit y Reason como partes que leen el contexto.
- site-header ya no congela la pagina al cruzar el breakpoint; site-footer
emite su commit; pricing no se vacia; cta llega a sangre de verdad.
DOCTRINA
- La frontera dura 1 nombra $libs/forms como puerta sancionada.
- El contrato B admite un segundo servicio: el anunciador. Un block que posee
las palabras de sus estados tiene que poder decirlas.
Gates: blocks:check 15/0 · vitest 20/20 en blocks y 402/402 en eidos+blocks ·
svelte-check 75/54 (linea base) · docs:check 0/0 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`$adom` , the public arts and `$libs/forms` (the door to `createForm` , which
the canon `Form` does not re-export and soma consumes the same way — sanctioned
2026-08-05, when the audit found the contract and the code disagreeing about
`contact` ); nothing in the canon
(`src/uix/{morfo,soma,sema,eidos,active-uix,langs}`) nor in
`src/{arts,libs,packs}` may import from `src/uix/blocks/` . Deleting the
tier must leave `npm run check` green — blocks is a tier under `src/uix/` ,
not a fifth layer.
2. **No morfo, no matrix** : a block declares no morfo, ships no sema pack,
and takes no row in `component:audit` . Its quality floor is the B contract
below, guarded by `blocks:check` .
3. **Layout-components-first** : layout is composed from the canon layout
components (`Container / Section / Stack / Flex / Grid / AutoGrid / Wrap /
fix(blocks,canon): el ledger dice la verdad y el arco perceptivo vuelve a sonar
La auditoria del tier tenia los veredictos desemparejados de sus hallazgos: un
join por POSICION, y el journal del workflow era de sesion. Reproducido en vivo
al re-verificar — el journal devuelve los resultados en otro orden que la
entrada. Ledger nuevo con 90 ids estables (AUDIT-blocks-ledger.md), union
SIEMPRE por id: 41 arreglados, 34 confirmados, 12 refutados con motivo escrito,
3 dato. La tasa real de refutacion es del 13%, no del 28%.
De las 12 ALTA de percepcion, 8 tenian la causa raiz en el CANON. Los blocks
componen bien; lo que estaba roto era el arco perceptivo.
CANON
- Link: la prop color era inerte en subtle/plain (inherit a 0-2-0 ganaba a la
paleta a 0-1-0), el hover clavaba primary y el active quedaba tapado por el
hover de variante. Medido: el enlace del footer pasa de la tinta del padre a
la suya.
- Card: prometia BoxProps y no los aplicaba — height="100%" era un atributo
inerte. El tipo dice la verdad y el eje de tamano se aplica con un helper
compartido (buildSizeStyle). Tarjetas al fin de igual alto.
- Form.Submit / Form.Reset: componen el Button del canon, asi que la accion
principal de un formulario recupera el contact-activate en el gesto. Y el
aria-label generico deja de pisar el texto propio (WCAG 2.5.3 para todo
consumidor), verificado con el AX tree de Chrome.
- Field: dejaba de duplicar commit-submit dentro de un Form — un Enter emitia
dos commits con intents contradictorios y dos earcons.
- NavigationMenu: commit-select se mueve al Link (navegar es el acto
evaluable), el despliegue habla como emerge-open/close en vez de fingir una
seleccion por hover, y el pack casa por fin con la parte que recibe el
estampado — antes sonaba a la ganancia base, 10x lo disenado.
- Badge: el boton de quitar compone IconButton; la altura del chip pasa a ser
la del control, asi que md significa lo mismo en todo el sistema.
- Fundacion: [data-on] arrastra la propiedad color, no solo las variables.
BLOCKS
- hero: la CTA secundaria pasa de 1.61:1 a 6.61:1.
- contact: separa incomplete de invalid — el camino de error por campo era
inalcanzable por construccion — y su frase llega a la AT.
- newsletter: coordina (fase 4 del plan). Maquina de cinco estados, palabras
propias, Submit y Reason como partes que leen el contexto.
- site-header ya no congela la pagina al cruzar el breakpoint; site-footer
emite su commit; pricing no se vacia; cta llega a sangre de verdad.
DOCTRINA
- La frontera dura 1 nombra $libs/forms como puerta sancionada.
- El contrato B admite un segundo servicio: el anunciador. Un block que posee
las palabras de sus estados tiene que poder decirlas.
Gates: blocks:check 15/0 · vitest 20/20 en blocks y 402/402 en eidos+blocks ·
svelte-check 75/54 (linea base) · docs:check 0/0 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Group / Separator / AspectRatio / Surface`) and their props. A block ships
no `.css` file; a scoped `<style>` is the justified exception, never the
pattern.
4. **Blocks are removable by construction** — an app that never imports a
block pays zero bytes for it.
## The B contract (the block quality floor)
Guarded mechanically by `npm run blocks:check` (self-testing: the guard
asserts its own detectors against inline fixtures on every run).
feat(blocks): el guard vigila la frontera, y siete filas del ledger caen medidas
FASE 5 DEL SANEAMIENTO — `blocks:check` deja de vigilar solo la forma del tier
y pasa a vigilar su frontera, su documentacion y su catalogo. Tres reglas, cada
una con su fixture negativo en el selfTest():
1. Lista blanca de importaciones (B-4). La frontera dura 1 era prosa; ahora es
codigo: $uix, los arts publicos, $libs/forms, svelte y los relativos del
propio block. Los arts se DERIVAN de src/arts/*, no se listan a mano — una
lista escrita se queda atras el dia que aterriza un art. El tier ya cumplia:
cero violaciones al encenderla.
2. README completo (B-9) + declaracion de landmark (B-8). Salieron 7 de 15 sin
ella; escritas leyendo la fuente de cada block, no la plantilla.
3. Ficha en el catalogo de demos (B-9), en los DOS sentidos: un block que el
catalogo no publica es invisible para el rail, y un slug publicado sin block
detras es un enlace muerto.
De paso el escaner deja de leer los comentarios como codigo: los index.ts
documentan su uso con un `// import { Cta } from '$blocks/cta'`, y una lista
blanca que lee prosa habria empezado a acusar a los ejemplos.
Verificado EN ROJO sobre el arbol real, no solo contra los fixtures: un zod y un
$libs/days metidos en hero/types.ts salen con su linea exacta mientras el mismo
import dentro de un comentario NO salta; el catalogo falla en las dos
direcciones; y al romper isAllowedSpec a proposito el self-test aborta el guard
en vez de dar verde.
ARREGLOS DEL CONTRASTE DOCTRINAL — siete filas, cada una con medicion o gate:
- A-88 team: `height="100%"` en el Stack del Member. El div que pinta Motion es
un bloque pelado, asi que el Stack se quedaba a altura de contenido y el
`margin-top:auto` de los enlaces repartia CERO. Medido: la desviacion entre
filas de iconos pasa de 20px a 0, y el margen reparte 20,297px.
- A-94/A-28 (feature-grid, team, testimonials): los tres declaraban su tipo como
`= AutoGridProps` y ponian {...rest} ANTES de las props que fijan, asi que un
consumidor podia escribir align="start" y no obtener nada. Cerrados con
Pick<AutoGridProps, ...>, que conserva los tipos exactos del canon.
- A-84 feature-grid: el eje `align` llega a las partes por contexto, como en
team, en vez de que el typedoc instruya al app a repetirlo. Medido: con
align=center el align-items computado es center en cabecera Y celdas.
- A-19 faq: `marginX="auto"` en el Header — Box no centra solo. Medido: el Box
de 768px resuelve margin-inline 248px/248px donde antes daba 0.
- A-27 testimonials y A-86 pricing: el typedoc decia `soft` donde el codigo hace
`outline`, y el ejemplo de la puerta de entrada no compilaba.
- A-91 banner (fila nueva): el block hereda el eje intent/color que el canon
declara fuera del sistema abierto en su propio typedoc, sin registrarlo.
LEDGER — dos lecciones de instrumento escritas dentro, porque ambas produjeron
hallazgos falsos publicados:
1. El dev server sirve codigo ANTERIOR a HEAD si reusa cache de Vite. Tres filas
(A-36, A-69, A-85) se dieron por vivas estando arregladas. El delator fue
aritmetico: las duraciones medidas eran EXACTAMENTE las que el comentario del
arreglo cita como estado anterior.
2. Contar nodos WebAudio no es medir sonido: la sintesis crea dos osciladores
por earcon, un sound pack de muestras cambia la aritmetica entera, y crear
nodos no es sonar. El contador responde "hubo actividad" y nada mas.
Y el error de metodo del que ambas son sintoma, tambien registrado: se fue a
re-medir A-67 sin leer su ficha, que ya traia los gains del catalogo, el empate
de applyDominance y la regla de CANON.md incumplida — mejor razonado que la nota
que lo sustituia.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 69 errores /
57 avisos = linea base exacta, ninguno en lo tocado · docs:check 0/0 · prettier
limpio en todo lo del commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| B | Obligation |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| B-1 | No morfo, no sema pack, no audit row. Behavior with contract surface is promoted to canon BEFORE the block composes it. |
| B-2 | Every interactive element is a canon eidos component (`Button`, `Link` , `Field` , …). Raw interactive natives (`button/input/select/textarea/a`) are an error. (Single exception: the already-rendered HTML that `Prose` receives — that content belongs to the app.) |
| B-3 | Composed components are consumed AS-IS through their public props (`variant/size/color/…`). Re-styling their internals from the block (CSS or `style=` ) is forbidden. Colors are always roles/tokens via props. |
| B-4 | One-way imports (hard boundary 1). Deleting the tier leaves `check` green. Blocks do not import each other (see B-10). The allowlist is mechanical since 2026-08-09: `$uix` , the public arts (**derived from `src/arts/*` **, never a hand list), `$libs/forms` , `svelte` / `svelte/elements` and the block's own relatives. Anything else is either promoted to canon or the app's, and the guard says so by name. |
| B-5 | Content enters by composition (children/snippets) — never `root={tree}` data-tree props (`items={...}` only where the composed canon component is already data-driven). A coordinating block MAY ship the default **shape** of the data it coordinates (a schema), which the app replaces by passing its own — a shape is not content. |
| B-6 | Responsive via the framework's mechanisms (responsive props of the layout components, canonical breakpoints). No `matchMedia` /listeners of its own — needing to observe something is the admission rule firing. |
| B-7 | Everything the APP says arrives as children/props — a block writes none of it. **Amended 2026-07-31** : a block DOES own the words of the states it coordinates (and only those), as idlangrefs with an English fallback resolved through the translator (`#?blocks.< block > .< key > \|…`). The reason a state names itself is the same reason it exists: the record that blocks the action writes the sentence, so it cannot go silent. |
| B-8 | Correct landmarks: sectioning element + `aria-label` /`aria-labelledby` where landmarks repeat; heading hierarchy coherent and documented in the block README (which level it emits, how to adjust). The declaration is a `**Landmark + headings**` paragraph and `blocks:check` requires it — a block with no landmark of its own says THAT, which is the answer the app needs to name the region itself. |
| B-9 | Every block ships `README.md` (**Function · Composition map** — which canon components, which props — ** · Decisions · Gaps-with-disposition**) and a live demo page under `web/routes/blocks/{kebab}/` , built on the shared demo shell (`web/routes/blocks/_lib/BlockDemo.svelte`): the block is shown FULL-BLEED on the page — never inside a padded frame or a scroll box, which would change what it does — with the device widths served by its own `preview` route. Anatomy in [`src/uix/blocks/README.md` ](../../src/uix/blocks/README.md ). `blocks:check` requires the README's four sections (Function · Composition map · Decisions · Gaps) and a `shipped` entry in the demo catalog (`web/routes/blocks/_lib/catalog.ts`), both ways: a block the catalog does not ship is invisible to the rail, and a slug it ships without a block behind it is a dead link. |
| B-10 | A block does not import another block. Shared structure is either a canon component or a conscious duplication (recorded in Gaps). Declared exception: the shells (`app-shell`, `docs-shell` ) compose F1 pieces and blocks by design — allow-listed in `blocks:check` . |
| B-11 | Motion only through the composed components' `motion` props/presets or `Cascade` for entrance choreography. No `@keyframes` /transitions of its own. |
## Conventions
- **API**: compound component with nested parts (`< SiteHeader > ` /
`<SiteHeader.Nav>` / `<SiteHeader.Actions>` ); content always via children.
- **Demos**: `web/routes/blocks/{kebab}/+page.svelte` , indexed by the gallery
at `web/routes/blocks/` .
feat(blocks): contact CERRADO — la coordinacion entra en el contrato B
El estado se le pregunta al ESQUEMA, no al Form. `form.isValid` con
`validationBehaviour: 'progressive'` significa «aun no se ha encontrado nada
mal»: un formulario vacio e intacto no tiene errores, asi que se declaraba
valido y `incomplete` era INALCANZABLE al cargar — la seccion pedia resolver la
verificacion con los tres campos vacios, justo el hueco que la doctrina existe
para cerrar. Ahora la raiz valida los valores con `validateSync` de SIUM:
sincrono y sin escribir errores (`form.validate()` habria encendido los tres
campos en rojo en el primer frame).
`state.test.ts` fija la maquina con 9 casos — primer test unitario del tier,
porque `state.ts` es su primera logica pura: el orden de prioridad, que solo
`ready` deja enviar, y que todo estado bloqueado tiene frase.
Contrato B (architecture/blocks.md): seccion «Coordination» con las tres cosas
que posee un block que coordina (estado, forma de sus datos, palabras), B-5
admite el esquema por defecto, B-7 enmendado (posee las palabras de SUS estados
como idlangref con fallback ingles) y la convencion de servicios acotada: el
traductor es el UNICO servicio sancionado. Dice tambien a quien NO aplica — los
blocks de layout no coordinan nada.
i18n por la via real: el arnes registra el namespace `blocks` en las DOS cadenas
de arranque (galeria y previews, que duplican el boot) y la demo se lee en
castellano en vez de caer al fallback ingles.
Ademas: README y pestanas de doc rehechos al reparto nuevo, ficha F2.13 y
bitacora en PLAN-blocks.md, y el control vivo que faltaba para un prop publico
— `verification` con reto / sin reto, porque omitir el prop no es lo mismo que
pasar un estado que nunca verifica.
Verificado en navegador, arco completo y las dos ramas: sin reto vacio → un
campo → tres campos (se desbloquea) → «Enviando…» → «Enviado»; con reto, con
los tres campos llenos el boton sigue bloqueado y la razon pasa a «Resuelve la
verificacion de arriba». `verifying` y `rejected` quedan cubiertos por test
unitario, no por el reto en vivo: el provider de ProofOfHuman escribe `status`
el mismo y el veredicto del app no se sostiene (hilo de canon abierto).
Gates: blocks:check verde (14 blocks) · vitest 9/9 · docs:check 0 ·
svelte-check sin errores propios · prettier limpio en los ficheros propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Services**: a block wires theme, transport and submits through
handlers/props from the app — it consumes no `uix.prefs` , no storage, no
fix(blocks,canon): el ledger dice la verdad y el arco perceptivo vuelve a sonar
La auditoria del tier tenia los veredictos desemparejados de sus hallazgos: un
join por POSICION, y el journal del workflow era de sesion. Reproducido en vivo
al re-verificar — el journal devuelve los resultados en otro orden que la
entrada. Ledger nuevo con 90 ids estables (AUDIT-blocks-ledger.md), union
SIEMPRE por id: 41 arreglados, 34 confirmados, 12 refutados con motivo escrito,
3 dato. La tasa real de refutacion es del 13%, no del 28%.
De las 12 ALTA de percepcion, 8 tenian la causa raiz en el CANON. Los blocks
componen bien; lo que estaba roto era el arco perceptivo.
CANON
- Link: la prop color era inerte en subtle/plain (inherit a 0-2-0 ganaba a la
paleta a 0-1-0), el hover clavaba primary y el active quedaba tapado por el
hover de variante. Medido: el enlace del footer pasa de la tinta del padre a
la suya.
- Card: prometia BoxProps y no los aplicaba — height="100%" era un atributo
inerte. El tipo dice la verdad y el eje de tamano se aplica con un helper
compartido (buildSizeStyle). Tarjetas al fin de igual alto.
- Form.Submit / Form.Reset: componen el Button del canon, asi que la accion
principal de un formulario recupera el contact-activate en el gesto. Y el
aria-label generico deja de pisar el texto propio (WCAG 2.5.3 para todo
consumidor), verificado con el AX tree de Chrome.
- Field: dejaba de duplicar commit-submit dentro de un Form — un Enter emitia
dos commits con intents contradictorios y dos earcons.
- NavigationMenu: commit-select se mueve al Link (navegar es el acto
evaluable), el despliegue habla como emerge-open/close en vez de fingir una
seleccion por hover, y el pack casa por fin con la parte que recibe el
estampado — antes sonaba a la ganancia base, 10x lo disenado.
- Badge: el boton de quitar compone IconButton; la altura del chip pasa a ser
la del control, asi que md significa lo mismo en todo el sistema.
- Fundacion: [data-on] arrastra la propiedad color, no solo las variables.
BLOCKS
- hero: la CTA secundaria pasa de 1.61:1 a 6.61:1.
- contact: separa incomplete de invalid — el camino de error por campo era
inalcanzable por construccion — y su frase llega a la AT.
- newsletter: coordina (fase 4 del plan). Maquina de cinco estados, palabras
propias, Submit y Reason como partes que leen el contexto.
- site-header ya no congela la pagina al cruzar el breakpoint; site-footer
emite su commit; pricing no se vacia; cta llega a sangre de verdad.
DOCTRINA
- La frontera dura 1 nombra $libs/forms como puerta sancionada.
- El contrato B admite un segundo servicio: el anunciador. Un block que posee
las palabras de sus estados tiene que poder decirlas.
Gates: blocks:check 15/0 · vitest 20/20 en blocks y 402/402 en eidos+blocks ·
svelte-check 75/54 (linea base) · docs:check 0/0 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
http. **Two services are sanctioned, and only for the block's own state
words (B-7)**: the translator (`ActiveEidos.require().langs`), which resolves
them, and — since 2026-08-06 — the announcer (`uix.announce`), which delivers
them to assistive tech. The second entered for the same reason as the first:
a block that OWNS the words of its states has to be able to say them, or
owning them is half a job. Measured case: `Contact.Reason` rendered the
sentence that explains a blocked send and nobody who could not see it ever
received it. The alternative — a live region of the block's own —
re-implements `Announce` , the system's live-region pair, and puts a second
announcer on the page. Anything beyond these two is the app's, or the
admission rule firing.
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema
Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que
existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que
había que preguntar: si su alcance tenía delante `active-app` y `active-uix`
enteros. No los tenía.
Las cuatro decisiones que se firman, y lo que las obligó:
- **Q1 — el block NO cablea servicios.** La integración del ecosistema se
demuestra en una app de referencia en app-land, no dentro del block. Un shell
que leyera `App.session` para dibujar el menú de usuario sería una raíz de
composición disfrazada: funcionaría, y metería el arranque del ecosistema
dentro de una pieza que ha de poder caer en cualquier app.
- **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed,
sin números) es lo que cierra A-95 por construcción; `body` es lo que
`docs-shell` necesitará para anclas y TOC pegada.
- **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila
flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho
exigiría partir ese provider: queda como gap de canon para v2.
- **Q4 — canon `skip-link` + uno por región montada** (técnica G124).
De paso, tres cosas que estaban mal escritas y ahora lo dicen:
- El dossier afirmaba que **ninguna referencia trae skip-link** y que era
superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera
un menú entero. La superación real es generarlos del mismo contrato que
estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es
booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró
nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí
miró está deprecado.
- **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la
doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y
agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni
cablea servicios.
- **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`,
`mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo.
- **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta
`update-password`, que no es un lujo — es donde aterriza el enlace de
recuperación, y sin ella el camino de `recover` no termina.
`blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría,
landmarks, alturas) y dónde está la línea (la raíz de composición es de la app).
Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box`
expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell
no necesita CSS propio—, y `Sticky` expone `root`.
docs:check 0 errores, 0 avisos (635 docs).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Shells (`app-shell`, `docs-shell` ) — geometría, no cableado.** A shell is
the block that owns a page's REGIONS: their landmarks, their skip links and
their heights. That last one is why the tier needs it at all — an affixed
notice and a pinned header fight for the same edge, and no component can
arbitrate because none of them owns the page (ledger A-95). What a shell owns:
the grid, the scroll model, the landmark set, and the state that its own
regions share (a collapsed nav, a folded aside). What a shell does NOT own,
and this is the line that matters: **the composition root** . Creating
`ActiveApp` , attaching `ActiveUix` , mounting `<Uix>` , wiring `modeSource` /
`densitySource` into `ActiveEidos` , projecting prefs onto the document,
persisting them — all of that is the application's, exactly as it is for every
other block (§Services above; only composition roots create services). A shell
that read `App.session` to draw a user menu would be a composition root
wearing a block's clothes: it would work, and it would put the ecosystem's
boot inside a piece meant to be droppable into any app. The seam is the same
one every block uses — snippets and props — and the proof that it suffices is
an application built on it, not a paragraph.
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>
2 months ago
- **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
behaviors the first blocks needed — `sticky` (stuck-state affix),
`anchor-nav` (scrollspy TOC), `empty-state` , `result` , `callout` , `prose` ,
`nav-tree` (data-driven navigation tree), `sidebar` — were identified as
contract surface and routed to the **canon first** , each through the full
9-phase route; the blocks compose them. Any
future block-shaped need with contract surface follows the same path. The
execution record (phases, per-item fiches, signed D-BLK decisions) lives in
[`process/PLAN-blocks.md` ](../process/PLAN-blocks.md ) — process, not
doctrine; this document is the standing truth.