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

621 lines
23 KiB

<script lang="ts">
import {
Separator,
type SeparatorOrientation,
type SeparatorProps
} from '$uix/eidos/components/separator';
import { compileMorfo } from '$uix/morfo';
import { separatorMorfo } from '@/uix/morfo/components/separator';
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 ───────────────────────────────────────────────────────
const orientations: SeparatorOrientation[] = ['horizontal', 'vertical'];
let orientation = $state<SeparatorOrientation>('horizontal');
let decorative = $state<boolean>(false);
type Demo = 'stacked' | 'toolbar' | 'menu';
const demoOptions = ['stacked', 'toolbar', 'menu'] as const;
let demo = $state<Demo>('stacked');
let thickness = $state<number>(1);
type ColorOption = 'default' | 'subtle' | 'strong';
const colorOptions = ['default', 'subtle', 'strong'] as const;
let color = $state<ColorOption>('default');
const separatorProps = $derived<Partial<SeparatorProps>>({
orientation,
decorative
});
const colorVar = $derived(
color === 'default'
? 'var(--color-border-default)'
: color === 'subtle'
? 'var(--color-border-subtle)'
: 'var(--color-border-strong)'
);
// ── Compiled morfo ───────────────────────────────────────────────────
const compiled = compileMorfo(separatorMorfo);
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();
});
// ── Code snippet ──────────────────────────────────────────────────────
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Separator } from '$uix/eidos/components/separator';",
'</' + 'script>',
'',
'<Separator',
orientation !== 'horizontal' && ` orientation="${orientation}"`,
decorative && ' decorative',
'/>'
]
.filter(Boolean)
.join('\n')
);
const styleOverride = $derived(
[
`--separator-thickness: ${thickness}px;`,
`--separator-color: ${colorVar};`
].join(' ')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Separator</div>
<h1 data-uix-page-title>Separator</h1>
<p data-uix-page-lede>
Visual divider between sections — a thin rule painted in the muted border color. The
<code>orientation</code> prop drives the geometry; <code>decorative</code> toggles between a
semantic
<code>role="separator"</code> + <code>aria-orientation</code> and a purely visual
<code>aria-hidden</code>. Eidos-native primitive: no soma backing, no semantic events. Logical
sizing (<code>inline-size</code> / <code>block-size</code>) so the rule mirrors automatically
in RTL.
</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>scope</span>eidos
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>role</span>{decorative ? '— (decorative)' : 'separator'}
</span>
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
{#if demo === 'stacked'}
<div
style="display: flex; flex-direction: column; gap: var(--space-3); inline-size: 100%; max-inline-size: 32rem; padding: var(--space-4); border-radius: var(--radius-md); border: 1px dashed var(--color-border-default);"
>
<div style="color: var(--color-content-primary); font-weight: 600;">Account</div>
<div style="color: var(--color-content-secondary); font-size: var(--font-size-sm);">
Personal information and password.
</div>
<Separator {...separatorProps} style={styleOverride} />
<div style="color: var(--color-content-primary); font-weight: 600;">Notifications</div>
<div style="color: var(--color-content-secondary); font-size: var(--font-size-sm);">
Email, SMS, and in-app delivery preferences.
</div>
<Separator {...separatorProps} style={styleOverride} />
<div style="color: var(--color-content-primary); font-weight: 600;">Danger zone</div>
<div style="color: var(--color-content-secondary); font-size: var(--font-size-sm);">
Export data, delete account.
</div>
</div>
{:else if demo === 'toolbar'}
<div
style="display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); border: 1px dashed var(--color-border-default); background: var(--color-surface-default);"
>
<button style="padding: 0 var(--space-2); background: transparent; border: 0; cursor: pointer;">Bold</button>
<button style="padding: 0 var(--space-2); background: transparent; border: 0; cursor: pointer;">Italic</button>
<Separator orientation="vertical" {decorative} style={styleOverride} />
<button style="padding: 0 var(--space-2); background: transparent; border: 0; cursor: pointer;">Left</button>
<button style="padding: 0 var(--space-2); background: transparent; border: 0; cursor: pointer;">Center</button>
<button style="padding: 0 var(--space-2); background: transparent; border: 0; cursor: pointer;">Right</button>
<Separator orientation="vertical" {decorative} style={styleOverride} />
<button style="padding: 0 var(--space-2); background: transparent; border: 0; cursor: pointer;">Link</button>
</div>
{:else}
<div
style="inline-size: 100%; max-inline-size: 16rem; padding: var(--space-2); border-radius: var(--radius-md); border: 1px dashed var(--color-border-default); background: var(--color-surface-overlay);"
>
<button style="display: block; inline-size: 100%; text-align: start; padding: var(--space-2) var(--space-3); background: transparent; border: 0; cursor: pointer;">New file</button>
<button style="display: block; inline-size: 100%; text-align: start; padding: var(--space-2) var(--space-3); background: transparent; border: 0; cursor: pointer;">Open</button>
<button style="display: block; inline-size: 100%; text-align: start; padding: var(--space-2) var(--space-3); background: transparent; border: 0; cursor: pointer;">Open recent</button>
<Separator {...separatorProps} style={styleOverride} />
<button style="display: block; inline-size: 100%; text-align: start; padding: var(--space-2) var(--space-3); background: transparent; border: 0; cursor: pointer;">Save</button>
<button style="display: block; inline-size: 100%; text-align: start; padding: var(--space-2) var(--space-3); background: transparent; border: 0; cursor: pointer;">Save as…</button>
<Separator {...separatorProps} style={styleOverride} />
<button style="display: block; inline-size: 100%; text-align: start; padding: var(--space-2) var(--space-3); background: transparent; border: 0; cursor: pointer; color: var(--color-loss-text);">Quit</button>
</div>
{/if}
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>orientation</span>
<span>{orientation}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>decorative</span> {String(decorative)} ·
<span data-uix-stage-trace-key>thickness</span> {thickness}px
</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>2</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>
Separator is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. Two
public props drive geometry + accessibility; tokens override thickness and color via the
<code>style</code> escape.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual + a11y treatment
</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>decorative</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={decorative} />
<span data-uix-switch-label>{decorative ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>thickness <span data-uix-control-hint
>pixels → <code>--separator-thickness</code></span
></span
>
<input type="range" min="1" max="6" step="1" bind:value={thickness} />
<span data-uix-control-value>{thickness}px</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>color <span data-uix-control-hint
>token chip → <code>--separator-color</code></span
></span
>
<span data-uix-chips role="radiogroup">
{#each colorOptions as opt}
<button data-uix-chip data-active={color === opt} onclick={() => (color = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>demo content</span>
<span data-uix-chips role="radiogroup">
{#each demoOptions as opt}
<button data-uix-chip data-active={demo === opt} onclick={() => (demo = opt)}
>{opt}</button
>
{/each}
</span>
</label>
</div>
<!-- ── Code snippet (eidos-native — no soma layer) ──────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · orientation drives geometry; decorative drops the role</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>
Two public props on the root. Visual tuning (thickness, color) is exposed via CSS custom
properties (<code>--separator-thickness</code> / <code>--separator-color</code>) rather
than typed props — there is no consumer-driven reason to manufacture a typed surface
around a single CSS value.
</p>
<div data-uix-subsection-head>Separator props</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">orientation</td>
<td class="type">{orientations.join(' | ')}</td>
<td class="default">'horizontal'</td>
<td>
Layout axis. Maps to <code>inline-size</code> / <code>block-size</code>. Responsive
via <code>ResponsiveProp&lt;SeparatorOrientation&gt;</code>.
</td>
</tr>
<tr>
<td class="name">decorative</td>
<td class="type">boolean</td>
<td class="default">false</td>
<td>
When true, drops <code>role="separator"</code> + <code>aria-orientation</code>
and adds <code>aria-hidden="true"</code>.
</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>CSS tokens</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Token</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">--separator-thickness</td>
<td class="type">var(--border-width)</td>
<td>Block-axis size on horizontal, inline-axis size on vertical.</td>
</tr>
<tr>
<td class="name">--separator-color</td>
<td class="type">var(--color-border-default)</td>
<td>Background of the rule. Use one of the canonical border colors.</td>
</tr>
<tr>
<td class="name">--separator-min-length</td>
<td class="type">var(--space-4)</td>
<td>Vertical separator's minimum block-axis size when stretched.</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-primitives</td>
<td><code>&lt;Separator&gt;</code></td>
<td>
Identical contract — <code>orientation</code> + <code>decorative</code>. UIX adds
responsive orientation + logical CSS axes.
</td>
</tr>
<tr>
<td class="name">mantine</td>
<td><code>&lt;Divider&gt;</code></td>
<td>
Mantine adds <code>label</code> and <code>variant</code> (dashed / dotted); UIX
defers those to consumer composition + style overrides.
</td>
</tr>
<tr>
<td class="name">chakra-ui</td>
<td><code>&lt;Divider&gt;</code></td>
<td>
Chakra mixes the prop with the style-prop system; UIX keeps it pure layout — no
theme-coloured variants on the divider itself.
</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/separator.ts</code>. Eidos-native (scope:
<code>['eidos']</code>). The Provider part declares conditional ARIA so the same DOM
element flips between semantic and decorative modes based on the
<code>decorative</code> prop.
</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>{separatorMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{separatorMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{separatorMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
<tr><td class="name">events</td><td>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>kebab</th>
<th>marker</th>
<th>element</th>
<th>archetype</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.archetype}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#each separatorMorfo.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;
}>;
}}
{@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 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>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})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}`
: '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>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
Separator declares no semantic events. As a passive layout primitive it paints a rule and
nothing else — no commit, no emerge, no contact. Components that animate sections (e.g.
Collapsible toggling visibility around a separator) own the relevant sema verbs in their
own morfo. The morfo is declared with <code>scope: ['eidos']</code> and the README's
Passive justification section makes the rationale explicit.
</p>
</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/separator/separator.css</code>. The
<code>data-orientation</code> attribute switches between horizontal and vertical sizing;
logical CSS axes (<code>inline-size</code> / <code>block-size</code>) make the rule
mirror in RTL.
</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-separator]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Emitted by the wrapper on the single element.</td>
</tr>
<tr>
<td class="name"
><code>{`[data-separator] { background: var(--separator-color, var(--color-border-default)); flex-shrink: 0; border: 0; }`}</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Base painting + shrink protection inside flex parents.</td>
</tr>
<tr>
<td class="name"
><code>[data-separator][data-orientation='horizontal']</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>
<code>inline-size: 100%</code> + <code>block-size:
var(--separator-thickness)</code>.
</td>
</tr>
<tr>
<td class="name"
><code>[data-separator][data-orientation='vertical']</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>
<code>inline-size: var(--separator-thickness)</code> +
<code>block-size: 100%</code> + <code>min-block-size</code> safeguard.
</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 conditional ARIA contract honours the WAI-ARIA <code>separator</code> role when
<code>decorative</code> is false (default). When true the element drops the role and is
hidden from assistive tech, matching Radix Primitives' contract.
</p>
<div data-uix-subsection-head>Roles</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Mode</th><th>Role</th><th>aria-orientation</th><th>aria-hidden</th></tr></thead>
<tbody>
<tr>
<td class="name">semantic (default)</td>
<td class="type">'separator'</td>
<td class="type">orientation</td>
<td class="default empty">—</td>
</tr>
<tr>
<td class="name">decorative</td>
<td class="default empty">—</td>
<td class="default empty">—</td>
<td class="type">'true'</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">Keyboard</td>
<td>Not focusable. Tab order follows surrounding content.</td>
</tr>
<tr>
<td class="name">Focus visible</td>
<td>No focus ring; the element never receives focus.</td>
</tr>
<tr>
<td class="name">Color contrast</td>
<td>
Default color is <code>--color-border-default</code>, which meets 3:1 against
the canonical surface tokens in both light and dark themes.
</td>
</tr>
<tr>
<td class="name">RTL</td>
<td>
Logical CSS axes mean the divider doesn't need to know about direction — the
horizontal rule fills the inline axis, which flips under <code>dir="rtl"</code>
automatically.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.