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

638 lines
28 KiB

This file contains ambiguous Unicode characters!

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

<script lang="ts">
import {
Tabs,
type TabsSize,
type TabsVariant,
type TabsMotionPreset,
type TabsCollapseAt
} from '$uix/eidos/components/tabs';
import type { ActivationMode } from '$soma/components/tabs';
import type { Orientation } from '$soma/types';
import { compileMorfo } from '$uix/morfo';
import { tabsMorfo } from '@/uix/morfo/components/tabs';
import { getActiveUix } from '$active-uix';
import { page } from '$app/state';
const uix = getActiveUix();
const params = page.url.searchParams;
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
// ── State ──────────────────────────────────────────────────────────────
let value = $state('overview');
let variant = $state<TabsVariant>('line');
let size = $state<TabsSize>('md');
let orientation = $state<Orientation>(
(params.get('perm.orientation') as Orientation | null) ?? 'horizontal'
);
let activationMode = $state<ActivationMode>('automatic');
let loop = $state(true);
let dir = $state<'ltr' | 'rtl'>((params.get('perm.dir') as 'ltr' | 'rtl' | null) ?? 'ltr');
let disabled = $state(params.get('perm.disabled') === 'true');
let fitted = $state(false);
let scrollable = $state(false);
let collapseAt = $state<TabsCollapseAt>('never');
let motionPreset = $state<TabsMotionPreset>('none');
let showIndicator = $state(true);
let listLabel = $state('Settings sections');
let billingDisabled = $state(false);
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(tabsMorfo);
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 baseItems = [
{ value: 'overview', label: 'Overview' },
{ value: 'billing', label: 'Billing' },
{ value: 'team', label: 'Team' },
{ value: 'integrations', label: 'Integrations' }
];
// Soma snippet
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import { Provider as Tabs } from '$soma/components/tabs';",
' let value = $state("overview");',
'</' + 'script>',
'',
'<Tabs',
' bind:value',
orientation !== 'horizontal' && ` orientation="${orientation}"`,
activationMode !== 'automatic' && ` activationMode="${activationMode}"`,
!loop && ' loop={false}',
dir !== 'ltr' && ` dir="${dir}"`,
disabled && ' disabled',
'>',
' <Tabs.List' + (listLabel ? ` aria-label="${listLabel}"` : '') + '>',
' <Tabs.Trigger value="overview">Overview</Tabs.Trigger>',
' <Tabs.Trigger value="billing">Billing</Tabs.Trigger>',
' ...',
' </Tabs.List>',
' <Tabs.Content value="overview">…</Tabs.Content>',
' <Tabs.Content value="billing">…</Tabs.Content>',
' ...',
'</Tabs>'
]
.filter(Boolean)
.join('\n')
);
// Eidos snippet
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Tabs } from '$uix/eidos/components/tabs';",
' let value = $state("overview");',
'</' + 'script>',
'',
'<Tabs',
' bind:value',
variant !== 'line' && ` variant="${variant}"`,
size !== 'md' && ` size="${size}"`,
orientation !== 'horizontal' && ` orientation="${orientation}"`,
activationMode !== 'automatic' && ` activationMode="${activationMode}"`,
!loop && ' loop={false}',
dir !== 'ltr' && ` dir="${dir}"`,
disabled && ' disabled',
fitted && ' fitted',
scrollable && ' scrollable',
collapseAt !== 'never' && ` collapseAt={${typeof collapseAt === 'string' ? `"${collapseAt}"` : collapseAt}}`,
motionPreset !== 'none' && ` motionPreset="${motionPreset}"`,
'>',
' <Tabs.List' + (listLabel ? ` aria-label="${listLabel}"` : '') + '>',
' <Tabs.Trigger value="overview">Overview</Tabs.Trigger>',
' <Tabs.Trigger value="billing">Billing</Tabs.Trigger>',
' ...',
showIndicator && variant !== 'surface' && ' <Tabs.Indicator />',
' </Tabs.List>',
' <Tabs.Content value="overview">…</Tabs.Content>',
' <Tabs.Content value="billing">…</Tabs.Content>',
' ...',
'</Tabs>'
]
.filter(Boolean)
.join('\n')
);
</script>
<svelte:head>
<meta name="perm-axes" content="dir,orientation,disabled" />
</svelte:head>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Controls · Tabs</div>
<h1 data-uix-page-title>Tabs</h1>
<p data-uix-page-lede>
Tablist with full APG semantics, automatic / manual activation, animated
indicator, three variants (<code>line</code> / <code>surface</code> /
<code>pills</code>), four sizes, fitted / scrollable list shapes,
optional content motion presets. Per-tab activation fires
<code>commit-select</code> sema event with a discreet haptic +
sub-audible sound.
</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>variants</span>3</span>
<span data-uix-meta-pill><span data-uix-meta-key>sizes</span>4</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={tabsMorfo.apg} target="_blank" rel="noreferrer">tabs ↗</a>
</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<Tabs
bind:value
{variant}
{size}
{orientation}
{activationMode}
{loop}
{dir}
{disabled}
{fitted}
{scrollable}
{collapseAt}
{motionPreset}
>
<Tabs.List aria-label={listLabel}>
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger
value="billing"
disabled={billingDisabled}
data-perm-step={disabled ? undefined : '0'}
data-perm-label={disabled ? undefined : 'select billing tab'}
>Billing</Tabs.Trigger>
<Tabs.Trigger
value="team"
data-perm-step={disabled ? undefined : '1'}
data-perm-label={disabled ? undefined : 'select team tab'}
>Team</Tabs.Trigger>
<Tabs.Trigger value="integrations">Integrations</Tabs.Trigger>
{#if showIndicator && variant !== 'surface'}<Tabs.Indicator />{/if}
</Tabs.List>
<Tabs.Content value="overview">
<h3 style="margin: 0 0 var(--uix-space-3); font-weight: 600;">Overview</h3>
<p style="margin: 0; color: var(--uix-text-muted);">High-level summary of the workspace.</p>
</Tabs.Content>
<Tabs.Content value="billing">
<h3 style="margin: 0 0 var(--uix-space-3); font-weight: 600;">Billing</h3>
<p style="margin: 0; color: var(--uix-text-muted);">Toggle billing-disabled to see the trigger ignored.</p>
</Tabs.Content>
<Tabs.Content value="team">
<h3 style="margin: 0 0 var(--uix-space-3); font-weight: 600;">Team</h3>
<p style="margin: 0; color: var(--uix-text-muted);">Switch activationMode to manual and arrow-key to feel the focus split.</p>
</Tabs.Content>
<Tabs.Content value="integrations">
<h3 style="margin: 0 0 var(--uix-space-3); font-weight: 600;">Integrations</h3>
<p style="margin: 0; color: var(--uix-text-muted);">Try variant='pills' with the indicator on — indicator IS the active background.</p>
</Tabs.Content>
</Tabs>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>click a tab to see select 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>active</span> {value}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API <span data-uix-tab-count>23</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>Props grouped by layer: <span data-uix-layer-badge="soma">soma</span> headless behavior, <span data-uix-layer-badge="eidos">eidos</span> visual treatment. Sema events live in the <a href="#sema" onclick={(e) => { e.preventDefault(); tab = 'sema'; }}>Sema</a> tab.</p>
<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>active value <span data-uix-control-hint>bindable</span></span>
<span data-uix-chips role="radiogroup">
{#each baseItems as item (item.value)}
<button data-uix-chip data-active={value === item.value} onclick={() => (value = item.value)}>{item.value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>orientation</span>
<span data-uix-chips role="radiogroup">
{#each ['horizontal', 'vertical'] as o}
<button data-uix-chip data-active={orientation === o} onclick={() => (orientation = o as Orientation)}>{o}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>activationMode</span>
<span data-uix-chips role="radiogroup">
{#each ['automatic', 'manual'] as a}
<button data-uix-chip data-active={activationMode === a} onclick={() => (activationMode = a as ActivationMode)}>{a}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>loop (keyboard wrap)</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>dir</span>
<span data-uix-chips role="radiogroup">
{#each ['ltr', 'rtl'] as d}
<button data-uix-chip data-active={dir === d} onclick={() => (dir = d as 'ltr' | 'rtl')}>{d}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>disabled (group)</span>
<span data-uix-switch><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label>{disabled ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>billing tab disabled</span>
<span data-uix-switch><input type="checkbox" bind:checked={billingDisabled} /><span data-uix-switch-label>{billingDisabled ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>List aria-label</span>
<input type="text" bind:value={listLabel} />
</label>
</div>
<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>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['line', 'surface', 'pills'] as v}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v as TabsVariant)}>{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'] as s}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as TabsSize)}>{s}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>fitted</span>
<span data-uix-switch><input type="checkbox" bind:checked={fitted} /><span data-uix-switch-label>{fitted ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>scrollable list</span>
<span data-uix-switch><input type="checkbox" bind:checked={scrollable} /><span data-uix-switch-label>{scrollable ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>collapseAt</span>
<span data-uix-chips role="radiogroup">
{#each ['never', 'sm', 'md', 'lg'] as c}
<button data-uix-chip data-active={collapseAt === c} onclick={() => (collapseAt = c as TabsCollapseAt)}>{c}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>motionPreset</span>
<span data-uix-chips role="radiogroup">
{#each ['none', 'fade', 'slide'] as m}
<button data-uix-chip data-active={motionPreset === m} onclick={() => (motionPreset = m as TabsMotionPreset)}>{m}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>render Indicator part</span>
<span data-uix-switch><input type="checkbox" bind:checked={showIndicator} /><span data-uix-switch-label>{showIndicator ? 'on' : 'off'}</span></span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · ARIA + behavior only</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 variant, size, fitted, scrollable, collapseAt, motionPreset, Indicator</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 (Tabs)</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>Active tab value.</td></tr>
<tr><td class="name">onValueChange / onFocusChange</td><td class="type">(value: string) =&gt; void</td><td class="default empty">—</td><td>Activation + focus callbacks.</td></tr>
<tr><td class="name">orientation</td><td class="type">'horizontal' | 'vertical'</td><td class="default">'horizontal'</td><td>Keyboard nav axis.</td></tr>
<tr><td class="name">activationMode</td><td class="type">'automatic' | 'manual'</td><td class="default">'automatic'</td><td>Activate on focus vs Enter/Space.</td></tr>
<tr><td class="name">loop</td><td class="type">boolean</td><td class="default">true</td><td>Wrap keyboard at boundaries.</td></tr>
<tr><td class="name">dir</td><td class="type">'ltr' | 'rtl'</td><td class="default empty">inherit</td><td>Keyboard direction.</td></tr>
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disable every trigger.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ResponsiveProp&lt;'xs'|'sm'|'md'|'lg'&gt;</td><td class="default">'md'</td><td>Trigger height + padding preset.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'line' | 'surface' | 'pills'</td><td class="default">'line'</td><td>Visual treatment.</td></tr>
<tr><td class="name">fitted <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">false</td><td>List fills width, equal-width triggers.</td></tr>
<tr><td class="name">scrollable <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">false</td><td>Horizontal scroll on overflow + edge fade.</td></tr>
<tr><td class="name">collapseAt <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'never'|Breakpoint|number</td><td class="default">'never'</td><td>Collapse vertical→horizontal below this width.</td></tr>
<tr><td class="name">motionPreset <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'none'|'fade'|'slide'</td><td class="default">'none'</td><td>Content panel transition.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>List / Trigger / Content / Indicator</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Prop</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">List</td><td class="name">aria-label / aria-labelledby</td><td>Required (one of). Tablist must have an accessible name.</td></tr>
<tr><td class="name">Trigger</td><td class="name">value (required), disabled</td><td>Identifies which Content this controls.</td></tr>
<tr><td class="name">Content</td><td class="name">value (required), forceMount</td><td>Identifies which Trigger this belongs to. Lazy-mounted by default.</td></tr>
<tr><td class="name">Indicator</td><td class="name">—</td><td>Animated active marker. Measures via MutationObserver + ResizeObserver.</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/tabs.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">"{tabsMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{tabsMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{tabsMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td><a href={tabsMorfo.apg} target="_blank" rel="noreferrer">{tabsMorfo.apg}</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{tabsMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{tabsMorfo.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 tabsMorfo.parts as rawPart}
{@const partAny = rawPart as unknown as { kebab: string; aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: 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</th></tr></thead>
<tbody>
{#each dataAttrs as attr}
<tr>
<td class="name">{attr.attr}</td>
<td class="type">{attr.values ? attr.values.join(' | ') : '—'}</td>
<td class="default">{'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if ariaAttrs.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>aria-attr</th><th>Source</th><th>Condition</th><th>Severity</th></tr></thead>
<tbody>
{#each ariaAttrs as a}
<tr>
<td class="name">{a.attr}</td>
<td class="type">{a.value.kind}</td>
<td class="default">{a.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}` : '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></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>
</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-select</code> — fires synchronously with the value mutation (sync-state + fire-and-forget sema, optimised for held-key arrow nav). Pack adds sub-audible cue (gain 0.04) + tap haptic.</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 : 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-tabs-trigger]') 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/tabs/tabs.css</code>. Three variants drive List + Trigger chrome; Indicator handles the animated active marker. ResizeObserver-driven <code>data-collapsed</code> for collapseAt. Honours <code>prefers-reduced-motion</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-tabs][data-orientation]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-tabs][data-size / data-variant / data-fitted / data-scrollable / data-collapsed]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-tabs-list][data-orientation]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-tabs-trigger][data-state]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-tabs-content][data-state] / [data-motion]</td><td><span data-uix-tag data-kind="soma">morfo + eidos</span></td></tr>
<tr><td class="name">[data-tabs-indicator]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">@media (prefers-reduced-motion: reduce)</td><td><span data-uix-tag data-kind="eidos">eidos · a11y</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>APG <a href={tabsMorfo.apg} target="_blank" rel="noreferrer">tabs pattern</a>. Roving tabindex, full keyboard nav, ARIA selection + disabled, List requires accessible name.</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>Tab</span></td><td>Focus enters / leaves the tablist.</td></tr>
<tr><td><span data-uix-kbd>← →</span> (horizontal)</td><td>Move focus between triggers. Activates immediately in automatic mode.</td></tr>
<tr><td><span data-uix-kbd>↑ ↓</span> (vertical)</td><td>Same as ← → for vertical orientation.</td></tr>
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>Move focus to first / last enabled trigger.</td></tr>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td>Activate the focused trigger (only needed in manual mode).</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">list</td><td>role / aria-orientation</td><td class="type">"tablist" / {`{orientation}`}</td></tr>
<tr><td class="name">trigger</td><td>role / aria-selected / aria-controls / tabindex</td><td class="type">"tab" / bool / id / 0|−1</td></tr>
<tr><td class="name">content</td><td>role / aria-labelledby / tabindex</td><td class="type">"tabpanel" / id / 0|−1</td></tr>
<tr><td class="name">indicator</td><td>aria-hidden</td><td class="type">"true"</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.