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

530 lines
18 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 { 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'
];
let open = $state(false);
let intent = $state<'neutral' | 'risk' | 'threat'>('risk');
let size = $state<DialogSize>('md');
let position = $state<DialogPosition>('middle-center');
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 actionText = $state('Delete project');
let cancelText = $state('Cancel');
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();
});
const compiled = compileMorfo(alertDialogMorfo);
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 AlertDialog from '$soma/components/alert-dialog';",
' let open = $state(false);',
'</' + 'script>',
'',
'<AlertDialog.Provider',
' bind:open',
intent !== 'neutral' && ` intent="${intent}"`,
'>',
' <AlertDialog.Trigger>Delete…</AlertDialog.Trigger>',
' <AlertDialog.Content>',
` <h2>${title}</h2>`,
` <p>${description}</p>`,
` <AlertDialog.Cancel>${cancelText}</AlertDialog.Cancel>`,
` <AlertDialog.Action>${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}"`,
'>',
' <AlertDialog.Trigger>Delete…</AlertDialog.Trigger>',
' <AlertDialog.Portal>',
' <AlertDialog.Overlay />',
' <AlertDialog.Content',
size !== 'md' && ` size="${size}"`,
position !== 'middle-center' && ` position="${position}"`,
' >',
' <AlertDialog.Header>',
` <AlertDialog.Title>${title}</AlertDialog.Title>`,
` <AlertDialog.Description>${description}</AlertDialog.Description>`,
' </AlertDialog.Header>',
' <AlertDialog.Footer>',
` <AlertDialog.Cancel>${cancelText}</AlertDialog.Cancel>`,
` <AlertDialog.Action>${actionText}</AlertDialog.Action>`,
' </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). <span data-uix-layer-badge="soma">soma</span>
delegates to the Dialog runtime — only the Action + Cancel
buttons are alert-dialog-specific.
</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>intent</span>{intent}
</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>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<AlertDialog bind:open {intent}>
<AlertDialog.Trigger onclick={() => (lastChoice = null)}>
Open AlertDialog
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialog.Content {size} {position}>
<AlertDialog.Header>
<AlertDialog.Title>{title}</AlertDialog.Title>
<AlertDialog.Description>{description}</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={handleCancel}>{cancelText}</AlertDialog.Cancel>
<AlertDialog.Action onclick={handleAction}>{actionText}</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
{#if trace[0]}
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>last</span>
<span>{trace[0].event} ({trace[0].family}) @{fmtTime(trace[0].at)}</span>
{/if}
</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>
</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) and primes the sema layer for a
destructive confirmation.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · semantic
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>intent</span>
<span data-uix-chips role="radiogroup">
{#each intents as 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>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 text</span>
<input type="text" bind:value={actionText} />
</label>
<label data-uix-control>
<span data-uix-control-label>cancel text</span>
<input type="text" bind:value={cancelText} />
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · shape (forwarded to Dialog)
</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}
<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>
<span data-uix-chips role="radiogroup">
{#each positions as opt}
<button data-uix-chip data-active={position === opt} onclick={() => (position = opt)}>{opt}</button>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · forced modal + role=alertdialog</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</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 re-uses Dialog's parts for Trigger / Overlay /
Title / Description / Header / Footer / Portal. The
alert-dialog-specific parts are Provider (sets the variant +
modal forcing), Content (forces Escape close), Action and
Cancel.
</p>
<div data-uix-subsection-head>AlertDialog (root)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">open <span data-uix-tag data-kind="soma">soma</span></td>
<td class="type">boolean</td>
<td class="default">false</td>
<td>Bindable visibility.</td>
</tr>
<tr>
<td class="name">intent <span data-uix-tag data-kind="soma">soma</span></td>
<td class="type">'neutral' | 'risk' | 'threat'</td>
<td class="default">'neutral'</td>
<td>Narrowed from Dialog's full set — alert dialogs gate destructive actions.</td>
</tr>
<tr>
<td class="name">disabled <span data-uix-tag data-kind="soma">soma</span></td>
<td class="type">boolean</td>
<td class="default">false</td>
<td>Disables the Trigger (dialog stays closed).</td>
</tr>
<tr>
<td class="name">onOpenChange <span data-uix-tag data-kind="soma">soma</span></td>
<td class="type">(open: boolean) =&gt; void</td>
<td class="default empty">—</td>
<td>Fired on open/close transition start.</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>Name</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td>
<td class="type">'sm' | 'md' | 'lg' | 'xl' | 'full'</td>
<td class="default">'md'</td>
<td>Modal width preset. Responsive.</td>
</tr>
<tr>
<td class="name">position <span data-uix-tag data-kind="eidos">eidos</span></td>
<td class="type">3×3 anchor grid</td>
<td class="default">'middle-center'</td>
<td>Same as Dialog.</td>
</tr>
<tr>
<td class="name">escapeKeydownBehavior <span data-uix-tag data-kind="soma">soma</span></td>
<td class="type">'close' | 'ignore'</td>
<td class="default">'close'</td>
<td><strong>Overrides Dialog default.</strong> Escape cancels by convention.</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>AlertDialog.Action / AlertDialog.Cancel</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">aria-label <span data-uix-tag data-kind="soma">soma</span></td>
<td class="type">string</td>
<td>Overrides the default accessible name ("Confirm" / "Cancel").</td>
</tr>
<tr>
<td class="name">onclick</td>
<td class="type">(ev: MouseEvent) =&gt; void</td>
<td>Business handler. Default close runs LAST — consumer logic gets first chance.</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo 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 Dialog morfo via soma's runtime delegation
(<code>variant='alertdialog'</code> on the Dialog provider).
</p>
<div data-uix-subsection-head>Parts ({partsList.length})</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Kebab</th><th>Role</th><th>Marker</th><th>Kind</th></tr></thead>
<tbody>
{#each partsList as part}
{@const partAny = part as unknown as Record<string, unknown>}
<tr>
<td class="name">{partAny.kebab}</td>
<td>{partAny.role ?? '—'}</td>
<td><code>{partAny.marker ?? '—'}</code></td>
<td>{partAny.kind ?? '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · perceptual flow
</h2>
<p data-uix-section-desc>
The alert-dialog morfo declares zero events of its own.
Perceptual feedback (open whoosh, button-press tick, close
exit) comes from the Dialog runtime that soma's AlertDialog
delegates to. See the Dialog demo's Sema tab for the full
event vocabulary.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Source</th><th>Events</th></tr></thead>
<tbody>
<tr>
<td>alert-dialog morfo</td>
<td>{events.length} (none)</td>
</tr>
<tr>
<td>dialog morfo (via runtime delegation)</td>
<td>open, close-*, commit-*</td>
</tr>
</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>
<code>alert-dialog.css</code> only styles the Action + Cancel
buttons. Chrome (overlay, content panel, header) comes from
<code>dialog.css</code> — same DOM markers reach both.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>[data-alert-dialog-action]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Solid button. Inherits Toggle palette.</td>
</tr>
<tr>
<td class="name"><code>[data-alert-dialog-cancel]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Outline button. Stays neutral.</td>
</tr>
<tr>
<td class="name"
><code>[data-dialog-content][data-color='risk'] [data-alert-dialog-action]</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Auto-color cascade: risk-flagged dialog gets a red Action.</td>
</tr>
<tr>
<td class="name"
><code>[data-dialog-content][data-color='threat'] [data-alert-dialog-action]</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Same cascade for threat-flagged.</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>A11y contract</h2>
<p data-uix-section-desc>
Pattern: WAI-ARIA APG <a
href="https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/"
target="_blank"
rel="noopener">Alert Dialog</a
>. The Content emits <code>role="alertdialog"</code> (vs
<code>dialog</code>) — screen readers announce it with alert
semantics.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Effect</th></tr></thead>
<tbody>
<tr><td><kbd>Escape</kbd></td><td>Cancel (closes via Cancel's onclick).</td></tr>
<tr><td><kbd>Tab</kbd></td><td>Move focus within the dialog (focus trap).</td></tr>
<tr><td><kbd>Shift+Tab</kbd></td><td>Reverse focus within the dialog.</td></tr>
<tr><td><kbd>Enter</kbd> on Action</td><td>Confirm (closes via Action's onclick).</td></tr>
<tr><td>Click outside</td><td><strong>Ignored.</strong> Must choose Action or Cancel.</td></tr>
</tbody>
</table>
</div>
<p data-uix-section-desc>
By convention, <code>Cancel</code> receives initial focus —
render it BEFORE <code>Action</code> in DOM order. This makes
the safe choice the default when the user just presses
<kbd>Enter</kbd>.
</p>
</section>
{/if}
</div>

Powered by TurnKey Linux.