|
|
<script lang="ts">
|
|
|
import {
|
|
|
SplitButton,
|
|
|
type SplitButtonVariant,
|
|
|
type SplitButtonSize,
|
|
|
type SplitButtonColor
|
|
|
} from '$uix/eidos/components/split-button';
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
import { splitButtonMorfo } from '@/uix/morfo/components/split-button';
|
|
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
|
type TraceEntry = { event: string; family: string; intent?: string; at: number };
|
|
|
let tab = $state<Tab>('live');
|
|
|
let trace = $state<TraceEntry[]>([]);
|
|
|
let stageRef = $state<HTMLElement | null>(null);
|
|
|
|
|
|
// ── Live state — every public prop is a control ────────────────────────
|
|
|
let variant = $state<SplitButtonVariant>('solid');
|
|
|
let size = $state<SplitButtonSize>('md');
|
|
|
let color = $state<SplitButtonColor>('primary');
|
|
|
let disabled = $state(false);
|
|
|
let open = $state(false);
|
|
|
let ariaLabel = $state('Save options');
|
|
|
let primaryLabel = $state('Save');
|
|
|
let lastAction = $state<string>('—');
|
|
|
// Menu (float) props
|
|
|
let matchAnchorWidth = $state(true);
|
|
|
let menuOffsetAuto = $state(true);
|
|
|
let menuSideOffset = $state(8);
|
|
|
|
|
|
const variants = ['solid', 'soft', 'surface', 'outline', 'ghost', 'plain'] as const;
|
|
|
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
|
|
const colors = ['primary', 'secondary', 'neutral'] as const;
|
|
|
|
|
|
// ── Compiled morfo ─────────────────────────────────────────────────────
|
|
|
const compiled = compileMorfo(splitButtonMorfo);
|
|
|
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')}`;
|
|
|
}
|
|
|
|
|
|
$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();
|
|
|
});
|
|
|
|
|
|
// ── Code snippet (eidos-native — no soma layer) ────────────────────────
|
|
|
const eidosSnippet = $derived(
|
|
|
[
|
|
|
"<script lang='ts'>",
|
|
|
" import { SplitButton } from '$uix/eidos/components/split-button';",
|
|
|
' let open = $state(false);',
|
|
|
'</' + 'script>',
|
|
|
'',
|
|
|
'<SplitButton',
|
|
|
' bind:open',
|
|
|
variant !== 'solid' && ` variant="${variant}"`,
|
|
|
size !== 'md' && ` size="${size}"`,
|
|
|
color !== 'primary' && ` color="${color}"`,
|
|
|
disabled && ' disabled',
|
|
|
` aria-label="${ariaLabel || 'Actions'}"`,
|
|
|
'>',
|
|
|
` <SplitButton.Primary onclick={save}>${primaryLabel}</SplitButton.Primary>`,
|
|
|
' <SplitButton.Menu' +
|
|
|
(!matchAnchorWidth ? ' matchAnchorWidth={false}' : '') +
|
|
|
(!menuOffsetAuto ? ` sideOffset={${menuSideOffset}}` : '') +
|
|
|
'>',
|
|
|
' <SplitButton.Item onSelect={saveAs}>Save as…</SplitButton.Item>',
|
|
|
' <SplitButton.Item onSelect={saveCopy}>Save a copy</SplitButton.Item>',
|
|
|
' <SplitButton.Separator />',
|
|
|
' <SplitButton.Item data-color="threat" onSelect={discard}>Discard</SplitButton.Item>',
|
|
|
' </SplitButton.Menu>',
|
|
|
'</SplitButton>'
|
|
|
]
|
|
|
.filter(Boolean)
|
|
|
.join('\n')
|
|
|
);
|
|
|
</script>
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
<header>
|
|
|
<div data-uix-eyebrow>Controls · SplitButton</div>
|
|
|
<h1 data-uix-page-title>SplitButton</h1>
|
|
|
<p data-uix-page-lede>
|
|
|
A primary action joined to a ▾ menu trigger — <code>Save | ▾</code> → secondary actions.
|
|
|
Pure composition: <a href="/uix/components/button-group"><ButtonGroup attached></a> +
|
|
|
<a href="/uix/components/button"><Button></a> +
|
|
|
<a href="/uix/components/dropdown-menu"><DropdownMenu></a>. The ▾ trigger is
|
|
|
auto-rendered and inherits the group's variant / size / color; only the ▾ opens the menu.
|
|
|
0 own events — interactive by composition.
|
|
|
</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>0
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>composes</span>ButtonGroup · Button · DropdownMenu
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>scope</span>eidos
|
|
|
</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<!-- Live preview always rendered -->
|
|
|
<div data-uix-stage>
|
|
|
<div data-uix-stage-area bind:this={stageRef}>
|
|
|
<SplitButton
|
|
|
{variant}
|
|
|
{size}
|
|
|
{color}
|
|
|
{disabled}
|
|
|
bind:open
|
|
|
aria-label={ariaLabel || undefined}
|
|
|
>
|
|
|
<SplitButton.Primary onclick={() => (lastAction = 'primary: Save')}>
|
|
|
{primaryLabel}
|
|
|
</SplitButton.Primary>
|
|
|
<SplitButton.Menu
|
|
|
{matchAnchorWidth}
|
|
|
sideOffset={menuOffsetAuto ? undefined : menuSideOffset}
|
|
|
>
|
|
|
<SplitButton.Item onSelect={() => (lastAction = 'Save as…')}>Save as…</SplitButton.Item>
|
|
|
<SplitButton.Item onSelect={() => (lastAction = 'Save a copy')}
|
|
|
>Save a copy</SplitButton.Item
|
|
|
>
|
|
|
<SplitButton.Separator />
|
|
|
<SplitButton.Item data-color="threat" onSelect={() => (lastAction = 'Discard')}
|
|
|
>Discard</SplitButton.Item
|
|
|
>
|
|
|
</SplitButton.Menu>
|
|
|
</SplitButton>
|
|
|
</div>
|
|
|
<div data-uix-stage-trace>
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
{#if trace.length === 0}
|
|
|
<span>click Save, open the ▾ menu, or pick an item</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>menu</span>
|
|
|
{open ? 'open' : 'closed'} · <span data-uix-stage-trace-key>last</span>
|
|
|
{lastAction}
|
|
|
</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>7</span>
|
|
|
</button>
|
|
|
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
|
|
<span data-uix-layer-badge="morfo">morfo</span>
|
|
|
<span data-uix-tab-count>{partsList.length}p · 0e</span>
|
|
|
</button>
|
|
|
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
|
|
<span data-uix-layer-badge="sema">sema</span>
|
|
|
<span data-uix-tab-count>{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>
|
|
|
SplitButton is eidos-only — no <span data-uix-layer-badge="soma">soma</span> split. The
|
|
|
visual props propagate to BOTH the primary and the ▾ trigger via the ButtonGroup context.
|
|
|
</p>
|
|
|
|
|
|
<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 data-uix-control-hint>both buttons</span></span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each variants as v}
|
|
|
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v)}>{v}</button
|
|
|
>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>size</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each sizes as s}
|
|
|
<button data-uix-chip data-active={size === s} onclick={() => (size = s)}>{s}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>color</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each colors as c}
|
|
|
<button data-uix-chip data-active={color === c} onclick={() => (color = c)}>{c}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>disabled</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>aria-label <span data-uix-control-hint>group name</span></span>
|
|
|
<input type="text" bind:value={ariaLabel} />
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
<span data-uix-layer-badge="eidos">eidos</span> props · menu (float) ·
|
|
|
<code><SplitButton.Menu></code>
|
|
|
</div>
|
|
|
<div data-uix-controls>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>
|
|
|
matchAnchorWidth <span data-uix-control-hint>≥ surface width</span>
|
|
|
</span>
|
|
|
<span data-uix-switch>
|
|
|
<input type="checkbox" bind:checked={matchAnchorWidth} />
|
|
|
<span data-uix-switch-label>{matchAnchorWidth ? 'on' : 'off'}</span>
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>
|
|
|
sideOffset <span data-uix-control-hint>
|
|
|
{menuOffsetAuto ? 'canonical (--space-* per size)' : menuSideOffset + 'px gap'}
|
|
|
</span>
|
|
|
</span>
|
|
|
<span data-uix-switch>
|
|
|
<input type="checkbox" bind:checked={menuOffsetAuto} />
|
|
|
<span data-uix-switch-label>{menuOffsetAuto ? 'auto' : 'manual'}</span>
|
|
|
</span>
|
|
|
</label>
|
|
|
{#if !menuOffsetAuto}
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>
|
|
|
sideOffset px <span data-uix-control-hint>override</span>
|
|
|
</span>
|
|
|
<input type="range" min="0" max="24" step="1" bind:value={menuSideOffset} />
|
|
|
</label>
|
|
|
{/if}
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Demo content <span style="font-weight: normal; color: var(--uix-text-muted);">· primary label</span></div>
|
|
|
<div data-uix-controls>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>primary label</span>
|
|
|
<input type="text" bind:value={primaryLabel} />
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-code>
|
|
|
<div data-uix-code-head>
|
|
|
<span data-uix-layer-badge="eidos">eidos</span>
|
|
|
<span>visual · composes ButtonGroup + Button + DropdownMenu</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>
|
|
|
Root props propagate to both buttons; DropdownMenu provider props (<code>open</code>,
|
|
|
<code>modal</code>, <code>onOpenChange</code>…) pass through. The menu surface
|
|
|
(<code>Menu</code> / <code>Item</code> / <code>Separator</code> / <code>Group</code> /
|
|
|
<code>CheckboxItem</code> / <code>RadioGroup</code> / <code>Sub</code>…) is re-exported from
|
|
|
<a href="/uix/components/dropdown-menu">DropdownMenu</a>.
|
|
|
</p>
|
|
|
<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>Menu open state.</td></tr>
|
|
|
<tr><td class="name">variant</td><td class="type">'solid' | 'soft' | 'surface' | 'outline' | 'ghost' | 'plain'</td><td class="default">'solid'</td><td>Propagated to both buttons.</td></tr>
|
|
|
<tr><td class="name">size</td><td class="type">ResponsiveProp<'xs' | 'sm' | 'md' | 'lg' | 'xl'></td><td class="default">'md'</td><td>Propagated to both buttons.</td></tr>
|
|
|
<tr><td class="name">color</td><td class="type">'primary' | 'secondary' | 'neutral'</td><td class="default">'primary'</td><td>Propagated to both buttons.</td></tr>
|
|
|
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disables both buttons.</td></tr>
|
|
|
<tr><td class="name">icon</td><td class="type">Snippet</td><td class="default empty">— (chevron)</td><td>Override the ▾ glyph. The default chevron rotates while open.</td></tr>
|
|
|
<tr><td class="name">menuLabel</td><td class="type">string</td><td class="default">'More actions'</td><td>Accessible name for the ▾ trigger.</td></tr>
|
|
|
<tr><td class="name">aria-label</td><td class="type">string</td><td class="default empty">—</td><td>Accessible name for the split group.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
<code><SplitButton.Menu></code> props <span style="font-weight: normal; color: var(--uix-text-muted);">· the float</span>
|
|
|
</div>
|
|
|
<p data-uix-section-desc>
|
|
|
The menu IS a <a href="/uix/components/dropdown-menu">DropdownMenu.Content</a> with
|
|
|
split-button-aware defaults: it anchors to the whole group, matches its width, and sits a
|
|
|
gap below it. Every Content positioning prop (<code>side</code>, <code>align</code>,
|
|
|
<code>collisionPadding</code>…) still passes through.
|
|
|
</p>
|
|
|
<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">matchAnchorWidth</td><td class="type">ResponsiveProp<boolean></td><td class="default">true</td><td>Min width tracks the split surface (the anchor width); content can grow it.</td></tr>
|
|
|
<tr><td class="name">sideOffset</td><td class="type">number</td><td class="default">canonical</td><td>Gap below the button. Unset → resolved from the size's <code>--space-*</code> step (density × scaling aware); pass a number to override.</td></tr>
|
|
|
<tr><td class="name">size</td><td class="type">ResponsiveProp<'xs'…'xl'></td><td class="default">split's size</td><td>Forwarded to <a href="/uix/components/dropdown-menu">DropdownMenu.Content's <code>size</code></a> (1:1 scale); defaults to the split's size, override to decouple.</td></tr>
|
|
|
<tr><td class="name">align</td><td class="type">'start' | 'center' | 'end'</td><td class="default">'start'</td><td>Edge of the surface the menu aligns to.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Reference comparison</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Library</th><th>Closest equivalent</th><th>Difference vs UIX</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">fluent-ui</td><td><code><SplitButton></code></td><td>Same model (primary + menu). UIX shares variant/size/color to both via the ButtonGroup context automatically.</td></tr>
|
|
|
<tr><td class="name">ant-design</td><td><code><Dropdown.Button></code></td><td>Same split. UIX's menu is the full DropdownMenu (groups/checkbox/radio/submenus), not a flat list.</td></tr>
|
|
|
<tr><td class="name">mui</td><td>ButtonGroup + Menu recipe</td><td>MUI documents it as a recipe; UIX ships it as a component with the chevron-rotates affordance.</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>
|
|
|
scope-eidos — no soma runtime. The morfo declares the split-specific contract: the
|
|
|
<code>role="group"</code> provider, the <code>data-state</code> that rotates the chevron,
|
|
|
and the Primary / Trigger parts (structural identities over the composed Button /
|
|
|
DropdownMenu.Trigger). Source: <code>src/uix/morfo/components/split-button.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">"{splitButtonMorfo.name}"</td></tr>
|
|
|
<tr><td class="name">kebab</td><td class="type">"{splitButtonMorfo.kebab}"</td></tr>
|
|
|
<tr><td class="name">scope</td><td class="type">[{splitButtonMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
|
|
|
<tr><td class="name">apg</td><td><a href={splitButtonMorfo.apg} target="_blank" rel="noreferrer">menu-button ↗</a></td></tr>
|
|
|
<tr><td class="name">parts.length</td><td class="default">{splitButtonMorfo.parts.length}</td></tr>
|
|
|
<tr><td class="name">events.length</td><td class="default">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></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"><{part.defaultElement}></td>
|
|
|
<td class="type">{part.role ?? '—'}</td>
|
|
|
<td class="default">{part.archetype ?? '—'}</td>
|
|
|
<td class="default">{part.states.length ? part.states.join(' | ') : '—'}</td>
|
|
|
</tr>
|
|
|
{/each}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
{#each splitButtonMorfo.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;
|
|
|
}>;
|
|
|
}}
|
|
|
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
|
|
|
{@const ariaAttrs = partAny.aria ?? []}
|
|
|
{#if dataAttrs.length || ariaAttrs.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}
|
|
|
{/each}
|
|
|
</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
|
|
|
</h2>
|
|
|
<p data-uix-section-desc>
|
|
|
SplitButton declares no events of its own — it is interactive <strong>by composition</strong>.
|
|
|
The primary fires <code>contact-activate</code> (Button), the ▾ fires
|
|
|
<code>emerge.open</code>/<code>close</code> (DropdownMenu), and each item fires
|
|
|
<code>commit-select</code> (DropdownMenu). Exercise the stage and watch the trace strip.
|
|
|
</p>
|
|
|
<div data-uix-empty-state>No Sema events declared.</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'recipe'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
|
<p data-uix-section-desc>
|
|
|
Layout + the segmented seam come from <a href="/uix/components/button-group"><ButtonGroup attached></a>;
|
|
|
Button + DropdownMenu paint the buttons. SplitButton's own CSS at
|
|
|
<code>src/uix/eidos/components/split-button/split-button.css</code> owns the
|
|
|
split-specific affordances: the ▾ rotates open and stays a slim tail, and the menu
|
|
|
tracks the surface width, scales with <code>size</code>, and sits a canonical gap below.
|
|
|
</p>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Selector</th><th>Source</th><th>Purpose</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name"><code>[data-split-button][data-state='open']</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Group open state (stamped from `open`).</td></tr>
|
|
|
<tr><td class="name"><code>… [data-split-button-trigger] [data-svg='chevron']</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Rotate the ▾ chevron 180° while the menu is open.</td></tr>
|
|
|
<tr><td class="name"><code>[data-split-button-trigger][data-icon-only]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Slim the ▾ to glyph + snug padding (not a square).</td></tr>
|
|
|
<tr><td class="name"><code>[data-dropdown-menu-content][data-split-button-menu]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td><code>min-inline-size</code> = the anchor (surface) width; content grows it.</td></tr>
|
|
|
<tr><td class="name"><code>[data-split-button-menu][data-size='…']</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Gap step per size (font / height / padding scale come from <a href="/uix/components/dropdown-menu">DropdownMenu's own size axis</a>).</td></tr>
|
|
|
<tr><td class="name"><code>[data-split-button-menu][data-canonical-gap][data-side='…']</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Canonical separation: a <code>--space-*</code> margin on the placed side.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'a11y'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>Accessibility</h2>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">Group</td><td><code>role="group"</code> (from ButtonGroup) + <code>aria-label</code> — names the split as a unit.</td></tr>
|
|
|
<tr><td class="name">Primary</td><td>A native <code><button></code> (from Button) — its own action, independently Tab-focusable.</td></tr>
|
|
|
<tr><td class="name">Menu trigger</td><td>Follows the APG <a href={splitButtonMorfo.apg} target="_blank" rel="noreferrer">Menu Button</a> (from DropdownMenu): <code>aria-haspopup="menu"</code> + <code>aria-expanded</code>, opens with Enter/Space/ArrowDown, names itself via <code>menuLabel</code>.</td></tr>
|
|
|
<tr><td class="name">Menu</td><td><code>role="menu"</code> with arrow-key navigation, typeahead, Escape-to-close, focus return to the ▾ (all from DropdownMenu).</td></tr>
|
|
|
<tr><td class="name">Keyboard</td><td><span data-uix-kbd>Tab</span> moves between the primary and the ▾ (no roving); each opens / activates with its native keys.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
</div>
|