jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

install — component-canvas

$npx jixoai-ui add component-canvas

https://ui.jixoai.com/r/component-canvas.json

LIVE stage · muted tint floating dock · the eight-axis bar supply-not-force · ambient lanes collapsible · draggable tree drawer · container queries recursion · depth 2

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.

ComponentCanvas usage
<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.

press-button-usage.svelte svelte
<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>
component-canvas-usage.svelte svelte
<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

PropertyTypeDefaultDescription
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 bindRecord<string, unknown>schema defaultsSchema-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 stringslug(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 ambientStage 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 ambientStage 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

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity 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.

PropertyTypeDefaultDescription
size --jx-size-effective → root font-sizeautoStamps 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-effectiveautoBoth 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-effectiveautoThe 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 supplyautoTWO 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-effectiveautoThe 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 classesautoThe §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-effectiveautoThe 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-effectiveautoThe 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.

TokenDefaultSource
--jx-textrung channel (13px at default)structural
--jx-text-small12.5pxstructural
--jx-text-label11pxstructural
--jx-text-micro10pxstructural
--jx-hitrung channelstructural
--jx-press-shadowper-element tuningstructural
the six lanes, driven
<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

canvas-axes-demo.svelte svelte
<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>
one real query() case
<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).

canvas-query-demo.svelte svelte
<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

KeyAction
TabMoves focus through header, dock chrome and body controls, then the open drawer
Enter / SpaceToggles 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

AttributeValueDescription
aria-expandedbooleanOn the drawer toggle and the dock collapse chevron; tracks the 0fr/1fr grid collapse
aria-controls{id}-drawer / {id}-dock-bodyPairs each toggle with its collapsible region
inertwhen collapsedRemoves collapsed drawer and dock-body content from tab and screen-reader order
aria-pressedbooleanOn the dock theme button — carries the light/dark state
aria-haspopupmenuOn the seven axis icon-buttons; each opens the family DropdownMenu with the current value check-marked (data-jx-canvas-axis-check)
aria-labelstringOn the source link, the stage ("{title} demo", stageLabel-overridable), the dock ("Controls for {title}"), and copy buttons
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h