|
|
<script lang="ts">
|
|
|
import {
|
|
|
Splitter,
|
|
|
type SplitterProps
|
|
|
} from '$uix/eidos/components/splitter';
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
import { splitterMorfo } from '@/uix/morfo/components/splitter';
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
|
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 ───────────────────────────────────────────────────────
|
|
|
type Orientation = 'horizontal' | 'vertical';
|
|
|
const orientations: Orientation[] = ['horizontal', 'vertical'];
|
|
|
let orientation = $state<Orientation>('horizontal');
|
|
|
let disabled = $state(false);
|
|
|
let keyboardStep = $state<number>(1);
|
|
|
let collapsible = $state(false);
|
|
|
let panelACount = $state<number>(40);
|
|
|
let panelBCount = $state<number>(60);
|
|
|
|
|
|
const splitterProps = $derived<Partial<SplitterProps>>({
|
|
|
orientation,
|
|
|
disabled,
|
|
|
keyboardStep
|
|
|
});
|
|
|
|
|
|
// ── Compiled morfo ───────────────────────────────────────────────────
|
|
|
const compiled = compileMorfo(splitterMorfo);
|
|
|
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
|
|
const events = $derived([...compiled.actions.byName.values()]);
|
|
|
|
|
|
$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();
|
|
|
});
|
|
|
|
|
|
function fmtTime(at: number): string {
|
|
|
const d = new Date(at);
|
|
|
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
|
}
|
|
|
|
|
|
// ── Snippets ─────────────────────────────────────────────────────────
|
|
|
const somaSnippet = $derived(
|
|
|
[
|
|
|
"<script lang='ts'>",
|
|
|
" import * as Splitter from '$soma/components/splitter';",
|
|
|
'</' + 'script>',
|
|
|
'',
|
|
|
'<Splitter.Provider',
|
|
|
orientation !== 'horizontal' && ` orientation="${orientation}"`,
|
|
|
disabled && ' disabled',
|
|
|
keyboardStep !== 1 && ` keyboardStep={${keyboardStep}}`,
|
|
|
'>',
|
|
|
' <Splitter.Panel panelId="a" defaultSize={50} minSize={20}' +
|
|
|
(collapsible ? ' collapsible collapsedSize={0}' : '') +
|
|
|
'>',
|
|
|
' {a}',
|
|
|
' </Splitter.Panel>',
|
|
|
' <Splitter.ResizeTrigger panelBefore="a" panelAfter="b" />',
|
|
|
' <Splitter.Panel panelId="b" defaultSize={50} minSize={20}>',
|
|
|
' {b}',
|
|
|
' </Splitter.Panel>',
|
|
|
'</Splitter.Provider>'
|
|
|
]
|
|
|
.filter(Boolean)
|
|
|
.join('\n')
|
|
|
);
|
|
|
|
|
|
const eidosSnippet = $derived(
|
|
|
[
|
|
|
"<script lang='ts'>",
|
|
|
" import { Splitter } from '$uix/eidos/components/splitter';",
|
|
|
'</' + 'script>',
|
|
|
'',
|
|
|
'<Splitter',
|
|
|
orientation !== 'horizontal' && ` orientation="${orientation}"`,
|
|
|
disabled && ' disabled',
|
|
|
keyboardStep !== 1 && ` keyboardStep={${keyboardStep}}`,
|
|
|
'>',
|
|
|
' <Splitter.Panel panelId="a" defaultSize={50} minSize={20}' +
|
|
|
(collapsible ? ' collapsible collapsedSize={0}' : '') +
|
|
|
'>',
|
|
|
' {a}',
|
|
|
' </Splitter.Panel>',
|
|
|
' <Splitter.ResizeTrigger panelBefore="a" panelAfter="b" />',
|
|
|
' <Splitter.Panel panelId="b" defaultSize={50} minSize={20}>',
|
|
|
' {b}',
|
|
|
' </Splitter.Panel>',
|
|
|
'</Splitter>'
|
|
|
]
|
|
|
.filter(Boolean)
|
|
|
.join('\n')
|
|
|
);
|
|
|
</script>
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
<header>
|
|
|
<div data-uix-eyebrow>Layout · Splitter</div>
|
|
|
<h1 data-uix-page-title>Splitter</h1>
|
|
|
<p data-uix-page-lede>
|
|
|
Two-or-more resizable panels separated by a draggable handle. Headless soma owns the resize
|
|
|
math (percentages, min / max clamping, collapsed state, keyboard arrow steps). The morfo
|
|
|
declares a single semantic event — <code>commit-resize</code> in the <code>commit</code>
|
|
|
family — fired on drag-end so perceptual channels react to the settled value, not to every
|
|
|
pixel of the drag. Eidos paints chrome plus a pulse on the handle when the commit fires.
|
|
|
</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>orientation</span>{orientation}
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>scope</span>{splitterMorfo.scope.join(', ')}
|
|
|
</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<!-- Live preview always rendered -->
|
|
|
<div data-uix-stage>
|
|
|
<div data-uix-stage-area bind:this={stageRef}>
|
|
|
<div
|
|
|
style="inline-size: 100%; max-inline-size: 36rem; block-size: 18rem;"
|
|
|
>
|
|
|
<Splitter {...splitterProps} style="inline-size: 100%; block-size: 100%;">
|
|
|
<Splitter.Panel
|
|
|
panelId="a"
|
|
|
defaultSize={50}
|
|
|
minSize={20}
|
|
|
collapsible={collapsible}
|
|
|
collapsedSize={0}
|
|
|
>
|
|
|
<div
|
|
|
style="block-size: 100%; padding: var(--space-3); overflow: auto; color: var(--color-content-primary); background: var(--color-primary-track);"
|
|
|
>
|
|
|
<strong>Panel A</strong>
|
|
|
<p style="margin: var(--space-2) 0; color: var(--color-content-secondary); font-size: var(--font-size-sm);">
|
|
|
Drag the handle to resize, use arrow keys when focused. Home jumps to min, End
|
|
|
to max. {collapsible ? 'Enter toggles collapse.' : ''}
|
|
|
</p>
|
|
|
<ul style="margin: 0; padding-inline-start: var(--space-5);">
|
|
|
{#each Array.from({ length: panelACount }, (_, i) => i + 1) as i}
|
|
|
<li>Row A-{i}</li>
|
|
|
{/each}
|
|
|
</ul>
|
|
|
</div>
|
|
|
</Splitter.Panel>
|
|
|
<Splitter.ResizeTrigger panelBefore="a" panelAfter="b" />
|
|
|
<Splitter.Panel panelId="b" defaultSize={50} minSize={20}>
|
|
|
<div
|
|
|
style="block-size: 100%; padding: var(--space-3); overflow: auto; color: var(--color-content-primary); background: var(--color-affirm-track);"
|
|
|
>
|
|
|
<strong>Panel B</strong>
|
|
|
<p style="margin: var(--space-2) 0; color: var(--color-content-secondary); font-size: var(--font-size-sm);">
|
|
|
The handle paints in the primary color while dragging; on commit (mouse up) it
|
|
|
pulses for the sema signal-hold duration.
|
|
|
</p>
|
|
|
<ul style="margin: 0; padding-inline-start: var(--space-5);">
|
|
|
{#each Array.from({ length: panelBCount }, (_, i) => i + 1) as i}
|
|
|
<li>Row B-{i}</li>
|
|
|
{/each}
|
|
|
</ul>
|
|
|
</div>
|
|
|
</Splitter.Panel>
|
|
|
</Splitter>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div data-uix-stage-trace>
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
{#if trace.length === 0}
|
|
|
<span>drag the handle or hit ▶ play on the Sema tab to see the commit-resize event</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>orientation</span> {orientation} ·
|
|
|
<span data-uix-stage-trace-key>step</span> {keyboardStep}%
|
|
|
</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>3</span>
|
|
|
</button>
|
|
|
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
|
|
<span data-uix-layer-badge="morfo">morfo</span>
|
|
|
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
|
|
|
</button>
|
|
|
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
|
|
<span data-uix-layer-badge="sema">sema</span>
|
|
|
<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 every knob — the eidos layer adds zero evaluative props. Sema events are
|
|
|
surfaced in the Sema tab; the stage trace shows them too once a drag commits or the play
|
|
|
button fires.
|
|
|
</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>orientation</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each orientations as opt}
|
|
|
<button
|
|
|
data-uix-chip
|
|
|
data-active={orientation === opt}
|
|
|
onclick={() => (orientation = opt)}>{opt}</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
|
|
|
>keyboardStep <span data-uix-control-hint
|
|
|
>percent per arrow-key press</span
|
|
|
></span
|
|
|
>
|
|
|
<input type="range" min="1" max="10" step="1" bind:value={keyboardStep} />
|
|
|
<span data-uix-control-value>{keyboardStep}%</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label
|
|
|
>collapsible (panel A) <span data-uix-control-hint
|
|
|
>Enter toggles collapse to <code>collapsedSize=0</code></span
|
|
|
></span
|
|
|
>
|
|
|
<span data-uix-switch>
|
|
|
<input type="checkbox" bind:checked={collapsible} />
|
|
|
<span data-uix-switch-label>{collapsible ? 'on' : 'off'}</span>
|
|
|
</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Demo content</div>
|
|
|
<div data-uix-controls>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>panel A rows</span>
|
|
|
<input type="range" min="10" max="120" step="10" bind:value={panelACount} />
|
|
|
<span data-uix-control-value>{panelACount}</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>panel B rows</span>
|
|
|
<input type="range" min="10" max="120" step="10" bind:value={panelBCount} />
|
|
|
<span data-uix-control-value>{panelBCount}</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<!-- ── Code snippets per layer ─────────────────────────────────── -->
|
|
|
<div data-uix-code>
|
|
|
<div data-uix-code-head>
|
|
|
<span data-uix-layer-badge="soma">soma</span>
|
|
|
<span>headless · resize math + keyboard + collapse</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 · pure pass-through; recipe pulses on commit-resize</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>
|
|
|
Pass-through from soma — no evaluative additions on the eidos layer.
|
|
|
</p>
|
|
|
|
|
|
<div data-uix-subsection-head>Splitter (root)</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">orientation</td>
|
|
|
<td class="type">'horizontal' | 'vertical'</td>
|
|
|
<td class="default">'horizontal'</td>
|
|
|
<td>Layout direction.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">disabled</td>
|
|
|
<td class="type">boolean</td>
|
|
|
<td class="default">false</td>
|
|
|
<td>Disables all resize triggers.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">keyboardStep</td>
|
|
|
<td class="type">number</td>
|
|
|
<td class="default">1</td>
|
|
|
<td>Percentage of resize per arrow-key press.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">onResize</td>
|
|
|
<td class="type">(sizes: number[]) => void</td>
|
|
|
<td class="default empty">—</td>
|
|
|
<td>Live callback during drag (every frame).</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">onResizeEnd</td>
|
|
|
<td class="type">(sizes: number[]) => void</td>
|
|
|
<td class="default empty">—</td>
|
|
|
<td>Drag-end callback. Also fires <code>commit-resize</code> in sema.</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Splitter.Panel</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">panelId</td>
|
|
|
<td class="type">string</td>
|
|
|
<td class="default empty">—</td>
|
|
|
<td>Required. Links the panel with its surrounding resize triggers.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">defaultSize</td>
|
|
|
<td class="type">number (0–100)</td>
|
|
|
<td class="default empty">—</td>
|
|
|
<td>Required. Initial size as a percentage.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">minSize</td>
|
|
|
<td class="type">number</td>
|
|
|
<td class="default">0</td>
|
|
|
<td>Lower clamp on the panel's size.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">maxSize</td>
|
|
|
<td class="type">number</td>
|
|
|
<td class="default">100</td>
|
|
|
<td>Upper clamp on the panel's size.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">collapsible</td>
|
|
|
<td class="type">boolean</td>
|
|
|
<td class="default">false</td>
|
|
|
<td>Allows double-click and Enter to toggle collapse.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">collapsedSize</td>
|
|
|
<td class="type">number</td>
|
|
|
<td class="default">0</td>
|
|
|
<td>Size when collapsed (percentage).</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Splitter.ResizeTrigger</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">panelBefore</td>
|
|
|
<td class="type">string</td>
|
|
|
<td class="default empty">—</td>
|
|
|
<td>Required. Panel ID that grows when the handle is dragged forward.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">panelAfter</td>
|
|
|
<td class="type">string</td>
|
|
|
<td class="default empty">—</td>
|
|
|
<td>Required. Panel ID that shrinks when the handle is dragged forward.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">disabled</td>
|
|
|
<td class="type">boolean</td>
|
|
|
<td class="default">false</td>
|
|
|
<td>Disables this specific resize trigger.</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</th></tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr>
|
|
|
<td class="name">radix-themes</td>
|
|
|
<td><code><Splitter></code></td>
|
|
|
<td>Identical compound shape. UIX adds collapsible panels + sema commit event.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">ark-ui</td>
|
|
|
<td><code>Splitter</code> headless</td>
|
|
|
<td>Soma layer mirrors Ark UI's headless contract closely.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">react-resizable-panels</td>
|
|
|
<td><code><PanelGroup> / <Panel> / <PanelResizeHandle></code></td>
|
|
|
<td>
|
|
|
UIX wraps the equivalent shape with a more compact naming
|
|
|
(<code>Panel</code> / <code>ResizeTrigger</code>).
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'morfo'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>Morfo contract</h2>
|
|
|
<p data-uix-section-desc>
|
|
|
Source: <code>src/uix/morfo/components/splitter.ts</code>. Scope is
|
|
|
<code>['soma']</code> — the headless layer owns state. Three parts, one event.
|
|
|
</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>{splitterMorfo.name}</td></tr>
|
|
|
<tr><td class="name">kebab</td><td><code>{splitterMorfo.kebab}</code></td></tr>
|
|
|
<tr><td class="name">scope</td><td>{splitterMorfo.scope.join(', ')}</td></tr>
|
|
|
<tr><td class="name">parts.length</td><td>{partsList.length}</td></tr>
|
|
|
<tr><td class="name">events.length</td><td>{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>kebab</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"><{part.defaultElement}></td>
|
|
|
<td class="type">{part.role ?? '—'}</td>
|
|
|
<td class="type">{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 splitterMorfo.parts as rawPart}
|
|
|
{@const partAny = rawPart as unknown as {
|
|
|
kebab: string;
|
|
|
aria?: ReadonlyArray<{
|
|
|
attr: string;
|
|
|
value: { kind: string };
|
|
|
condition?: { when: string; prop?: string; part?: 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 kind</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 kind</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.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 k}
|
|
|
<tr>
|
|
|
<td><span data-uix-kbd>{k.key}</span></td>
|
|
|
<td>{k.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>
|
|
|
A single <code>commit-resize</code> event in the <code>commit</code> family. Sequence is
|
|
|
<em>post</em> — the perceptual signal lands AFTER the resize math has settled. Press ▶
|
|
|
play to emit the event on the live preview's resize trigger; the stage trace strip records
|
|
|
it and the recipe pulses the handle for the signal-hold duration.
|
|
|
</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 sem = action.semantic}
|
|
|
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
|
|
|
{@const effectiveIntent =
|
|
|
typeof intentDecl === 'string'
|
|
|
? intentDecl
|
|
|
: intentDecl
|
|
|
? undefined
|
|
|
: undefined}
|
|
|
<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">{typeof intentDecl === 'string' ? intentDecl : '—'}</td>
|
|
|
<td>
|
|
|
<button
|
|
|
data-uix-play
|
|
|
onclick={() => {
|
|
|
const target = (stageRef?.querySelector(
|
|
|
'[data-splitter-resize-trigger]'
|
|
|
) ?? stageRef?.querySelector('[data-splitter]')) 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>
|
|
|
Recipe lives in <code>src/uix/eidos/components/splitter/splitter.css</code>. The
|
|
|
<code>data-orientation</code> attribute drives the flex direction; the
|
|
|
<code>data-dragging</code> presence flag swaps the handle to the active color; the
|
|
|
<code>data-event='commit-resize'</code> hook pulses on commit.
|
|
|
</p>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr>
|
|
|
<td class="name"><code>[data-splitter]</code></td>
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
<td>Provider marker. Sets up the flex container.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name"
|
|
|
><code>[data-splitter][data-orientation='vertical']</code></td
|
|
|
>
|
|
|
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
|
|
<td>Swaps to <code>flex-direction: column</code>.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name"><code>[data-splitter-panel]</code></td>
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
<td>Panel marker. Soma sets the inline / block size via inline style.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name"><code>[data-splitter-resize-trigger]</code></td>
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
<td>Handle marker. Paints the drag affordance.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name"
|
|
|
><code>[data-splitter-resize-trigger][data-dragging]</code></td
|
|
|
>
|
|
|
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
|
|
<td>Active drag — paint in <code>--color-primary-solid</code>.</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name"
|
|
|
><code>[data-splitter-resize-trigger][data-event='commit-resize']</code></td
|
|
|
>
|
|
|
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
|
|
<td>
|
|
|
Sema hook — pulse the handle during the signal-hold (sequence:
|
|
|
<em>post</em>).
|
|
|
</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name"
|
|
|
><code>[data-splitter-resize-trigger]::before</code></td
|
|
|
>
|
|
|
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
|
|
<td>Center grip — small pill sized to match orientation.</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>
|
|
|
The resize trigger advertises <code>role="separator"</code> with
|
|
|
<code>aria-valuenow</code> / <code>aria-valuemin</code> / <code>aria-valuemax</code>
|
|
|
bound to the soma-tracked size, plus <code>aria-orientation</code> matching the
|
|
|
splitter's layout.
|
|
|
</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>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span></td><td>Resize by <code>keyboardStep</code> percent (horizontal).</td></tr>
|
|
|
<tr><td><span data-uix-kbd>ArrowUp</span> / <span data-uix-kbd>ArrowDown</span></td><td>Resize by <code>keyboardStep</code> percent (vertical).</td></tr>
|
|
|
<tr><td><span data-uix-kbd>Home</span></td><td>Jump to the minimum size.</td></tr>
|
|
|
<tr><td><span data-uix-kbd>End</span></td><td>Jump to the maximum size.</td></tr>
|
|
|
<tr><td><span data-uix-kbd>Enter</span></td><td>Toggle collapse (when <code>collapsible</code>).</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">resize-trigger</td><td>role</td><td class="type">'separator'</td></tr>
|
|
|
<tr><td class="name">resize-trigger</td><td>aria-orientation</td><td class="type">'horizontal' | 'vertical'</td></tr>
|
|
|
<tr><td class="name">resize-trigger</td><td>aria-valuenow</td><td class="type">current size %</td></tr>
|
|
|
<tr><td class="name">resize-trigger</td><td>aria-valuemin</td><td class="type">minSize</td></tr>
|
|
|
<tr><td class="name">resize-trigger</td><td>aria-valuemax</td><td class="type">maxSize</td></tr>
|
|
|
<tr><td class="name">resize-trigger</td><td>aria-label</td><td class="type">'Resize panels'</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Other concerns</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr>
|
|
|
<td class="name">Focus visible</td>
|
|
|
<td>
|
|
|
<code>:focus-visible</code> on the resize trigger surfaces the canonical UIX
|
|
|
focus ring.
|
|
|
</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">Pointer / touch</td>
|
|
|
<td>
|
|
|
Cursor flips to <code>col-resize</code> / <code>row-resize</code> on the
|
|
|
handle. Soma owns pointer-down / pointer-move / pointer-up.
|
|
|
</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">Reduced motion</td>
|
|
|
<td>
|
|
|
The handle's hover / drag transition uses <code>--duration-fast</code>; the
|
|
|
commit pulse honours the sema visual channel's reduced-motion override.
|
|
|
</td>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<td class="name">RTL</td>
|
|
|
<td>
|
|
|
Soma's <code>dir</code> prop swaps the direction of <code>ArrowLeft</code> /
|
|
|
<code>ArrowRight</code> in horizontal splitters so the user always
|
|
|
"grows-forward" with the trailing-edge key.
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
</div>
|