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

709 lines
31 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden 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 {
Command,
type CommandSize
} from '$uix/eidos/components/command';
import { compileMorfo } from '$uix/morfo';
import { commandMorfo } from '@/uix/morfo/components/command';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
// ── Live state ────────────────────────────────────────────────────────
let size = $state<CommandSize>('md');
let search = $state('');
let value = $state('');
let loop = $state(false);
let vimBindings = $state(true);
let columns = $state(1);
let disablePointerSelection = $state(false);
let shouldFilter = $state(true);
// Dialog mode
let dialogOpen = $state(false);
let dialogShortcut = $state('mod+k');
// Track invocations
let lastInvoked = $state<string | null>(null);
function invoke(name: string) {
return () => {
lastInvoked = name;
};
}
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(commandMorfo);
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 Command from '$soma/components/command';",
' let value = $state("");',
' let search = $state("");',
'</' + 'script>',
'',
'<Command.Provider',
' bind:value',
' bind:search',
loop && ' loop',
!vimBindings && ' vimBindings={false}',
columns !== 1 && ` columns={${columns}}`,
disablePointerSelection && ' disablePointerSelection',
!shouldFilter && ' shouldFilter={false}',
'>',
' <Command.Input placeholder="Search…" />',
' <Command.List>',
' <Command.Empty>No results.</Command.Empty>',
' <Command.Group>',
' <Command.GroupHeading>Files</Command.GroupHeading>',
' <Command.GroupItems>',
' <Command.Item value="open" onSelect={…}>Open file</Command.Item>',
' </Command.GroupItems>',
' </Command.Group>',
' </Command.List>',
'</Command.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Command } from '$uix/eidos/components/command';",
' let value = $state("");',
' let search = $state("");',
'</' + 'script>',
'',
'<Command',
' bind:value',
' bind:search',
size !== 'md' && ` size="${size}"`,
loop && ' loop',
!vimBindings && ' vimBindings={false}',
columns !== 1 && ` columns={${columns}}`,
disablePointerSelection && ' disablePointerSelection',
!shouldFilter && ' shouldFilter={false}',
'>',
' <Command.Input placeholder="Search…" />',
' <Command.List>',
' <Command.Empty>No results.</Command.Empty>',
' <Command.Group>',
' <Command.GroupHeading>Files</Command.GroupHeading>',
' <Command.GroupItems>',
' <Command.Item value="open" onSelect={…}>',
' <Command.Item.Icon>📁</Command.Item.Icon>',
' Open file',
' <Command.Item.Shortcut>⌘O</Command.Item.Shortcut>',
' </Command.Item>',
' </Command.GroupItems>',
' </Command.Group>',
' </Command.List>',
'</Command>'
]
.filter(Boolean)
.join('\n')
);
const dialogSnippet = $derived(
[
"<script lang='ts'>",
" import { Command } from '$uix/eidos/components/command';",
' let open = $state(false);',
'</' + 'script>',
'',
`<Command.Dialog bind:open shortcut="${dialogShortcut}">`,
' <Command.Input placeholder="Type a command…" />',
' <Command.List>…</Command.List>',
'</Command.Dialog>'
].join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Overlays · Command</div>
<h1 data-uix-page-title>Command</h1>
<p data-uix-page-lede>
⌘K command palette. Soma owns fuzzy scoring
(<code>computeFuzzyScore</code>), keyboard nav (arrow + vim
bindings + typeahead + loop + grid columns + RTL), item
visibility and the imperative <code>CommandApi</code>. Eidos
paints the palette chrome, exposes <code>size</code>
(sm/md/lg), adds <code>Item.Icon</code> / <code>Item.Shortcut</code>
sub-parts, and provides a <code>&lt;Command.Dialog&gt;</code>
composition wired to the eidos Dialog with an optional
<code>mod+k</code> shortcut binding.
</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>3
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/" target="_blank" rel="noreferrer">combobox ↗</a>
</span>
{#if lastInvoked}
<span data-uix-meta-pill>
<span data-uix-meta-key>last</span>{lastInvoked}
</span>
{/if}
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef} style="min-block-size: 20rem; padding: var(--uix-space-3); display: flex; justify-content: center;">
<div style="inline-size: min(28rem, 100%);">
<Command
bind:value
bind:search
{size}
{loop}
{vimBindings}
{columns}
{disablePointerSelection}
{shouldFilter}
>
<Command.Input placeholder="Search commands…" />
<Command.List>
<Command.Empty>No commands match.</Command.Empty>
<Command.Group>
<Command.GroupHeading>Files</Command.GroupHeading>
<Command.GroupItems>
<Command.Item value="new-file" onSelect={invoke('New file')}>
<Command.Item.Icon>📄</Command.Item.Icon>
New file
<Command.Item.Shortcut>⌘N</Command.Item.Shortcut>
</Command.Item>
<Command.Item value="open-file" onSelect={invoke('Open file')}>
<Command.Item.Icon>📁</Command.Item.Icon>
Open file…
<Command.Item.Shortcut>⌘O</Command.Item.Shortcut>
</Command.Item>
<Command.Item value="save-file" onSelect={invoke('Save file')}>
<Command.Item.Icon>💾</Command.Item.Icon>
Save file
<Command.Item.Shortcut>⌘S</Command.Item.Shortcut>
</Command.Item>
</Command.GroupItems>
</Command.Group>
<Command.Separator />
<Command.Group>
<Command.GroupHeading>Edit</Command.GroupHeading>
<Command.GroupItems>
<Command.Item value="undo" keywords={['ctrl-z', 'rollback']} onSelect={invoke('Undo')}>
<Command.Item.Icon>↶</Command.Item.Icon>
Undo
<Command.Item.Shortcut>⌘Z</Command.Item.Shortcut>
</Command.Item>
<Command.Item value="redo" keywords={['ctrl-y']} onSelect={invoke('Redo')}>
<Command.Item.Icon>↷</Command.Item.Icon>
Redo
<Command.Item.Shortcut>⇧⌘Z</Command.Item.Shortcut>
</Command.Item>
<Command.Item value="find" keywords={['search']} onSelect={invoke('Find')}>
<Command.Item.Icon>🔍</Command.Item.Icon>
Find in file
<Command.Item.Shortcut>⌘F</Command.Item.Shortcut>
</Command.Item>
</Command.GroupItems>
</Command.Group>
<Command.Separator />
<Command.Group>
<Command.GroupHeading>Help</Command.GroupHeading>
<Command.GroupItems>
<Command.LinkItem value="docs" href="https://example.com/docs" onSelect={invoke('Docs')}>
<Command.Item.Icon>📚</Command.Item.Icon>
Open documentation
<Command.Item.Shortcut>↗</Command.Item.Shortcut>
</Command.LinkItem>
<Command.Item value="about" disabled onSelect={invoke('About')}>
<Command.Item.Icon>ℹ︎</Command.Item.Icon>
About (disabled)
</Command.Item>
</Command.GroupItems>
</Command.Group>
</Command.List>
</Command>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>type or arrow-nav to highlight, Enter to invoke</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>value</span> {value || '—'}
</span>
</div>
</div>
<!-- Modal Dialog opener — separate stage row so consumers see both modes -->
<div data-uix-stage style="margin-top: var(--uix-space-3);">
<div data-uix-stage-area style="min-block-size: 6rem; display: flex; align-items: center; justify-content: center; gap: var(--uix-space-3);">
<button
onclick={() => (dialogOpen = !dialogOpen)}
style="padding: var(--uix-space-2) var(--uix-space-4); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-raised); cursor: pointer;"
>
{dialogOpen ? 'Close' : 'Open'} Command.Dialog (or press {dialogShortcut})
</button>
</div>
</div>
<Command.Dialog bind:open={dialogOpen} shortcut={dialogShortcut} {size}>
<Command.Input placeholder="Type a command or search…" />
<Command.List>
<Command.Empty>No matches.</Command.Empty>
<Command.Group>
<Command.GroupHeading>Quick actions</Command.GroupHeading>
<Command.GroupItems>
<Command.Item value="dlg-new" onSelect={invoke('Dialog · New')}>
<Command.Item.Icon>+</Command.Item.Icon>
New project
<Command.Item.Shortcut>⌘N</Command.Item.Shortcut>
</Command.Item>
<Command.Item value="dlg-search" onSelect={invoke('Dialog · Search')}>
<Command.Item.Icon>🔍</Command.Item.Icon>
Global search
<Command.Item.Shortcut>⌘P</Command.Item.Shortcut>
</Command.Item>
<Command.Item value="dlg-settings" onSelect={invoke('Dialog · Settings')}>
<Command.Item.Icon>⚙︎</Command.Item.Icon>
Open settings
<Command.Item.Shortcut>⌘,</Command.Item.Shortcut>
</Command.Item>
</Command.GroupItems>
</Command.Group>
</Command.List>
</Command.Dialog>
<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>15</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 scoring + keyboard nav. Eidos adds <code>size</code> on the root and the <code>Item.Icon</code> / <code>Item.Shortcut</code> layout sub-parts.
</p>
<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>search <span data-uix-control-hint>bindable</span></span>
<input type="text" bind:value={search} placeholder="(empty)" />
</label>
<label data-uix-control>
<span data-uix-control-label>value <span data-uix-control-hint>highlighted item</span></span>
<input type="text" value={value || '—'} readonly />
</label>
<label data-uix-control>
<span data-uix-control-label>loop <span data-uix-control-hint>arrow nav wraps</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={loop} />
<span data-uix-switch-label>{loop ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>vimBindings <span data-uix-control-hint>Ctrl+N/P/J/K</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={vimBindings} />
<span data-uix-switch-label>{vimBindings ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>columns <span data-uix-control-hint>{columns}</span></span>
<input type="range" min="1" max="4" step="1" bind:value={columns} />
</label>
<label data-uix-control>
<span data-uix-control-label>disablePointerSelection</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={disablePointerSelection} />
<span data-uix-switch-label>{disablePointerSelection ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>shouldFilter</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={shouldFilter} />
<span data-uix-switch-label>{shouldFilter ? 'on' : 'off'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> visual size
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['sm', 'md', 'lg'] as s (s)}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as CommandSize)}>{s}</button>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> Dialog mode
<span style="font-weight: normal; color: var(--uix-text-muted);">
· built on top of &lt;Dialog&gt; with optional shortcut binding
</span>
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>shortcut <span data-uix-control-hint>e.g. mod+k, ctrl+shift+p</span></span>
<input type="text" bind:value={dialogShortcut} />
</label>
<label data-uix-control>
<span data-uix-control-label>dialog open <span data-uix-control-hint>bindable</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={dialogOpen} />
<span data-uix-switch-label>{dialogOpen ? 'open' : 'closed'}</span>
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · inline palette</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 · size + Item sub-parts</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</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>modal · Command.Dialog with mod+k binding</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{dialogSnippet}</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>
13 soma parts plus 3 eidos additions: <code>Command.Dialog</code>
(wraps eidos Dialog), <code>Command.Item.Icon</code> and
<code>Command.Item.Shortcut</code> (Item layout sub-parts).
</p>
<div data-uix-subsection-head>Provider (Command)</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">value <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">string</td><td class="default">''</td><td>Currently highlighted item value.</td></tr>
<tr><td class="name">search <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">string</td><td class="default">''</td><td>Current search string.</td></tr>
<tr><td class="name">shouldFilter</td><td class="type">boolean</td><td class="default">true</td><td>Whether to run the scoring filter over items.</td></tr>
<tr><td class="name">filter</td><td class="type">CommandFilterFn</td><td class="default">computeFuzzyScore</td><td>Custom scoring function. Return 0 to exclude.</td></tr>
<tr><td class="name">loop</td><td class="type">boolean</td><td class="default">false</td><td>Loop keyboard navigation at the edges.</td></tr>
<tr><td class="name">vimBindings</td><td class="type">boolean</td><td class="default">true</td><td>Ctrl+N/J = next, Ctrl+P/K = previous.</td></tr>
<tr><td class="name">columns</td><td class="type">number</td><td class="default">1</td><td>Grid columns — arrows move ±1 (Right/Left) or ±columns (Down/Up).</td></tr>
<tr><td class="name">disablePointerSelection</td><td class="type">boolean</td><td class="default">false</td><td>Hovering items doesn't change the highlight.</td></tr>
<tr><td class="name">dir</td><td class="type">'ltr' | 'rtl'</td><td class="default empty">from soma config</td><td>Grid arrow inversion in RTL.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ResponsiveProp&lt;'sm' | 'md' | 'lg'&gt;</td><td class="default">'md'</td><td>Visual size — input height + item height + font.</td></tr>
<tr><td class="name">shortcut <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">string | string[]</td><td class="default empty">—</td><td>Optional keyboard shortcut (e.g. <code>'mod+k'</code>) that toggles open. Requires bindable open.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Item</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">value</td><td class="type">string</td><td class="default empty">required</td><td>Unique value used by the scorer.</td></tr>
<tr><td class="name">keywords</td><td class="type">readonly string[]</td><td class="default">[]</td><td>Text aliases the scorer also matches.</td></tr>
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disable activation. Still focusable.</td></tr>
<tr><td class="name">forceMount</td><td class="type">boolean</td><td class="default">false</td><td>Render even when filtered out.</td></tr>
<tr><td class="name">onSelect</td><td class="type">() =&gt; void</td><td class="default empty">—</td><td>Fired on Enter or click.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Item.Icon / Item.Shortcut <span data-uix-tag data-kind="eidos">eidos</span></div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
Sub-parts of <code>Item</code>. Both accept <code>children</code>; the recipe handles layout — Icon is a leading flex slot, Shortcut is pushed to row's end via <code>margin-inline-start: auto</code>. Pair with <code>&lt;Kbd&gt;</code> for richer shortcut styling.
</p>
<div data-uix-subsection-head>Command.Dialog <span data-uix-tag data-kind="eidos">eidos</span></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>Dialog open state.</td></tr>
<tr><td class="name">shortcut</td><td class="type">string | string[] | null</td><td class="default">'mod+k'</td><td>Keyboard binding(s) to toggle. <code>null</code> disables.</td></tr>
<tr><td class="name">size</td><td class="type">'sm' | 'md' | 'lg'</td><td class="default">'md'</td><td>Inner Command palette size.</td></tr>
<tr><td class="name">dialogSize</td><td class="type">DialogSize</td><td class="default">'md'</td><td>Eidos Dialog panel size (sm | md | lg | xl | full).</td></tr>
<tr><td class="name">modal · label · value · search · shouldFilter · filter · loop · …</td><td class="type">pass-through</td><td class="default empty">—</td><td>Forwarded to Dialog + Command.</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/command.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">"{commandMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{commandMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{commandMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">parts.length</td><td class="default">{commandMorfo.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><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.states.length ? part.states.join(' | ') : '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<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></tr></thead>
<tbody>
{#each events 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>
One event: <code>commit-invoke</code> fires when the user
activates a highlighted Item (Enter or click). Verb <code>submit</code>,
intent <code>fulfill</code>.
</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 (action.name)}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : 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-command-item]') ?? 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/command/command.css</code>.
Inside <code>&lt;Command.Dialog&gt;</code> the dialog panel chrome
comes from <code>dialog.css</code> and the inner palette uses these
selectors.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>What it paints</th></tr></thead>
<tbody>
<tr><td class="name">[data-command]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Palette container — flex column, border, surface bg.</td></tr>
<tr><td class="name">[data-command][data-size]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Size variant — input + item heights, font-size.</td></tr>
<tr><td class="name">[data-command-input]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Search input row + divider.</td></tr>
<tr><td class="name">[data-command-list], [data-command-viewport]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Scrolling list.</td></tr>
<tr><td class="name">[data-command-item], [data-command-link-item]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Row — flex, padding, gap.</td></tr>
<tr><td class="name">[data-command-item][data-selected]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Highlighted row tint.</td></tr>
<tr><td class="name">[data-command-item-icon], [data-command-item-shortcut]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Item sub-part layout slots — icon fixed, shortcut auto-end.</td></tr>
<tr><td class="name">[data-command-group-heading]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Muted uppercase group label.</td></tr>
<tr><td class="name">[data-command-separator]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>1px divider between groups.</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="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/" target="_blank" rel="noreferrer">combobox pattern</a> with a listbox popup. Input has <code>role="combobox"</code>; List is <code>role="listbox"</code>; each Item is <code>role="option"</code>.
</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>ArrowDown</span> / <span data-uix-kbd>ArrowUp</span></td><td>Move highlight ±1 (or ±columns if columns &gt; 1).</td></tr>
<tr><td><span data-uix-kbd>ArrowRight</span> / <span data-uix-kbd>ArrowLeft</span></td><td>In grid mode, move ±1 column (RTL inverts).</td></tr>
<tr><td><span data-uix-kbd>Ctrl+N</span> / <span data-uix-kbd>Ctrl+J</span></td><td>Next (vim binding, if <code>vimBindings=true</code>).</td></tr>
<tr><td><span data-uix-kbd>Ctrl+P</span> / <span data-uix-kbd>Ctrl+K</span></td><td>Previous (vim binding).</td></tr>
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>First / last visible item.</td></tr>
<tr><td><span data-uix-kbd>Enter</span></td><td>Activate highlighted item — fires <code>onSelect</code> + <code>commit-invoke</code>.</td></tr>
<tr><td><span data-uix-kbd>Escape</span></td><td>Clear search (or close dialog if inside <code>Command.Dialog</code>).</td></tr>
<tr><td><span data-uix-kbd>mod+k</span></td><td>Toggle dialog open/close (if <code>shortcut</code> is configured).</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">input</td><td>role</td><td class="type">"combobox"</td></tr>
<tr><td class="name">input</td><td>aria-expanded</td><td class="type">"true" (always while palette is mounted)</td></tr>
<tr><td class="name">input</td><td>aria-controls</td><td class="type">{`<list id>`}</td></tr>
<tr><td class="name">input</td><td>aria-activedescendant</td><td class="type">{`<highlighted item id>`}</td></tr>
<tr><td class="name">list</td><td>role</td><td class="type">"listbox"</td></tr>
<tr><td class="name">item</td><td>role</td><td class="type">"option"</td></tr>
<tr><td class="name">item</td><td>aria-selected</td><td class="type">"true" (when highlighted)</td></tr>
<tr><td class="name">item</td><td>aria-disabled</td><td class="type">"true" (when disabled)</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.