registry:ui · Docs Tooling
component-canvas — the documentation workbench
One bordered surface per component: a LIVE demo stage, the floating playground dock — the eight-axis bar on every canvas — and a collapsible code drawer. Every component page on this site is one canvas; this one renders the component inside itself.
$npx jixoai-ui add component-canvas
https://ui.jixoai.com/r/component-canvas.json
overview
Overview
Three regions and a platform-first law: the header, the stage, the dock, the drawer — every structural behavior is a platform feature composed into the workbench contract.
ComponentCanvas is the documentation workbench: a header (font-nav title, description,
copy-command install badge, icon-only sourceUrl anchor), a LIVE
demo stage on the muted tint so components prove themselves on a differently-toned
ground, the floating playground dock over the stage's top-right corner, and a collapsible
code drawer pairing a file tree with one code-card. The page owns every byte of state
through the P1 protocol — onreset, output, resolveFileContent are callbacks, never state pushed into the canvas.
Platform first: the stage and dock are Svelte 5 snippets (render seams, live by
construction), the drawer tiers and the dock's width switch on CSS container queries (the
host tier @container/jx-canvas-host at 48rem; the named demo container rides
the scrollport itself, so demo queries see the width the scrollbar actually leaves), and
the collapses are grid-template-rows: 0fr→1fr with inert keeping
closed content out of the tab order. The workbench adds the layer law (muted stage under
a surface-card dock), the outline law — the root carries data-toc-skip and
the title is a styled paragraph, so no canvas chrome joins a ToC — and deterministic
aria ids slug-derived from the title.
The dock ships on EVERY canvas: its head is the eight-axis bar (below), and a canvas
without a playground snippet, schema, or output stands as the bar alone — no chevron, no
body. Stage posture (fill/center/start) and the
scroll cap (capped/grow) are props; the id is also the
same-source extraction key (its own section below). This page's demo nests one simplified
canvas inside the stage — a canvas may showcase a canvas exactly one level down.
usage
Usage
Children are the LIVE stage; files feed the drawer; the playground snippet is optional.
<script lang="ts">
import ComponentCanvas from '@ui/component-canvas.svelte';
import { registrySourceUrl } from '$lib/registry-source';
</script>
<!-- files: flat TreeFile list; the drawer's tree pane splits their "/"
paths into levels — one shape at every file count -->
<ComponentCanvas
title="press-button"
description="hover grows the shadow, active presses on an anchored shadow."
install="press-button"
sourceUrl={registrySourceUrl('press-button')}
files={[{ name: 'src/lib/ui/press-button-usage.svelte', content: usage }]}
>
<PressButton variant="fill">deploy</PressButton>
</ComponentCanvas>component-canvas
The canvas rendering a canvas: the LIVE stage below embeds a simplified second instance. The Playground checkbox toggles the inner canvas's dock BODY — its chevron appears and disappears with it — and the usage file in this drawer tracks it. The dock bar (the eight axes) re-scopes THIS canvas's supply only; the inner canvas's dock keeps its own seats.
press-button
The inner canvas — a simplified instance living in the outer LIVE stage. Its dock chrome re-scopes ITS stage only; the outer canvas keeps its seats (the scoping law). Without a playground snippet, its dock stands as the chrome chip alone.
<script lang="ts">
import PressButton from '@ui/press-button.svelte';
</script>
<!-- one physics for every variant: hover grows the shadow, active presses -->
<PressButton variant="fill">deploy</PressButton><script lang="ts">
import ComponentCanvas from '@ui/component-canvas.svelte';
import PressButton from '@ui/press-button.svelte';
import { registrySourceUrl } from '$lib/registry-source';
import { PlayFields, PlayRow, PlaySelect } from '$lib/playground';
</script>
<!-- files: flat TreeFile list; the drawer's tree pane splits their "/"
paths into levels — one shape at every file count -->
<ComponentCanvas
title="press-button"
description="hover grows the shadow, active presses on an anchored shadow."
install="press-button"
sourceUrl={registrySourceUrl('press-button')}
files={[{ name: 'src/lib/ui/press-button-usage.svelte', content: usage }]}
>
<PressButton variant="fill">deploy</PressButton>
</ComponentCanvas>api
Props
The table renders from the GENERATED meta; the six axis-lane rows split into the shared section beneath. theme and density are family seats sharing axis names (the §13 no-rename law) — the generated filter would drop them, so they render from the extra lane with their real unions (the chip precedent, self-documenting here).
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| title * | string | — | Component name shown in the header. Also the aria slug seed: ids derive jx-canvas-<slug>-title/drawer deterministically (SSR and client agree). |
| description | string | — | One-line description under the title. |
| sourceUrl | string | — | GitHub source link — header right, icon-only external anchor with press physics. The VALUE is page-side derived from the registry path projection ($lib/registry-source), never hand-written. |
| install | string | — | Registry item name — renders the header copy-command badge (npx jixoai-ui add <name>) with a clipboard flash. Absent on canvases whose title is not a registry item. |
| files * | TreeFile[] | — | Demo code files; flat list, names may carry paths — the drawer's tree pane splits "/" paths into levels, one shape at every file count. Content comes from the page's ?raw imports (the files always arrive as data). |
| children | Snippet | — | LIVE demo area — the consumer renders the component instance on the muted stage. |
| stage | 'fill' | 'center' | 'start' | 'fill' | Stage posture: fill (default — children span the width), center (intrinsic specimens shrink + center), start (intrinsic, left). |
| scroll | 'capped' | 'grow' | 'capped' | Stage scroll posture: capped (default — the scroll layer bounds at min(32rem, 60vh) with native auto-scroll) or grow — lifts the cap for full-composition demos whose own stacking IS the presentation. |
| playground | Snippet | — | Consumer-authored controls inside the floating dock's body. Absent snippet = the chrome-only dock (the eight-axis bar, no chevron, no body). Takes precedence over schema rows (escape-hatch law). |
| schema | CanvasSchema | — | jsonSchema control mode: a LOWERED schema (the toJSONSchema export, passed by value) whose control rows the dock renders — the canvas becomes a jsonSchema2Form consumer; values initialize from the schema defaults. |
values bind | Record<string, unknown> | schema defaults | Schema-mode dock values — two-way. Initialized from the schema defaults when the page binds none; a bound page object always wins. |
| onvalue | (key: string, value: unknown) => void | — | Schema-mode change seam: the page intercepts and owns value semantics for non-representable props (effect builders, …), writing back through bind:values. |
| onreset | () => void | — | Page-owned reset: shows the dock body foot's reset button and calls back; absent, schema mode falls back to the schema defaults. |
| output | readonly PlayOutput[] | — | Read-only state projection rows at the dock's foot — deliberately NOT a live region. Body-bearing on its own: an output-only canvas still gets a dock body. |
| resolveFileContent | (file: TreeFile) => string | — | Code-drawer content override — lets usage files track live state (one source, two surfaces). |
| id | string | slug(title) | Explicit id override when two canvases on one page would slug-collide — AND the canvas same-source extraction key: an id-carrying canvas's children are extracted by canvasPlugin into the page's virtual:jixoai-canvas module (no id = no extraction, zero cost). See the same-source section. |
| stageLabel | string | — | The stage element's aria-label override — default `${title} demo`. For frozen probe identifiers a default cannot compose (the timeline family's mapping table). |
| class | string | '' | Forwarded to the root section, after the workbench atoms. |
| style | string | — | Extra inline style on the workbench root — JOINS the axis carriers (the merge law), never replaces them. |
theme bind | 'light' | 'dark' | 'light' · Own default, not ambient | Stage preview theme — page-owned bindable, flipped by the dock bar's theme axis button (aria-pressed carries state). Projects data-theme plus the theme sheet's dark/jx-light token-scope classes onto the STAGE element only; the docs chrome and sibling canvases never re-theme. Renders from the extra lane: the name is the §13-owned stage-preview seat, not the universal ThemeLane (the census D-fold row). |
density bind | 'xs' | 'sm' | 'default' | 'lg' | 'default' · Own default, not ambient | Stage preview density — page-owned bindable, the repo-standard rung union, stamped as data-density on the STAGE element (the rung half of the axis; a scope boundary for the demo subtree). The destructure default keeps the lane permanently hot — the ambient zone never rides the stage. The universal density LANE is a separate channel: it rides the dock bar's menu into the root supply. Renders from the extra lane (the §13-owned seat, census D-fold row). |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
axes
The eight axes on component-canvas
SIX axes join the workbench section as page-owned props (the W3-D5 surface): size · shape · radius · color · elevation · motion — each resolved under the consumer's explicit lane, stamped as §11 carriers joined into the root style attr, and supplied downward through the broadcast protocol (吃也供, supply-and-consume). The W7 eight-axis bar is the second seat: dock-owned lane state (all-auto seed) joined explicit ?? bar — a page-owned prop always wins, the bar is honestly inert there — and `auto` translates to no opinion, the ambient keeps flowing. Supply, never force: any stage specimen may consume or ignore the lanes. None of the six repaints the canvas's own chrome (the fixed-voice table below); the seats are the stage's specimens. theme and density are the §13-owned stage-preview seats documented in their rows.
Reading the table: Property is the axis, Type is the real carrier or seat it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption on this family are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| size | --jx-size-effective → root font-size | auto | Stamps the workbench section and re-bases its font-size. The chrome does not follow — the head title, description and labels read fixed kernel channels (--jx-text, --jx-text-small, --jx-text-label, --jx-text-micro) — but the STAGE inherits the root font-size, so em-keyed specimens move with it (the query() demo below). Seat pair: an explicit prop beats the bar for that axis (the bar is honestly inert there). Steps small · medium · large; a number is px; query() wraps any lane. |
| shape | --jx-shape-effective · --jx-radius-factor-effective | auto | Both carriers stamp on the section; no canvas css reads them — the corner consumers are the stage's own (a press-button specimen's corner-shape rides the supply). Steps round · scoop · bevel · notch · square · squircle; no number lane. |
| radius | --jx-radius-effective | auto | The carrier stamps; no canvas css reads it — the concentric consumers are elsewhere (card, press-button). The schema demo's anchor panels pin --jx-radius-effective + --jx-inset-effective so the auto seat's §3 expression computes live. Steps small · medium · large; a number is px. |
| density | the stage's data-density rung · the bar's lane supply | auto | TWO SEATS, one name (the §13 no-rename law). The PROP is the rung seat: a page-owned 'xs' | 'sm' | 'default' | 'lg' bindable stamped as data-density on the STAGE element — the demo subtree's scope boundary; the ambient zone never rides the stage, and the prop's own default ('default', through the legacy slot) keeps a --jx-density-coefficient: 1 stamp on the workbench root (grep any canvas root). The AXIS lane is the supply seat: the dock bar's density menu speaks the universal grammar (auto/small/medium/large) into the same root resolution as the other six lanes. Separate channels since the bar retired the rung select (the W7 directive, 2026-09-21). |
| color | --jx-color-effective | auto | The carrier stamps; no canvas css reads it. One caveat the schema demo makes visible: the stage's island scope (.dark/.jx-light + the token-scope stamp) re-declares this carrier per theme profile (the canvas-bug law), so a root color stamp cannot cross the island — the demo re-stamps the resolved lanes element-level below it. Steps primary · secondary · error · warn · success · info; a number is hue degrees; a raw string passes through verbatim. |
| theme | the stage's data-theme + token-scope classes | auto | The §13-owned preview seat, not the universal ThemeLane: a page-owned 'light' | 'dark' bindable projects data-theme plus the theme sheet's dark/jx-light token-scope classes onto the STAGE element only — the docs chrome and sibling canvases never re-theme, and the stage anchors its own text ink. The bar's theme button flips it (aria-pressed carries state). The universal theme lane itself forwards ambient-only through inheritance (the census D-fold, W6-dossier-flagged beside code-card/mermaid). |
| elevation | --jx-elevation-effective | auto | The carrier stamps; no canvas css reads it — the supply is ambient (the schema demo's fill seat re-shadows through press-button's §7 pair). Steps level-1 · level0 · level1 · level2 · level3 · level4 · level5; a number is exact dp. |
| motion | --jx-motion-effective | auto | The carrier stamps; no canvas css reads it. The page owns the one rest-state consumer: the sweep seat below divides its period by the coefficient (expressive 1.5 speeds it, subtle 0.5 slows it; reduced motion freezes it into a width gauge). Steps reduced · subtle · normal · expressive; a number is a coefficient. |
Deviations, cited: the six-lane adoption is the census fold row — component-canvas
carries SIX lanes, theme LEFT OUT (the stage-preview bindable owns the name) and
density's universal lane absent at the fold while the legacy local densitySlot('default') stayed (both under the §13 no-rename law,
W6-dossier-flagged beside code-card/mermaid;
openspec/changes/explicit-props/research/migration-census.md). The W7 bar — the Owner's
eight-axis directive (2026-09-21) — later gave the density AXIS its supply seat beside
the other six. No §13 renames; the two name-shadowing seats ride the generated table's
extra lane.
| Token | Default | Source |
|---|---|---|
| --jx-text | rung channel (13px at default) | structural |
| --jx-text-small | 12.5px | structural |
| --jx-text-label | 11px | structural |
| --jx-text-micro | 10px | structural |
| --jx-hit | rung channel | structural |
| --jx-press-shadow | per-element tuning | structural |
<script lang="ts">
import ComponentCanvas from '@ui/component-canvas.svelte';
import PressButton from '@ui/press-button.svelte';
import { meta, annotations } from '$lib/meta/component-canvas.meta';
import { withAnnotations } from '$lib/schema/ir';
import { toJSONSchema } from '$lib/schema/lower';
</script>
<!-- the family's own generated meta lowers to dock rows; the page
owns the lane state and drives the six explicit lane props -->
<ComponentCanvas
title="component-canvas · the six explicit lanes"
files={files}
schema={toJSONSchema(withAnnotations(meta, annotations))}
bind:values
{size} {shape} {radius} {color} {elevation} {motion}
>
<!-- seats: an em-keyed caption (size), anchor panels (radius), a
fill button (shape/color/elevation), a sweep bar (motion) -->
<div style={axisRestamp || undefined}>
<p style="font-size: 0.875em">size keys the root font-size — this caption rides em</p>
<PressButton variant="fill" {size} {shape} {radius} {color} {elevation} {motion}>
fill seat — color re-hues, elevation re-shadows
</PressButton>
<div data-cc-axis-motion-seat aria-hidden="true"><span data-cc-axis-motion-bar></span></div>
</div>
</ComponentCanvas>component-canvas · the six explicit lanes
size keys the root font-size — this caption rides em, the parts follow
motion keys the sweep — expressive halves the period, reduced freezes it
<script lang="ts">
import ComponentCanvas from '@ui/component-canvas.svelte';
import PressButton from '@ui/press-button.svelte';
import { meta, annotations } from '$lib/meta/component-canvas.meta';
import { withAnnotations } from '$lib/schema/ir';
import { toJSONSchema } from '$lib/schema/lower';
</script>
<!-- the family's own generated meta lowers to dock rows; the page
owns the lane state and drives the six explicit lane props -->
<ComponentCanvas
title="component-canvas · the six explicit lanes"
files={files}
schema={toJSONSchema(withAnnotations(meta, annotations))}
bind:values
{size} {shape} {radius} {color} {elevation} {motion}
>
<!-- seats: an em-keyed caption (size), anchor panels (radius), a
fill button (shape/color/elevation), a sweep bar (motion) -->
<div style={axisRestamp || undefined}>
<p style="font-size: 0.875em">size keys the root font-size — this caption rides em</p>
<PressButton variant="fill" {size} {shape} {radius} {color} {elevation} {motion}>
fill seat — color re-hues, elevation re-shadows
</PressButton>
<div data-cc-axis-motion-seat aria-hidden="true"><span data-cc-axis-motion-bar></span></div>
</div>
</ComponentCanvas><script lang="ts">
import ComponentCanvas from '@ui/component-canvas.svelte';
import { query } from '$lib/universal-props-query.svelte';
</script>
<!-- the explicit size lane wraps query(): SSR paints the base (14px);
at the md viewport rung (≥48rem) the root font-size steps to 18px
and the em-keyed caption follows -->
<ComponentCanvas title="responsive size" files={files} size={query({ md: 18 }, 14)}>
<p style="font-size: 0.875em">this caption rides em off the root font-size</p>
</ComponentCanvas>component-canvas · responsive size
this caption rides em off the root font-size
SSR paints the base (14px); at the md viewport rung (≥48rem) the root font-size steps to 18px and the caption follows — resize across 48rem. The head, dock and drawer chrome keep their fixed voices either way (the table's size row).
<script lang="ts">
import ComponentCanvas from '@ui/component-canvas.svelte';
import { query } from '$lib/universal-props-query.svelte';
</script>
<!-- the explicit size lane wraps query(): SSR paints the base (14px);
at the md viewport rung (≥48rem) the root font-size steps to 18px
and the em-keyed caption follows -->
<ComponentCanvas title="responsive size" files={files} size={query({ md: 18 }, 14)}>
<p style="font-size: 0.875em">this caption rides em off the root font-size</p>
</ComponentCanvas>law
Same-source — the id is the extraction key
An id-carrying canvas is EXTRACTABLE: the canvasPlugin compiler (packages/vite-plugin, standalone — never the jixoai() umbrella) parses the page and serves its own per-page module, virtual:jixoai-canvas/<route>/+page, whose resolveRawCode(id) returns THIS canvas's children markup — dedented, direct-child canvas-protocol snippets stripped, comments kept, byte-honest. The page composes that string into the usage TreeFile (usageFile from $lib/canvas-usage) and the Usage CodeBlock: one source, two surfaces — the drawer's code sample can never drift from the stage again.
The contract is opt-in per canvas: no id = no extraction (zero cost — the unextracted posture keeps hand-authored usage samples). An id must be a static string literal and unique on its page; the extractor named-errors duplicates and non-static ids. The self-containment guard rejects slices that reference page-level identifiers (a snippet defined outside the canvas, a page const): the emitted usage must be copy-paste-runnable — move the snippet into the canvas or drop the id.
The same id also feeds the aria slug
(jx-canvas-<id>-title), so id-carrying canvases
are collision-free by construction — the explicit-id collision escape and the
extraction key are ONE mechanism, not two.
a11y
Accessibility
The code drawer and the dock body are disclosures: aria-expanded/controls plus inert keeps collapsed content out of the tab order. The dock's drag is decorative and pointer-only — every function stays keyboard-reachable without it.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus through header, dock chrome and body controls, then the open drawer |
| Enter / Space | Toggles the code drawer and dock disclosures; triggers copy, theme, reset, and the axis menus |
| ↑ / ↓ | Moves through an open axis menu's items (auto + the named steps), Enter commits |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-expanded | boolean | On the drawer toggle and the dock collapse chevron; tracks the 0fr/1fr grid collapse |
| aria-controls | {id}-drawer / {id}-dock-body | Pairs each toggle with its collapsible region |
| inert | when collapsed | Removes collapsed drawer and dock-body content from tab and screen-reader order |
| aria-pressed | boolean | On the dock theme button — carries the light/dark state |
| aria-haspopup | menu | On the seven axis icon-buttons; each opens the family DropdownMenu with the current value check-marked (data-jx-canvas-axis-check) |
| aria-label | string | On the source link, the stage ("{title} demo", stageLabel-overridable), the dock ("Controls for {title}"), and copy buttons |