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

575 lines
25 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 {
RadioGroup,
type RadioGroupSize,
type RadioGroupVariant,
type RadioGroupColor,
type RadioGroupIconSnippetArgs
} from '$uix/eidos/components/radio-group';
import type { Orientation } from '$soma/types';
import { compileMorfo } from '$uix/morfo';
import { radioGroupMorfo } from '@/uix/morfo/components/radio-group';
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');
let value = $state('pro');
let disabled = $state(params.get('perm.disabled') === 'true');
let readonly = $state(false);
let required = $state(false);
let invalid = $state(params.get('perm.invalid') === 'true');
let size = $state<RadioGroupSize>('md');
let variant = $state<RadioGroupVariant>('solid');
let color = $state<RadioGroupColor>('primary');
let orientation = $state<Orientation>('vertical');
let columns = $state<number | undefined>(undefined);
let loop = $state(false);
let dir = $state<'ltr' | 'rtl'>((params.get('perm.dir') as 'ltr' | 'rtl' | null) ?? 'ltr');
let nameAttr = $state('plan');
let labelText = $state('Choose a plan');
let useCustomIcon = $state(false);
let proItemDisabled = $state(false);
let lastFormSubmit = $state<Record<string, unknown> | null>(null);
const items = $derived([
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro', disabled: proItemDisabled },
{ value: 'team', label: 'Team' },
{ value: 'enterprise', label: 'Enterprise' }
]);
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(radioGroupMorfo);
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')}`;
}
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
const data = new FormData(e.currentTarget as HTMLFormElement);
const obj: Record<string, unknown> = {};
for (const [k, v] of data.entries()) obj[k] = v;
lastFormSubmit = obj;
}
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import { RadioGroup } from '$soma/components/radio-group';",
' let value = $state("pro");',
'</' + 'script>',
'',
'<RadioGroup.Provider',
' bind:value',
disabled && ' disabled',
readonly && ' readonly',
required && ' required',
invalid && ' invalid',
orientation !== 'vertical' && ` orientation="${orientation}"`,
loop && ' loop',
dir !== 'ltr' && ` dir="${dir}"`,
nameAttr && ` name="${nameAttr}"`,
'>',
' <RadioGroup.Label>Choose a plan</RadioGroup.Label>',
' <RadioGroup.Item value="free">',
' <RadioGroup.Indicator />',
nameAttr && ' <RadioGroup.HiddenInput />',
' </RadioGroup.Item>',
' ...',
'</RadioGroup.Provider>'
].filter(Boolean).join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { RadioGroup } from '$uix/eidos/components/radio-group';",
' let value = $state("pro");',
'</' + 'script>',
'',
'<RadioGroup',
' bind:value',
variant !== 'solid' && ` variant="${variant}"`,
size !== 'md' && ` size="${size}"`,
color !== 'primary' && ` color="${color}"`,
disabled && ' disabled',
invalid && ' invalid',
orientation !== 'vertical' && ` orientation="${orientation}"`,
columns !== undefined && ` columns={${columns}}`,
loop && ' loop',
nameAttr && ` name="${nameAttr}"`,
'>',
' <RadioGroup.Label>Choose a plan</RadioGroup.Label>',
' <RadioGroup.Item value="free">',
' <RadioGroup.Indicator />',
nameAttr && ' <RadioGroup.HiddenInput />',
' </RadioGroup.Item>',
' ...',
'</RadioGroup>'
].filter(Boolean).join('\n')
);
</script>
<svelte:head>
<meta name="perm-axes" content="dir,disabled,invalid" />
</svelte:head>
{#snippet customIcon({ checked: c }: RadioGroupIconSnippetArgs)}
{#if c}
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2l2.39 6.95H22l-6.31 4.58L18.07 21 12 16.55 5.93 21l2.38-7.47L2 8.95h7.61L12 2z" />
</svg>
{/if}
{/snippet}
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Controls · Radio group</div>
<h1 data-uix-page-title>Radio group</h1>
<p data-uix-page-lede>
Single-select group with full APG semantics, native form integration via <code>HiddenInput</code>, three variants, doctrinal color subset (no risk/threat/loss; use <code>invalid</code>), <code>commit-select</code> sema event with intent <code>affirm</code>.
</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>colors</span>4</span>
<span data-uix-meta-pill><span data-uix-meta-key>apg</span><a href={radioGroupMorfo.apg} target="_blank" rel="noreferrer">radio ↗</a></span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<form onsubmit={handleSubmit} style="display: flex; flex-direction: column; gap: 1rem;">
<RadioGroup
bind:value
{disabled}
{readonly}
{required}
{invalid}
{orientation}
{columns}
{loop}
{dir}
{size}
{variant}
{color}
name={nameAttr || undefined}
>
<RadioGroup.Label>{labelText}</RadioGroup.Label>
{#each items as item (item.value)}
<label data-radio-group-row data-disabled={item.disabled ? '' : undefined}>
<RadioGroup.Item
value={item.value}
disabled={item.disabled}
data-perm-step={!disabled && item.value === 'team' ? '0' : undefined}
data-perm-label={!disabled && item.value === 'team' ? 'select team plan' : undefined}
>
<RadioGroup.Indicator icon={useCustomIcon ? customIcon : undefined} />
{#if nameAttr}<RadioGroup.HiddenInput />{/if}
</RadioGroup.Item>
<span data-radio-group-row-label>{item.label}</span>
</label>
{/each}
</RadioGroup>
<button type="submit" data-uix-play>Submit form</button>
</form>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>select an option to see commit 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>value</span> {value || '∅'}
</span>
</div>
{#if lastFormSubmit !== null}
<div data-uix-stage-trace style="margin-top: 0.5rem;">
<span data-uix-stage-trace-key>FormData</span>
<code style="font-size: 0.85em;">{JSON.stringify(lastFormSubmit)}</code>
</div>
{/if}
</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>20</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 + form, <span data-uix-layer-badge="eidos">eidos</span> visual treatment.</p>
<div data-uix-subsection-head><span data-uix-layer-badge="soma">soma</span> props · headless behavior + form</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>value <span data-uix-control-hint>bindable</span></span>
<span data-uix-chips role="radiogroup">
{#each items 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>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>readonly</span>
<span data-uix-switch><input type="checkbox" bind:checked={readonly} /><span data-uix-switch-label>{readonly ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>required</span>
<span data-uix-switch><input type="checkbox" bind:checked={required} /><span data-uix-switch-label>{required ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>invalid</span>
<span data-uix-switch><input type="checkbox" bind:checked={invalid} /><span data-uix-switch-label>{invalid ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>"Pro" item disabled</span>
<span data-uix-switch><input type="checkbox" bind:checked={proItemDisabled} /><span data-uix-switch-label>{proItemDisabled ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>orientation</span>
<span data-uix-chips role="radiogroup">
{#each ['vertical', 'horizontal'] 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>loop</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>name</span>
<input type="text" bind:value={nameAttr} placeholder="(no HiddenInput)" />
</label>
<label data-uix-control>
<span data-uix-control-label>group label</span>
<input type="text" bind:value={labelText} />
</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 ['solid', 'outline', 'ghost'] as v}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v as RadioGroupVariant)}>{v}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['sm', 'md', 'lg'] as s}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as RadioGroupSize)}>{s}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each ['primary', 'secondary', 'neutral', 'affirm'] as c}
<button data-uix-chip data-active={color === c} onclick={() => (color = c as RadioGroupColor)}>{c}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>columns</span>
<span data-uix-chips role="radiogroup">
{#each [undefined, 2, 3, 4] as c}
<button data-uix-chip data-active={columns === c} onclick={() => (columns = c)}>{c === undefined ? 'flex' : c + ' cols'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>custom icon snippet</span>
<span data-uix-switch><input type="checkbox" bind:checked={useCustomIcon} /><span data-uix-switch-label>{useCustomIcon ? 'star' : 'default •'}</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, color, columns, custom icon</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-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>Selected value.</td></tr>
<tr><td class="name">disabled / readonly / required / invalid</td><td class="type">boolean</td><td class="default">false</td><td>Field-integration flags.</td></tr>
<tr><td class="name">orientation / loop / dir</td><td class="type">'vertical'|'horizontal' / boolean / 'ltr'|'rtl'</td><td class="default">'vertical' / false / inherit</td><td>Keyboard nav config.</td></tr>
<tr><td class="name">name</td><td class="type">string</td><td class="default empty">—</td><td>Form field name (auto-renders HiddenInput per item).</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>Box + dot + font preset.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'solid'|'outline'|'ghost'</td><td class="default">'solid'</td><td>Visual treatment.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'primary'|'secondary'|'neutral'|'affirm'</td><td class="default">'primary'</td><td>Palette subset.</td></tr>
<tr><td class="name">columns <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">number</td><td class="default empty">—</td><td>CSS grid columns.</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/radio-group.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">"{radioGroupMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{radioGroupMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{radioGroupMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td><a href={radioGroupMorfo.apg} target="_blank" rel="noreferrer">{radioGroupMorfo.apg}</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{radioGroupMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{radioGroupMorfo.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 radioGroupMorfo.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. Pack adds sub-audible cue (gain 0.05) + tap haptic. Selecting is always affirm — there's no unselect.</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-radio-group-item]') 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/radio-group/radio-group.css</code>. Indicator uses SvgDot with <code>scale(0)→scale(1)</code>. 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-radio-group][data-orientation]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-radio-group][data-disabled / readonly / invalid]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-radio-group][data-size / data-variant / data-color / data-columns]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-radio-group-item][data-state]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-radio-group-indicator] svg[data-svg='dot']</td><td><span data-uix-tag data-kind="eidos">eidos · svg primitives</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={radioGroupMorfo.apg} target="_blank" rel="noreferrer">radio pattern</a>. Roving tabindex. Auto-select on focus (canonical radio behaviour). Group 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 group at the checked (or first enabled) item.</td></tr>
<tr><td><span data-uix-kbd>↓</span> / <span data-uix-kbd>→</span></td><td>Move focus + select next enabled item.</td></tr>
<tr><td><span data-uix-kbd>↑</span> / <span data-uix-kbd>←</span></td><td>Move focus + select previous enabled item.</td></tr>
<tr><td><span data-uix-kbd>Space</span></td><td>Select focused item.</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">provider</td><td>role / aria-orientation / aria-required / aria-invalid / aria-labelledby</td><td class="type">"radiogroup" / vary</td></tr>
<tr><td class="name">item</td><td>role / aria-checked / tabindex</td><td class="type">"radio" / bool / 0|−1</td></tr>
<tr><td class="name">indicator / hidden-input</td><td>aria-hidden</td><td class="type">"true"</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.