--- title: Demo authoring guide — v2 layout type: guide audience: human + agent authority: canonical — the locked template for component demo pages status: current source: migrated from web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md (2026-07-02, docs-book F7.5) --- # Demo authoring guide — v2 layout This is the canonical reference for authoring component demo pages under `web/routes/uix/components/{name}/+page.svelte`. The **v1** template (6 tabs: Live · API · Morfo · Sema · Recipe · A11y) only exposed `variant`/`size`/`color-role`. It hid most of the system: the **33-scale palette**, **border** width/style, **shape** families, **depth** planes, the **motion** catalog, and the perceptual **sema firma + sound**. The v2 layout exposes the whole surface through a **shared harness**, so every demo teaches the same axes and no demo re-implements (or omits) them. > **The canary is `web/routes/uix/components/button/+page.svelte`.** It is the > worked reference for every tab. When in doubt, copy from Button. --- ## 1. The shared harness — `web/routes/uix/lib/` | Module | What it is | | --- | --- | | [`harness.svelte.ts`](../../web/routes/uix/lib/harness.svelte.ts) | Constants (palette · border · shape · depth · density · motion · durations · easings) + `DemoTrace` (the `data-event` MutationObserver) + `signatureFor()` (wraps `resolveSignature`). | | [`PalettePicker.svelte`](../../web/routes/uix/lib/PalettePicker.svelte) | The **color** control — intent axis + `color` axis (hierarchy roles **and** the 33 palette scales), gated by the intent↔color doctrine. | | [`SystemAxes.svelte`](../../web/routes/uix/lib/SystemAxes.svelte) | The **foundation** axes applied to the stage: density · scaling · mode (local) · dir · border-width. | | [`MotionPanel.svelte`](../../web/routes/uix/lib/MotionPanel.svelte) | The **motion** catalog — preset chips + a live sample + duration/easing tokens + reduced-motion preview. | | [`SemaPanel.svelte`](../../web/routes/uix/lib/SemaPanel.svelte) | The **sema** firma — per event the resolved sound (pitch/gain/contour/envelope) + haptic (kind/pattern) + active channels + ▶ play (audible). | Per-component creativity is still forbidden: a demo declares the component's data and composes the harness; it does not invent its own panels. ## 2. File frame + imports Every demo lives at `web/routes/uix/components/{kebab}/+page.svelte`, wrapped by `web/routes/uix/+layout@.svelte` (the `@` resets layouts; it provides the chrome + boots `ActiveUix` + imports `generated/palette.css` so the full palette resolves). The outermost element is `
`. ```ts import { X, type XVariant, type XSize } from '$uix/eidos/components/{name}'; import { SHAPE_FAMILIES, type ComponentColor } from '$uix/eidos/lib/types'; import { compileMorfo } from '$uix/morfo'; import { xMorfo } from '@/uix/morfo/components/{name}'; import { getActiveUix } from '$active-uix'; import PalettePicker from '../../lib/PalettePicker.svelte'; import SystemAxes from '../../lib/SystemAxes.svelte'; import MotionPanel from '../../lib/MotionPanel.svelte'; import SemaPanel from '../../lib/SemaPanel.svelte'; import { DemoTrace } from '../../lib/harness.svelte'; const uix = getActiveUix(); ``` ## 3. Tabs Nine tabs, in this order. **System · Motion** are unconditional for any visual component; **Services** is conditional (see §9). ``` Live · System · Motion · Sema · Services · API · Morfo · Recipe · A11y ``` ```ts type Tab = 'live' | 'system' | 'motion' | 'sema' | 'services' | 'api' | 'morfo' | 'recipe' | 'a11y'; let tab = $state('live'); ``` A tab is never silently dropped for laziness. Omit **Services** only when the component consumes no app service. Omit **Motion**/**System** only for a pure data/logic component with no visual surface (rare) — and say why in the README. ## 4. The always-on stage carries the System axes The stage sits **between** the header and the tablist (so Sema ▶ play fires on a real instance and the trace stays visible across tabs). In v2 the stage **wrapper also applies the System axes**, so changing density/mode/border in the System tab is visible immediately on the live preview: ```svelte
…
…
``` The trace uses `DemoTrace`: ```ts const trace = new DemoTrace(); let stageRef = $state(null); $effect(() => { if (stageRef) return trace.observe(stageRef); }); ``` ## 5. Live tab — the component API plane Controls grouped by the layer that owns them (`soma` / `eidos` badges), then the reactive snippet(s). **Color is part of the API plane** (it's a real prop), so it lives here via `PalettePicker`, not in System: ```svelte
eidos color · intent ↔ palette
``` Then `variant` / `size` / `rounded` (radius magnitude) / `shape` (`SHAPE_FAMILIES`), the layout flags, and the snippet. Snippet **parity** is mandatory (the snippet shows the active prop values; see §12). ## 6. Color — the full palette, not just roles (replaces v1 §12.7/§12.8) The v1 norm "every chip enumerates the role/variant/size union" was too narrow: it hid the palette. The component `color` prop accepts **the full surface** ([`theming/reference.md`](../theming/reference.md) §4 / §25): - **intent** (`neutral · affirm · fulfill · risk · threat · loss`) — evaluative. - **color** — under `intent='neutral'`: a **hierarchy** role (`primary · secondary · tertiary · neutral`) **or any of the 33 palette scales** (`teal`, `plum`, `gold`, … — the closed set is `PALETTE_SCALES` in `src/uix/eidos/lib/types.ts`; never count it by hand). This is the Radix `color="grass"` per-instance override. `PalettePicker` renders all of this and enforces the doctrine: when `intent` is evaluative it dims the `color` section (intent wins). The picker drives the **real prop** — `` — not a theme override. > **Wiring a new component's palette.** The per-instance palette (` color="teal">` resolving to the actual teal scale, not just roles) is > served by the shared `--palette-*` layer (THM-2, 2026-07-12 — the mechanism > is in [`theming/reference.md §25`](../theming/reference.md), "The > per-instance palette layer"). A component gains ALL 33 scales by declaring > `palette-*` / `_palette-*` tokens (host default) and consuming its own > `--{c}-palette-*` — the generator emits a presence-guarded forward to the > shared layer, so a private-palette recipe needs ZERO CSS change. **Live on: > button, toggle, checkbox, radio-group, switch** (5/17 data-color surfaces). > The remaining 12 are a tracked rollout in > `docs/process/continue-cleanroom-fixes-2026-07.md` §THM-2 (successor of the > deleted `pendiente_color_demos.md`). Until a surface is wired it resolves > ROLES everywhere and scales only where wired. `size`/`variant` chip parity rules still hold: enumerate the component's full declared union, narrowings justified in the README. ## 7. System tab — the foundation plane `` binds the foundation knobs that are **not** component props and applies them to the stage (§4): ```svelte ``` - **density** (`data-density`) · **scaling** (global zoom) · **mode** (light/dark, local to the stage so you can preview dark while the docs stay light) · **dir** (RTL) · **border-width** (`--border-width`). Radius / shape / depth that a component exposes **as a prop** belong in Live, not here. Only surface a System axis with a visible effect (the §12.6 rule): e.g. `border-style` is omitted for components that hardcode `solid`. ## 8. Motion tab `` renders the whole preset catalog applied to a live sample, with retunable duration/easing tokens and a reduced-motion preview. The selected preset name is the value a component's `motion` prop takes where it has one. For components without a `motion` prop (most), the panel still teaches the system + the component notes its own motion (press squeeze, the event firma). ## 9. Sema tab — firma + sound (first-class, not a flat table) `` shows, per declared event, the resolved firma via `resolveSignature` — sound (pitch / gain / contour / envelope / centroid / roughness) + haptic (kind / intensity / pattern) + active channels + hold — and a ▶ play that emits the real signal on the live target (audible with **Semantics → Sound** on in the topbar): ```svelte stageRef?.querySelector('[data-{component}]') ?? stageRef} /> ``` `actions` = `[...compileMorfo(xMorfo).actions.byName.values()]`. When the morfo declares 0 events, `SemaPanel` renders the justified empty state — the absence must be defended in the component README, never assumed. ## 10. Services tab (conditional) Render it when the component consumes an app service: - **langs** (i18n) — translatable `texts` in the morfo (every component with user-facing copy). Switch language in the topbar to see them follow. - **format** (`FormatNumber`/`FormatDate`/`RelativeTime`, date/time fields) — locale / currency / unit controls. - **announce** (live region) — components that fire `requiresLiveRegion`. Button's Services tab is minimal (just `langs` for `label`/`loading`). A date or number component expands it with locale/currency/unit pickers + their own `demoLocale` state (see the service-component demos). ## 11. API · Morfo · Recipe · A11y - **API** — every public prop: name / type / default / description; eidos additions tagged. - **Morfo** — iterate the raw morfo (`xMorfo.parts`, `xMorfo.events`) + the compiled contract (`compiled.contracts.dataAttrsByPart`). Parts table + per-part data/aria/keyboard + events table. - **Recipe** — selectors tagged morfo-backed / eidos-only. Note the palette cascade for color-bearing components. - **A11y** — keyboard + ARIA contract; for overlays, the intent → role / aria-live mapping. ## 12. Snippet parity (unchanged from v1) The snippet must match the live preview's real API surface: every rendered field/part/prop appears, and active control values are reflected. Two parser rules: split `''`, and a `` element name inside a `

` must be written `<Foo>` (and `{ … }` literal text wrapped in `{`…`}`). ## 13. Verification checklist Before declaring a demo done: 1. `npm run check` reports 0 errors **for that file** (the repo has unrelated pre-existing errors; filter to your paths). 2. Every tab renders; every control changes something visible on the live preview. 3. **Color**: the palette picker drives the real `color` prop (a swatch click → `data-color="{scale}"` → the right scale renders). Verify a couple of scales in light + dark. 4. **Sema**: every ▶ play stamps `data-event` (watch the trace) and is audible with sound on; the firma values match `SEMA_MAP` for the family/intent. 5. **Motion**: presets apply to the sample; reduced-motion preview neutralises them. 6. Snippets reflect the active control values. > **Screenshots in this preview.** `preview_screenshot` hangs on the docs shell > (backdrop-filter + the large DOM). Verify rendered values with > `preview_eval` + `getComputedStyle` (decisive for color/size/border), and for > a real pixel check use the Chrome MCP on your own tab. A frozen CSS > `transition` in the headless renderer can report a stale `background-color` > mid-flight — read `--_{component}-bg` (the target) or set `transition:none` > to confirm the resolved value.