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/guides/demo-authoring.md

266 lines
12 KiB

---
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 `<div data-uix-canvas-inner>`.
```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<Tab>('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
<div data-uix-stage>
<div
data-uix-stage-area
bind:this={stageRef}
data-density={density}
data-theme={mode === 'inherit' ? undefined : mode}
data-mode={mode === 'inherit' ? undefined : mode}
dir={dir}
style={`--scaling: ${scaling}; --border-width: ${borderWidth}px;`}
>
<X …>…</X>
</div>
<div data-uix-stage-trace>…</div>
</div>
```
The trace uses `DemoTrace`:
```ts
const trace = new DemoTrace();
let stageRef = $state<HTMLElement | null>(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
<div data-uix-subsection-head><span data-uix-layer-badge="eidos">eidos</span> color · intent ↔ palette</div>
<div data-uix-controls>
<PalettePicker bind:intent bind:color />
</div>
```
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.5):
- **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** — `<X color="teal">` — not a theme override.
> **Wiring a new component's palette.** Today only **button and toggle**
> resolve `color="teal"` visually — their recipes carry the 33-scale cascade
> the generator fills via `universalPaletteDecls` (`lib/render-css.ts`; the
> `paletteScaleDecls` name this note used to cite never existed). The
> catalog-wide rollout was DECIDED 2026-07-11 (THM-2): a **universal
> `--palette-*` cascade** — one generic 33-block layer every recipe consumes
> (~3-4 KB total) instead of per-component cascades (≈21 KB each). The
> initiative's scope + status live in
> `docs/process/continue-cleanroom-fixes-2026-07.md` §F4-A (the successor of
> the deleted `pendiente_color_demos.md` tracker). Until it lands, a new
> color-bearing component resolves ROLES everywhere, and full scales only
> via its own `palette-*` recipe declarations.
`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
`<SystemAxes>` binds the foundation knobs that are **not** component props and
applies them to the stage (§4):
```svelte
<SystemAxes bind:density bind:scaling bind:mode bind:dir bind:borderWidth />
```
- **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
`<MotionPanel note="…">` 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)
`<SemaPanel>` 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
<SemaPanel
{actions}
{uix}
boundIntent={intent}
getTarget={() => 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 `'</' + 'script>'`, and a `<Foo>` element name inside a `<p>` must
be written `&lt;Foo&gt;` (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.

Powered by TurnKey Linux.