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

12 KiB

title type audience authority status source
Demo authoring guide — v2 layout guide human + agent canonical — the locked template for component demo pages current 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 Constants (palette · border · shape · depth · density · motion · durations · easings) + DemoTrace (the data-event MutationObserver) + signatureFor() (wraps resolveSignature).
PalettePicker.svelte The color control — intent axis + color axis (hierarchy roles and the 33 palette scales), gated by the intent↔color doctrine.
SystemAxes.svelte The foundation axes applied to the stage: density · scaling · mode (local) · dir · border-width.
MotionPanel.svelte The motion catalog — preset chips + a live sample + duration/easing tokens + reduced-motion preview.
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>.

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
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:

<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:

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:

<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 §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):

<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):

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