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.
261 lines
12 KiB
261 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 components whose recipe has
|
|
> the `palette-{slot}` cascade resolve `color="teal"` visually (Button is the
|
|
> pilot).<!-- TODO(reconcile): the rollout tracker src/uix/eidos/pendiente_color_demos.md was deleted in the worktree; the per-component wiring status has no home now -->
|
|
> When you add a new color-bearing component, extend its recipe's
|
|
> `palette-*` declarations with the full-palette cascade (the `paletteScaleDecls`
|
|
> helper in `lib/recipes/base.ts`), regenerate (`npm run generate:eidos-css`),
|
|
> and the picker just works.
|
|
|
|
`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 `<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.
|