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/dialog/+page.svelte

712 lines
31 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 {
Dialog,
type DialogSize,
type DialogPosition,
type DialogClosePosition
} from '$uix/eidos/components/dialog';
import { compileMorfo } from '$uix/morfo';
import { dialogMorfo } from '@/uix/morfo/components/dialog';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
type DialogIntent = 'neutral' | 'risk' | 'threat';
// ── Live state ────────────────────────────────────────────────────────
let open = $state(false);
let modal = $state(true);
let intent = $state<DialogIntent>('neutral');
let size = $state<DialogSize>('md');
let position = $state<DialogPosition>('middle-center');
let overlayOpacity = $state(0.6);
let closePosition = $state<DialogClosePosition>('top-right');
let trapFocus = $state(true);
let preventScroll = $state(true);
let showHeader = $state(true);
let showFooter = $state(true);
let showOverlay = $state(true);
let showClose = $state(true);
let triggerLabel = $state('Open dialog');
let titleText = $state('Confirm action');
let descText = $state('This will permanently delete the selected items. You cannot undo this.');
let bodyText = $state(
'Dialogs ship the full asymmetric exit set: save (commit, fulfill), cancel/dismiss/dismiss-outside (emerge, neutral), failed (signal, threat). Each stamps data-last-action via prewrite so the recipe can tint the closing animation by causa.'
);
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
$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();
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(dialogMorfo);
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')}`;
}
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as Dialog from '$soma/components/dialog';",
' let open = $state(false);',
'</' + 'script>',
'',
'<Dialog.Provider',
' bind:open',
!modal && ' modal={false}',
intent !== 'neutral' && ` intent="${intent}"`,
'>',
` <Dialog.Trigger>${triggerLabel}</Dialog.Trigger>`,
' <Dialog.Portal>',
showOverlay && ' <Dialog.Overlay />',
' <Dialog.Content',
!trapFocus && ' trapFocus={false}',
!preventScroll && ' preventScroll={false}',
' >',
` <Dialog.Title>${titleText}</Dialog.Title>`,
` <Dialog.Description>${descText}</Dialog.Description>`,
' <p>...</p>',
showClose && ' <Dialog.Close>Close</Dialog.Close>',
' </Dialog.Content>',
' </Dialog.Portal>',
'</Dialog.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Dialog } from '$uix/eidos/components/dialog';",
' let open = $state(false);',
'</' + 'script>',
'',
'<Dialog',
' bind:open',
!modal && ' modal={false}',
intent !== 'neutral' && ` intent="${intent}"`,
'>',
` <Dialog.Trigger>${triggerLabel}</Dialog.Trigger>`,
' <Dialog.Portal>',
showOverlay && ` <Dialog.Overlay${overlayOpacity !== 0.6 ? ` overlayOpacity={${overlayOpacity}}` : ''} />`,
' <Dialog.Content',
size !== 'md' && ` size="${size}"`,
position !== 'middle-center' && ` position="${position}"`,
' >',
showHeader && ' <Dialog.Header>',
showHeader && ` <Dialog.Title>${titleText}</Dialog.Title>`,
showHeader && ` <Dialog.Description>${descText}</Dialog.Description>`,
showHeader && ' </Dialog.Header>',
' <p>...</p>',
showFooter && showClose && ' <Dialog.Footer>',
showFooter && showClose && ' <Dialog.Close>Close</Dialog.Close>',
showFooter && showClose && ' </Dialog.Footer>',
(!showFooter && showClose) && ` <Dialog.Close${closePosition !== 'top-right' ? ` position="${closePosition}"` : ''}>×</Dialog.Close>`,
' </Dialog.Content>',
' </Dialog.Portal>',
'</Dialog>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Overlays · Dialog</div>
<h1 data-uix-page-title>Dialog</h1>
<p data-uix-page-lede>
Modal blocking surface. Eight parts; two events
(<code>open</code> + a polymorphic <code>close</code>) with the
full asymmetric exit set (saved · cancelled · dismissed ·
dismissed-outside · failed) surfaced via
<code>data-last-action</code>. The provider concretes the
<code>close</code> event's family at trigger time —
<code>emerge.close</code> (cancel / dismiss / dismiss-outside),
<code>commit.save + fulfill</code> (save) or
<code>signal.alert + threat</code> (fail) — per book §5.3
polymorphism. Modal subset of intent — only
<code>neutral</code> / <code>risk</code> /
<code>threat</code>; affirm / fulfill / loss are intentionally
off the table per the modal canon.
</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>sizes</span>5
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>positions</span>9
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={dialogMorfo.apg} target="_blank" rel="noreferrer">dialog ↗</a>
</span>
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<Dialog bind:open {modal} {intent}>
<Dialog.Trigger data-perm-step="0" data-perm-label="open dialog">
{triggerLabel}
</Dialog.Trigger>
<Dialog.Portal>
{#if showOverlay}<Dialog.Overlay {overlayOpacity} />{/if}
<Dialog.Content {size} {position} {trapFocus} {preventScroll}>
{#if showHeader}
<Dialog.Header>
<Dialog.Title>{titleText}</Dialog.Title>
<Dialog.Description>{descText}</Dialog.Description>
</Dialog.Header>
{/if}
<p style="margin: var(--uix-space-3) 0; color: var(--uix-text-muted);">{bodyText}</p>
{#if showFooter && showClose}
<Dialog.Footer>
<Dialog.Close data-perm-step="1" data-perm-label="close dialog">
Close
</Dialog.Close>
</Dialog.Footer>
{:else if showClose}
<Dialog.Close
position={closePosition}
data-perm-step="1"
data-perm-label="close dialog"
>×</Dialog.Close>
{/if}
</Dialog.Content>
</Dialog.Portal>
</Dialog>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>open the dialog to see events</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>open</span> {String(open)}
</span>
</div>
</div>
<!-- Tabs -->
<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>13</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>
Soma owns modal mode + intent resolution. Eidos adds size,
position (3×3 grid), overlay opacity, and the eidos-only
Header / Footer layout shells.
</p>
<!-- ── Soma props ──────────────────────────────────────────────── -->
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · headless behavior
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>open <span data-uix-control-hint>bindable</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={open} />
<span data-uix-switch-label>{open ? 'open' : 'closed'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>modal <span data-uix-control-hint>traps focus, locks scroll</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={modal} />
<span data-uix-switch-label>{modal ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>intent <span data-uix-control-hint>modal subset</span></span>
<span data-uix-chips role="radiogroup">
{#each ['neutral', 'risk', 'threat'] as i}
<button data-uix-chip data-active={intent === i} onclick={() => (intent = i as DialogIntent)}>{i}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>trapFocus <span data-uix-control-hint>Content prop</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={trapFocus} />
<span data-uix-switch-label>{trapFocus ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>preventScroll <span data-uix-control-hint>Content prop</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={preventScroll} />
<span data-uix-switch-label>{preventScroll ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>render Header / Footer parts</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showHeader} />
<span data-uix-switch-label>header {showHeader ? 'on' : 'off'}</span>
</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showFooter} />
<span data-uix-switch-label>footer {showFooter ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>render Overlay / Close</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showOverlay} />
<span data-uix-switch-label>overlay {showOverlay ? 'on' : 'off'}</span>
</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showClose} />
<span data-uix-switch-label>close {showClose ? 'on' : 'off'}</span>
</span>
</label>
</div>
<!-- ── Eidos props ─────────────────────────────────────────────── -->
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size <span data-uix-control-hint>Content · width</span></span>
<span data-uix-chips role="radiogroup">
{#each ['sm', 'md', 'lg', 'xl', 'full'] as s}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as DialogSize)}>{s}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>position <span data-uix-control-hint>Content · 3×3 grid</span></span>
<span data-uix-chips role="radiogroup">
{#each ['top-left', 'top-center', 'top-right', 'middle-left', 'middle-center', 'middle-right', 'bottom-left', 'bottom-center', 'bottom-right'] as p}
<button data-uix-chip data-active={position === p} onclick={() => (position = p as DialogPosition)}>{p}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>overlayOpacity <span data-uix-control-hint>{overlayOpacity}</span></span>
<input type="range" min="0" max="1" step="0.02" bind:value={overlayOpacity} disabled={!showOverlay} />
</label>
<label data-uix-control>
<span data-uix-control-label>Close.position <span data-uix-control-hint>only when no Footer</span></span>
<span data-uix-chips role="radiogroup">
{#each ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'] as p}
<button data-uix-chip data-active={closePosition === p} onclick={() => (closePosition = p as DialogClosePosition)} disabled={showFooter}>{p}</button>
{/each}
</span>
</label>
</div>
<!-- ── Demo content ────────────────────────────────────────────── -->
<div data-uix-subsection-head>Demo content <span style="font-weight: normal; color: var(--uix-text-muted);">· literal text</span></div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>trigger label</span>
<input type="text" bind:value={triggerLabel} />
</label>
<label data-uix-control>
<span data-uix-control-label>title</span>
<input type="text" bind:value={titleText} />
</label>
<label data-uix-control>
<span data-uix-control-label>description</span>
<input type="text" bind:value={descText} />
</label>
<label data-uix-control>
<span data-uix-control-label>body</span>
<textarea bind:value={bodyText}></textarea>
</label>
</div>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · ARIA + behavior + intent resolution</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · adds size, position, overlayOpacity, Header/Footer shells</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>
<div data-uix-subsection-head>Provider</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">open <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">boolean</td><td class="default">false</td><td>Whether the dialog is open.</td></tr>
<tr><td class="name">modal</td><td class="type">boolean</td><td class="default">true</td><td>Modal blocks the page and traps focus. Set false for non-blocking sheet behaviour.</td></tr>
<tr><td class="name">intent</td><td class="type">'neutral' | 'risk' | 'threat'</td><td class="default">'neutral'</td><td>Modal subset (anexo §3.1). Drives data-color and the open event's perceptual signature.</td></tr>
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disables the trigger.</td></tr>
<tr><td class="name">onOpenChange / onOpenChangeComplete</td><td class="type">(open: boolean) =&gt; void</td><td class="default empty">—</td><td>Fire on every transition / after settle.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Content</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">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ResponsiveProp&lt;'sm' | 'md' | 'lg' | 'xl' | 'full'&gt;</td><td class="default">'md'</td><td>Width preset. Below sm breakpoint, non-full sizes auto-snap to bottom sheet.</td></tr>
<tr><td class="name">position <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ResponsiveProp&lt;DialogPosition&gt;</td><td class="default">'middle-center'</td><td>3×3 anchor grid. Ignored when size='full' or sheet auto-kicks in.</td></tr>
<tr><td class="name">trapFocus</td><td class="type">boolean</td><td class="default">true</td><td>Tab cycles within Content.</td></tr>
<tr><td class="name">preventScroll</td><td class="type">boolean</td><td class="default">true</td><td>Body scroll lock — only effective when modal=true.</td></tr>
<tr><td class="name">forceMount</td><td class="type">boolean</td><td class="default">false</td><td>Keep in DOM even when closed.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Overlay</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">overlayOpacity <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">number | string</td><td class="default empty">recipe</td><td>Override the recipe's default. Fraction (0.4 → 40%) or CSS string.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Close</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">position <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ResponsiveProp&lt;DialogClosePosition&gt;</td><td class="default">'top-right'</td><td>Close anchor inside Content. Subset excludes middle-* (collides with body).</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>
Source: <code>src/uix/morfo/components/dialog.ts</code>. Two
events — <code>open</code> (intent-bound via fromProp) and
a polymorphic <code>close</code> (book §5.3) with
<code>allowedFamilies: ['emerge', 'commit', 'signal']</code>.
The provider sets <code>data-last-action</code> imperatively
before each close and passes the concrete semantic per cause:
cancel / dismiss / dismiss-outside fire as plain emerge
(backing out has no evaluative load); save fires as
commit.save + fulfill; fail fires as signal.alert + threat
regardless of provider intent.
</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">"{dialogMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{dialogMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{dialogMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td><a href={dialogMorfo.apg} target="_blank" rel="noreferrer">{dialogMorfo.apg}</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{dialogMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{dialogMorfo.events?.length ?? 0}</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><th>Optional</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>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#each dialogMorfo.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 keyboard = partAny.keyboard ?? []}
{#if dataAttrs.length || ariaAttrs.length || keyboard.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 kind</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 kind</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 keyboard.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 keyboard as kb}
<tr>
<td><span data-uix-kbd>{kb.key === ' ' ? 'Space' : kb.key}</span></td>
<td class="default">{kb.action}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/if}
{/each}
<div data-uix-subsection-head>Events declaration</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><th>prewrite</th><th>commit</th></tr></thead>
<tbody>
{#each events as action}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
<tr>
<td class="name">{action.name}</td>
<td class="type">{sem.family}</td>
<td>{sem.verb ?? '—'}</td>
<td>{sem.sequence ?? 'pre'}</td>
<td class="default">{intentStr}</td>
<td>{action.target}</td>
<td class="default">{action.prewrite.length ? action.prewrite.map((p) => `${p.attr}=${p.value}`).join(', ') : '—'}</td>
<td class="default">{action.commit ? `${action.commit.attr}=${action.commit.value}` : '—'}</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 + perceptual signature
</h2>
<p data-uix-section-desc>
Two events. <code>open</code> is fromProp-bound to the
provider's <code>intent</code> — a threat dialog opens with
threat-flavoured perceptual weight. <code>close</code> is
polymorphic (book §5.3) — the table shows its declared
default family (<code>emerge</code>); the runtime swaps the
family per cause via <code>opts.semantic</code> in
<code>dismissWith()</code>: save → commit.save + fulfill;
cancel / dismiss / dismiss-outside → emerge.close (no
evaluative load on backing out); fail → signal.alert +
threat regardless of provider intent. Click
<strong>play</strong> to fire on the live dialog.
</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 events as action}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : (intentDecl ? intent : undefined)}
<tr>
<td class="name">{action.name}</td>
<td class="type">{action.semantic.family}</td>
<td>{action.semantic.verb ?? '—'}</td>
<td>{action.semantic.sequence ?? 'pre'}</td>
<td class="default">{effectiveIntent ?? '—'}</td>
<td>
<button
data-uix-play
onclick={() => {
const target = (stageRef?.querySelector('[data-dialog-content]') ?? stageRef?.querySelector('[data-dialog-trigger]') ?? stageRef) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
target,
...(effectiveIntent ? { intent: effectiveIntent } : {})
});
}}
>▶ 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>
Selectors at <code>src/uix/eidos/components/dialog/dialog.css</code>. Tokens bare-prefixed (<code>--dialog-*</code>).
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th></tr></thead>
<tbody>
<tr><td class="name">[data-dialog-content][data-state]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-dialog-content][data-color]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-dialog-content][data-size]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-dialog-content][data-position]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-dialog-content][data-state='closed'][data-last-action]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-dialog-overlay][data-state]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-dialog-header] / [data-dialog-footer]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-dialog-close][data-position]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
Follows the APG <a href={dialogMorfo.apg} target="_blank" rel="noreferrer">dialog pattern</a>. Focus is initial-trapped and restored to the trigger on close.
</p>
<div data-uix-subsection-head>Keyboard</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td>Open from Trigger.</td></tr>
<tr><td><span data-uix-kbd>Escape</span></td><td>Close (close with data-last-action='dismissed').</td></tr>
<tr><td><span data-uix-kbd>Tab</span> / <span data-uix-kbd>Shift+Tab</span></td><td>Cycle focus inside Content.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>ARIA contract</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Attribute</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">trigger</td><td>aria-haspopup / aria-expanded / aria-controls</td><td class="type">"dialog" / {`{open}`} / {`<content id>`}</td></tr>
<tr><td class="name">content</td><td>role / aria-modal</td><td class="type">"dialog" / "true" (modal only)</td></tr>
<tr><td class="name">content</td><td>aria-labelledby / aria-describedby</td><td class="type">{`<title id> / <description id>`}</td></tr>
<tr><td class="name">content</td><td>aria-roledescription</td><td class="type">"dialog window" (translated)</td></tr>
<tr><td class="name">title</td><td>role / aria-level</td><td class="type">"heading" / level</td></tr>
<tr><td class="name">overlay</td><td>aria-hidden</td><td class="type">"true"</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.