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/web/routes/uix/components/onion-menu/+page.svelte

611 lines
28 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
import {
OnionMenu,
OnionLayer,
OnionOption,
type OnionPlacement,
type OnionDisposition,
type OnionDisplay
} from '$uix/eidos/components/onion-menu';
import { SvgPlus, SvgDot, SvgCheck, SvgChevron, SvgX, SvgMinus } from '$uix/eidos/components/svg';
import { compileMorfo } from '$uix/morfo';
import { onionMenuMorfo } from '@/uix/morfo/components/onion-menu';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Menu state (the OnionMenu owns drilling; we bind to read it) ───────
let open = $state(true);
let drillPath = $state<number[]>([2]);
let selected = $state<number[] | null>(null);
let last = $state('—');
// ── Live props — every public knob is a control ────────────────────────
let placement = $state<OnionPlacement>('bottom-end');
let display = $state<OnionDisplay>('both');
let disposition = $state<OnionDisposition>('angular');
let gap = $state(5);
let innerRadius = $state(82);
let ringWidth = $state(66);
let fontSize = $state(13);
let width = $state(560);
let height = $state(460);
let customTrigger = $state(false);
const placements: OnionPlacement[][] = [
['top-start', 'top-center', 'top-end'],
['left-center', 'center', 'right-center'],
['bottom-start', 'bottom-center', 'bottom-end']
];
const displays: OnionDisplay[] = ['icon', 'text', 'both'];
const dispositions: OnionDisposition[] = ['angular', 'horizontal', 'vertical'];
const compiled = compileMorfo(onionMenuMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { OnionMenu, OnionLayer, OnionOption } from '$uix/eidos/components/onion-menu';",
'</' + 'script>',
'',
'<OnionMenu',
` placement="${placement}"`,
' bind:open bind:drillPath bind:selected',
gap !== 5 && ` gap={${gap}}`,
innerRadius !== 82 && ` innerRadius={${innerRadius}}`,
ringWidth !== 66 && ` ringWidth={${ringWidth}}`,
fontSize !== 13 && ` fontSize={${fontSize}}`,
'>',
customTrigger && ' {#snippet trigger()}<Button>Go</Button>{/snippet}',
` <OnionLayer textDisposition="${disposition}" display="${display}">`,
' <OnionOption label="Create" icon={createIcon}>',
' <OnionLayer textDisposition="vertical">',
' <OnionOption label="Doc" onSelect={pick} />',
' <OnionOption label="Folder" onSelect={pick} />',
' </OnionLayer>',
' </OnionOption>',
' <OnionOption label="Share" icon={shareIcon} onSelect={pick} />',
' </OnionLayer>',
'</OnionMenu>'
]
.filter(Boolean)
.join('\n')
);
function playTarget(part: string): HTMLElement | null {
return (stageRef?.querySelector(`[data-onion-menu-${part}]`) ??
stageRef?.querySelector('[data-onion-menu-surface]') ??
stageRef) as HTMLElement | null;
}
</script>
{#snippet iCreate()}<SvgPlus />{/snippet}
{#snippet iShare()}<SvgChevron direction="right" />{/snippet}
{#snippet iEdit()}<SvgMinus />{/snippet}
{#snippet iDoc()}<SvgChevron direction="down" />{/snippet}
{#snippet iDot()}<SvgDot />{/snippet}
{#snippet iCheck()}<SvgCheck />{/snippet}
{#snippet iX()}<SvgX />{/snippet}
{#snippet iMinus()}<SvgMinus />{/snippet}
{#snippet triggerSnippet()}<span style="color:#fff; font-weight:600; font-size:13px;">Go</span>{/snippet}
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Overlay · OnionMenu</div>
<h1 data-uix-page-title>OnionMenu</h1>
<p data-uix-page-lede>
A trigger-agnostic radial menu rendered as concentric <strong>onion</strong> rings.
<strong>Compositional</strong>: a root <code>&lt;OnionLayer&gt;</code> of
<code>&lt;OnionOption&gt;</code>s, each option nesting a <code>&lt;OnionLayer&gt;</code> for
its children. Each top option owns a palette colour; drilling derives the deeper rings and the
passed rings mute to a trail. The arc follows the placement (90 / 180 / 360). Built on two
pure engines — geometry + colour (OKLCH via <code>uix.color</code>).
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>parts</span>{compiled.parts.order.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>events</span>{events.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>placements</span>9</span>
<span data-uix-meta-pill><span data-uix-meta-key>composes</span>uix.color · geometry</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={onionMenuMorfo.apg} target="_blank" rel="noreferrer">menu-button ↗</a>
</span>
</div>
</header>
<div data-uix-stage>
<div
data-uix-stage-area
bind:this={stageRef}
style="position: relative; contain: layout; min-block-size: {height}px; overflow: hidden; border-radius: var(--uix-radius-lg);"
>
<OnionMenu
{placement}
bind:open
bind:drillPath
bind:selected
{gap}
{innerRadius}
{ringWidth}
{fontSize}
{width}
{height}
trigger={customTrigger ? triggerSnippet : undefined}
>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Create" icon={iCreate}>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Doc" icon={iDoc}>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Blank" icon={iDot} onSelect={() => (last = 'Blank')} />
<OnionOption label="Template" icon={iDot} onSelect={() => (last = 'Template')} />
<OnionOption label="Import" icon={iDot} onSelect={() => (last = 'Import')} />
</OnionLayer>
</OnionOption>
<OnionOption label="Folder" icon={iDot} onSelect={() => (last = 'Folder')} />
<OnionOption label="Board" icon={iDot} onSelect={() => (last = 'Board')} />
<OnionOption label="Note" icon={iDot} onSelect={() => (last = 'Note')} />
</OnionLayer>
</OnionOption>
<OnionOption label="Share" icon={iShare}>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Link" icon={iDot} onSelect={() => (last = 'Link')} />
<OnionOption label="Email" icon={iDot} onSelect={() => (last = 'Email')} />
<OnionOption label="Embed" icon={iDot} onSelect={() => (last = 'Embed')} />
</OnionLayer>
</OnionOption>
<OnionOption label="Edit" icon={iEdit}>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Cut" icon={iX} onSelect={() => (last = 'Cut')} />
<OnionOption label="Copy" icon={iDot} onSelect={() => (last = 'Copy')} />
<OnionOption label="Paste" icon={iDot} onSelect={() => (last = 'Paste')} />
<OnionOption label="Color" icon={iDot}>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Red" icon={iDot} onSelect={() => (last = 'Red')} />
<OnionOption label="Blue" icon={iDot} onSelect={() => (last = 'Blue')} />
<OnionOption label="Green" icon={iDot} onSelect={() => (last = 'Green')} />
<OnionOption label="Gold" icon={iDot} onSelect={() => (last = 'Gold')} />
</OnionLayer>
</OnionOption>
</OnionLayer>
</OnionOption>
<OnionOption label="View" icon={iCheck}>
<OnionLayer textDisposition={disposition} {display}>
<OnionOption label="Zoom" icon={iDot} onSelect={() => (last = 'Zoom')} />
<OnionOption label="Grid" icon={iMinus} onSelect={() => (last = 'Grid')} />
</OnionLayer>
</OnionOption>
</OnionLayer>
</OnionMenu>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>open the menu, drill a branch, or fire a Sema event</span>
{:else}
{#each trace.slice(0, 3) as entry}
<span
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
? ' · ' + entry.intent
: ''}</span
>
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
{/each}
{/if}
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>state</span>
{open ? 'open' : 'closed'} · <span data-uix-stage-trace-key>path</span>[{drillPath.join(',')}]
· <span data-uix-stage-trace-key>last</span>
{last}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>3</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button
>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
OnionMenu is eidos render — the geometry + colour engines react to these props live.
Events live in the <button data-uix-inline-link onclick={() => (tab = 'sema')}>Sema</button> tab.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · OnionMenu (root) · layout + placement
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>placement <span data-uix-control-hint>→ arc 90/180/360</span></span>
<div style="display:grid; grid-template-columns:repeat(3,auto); gap:6px; width:max-content;">
{#each placements as row}
{#each row as p}
<button data-uix-chip data-active={placement === p} title={p} style="min-width:30px;" onclick={() => (placement = p)}>·</button>
{/each}
{/each}
</div>
</label>
<label data-uix-control>
<span data-uix-control-label>open</span>
<span data-uix-switch><input type="checkbox" bind:checked={open} /><span data-uix-switch-label>{open ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>innerRadius <span data-uix-control-hint>{innerRadius}px</span></span>
<input type="range" min="50" max="130" step="2" bind:value={innerRadius} />
</label>
<label data-uix-control>
<span data-uix-control-label>ringWidth <span data-uix-control-hint>{ringWidth}px</span></span>
<input type="range" min="44" max="96" step="2" bind:value={ringWidth} />
</label>
<label data-uix-control>
<span data-uix-control-label>gap <span data-uix-control-hint>{gap}px fixed</span></span>
<input type="range" min="0" max="12" step="1" bind:value={gap} />
</label>
<label data-uix-control>
<span data-uix-control-label>trigger <span data-uix-control-hint>custom vs default +</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={customTrigger} /><span data-uix-switch-label>{customTrigger ? 'custom' : 'default'}</span></span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · OnionLayer / OnionOption · content
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>display <span data-uix-control-hint>OnionOption · per option</span></span>
<span data-uix-chips role="radiogroup">
{#each displays as d}
<button data-uix-chip data-active={display === d} onclick={() => (display = d)}>{d}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>textDisposition <span data-uix-control-hint>OnionLayer · per level</span></span>
<span data-uix-chips role="radiogroup">
{#each dispositions as t}
<button data-uix-chip data-active={disposition === t} onclick={() => (disposition = t)}>{t}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>fontSize <span data-uix-control-hint>{fontSize}px (≥ sm)</span></span>
<input type="range" min="11" max="18" step="1" bind:value={fontSize} />
</label>
<label data-uix-control>
<span data-uix-control-label>width <span data-uix-control-hint>{width}px</span></span>
<input type="range" min="420" max="680" step="10" bind:value={width} />
</label>
<label data-uix-control>
<span data-uix-control-label>height <span data-uix-control-hint>{height}px</span></span>
<input type="range" min="360" max="600" step="10" bind:value={height} />
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>compositional · OnionMenu › OnionLayer › OnionOption</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Three components. <code>&lt;OnionMenu&gt;</code> holds the trigger + the root
<code>&lt;OnionLayer&gt;</code>; an <code>&lt;OnionOption&gt;</code> nests its children as a
<code>&lt;OnionLayer&gt;</code> composed inside it.
</p>
<div data-uix-subsection-head>OnionMenu</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">placement</td><td class="type">'bottom-end' | … | 'center'</td><td class="default">'bottom-end'</td><td>Anchor → arc span (90/180/360) + orientation.</td></tr>
<tr><td class="name">open <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">boolean</td><td class="default">false</td><td>Whether the rings show.</td></tr>
<tr><td class="name">drillPath <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">number[]</td><td class="default">[]</td><td>Child indices of the active drill path.</td></tr>
<tr><td class="name">selected <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">number[] | null</td><td class="default">null</td><td>Index path to the selected leaf.</td></tr>
<tr><td class="name">innerRadius / ringWidth / gap / fontSize</td><td class="type">number</td><td class="default">82 / 66 / 5 / 13</td><td>Geometry knobs (a layer can override ringWidth/gap).</td></tr>
<tr><td class="name">width / height</td><td class="type">number</td><td class="default">680 / 620</td><td>SVG viewBox size.</td></tr>
<tr><td class="name">trigger</td><td class="type">Snippet</td><td class="default empty">— (+)</td><td>Center trigger content (any element).</td></tr>
<tr><td class="name">onItemClick / onTriggerClick</td><td class="type">callback</td><td class="default empty">—</td><td>Notified on click (drill is owned internally).</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>OnionLayer</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">textDisposition</td><td class="type">'angular' | 'horizontal' | 'vertical'</td><td class="default">'angular'</td><td>Label layout for this ring.</td></tr>
<tr><td class="name">color</td><td class="type">theme scale / role</td><td class="default empty">—</td><td>Branch colour for this level — a theme scale (<code>'teal'</code>) or role (<code>'primary'</code>).</td></tr>
<tr><td class="name">display</td><td class="type">'icon' | 'text' | 'both'</td><td class="default">'both'</td><td>Default display for the level's options.</td></tr>
<tr><td class="name">ringWidth / gap</td><td class="type">number</td><td class="default empty">— (root)</td><td>Override the ring thickness / gap.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>OnionOption</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">label</td><td class="type">string</td><td class="default empty">—</td><td>Visible label + accessible name.</td></tr>
<tr><td class="name">icon</td><td class="type">Snippet</td><td class="default empty">—</td><td>Glyph (a framework <code>&lt;Svg*&gt;</code> / <code>&lt;Icon&gt;</code>).</td></tr>
<tr><td class="name">color</td><td class="type">theme scale / role</td><td class="default empty">—</td><td>Branch colour (top) / per-option override — a theme scale or role. Auto-distinct if omitted.</td></tr>
<tr><td class="name">display</td><td class="type">'icon' | 'text' | 'both'</td><td class="default empty">— (layer)</td><td>Per-option display override.</td></tr>
<tr><td class="name">disabled / onSelect</td><td class="type">boolean / () => void</td><td class="default empty">—</td><td>Disable / fire on a leaf choose.</td></tr>
<tr><td class="name">children</td><td class="type">&lt;OnionLayer&gt;</td><td class="default empty">—</td><td>A nested layer = this option's children (makes it a branch).</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span> · declarative contract
</h2>
<p data-uix-section-desc>
The morfo declares the menu-button contract: the trigger, the <code>role="menu"</code>
surface, the <code>menuitem</code> sectors with <code>data-depth</code> /
<code>data-active</code> / <code>data-muted</code>, and the open / close / select events.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">name</td><td class="type">"{onionMenuMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{onionMenuMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{onionMenuMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td><a href={onionMenuMorfo.apg} target="_blank" rel="noreferrer">menu-button ↗</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{onionMenuMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{events.length}</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>States</th></tr></thead>
<tbody>
{#each partsList as part}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.role ?? '—'}</td>
<td class="default">{part.archetype ?? '—'}</td>
<td class="default">{part.states.length ? part.states.join(' | ') : '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#each onionMenuMorfo.parts as rawPart}
{@const partAny = rawPart as unknown as {
kebab: string;
aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: string; part?: string }; severity?: string }>;
keyboard?: ReadonlyArray<{ key: string; action: string }>;
}}
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
{@const ariaAttrs = partAny.aria ?? []}
{@const keys = partAny.keyboard ?? []}
{#if dataAttrs.length || ariaAttrs.length || keys.length}
<div data-uix-subsection-head>{partAny.kebab}</div>
{#if dataAttrs.length}
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>data-attr</th><th>Values</th><th>Source</th></tr></thead>
<tbody>
{#each dataAttrs as attr}
<tr>
<td class="name">{attr.attr}</td>
<td class="type">{attr.values ? attr.values.join(' | ') : '—'}</td>
<td class="default">{'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if ariaAttrs.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>aria-attr</th><th>Source</th><th>Condition</th><th>Severity</th></tr></thead>
<tbody>
{#each ariaAttrs as a}
<tr>
<td class="name">{a.attr}</td>
<td class="type">{a.value.kind}</td>
<td class="default">{a.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}` : 'always'}</td>
<td class="default">{a.severity ?? 'required'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if keys.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>key</th><th>action</th></tr></thead>
<tbody>
{#each keys as k}
<tr><td><span data-uix-kbd>{k.key === ' ' ? 'Space' : k.key}</span></td><td class="default">{k.action}</td></tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/if}
{/each}
<div data-uix-subsection-head>Events</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th></tr></thead>
<tbody>
{#each onionMenuMorfo.events as ev}
{@const sem = ev.semantic as { family: string; verb?: string; sequence?: string; intent?: string; target?: { kind: string; ref?: string } }}
<tr>
<td class="name">{ev.name}</td>
<td class="type">{sem.family}</td>
<td class="default">{sem.verb ?? '—'}</td>
<td class="default">{sem.sequence ?? '—'}</td>
<td class="default">{sem.intent ?? '—'}</td>
<td class="default">{sem.target?.ref ?? sem.target?.kind ?? '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section id="sema">
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
<code>open</code> / <code>close</code> (emerge · surface) and <code>commit-select</code>
(commit · affirm · item). Expression <code>family-default</code> — the family bases carry
the signal. The soma layer fires these on real interaction; here ▶ play emits onto the live
target.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>play</th></tr></thead>
<tbody>
{#each onionMenuMorfo.events as ev}
{@const sem = ev.semantic as { family: string; verb?: string; sequence?: string; intent?: string; target?: { kind: string; ref?: string } }}
{@const part = sem.target?.ref ?? 'surface'}
<tr>
<td class="name">{ev.name}</td>
<td class="type">{sem.family}</td>
<td class="default">{sem.verb ?? '—'}</td>
<td class="default">{sem.sequence ?? '—'}</td>
<td class="default">{sem.intent ?? '—'}</td>
<td>
<button
data-uix-play
onclick={() => {
const target = playTarget(part);
if (!target) return;
void uix.events?.emit({
name: ev.name,
family: sem.family as never,
target,
...(sem.intent ? { intent: sem.intent as never } : {})
});
}}>▶ play</button
>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Fills / text / stroke are computed per sector by the colour engine and applied inline.
<code>onion-menu.css</code> owns only the structural chrome.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td class="name"><code>[data-onion-menu-surface]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>The SVG menu (role="menu").</td></tr>
<tr><td class="name"><code>.onion-menu-sector</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Sector affordance + fade-in + focus ring.</td></tr>
<tr><td class="name"><code>.onion-menu-label</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Label typography (curved / flat / radial).</td></tr>
<tr><td class="name"><code>.onion-menu-icon</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Icon box inside the foreignObject.</td></tr>
<tr><td class="name"><code>.onion-menu-trigger-glyph[data-state='open']</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Rotate the + → × on open.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr><td class="name">Trigger</td><td>Any element; the soma stamps <code>role="button"</code> + <code>aria-haspopup="menu"</code> + <code>aria-expanded</code>.</td></tr>
<tr><td class="name">Surface</td><td><code>role="menu"</code>; <code>menuitem</code> sectors; branch items expose <code>aria-haspopup</code> + <code>aria-expanded</code>.</td></tr>
<tr><td class="name">Keyboard <span data-uix-tag data-kind="soma">soma</span></td><td><span data-uix-kbd>Enter</span>/<span data-uix-kbd>Space</span>/<span data-uix-kbd>↓</span> open · <span data-uix-kbd>←</span>/<span data-uix-kbd>→</span> rotate · <span data-uix-kbd>Enter</span> drill/activate · <span data-uix-kbd>Backspace</span> up · <span data-uix-kbd>Esc</span> close.</td></tr>
<tr><td class="name">Motion</td><td>Sector fade-in + the trigger glyph rotation are suppressed under <span data-uix-kbd>prefers-reduced-motion</span>.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.