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

919 lines
42 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 { AlertDialog } from '$uix/eidos/components/alert-dialog';
import type { DialogSize, DialogPosition } from '$uix/eidos/components/dialog';
import { compileMorfo } from '$uix/morfo';
import { alertDialogMorfo } from '@/uix/morfo/components/alert-dialog';
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');
const intents: Array<'neutral' | 'risk' | 'threat'> = ['neutral', 'risk', 'threat'];
const sizes: DialogSize[] = ['sm', 'md', 'lg', 'xl', 'full'];
const positions: DialogPosition[] = [
'top-left',
'top-center',
'top-right',
'middle-left',
'middle-center',
'middle-right',
'bottom-left',
'bottom-center',
'bottom-right'
];
// ── Live state ────────────────────────────────────────────────────────
// Soma · Provider
let open = $state(false);
let intent = $state<'neutral' | 'risk' | 'threat'>('risk');
let disabled = $state(false);
// Eidos · Content
let size = $state<DialogSize>('md');
let position = $state<DialogPosition>('middle-center');
// Demo content
let title = $state('Delete project?');
let description = $state(
'This action cannot be undone. All project data, including history and member access, will be permanently lost.'
);
let triggerLabel = $state('Delete project…');
let actionText = $state('Delete project');
let cancelText = $state('Cancel');
// Eidos · Action visual props (forwarded to <Button>). variant narrows
// ButtonVariant → SelectionVariant (3-set: solid · outline · ghost)
// per DEMO_AUTHORING_GUIDE §12.7. Default stays 'solid' (CTA).
let actionVariant = $state<'solid' | 'outline' | 'ghost'>('solid');
let actionSize = $state<'xs' | 'sm' | 'md' | 'lg' | 'xl'>('md');
// Cancel narrows variant to ControlVariant per DEMO_AUTHORING_GUIDE §12.7.
let cancelVariant = $state<'surface' | 'outline' | 'ghost'>('outline');
let cancelSize = $state<'xs' | 'sm' | 'md' | 'lg' | 'xl'>('md');
// Composition toggles
let showHeader = $state(true);
let showFooter = $state(true);
let showOverlay = $state(true);
let lastChoice = $state<'action' | 'cancel' | null>(null);
function handleAction() {
lastChoice = 'action';
}
function handleCancel() {
lastChoice = 'cancel';
}
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();
});
// Watch the document root too — Dialog content portals out of the stage
$effect(() => {
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
if (!target.matches('[data-dialog-content], [data-dialog-trigger], [data-alert-dialog-action], [data-alert-dialog-cancel]')) continue;
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(document.body, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
const compiled = compileMorfo(alertDialogMorfo);
const compiledDialog = compileMorfo(dialogMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const dialogEvents = $derived([...compiledDialog.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 AlertDialog from '$soma/components/alert-dialog';",
' let open = $state(false);',
'</' + 'script>',
'',
'<AlertDialog.Provider',
' bind:open',
intent !== 'neutral' && ` intent="${intent}"`,
disabled && ' disabled',
' onOpenChange={(o) => …}',
'>',
` <AlertDialog.Trigger>${triggerLabel}</AlertDialog.Trigger>`,
' <AlertDialog.Content',
' escapeKeydownBehavior="close"',
' interactOutsideBehavior="ignore"',
' >',
` <AlertDialog.Title>${title}</AlertDialog.Title>`,
` <AlertDialog.Description>${description}</AlertDialog.Description>`,
` <AlertDialog.Cancel onclick={…}>${cancelText}</AlertDialog.Cancel>`,
` <AlertDialog.Action onclick={…}>${actionText}</AlertDialog.Action>`,
' </AlertDialog.Content>',
'</AlertDialog.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { AlertDialog } from '$uix/eidos/components/alert-dialog';",
' let open = $state(false);',
'</' + 'script>',
'',
'<AlertDialog',
' bind:open',
intent !== 'neutral' && ` intent="${intent}"`,
disabled && ' disabled',
'>',
` <AlertDialog.Trigger>${triggerLabel}</AlertDialog.Trigger>`,
' <AlertDialog.Portal>',
showOverlay && ' <AlertDialog.Overlay />',
' <AlertDialog.Content',
size !== 'md' && ` size="${size}"`,
position !== 'middle-center' && ` position="${position}"`,
' >',
showHeader && ' <AlertDialog.Header>',
showHeader ? ` <AlertDialog.Title>${title}</AlertDialog.Title>` : ` <AlertDialog.Title>${title}</AlertDialog.Title>`,
showHeader ? ` <AlertDialog.Description>${description}</AlertDialog.Description>` : ` <AlertDialog.Description>${description}</AlertDialog.Description>`,
showHeader && ' </AlertDialog.Header>',
showFooter && ' <AlertDialog.Footer>',
showFooter ? ` <AlertDialog.Cancel onclick={…}>${cancelText}</AlertDialog.Cancel>` : ` <AlertDialog.Cancel onclick={…}>${cancelText}</AlertDialog.Cancel>`,
showFooter ? ` <AlertDialog.Action onclick={…}>${actionText}</AlertDialog.Action>` : ` <AlertDialog.Action onclick={…}>${actionText}</AlertDialog.Action>`,
showFooter && ' </AlertDialog.Footer>',
' </AlertDialog.Content>',
' </AlertDialog.Portal>',
'</AlertDialog>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Feedback · AlertDialog</div>
<h1 data-uix-page-title>AlertDialog</h1>
<p data-uix-page-lede>
Modal Dialog with <code>role='alertdialog'</code>, forced
<code>modal=true</code> and click-outside disabled. The user MUST
choose <code>Action</code> or <code>Cancel</code> (Escape still
cancels). The morfo declares only 3 parts (Provider + Action +
Cancel) — every other part (Trigger / Portal / Overlay / Content /
Title / Description / Header / Footer) is delegated to Dialog's
runtime via <code>variant='alertdialog'</code>, including all 6
semantic events. Auto-coloring on <code>intent='risk' | 'threat'</code>
cascades the destructive tint to the Action button.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
<span data-uix-meta-key>parts</span>{compiled.parts.order.length} <span style="color: var(--uix-text-faint)">+ 7 inherited</span>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>events</span>0 <span style="color: var(--uix-text-faint)">+ {dialogEvents.length} via Dialog</span>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>intent</span>{intent}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={alertDialogMorfo.apg} target="_blank" rel="noreferrer">alertdialog ↗</a>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>open</span>{open ? 'true' : 'false'}
</span>
{#if lastChoice}
<span data-uix-meta-pill>
<span data-uix-meta-key>last</span>{lastChoice}
</span>
{/if}
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<AlertDialog bind:open {intent} {disabled}>
<AlertDialog.Trigger onclick={() => (lastChoice = null)}>
{triggerLabel}
</AlertDialog.Trigger>
<AlertDialog.Portal>
{#if showOverlay}<AlertDialog.Overlay />{/if}
<AlertDialog.Content {size} {position}>
{#if showHeader}
<AlertDialog.Header>
<AlertDialog.Title>{title}</AlertDialog.Title>
<AlertDialog.Description>{description}</AlertDialog.Description>
</AlertDialog.Header>
{:else}
<AlertDialog.Title>{title}</AlertDialog.Title>
<AlertDialog.Description>{description}</AlertDialog.Description>
{/if}
{#if showFooter}
<AlertDialog.Footer>
<AlertDialog.Cancel
variant={cancelVariant}
size={cancelSize}
onclick={handleCancel}
>
{cancelText}
</AlertDialog.Cancel>
<AlertDialog.Action
variant={actionVariant}
size={actionSize}
onclick={handleAction}
>
{actionText}
</AlertDialog.Action>
</AlertDialog.Footer>
{:else}
<AlertDialog.Cancel onclick={handleCancel}>{cancelText}</AlertDialog.Cancel>
<AlertDialog.Action onclick={handleAction}>{actionText}</AlertDialog.Action>
{/if}
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>open the dialog, click Cancel/Action to see Dialog 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)}
{#if lastChoice}
<span data-uix-stage-trace-key style="margin-inline-start: 0.5rem;">last</span> {lastChoice}
{/if}
</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>10</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 · 0e</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>{dialogEvents.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>
<code>intent</code> is the key knob — switching to
<code>risk</code> / <code>threat</code> auto-colors the Action
button (red / amber) via an eidos cascade, and primes the sema
layer for a destructive-confirmation perceptual signature.
</p>
<!-- ── Soma · Provider ─────────────────────────────────────────── -->
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> Provider props
</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>intent <span data-uix-control-hint>narrowed: neutral / risk / threat</span></span>
<span data-uix-chips role="radiogroup">
{#each intents as opt (opt)}
<button data-uix-chip data-active={intent === opt} onclick={() => (intent = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>disabled <span data-uix-control-hint>Trigger inert</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={disabled} />
<span data-uix-switch-label>{disabled ? 'on' : 'off'}</span>
</span>
</label>
</div>
<!-- ── Eidos · Content ─────────────────────────────────────────── -->
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> Content props
<span style="font-weight: normal; color: var(--uix-text-muted);">
· forwarded to Dialog
</span>
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each sizes as opt (opt)}
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>position <span data-uix-control-hint>3×3 anchor grid</span></span>
<span data-uix-chips role="radiogroup">
{#each positions as opt (opt)}
<button data-uix-chip data-active={position === opt} onclick={() => (position = opt)}>{opt.replace('-', ' / ')}</button>
{/each}
</span>
</label>
</div>
<!-- ── Composition ─────────────────────────────────────────────── -->
<div data-uix-subsection-head>
Composition
<span style="font-weight: normal; color: var(--uix-text-muted);">
· toggle structural wrappers in / out
</span>
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>Overlay <span data-uix-control-hint>backdrop dim</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showOverlay} />
<span data-uix-switch-label>{showOverlay ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>Header wrapper</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showHeader} />
<span data-uix-switch-label>{showHeader ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>Footer wrapper</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showFooter} />
<span data-uix-switch-label>{showFooter ? 'on' : 'off'}</span>
</span>
</label>
</div>
<!-- ── Demo content ────────────────────────────────────────────── -->
<div data-uix-subsection-head>
Demo content
<span style="font-weight: normal; color: var(--uix-text-muted);">
· literal text rendered into the dialog
</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={title} />
</label>
<label data-uix-control>
<span data-uix-control-label>Description</span>
<input type="text" bind:value={description} />
</label>
<label data-uix-control>
<span data-uix-control-label>Action button</span>
<input type="text" bind:value={actionText} />
</label>
<label data-uix-control>
<span data-uix-control-label>Cancel button</span>
<input type="text" bind:value={cancelText} />
</label>
</div>
<!-- ── Eidos · Action visual props ────────────────────────────── -->
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> Action
<span style="font-weight: normal; color: var(--uix-text-muted);">
· visual props forwarded to &lt;Button&gt;; intent inherits from the dialog
</span>
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['solid', 'outline', 'ghost'] as v (v)}
<button
data-uix-chip
data-active={actionVariant === v}
onclick={() => (actionVariant = v as typeof actionVariant)}
>
{v}
</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
<button
data-uix-chip
data-active={actionSize === s}
onclick={() => (actionSize = s as typeof actionSize)}
>
{s}
</button>
{/each}
</span>
</label>
</div>
<!-- ── Eidos · Cancel visual props ────────────────────────────── -->
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> Cancel
<span style="font-weight: normal; color: var(--uix-text-muted);">
· variant narrowed to ControlVariant; always neutral palette
</span>
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['surface', 'outline', 'ghost'] as v (v)}
<button
data-uix-chip
data-active={cancelVariant === v}
onclick={() => (cancelVariant = v as typeof cancelVariant)}
>
{v}
</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
<button
data-uix-chip
data-active={cancelSize === s}
onclick={() => (cancelSize = s as typeof cancelSize)}
>
{s}
</button>
{/each}
</span>
</label>
</div>
<!-- ── Code snippets ───────────────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · forced modal + role='alertdialog' + click-outside ignore</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 · full composition with Portal/Overlay/Header/Footer</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>
AlertDialog declares 3 parts of its own (Provider + Action +
Cancel). Trigger / Portal / Overlay / Content / Title /
Description / Header / Footer come from <code>Dialog</code> via
soma's runtime delegation. Below are the alert-dialog-specific
props; for the inherited props see the <a href="/uix/components/dialog">Dialog</a> demo.
</p>
<div data-uix-subsection-head>AlertDialog (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">id</td><td class="type">string</td><td class="default empty">auto</td><td>Stable identifier.</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 dialog is open.</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 start / animation settle.</td></tr>
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disable the Trigger; dialog stays closed.</td></tr>
<tr><td class="name">intent</td><td class="type">'neutral' | 'risk' | 'threat'</td><td class="default">'neutral'</td><td>Doctrinal intent. Narrowed from Dialog's full set — alert dialogs gate destructive actions. Drives the auto-color cascade on Action.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>AlertDialog.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>Modal width preset (forwarded to Dialog).</td></tr>
<tr><td class="name">position <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">DialogPosition (3×3 anchor grid)</td><td class="default">'middle-center'</td><td>Where on the viewport.</td></tr>
<tr><td class="name">escapeKeydownBehavior</td><td class="type">'close' | 'ignore' | 'defer-otherwise-close' | 'defer-otherwise-ignore'</td><td class="default"><strong>'close'</strong></td><td>Escape <em>does</em> cancel by convention (overrides Dialog's 'defer' default).</td></tr>
<tr><td class="name">interactOutsideBehavior</td><td class="type">same as above</td><td class="default"><strong>'ignore'</strong></td><td><em>Forced</em> — click outside cannot dismiss. The user MUST choose Action or Cancel.</td></tr>
<tr><td class="name">…inherited</td><td class="type">DialogContentProps</td><td class="default empty">—</td><td>trapFocus, loop, preventScroll, forceMount, onOpenAutoFocus, onCloseAutoFocus, onEscapeKeydown, etc.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>AlertDialog.Action</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">aria-label</td><td class="type">string</td><td class="default empty">"Confirm"</td><td>Overrides the default accessible name.</td></tr>
<tr><td class="name">onclick</td><td class="type">(e: MouseEvent) =&gt; void</td><td class="default empty">—</td><td>Business handler. The default close runs LAST — consumer logic gets first chance to gate.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">SelectionVariant: 'solid' | 'outline' | 'ghost'</td><td class="default">'solid'</td><td>Forwarded to the inner <code>&lt;Button&gt;</code>. Narrowed from ButtonVariant per DEMO_AUTHORING_GUIDE §12.7 — 3-value SelectionVariant keeps 'solid' as the CTA default while staying in the canonical chip rhythm.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td><td class="default">'md'</td><td>Button size scale.</td></tr>
<tr><td class="name">intent <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'neutral' | 'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss'</td><td class="default empty">inherits</td><td>When omitted, reads the parent <code>AlertDialog.intent</code> (neutral · risk · threat) so a flagged dialog paints a matching Action automatically.</td></tr>
<tr><td class="name">color · rounded · iconOnly · icon · endIcon <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">Button props</td><td class="default empty">—</td><td>Standard Button visual knobs forwarded through.</td></tr>
<tr><td class="name">child</td><td class="type">{`Snippet<[{ props; intent }]>`}</td><td class="default empty">—</td><td>Opt out of <code>&lt;Button&gt;</code> entirely. The dialog's resolved intent is passed alongside <code>props</code>.</td></tr>
<tr><td class="name">…native button attrs</td><td class="type">PrimitiveButtonAttributes</td><td class="default empty">—</td><td>Renders a native <code>&lt;button type='button'&gt;</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>AlertDialog.Cancel</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">aria-label</td><td class="type">string</td><td class="default empty">"Cancel"</td><td>Overrides the default accessible name.</td></tr>
<tr><td class="name">onclick</td><td class="type">(e: MouseEvent) =&gt; void</td><td class="default empty">—</td><td>Business handler. Default close runs last.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ControlVariant: 'surface' | 'outline' | 'ghost'</td><td class="default">'outline'</td><td>Forwarded to <code>&lt;Button&gt;</code>. Narrowed from ButtonVariant per DEMO_AUTHORING_GUIDE §12.7 (form-control surface).</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td><td class="default">'md'</td><td>Button size scale.</td></tr>
<tr><td class="name">intent <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ButtonIntent</td><td class="default">'neutral'</td><td>Stays neutral by default so it contrasts with the (potentially destructive) Action.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ColorRole</td><td class="default">'neutral'</td><td>Forced to neutral so the outline reads as gray regardless of Button's default primary.</td></tr>
<tr><td class="name">rounded · iconOnly · icon · endIcon <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">Button props</td><td class="default empty">—</td><td>Standard Button visual knobs.</td></tr>
<tr><td class="name">…native button attrs</td><td class="type">PrimitiveButtonAttributes</td><td class="default empty">—</td><td>By convention, receives initial focus — render BEFORE Action in DOM order.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Inherited from Dialog</div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
Same surface as <a href="/uix/components/dialog">Dialog</a>. Listed
for at-a-glance reference; the prop tables live in the Dialog
demo.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">Trigger</td><td>Renders a native <code>&lt;button&gt;</code> with <code>aria-haspopup='dialog'</code> + <code>aria-expanded</code>.</td></tr>
<tr><td class="name">Portal</td><td>Portal to <code>document.body</code> (no props).</td></tr>
<tr><td class="name">Overlay</td><td>Backdrop. Optional via composition (toggle in / out).</td></tr>
<tr><td class="name">Title</td><td>Wired to Content's <code>aria-labelledby</code>.</td></tr>
<tr><td class="name">Description</td><td>Wired to Content's <code>aria-describedby</code>.</td></tr>
<tr><td class="name">Header / Footer</td><td>Structural wrappers — pure layout.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Reference parity</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Feature</th><th>Radix</th><th>bits-ui</th><th>ark-ui</th><th>Eidos (this)</th></tr></thead>
<tbody>
<tr><td>Forced modal=true</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td></tr>
<tr><td>role='alertdialog'</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td></tr>
<tr><td>Click-outside ignored</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td></tr>
<tr><td>Escape cancels (overrides Dialog)</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td></tr>
<tr><td>Dedicated Action / Cancel parts</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td></tr>
<tr><td>intent prop (risk/threat auto-color)</td><td>—</td><td>—</td><td>—</td><td>✓</td></tr>
<tr><td>Runtime delegation to Dialog</td><td>—</td><td>—</td><td>—</td><td>✓</td></tr>
<tr><td>Sema event inheritance (6 events)</td><td>—</td><td>—</td><td>—</td><td>✓</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 alert-dialog morfo declares only the Action + Cancel button
parts (Provider is virtual). Trigger / Content / Overlay / Title
/ Description / Header / Footer come from the
<a href="/uix/components/dialog">Dialog</a> morfo via soma's
runtime delegation
(<code>variant='alertdialog'</code> on the Dialog provider) —
which also brings the full Dialog event vocabulary. Source:
<code>src/uix/morfo/components/alert-dialog.ts</code>.
</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">"{alertDialogMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{alertDialogMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{alertDialogMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td><a href={alertDialogMorfo.apg} target="_blank" rel="noreferrer">{alertDialogMorfo.apg}</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{alertDialogMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">0 <span style="color: var(--uix-text-faint)">(delegates to Dialog · {dialogEvents.length})</span></td></tr>
<tr><td class="name">focus.initial</td><td class="type">"first-focusable"</td></tr>
<tr><td class="name">focus.trap</td><td class="type">true</td></tr>
<tr><td class="name">focus.return</td><td class="type">"trigger"</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts (own)</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>Optional</th></tr></thead>
<tbody>
{#each partsList as part (part.kebab)}
<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.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#each alertDialogMorfo.parts as rawPart (rawPart.kebab)}
{@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 (attr.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 (a.attr)}
<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}
{/each}
<div data-uix-subsection-head>Inherited events (from Dialog)</div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
The runtime stamps these events on the Content / Action / Cancel
targets when the dialog opens / closes / commits. AlertDialog
does NOT declare its own events array — the perceptual surface
is identical to Dialog's. The Sema tab plays them.
</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>target</th></tr></thead>
<tbody>
{#each dialogEvents as action (action.name)}
{@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>
</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>
<strong>0 events declared by alert-dialog morfo</strong>;
perceptual surface inherited from Dialog (6 events). The
<code>intent</code> prop on the Provider drives intent for the
<code>commit-*</code> family on the Action button — risk /
threat gets the destructive-confirmation signature (sharper
timbre + saturated motion).
</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>Target part</th><th>Play</th></tr></thead>
<tbody>
{#each dialogEvents as action (action.name)}
{@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 ?? (intentDecl ? `fromProp → ${intent}` : '—')}</td>
<td class="default">{action.target}</td>
<td>
<button
data-uix-play
onclick={() => {
const targetSel =
action.target === 'action' || action.target === 'cancel'
? `[data-alert-dialog-${action.target}]`
: action.target === 'content'
? '[data-dialog-content]'
: '[data-dialog-trigger]';
const target = (
document.querySelector(targetSel) ??
document.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>
Source: <code>src/uix/eidos/components/alert-dialog/alert-dialog.css</code>.
Only the Action + Cancel buttons get bespoke selectors — the
chrome (overlay, content panel, header, footer, title,
description) is inherited from <code>dialog.css</code> because
the soma runtime emits the same <code>data-dialog-*</code>
markers under <code>variant='alertdialog'</code>.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>Concern</th></tr></thead>
<tbody>
<tr><td class="name">[data-alert-dialog-action]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Solid primary button (neutral intent).</td></tr>
<tr><td class="name">[data-alert-dialog-cancel]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Outline secondary button. Stays neutral regardless of intent.</td></tr>
<tr><td class="name">[data-dialog-content][data-color='risk'] [data-alert-dialog-action]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Auto-color cascade: risk-flagged dialog ⇒ red Action.</td></tr>
<tr><td class="name">[data-dialog-content][data-color='threat'] [data-alert-dialog-action]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Same cascade for threat-flagged ⇒ amber Action.</td></tr>
<tr><td class="name">[data-dialog-overlay]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Inherited backdrop dim.</td></tr>
<tr><td class="name">[data-dialog-content]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Inherited modal panel chrome.</td></tr>
<tr><td class="name">[data-dialog-content][data-state='open' | 'closed']</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Inherited enter / exit transitions.</td></tr>
<tr><td class="name">[data-dialog-header] / [data-dialog-footer]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Inherited layout wrappers.</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={alertDialogMorfo.apg} target="_blank" rel="noreferrer">alertdialog pattern</a>.
Content emits <code>role='alertdialog'</code> (vs Dialog's
<code>role='dialog'</code>) so screen readers announce with
alert semantics. <code>Cancel</code> receives initial focus by
convention (render it BEFORE <code>Action</code> in DOM order)
— the safe choice becomes the default when the user just
presses <span data-uix-kbd>Enter</span>.
</p>
<div data-uix-subsection-head>Keyboard</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Effect</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>Escape</span></td><td>Cancel (fires Cancel's <code>onclick</code> + closes).</td></tr>
<tr><td><span data-uix-kbd>Tab</span> / <span data-uix-kbd>Shift+Tab</span></td><td>Cycle focus inside the dialog (focus trap).</td></tr>
<tr><td><span data-uix-kbd>Enter</span> on Action</td><td>Confirm (fires Action's <code>onclick</code> + closes).</td></tr>
<tr><td><span data-uix-kbd>Enter</span> on Cancel</td><td>Cancel (fires Cancel's <code>onclick</code> + closes).</td></tr>
<tr><td>Click outside</td><td><strong>Ignored.</strong> The user MUST choose Action or Cancel.</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</td><td class="type">"dialog"</td></tr>
<tr><td class="name">trigger</td><td>aria-expanded</td><td class="type">{`{open}`}</td></tr>
<tr><td class="name">trigger</td><td>aria-controls</td><td class="type">{`<content id>`}</td></tr>
<tr><td class="name">content</td><td>role</td><td class="type"><strong>"alertdialog"</strong></td></tr>
<tr><td class="name">content</td><td>aria-modal</td><td class="type">"true" (forced)</td></tr>
<tr><td class="name">content</td><td>aria-labelledby</td><td class="type">{`<title id>`}</td></tr>
<tr><td class="name">content</td><td>aria-describedby</td><td class="type">{`<description id>`}</td></tr>
<tr><td class="name">action</td><td>type</td><td class="type">"button"</td></tr>
<tr><td class="name">action</td><td>aria-label</td><td class="type">"Confirm" (default) or <code>{`{aria-label}`}</code></td></tr>
<tr><td class="name">cancel</td><td>type</td><td class="type">"button"</td></tr>
<tr><td class="name">cancel</td><td>aria-label</td><td class="type">"Cancel" (default) or <code>{`{aria-label}`}</code></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.