You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
21 KiB
195 lines
21 KiB
---
|
|
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
|
|
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:
|
|
|
|
| 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.
|
|
|
|
## 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`,
|
|
`$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 /
|
|
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).
|
|
|
|
| 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/`.
|
|
- **Services**: a block wires theme, transport and submits through
|
|
handlers/props from the app — it consumes no `uix.prefs`, no storage, no
|
|
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.
|
|
|
|
- **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.
|
|
|
|
- **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.
|