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

19 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.

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.