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.
svelte-kit-vice/docs/architecture/blocks.md

20 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)
Packs (src/packs/) parameterized leaf decoration P contract (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. 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.

  • 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, not doctrine; this document is the standing truth.

Powered by TurnKey Linux.