7.0 KiB
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.
Hard boundaries
- Dependency direction is one-way:
src/uix/blocks/*may import$uix,$adomand the public arts; nothing in the canon (src/uix/{morfo,soma,sema,eidos,active-uix,langs}) nor insrc/{arts,libs,packs}may import fromsrc/uix/blocks/. Deleting the tier must leavenpm run checkgreen — blocks is a tier undersrc/uix/, not a fifth layer. - 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 byblocks:check. - 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.cssfile; a scoped<style>is the justified exception, never the pattern. - 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). |
| 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). |
| 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 | A block owns NO visible string: all text arrives from the app as children/props. An unavoidable string is contract surface → the underlying canon component owns it (texts: + langs). |
| 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). |
| 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}/. |
| 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 atweb/routes/blocks/. - Services: blocks do not consume
uix.prefs/ langs / eidos directly (v1) — wiring (theme, language, submits, transport) arrives as handlers/props from the app. Revisable when ≥2 blocks demonstrate a real need (same bar as the 2-of-3 rule).
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.