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

459 lines
18 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 { Group, type GroupProps } from '$uix/eidos/components/group';
import { Box } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { groupMorfo } from '@/uix/morfo/components/group';
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 — Group container props ────────────────────────────────
let direction = $state<'row' | 'column'>('row');
let align = $state<'stretch' | 'start' | 'center' | 'end' | 'baseline'>('center');
let justify = $state<
'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch'
>('start');
let gap = $state<number>(2);
let grow = $state<boolean>(false);
let attached = $state<boolean>(false);
let padding = $state<number>(3);
const directionOptions = ['row', 'column'] as const;
const alignOptions = ['stretch', 'start', 'center', 'end', 'baseline'] as const;
const justifyOptions = [
'start',
'center',
'end',
'space-between',
'space-around',
'space-evenly',
'stretch'
] as const;
const groupProps = $derived<Partial<GroupProps>>({
direction,
align,
justify,
gap: attached && gap === 0 ? undefined : gap,
grow,
attached,
padding
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(groupMorfo);
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 snippets ─────────────────────────────────────────────────────
const somaSnippet = $derived(
[
'<!-- Group is eidos-native — no soma layer. -->',
'<!-- Equivalent semantic markup (not real soma): -->',
'',
'<div',
' data-box',
' data-flex',
' data-group',
' style="display: flex; flex-direction: row; gap: var(--space-2);"',
'>',
' …',
'</div>'
].join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Group } from '$uix/eidos/components/group';",
'</' + 'script>',
'',
'<Group',
direction !== 'row' && ` direction="${direction}"`,
align !== 'stretch' && ` align="${align}"`,
justify !== 'start' && ` justify="${justify}"`,
gap > 0 && !attached && ` gap={${gap}}`,
grow && ' grow',
attached && ' attached',
padding > 0 && ` padding={${padding}}`,
'>',
' …',
'</Group>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Group</div>
<h1 data-uix-page-title>Group</h1>
<p data-uix-page-lede>
Inline cluster — flex row by default, intended for toolbars, button groups and
segmented controls. Adds two cluster-specific behaviours on top of
<a href="/uix/components/flex">&lt;Flex&gt;</a>: <code>grow</code> (every child stretches
to equal width via <code>flex: 1 1 0</code>) and <code>attached</code> (children share an
edge — used for segmented controls). For vertical clusters use
<a href="/uix/components/stack">&lt;Stack&gt;</a>; for chips that should wrap across lines
use <a href="/uix/components/wrap">&lt;Wrap&gt;</a>; for the full flex API drop down to
<code>&lt;Flex&gt;</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>0
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>extends</span>Flex
</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}>
<Group
{...groupProps}
style="border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); min-inline-size: 24rem;"
>
<Box
padding={2}
style="border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); background: var(--color-surface-2);"
>One</Box>
<Box
padding={2}
style="border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); background: var(--color-surface-2);"
>Two</Box>
<Box
padding={2}
style="border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); background: var(--color-surface-2);"
>Three</Box>
</Group>
</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>direction</span>
<span>{direction}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>grow</span> {String(grow)} ·
<span data-uix-stage-trace-key>attached</span> {String(attached)}
</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>6</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>
Group is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. Five
container props plus two cluster-specific booleans (<code>grow</code>,
<code>attached</code>) on top of <code>&lt;Flex&gt;</code>. Wrap is intentionally
disabled — use <a href="/uix/components/wrap">&lt;Wrap&gt;</a> when you need wrapping.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · cluster behaviour
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>direction</span>
<span data-uix-chips role="radiogroup">
{#each directionOptions as opt}
<button
data-uix-chip
data-active={direction === opt}
onclick={() => (direction = opt)}>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>align <span data-uix-control-hint>cross-axis</span></span>
<span data-uix-chips role="radiogroup">
{#each alignOptions as opt}
<button data-uix-chip data-active={align === opt} onclick={() => (align = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>justify <span data-uix-control-hint>main-axis</span></span>
<span data-uix-chips role="radiogroup">
{#each justifyOptions as opt}
<button data-uix-chip data-active={justify === opt} onclick={() => (justify = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>gap <span data-uix-control-hint>0–8 → var(--space-N); attached locks to 0</span></span>
<input type="number" min="0" max="8" step="1" bind:value={gap} style="inline-size: 6rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>grow <span data-uix-control-hint>flex: 1 1 0 on every child</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={grow} />
<span data-uix-switch-label>{grow ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>attached <span data-uix-control-hint>children share an edge</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={attached} />
<span data-uix-switch-label>{attached ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>padding <span data-uix-control-hint>inherited from Box</span></span>
<input
type="number"
min="0"
max="8"
step="1"
bind:value={padding}
style="inline-size: 6rem;"
/>
</label>
</div>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>n/a · group is eidos-native — equivalent markup shown</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 · cluster props on top of Flex</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>
Group narrows <a href="/uix/components/flex"><code>&lt;Flex&gt;</code></a>'s
<code>direction</code> to <code>row | column</code> (no reverse variants), drops
<code>wrap</code> entirely (use <a href="/uix/components/wrap"
><code>&lt;Wrap&gt;</code></a
> for that), and adds two cluster-specific booleans.
</p>
<div data-uix-subsection-head>Container 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">direction</td><td class="type">row | column</td><td class="default">'row'</td><td>Narrowed from Flex's full direction union.</td></tr>
<tr><td class="name">gap</td><td class="type">number | string</td><td class="default">attached ? 0 : —</td><td>Number → <code>var(--space-N)</code>. <code>attached</code> implies <code>gap=0</code>; explicit prop wins.</td></tr>
<tr><td class="name">align</td><td class="type">stretch | start | center | end | baseline | flex-start | flex-end</td><td class="default empty">—</td><td><code>align-items</code> — cross-axis.</td></tr>
<tr><td class="name">justify</td><td class="type">start | center | end | space-between | space-around | space-evenly | stretch | flex-start | flex-end</td><td class="default empty">—</td><td><code>justify-content</code> — main-axis.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Cluster behaviour</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">grow</td><td class="type">boolean</td><td class="default">false</td><td>Every child gets <code>flex: 1 1 0</code> — fills the row evenly.</td></tr>
<tr><td class="name">attached</td><td class="type">boolean</td><td class="default">false</td><td>Adjacent children share an edge: inner radii squared, 1px overlap absorbs doubled borders. Implies <code>gap=0</code> unless explicitly set.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Inherited from Box</div>
<p data-uix-section-desc>
Every <a href="/uix/components/box">BoxProps</a> field is forwarded through Flex:
<code>padding*</code>, <code>margin*</code>, <code>width</code>, <code>height</code>,
<code>position</code>, <code>overflow*</code>, plus item-side props.
</p>
<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>&lt;Flex&gt;</code></td><td>Radix doesn't ship a separate Group — they recommend Flex with <code>gap</code>. UIX adds <code>attached</code> for segmented controls and <code>grow</code> for equal-width toolbars.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;ButtonGroup&gt;</code> / <code>&lt;HStack&gt;</code></td><td>Chakra splits attached behaviour into ButtonGroup (button-specific). UIX's Group is element-agnostic — works for any cluster.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;Group&gt;</code></td><td>Mantine has the same name and shape; <code>grow</code> maps 1:1. Mantine doesn't have <code>attached</code> — UIX adds it.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<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>{groupMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{groupMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{groupMorfo.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>
<p data-uix-section-desc style="margin-top: var(--uix-space-4);">
Group composes through <code>&lt;Flex&gt;</code> which composes through
<code>&lt;Box&gt;</code>, so the rendered DOM is a single
<code>div[data-box][data-flex][data-group]</code> shell with optional
<code>data-grow</code> / <code>data-attached</code> attributes for the cluster
behaviours.
</p>
</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>
Group declares no semantic events. As a passive layout primitive, it does not commit,
emerge, or react to anything — it just arranges its children. The interactive elements
inside the group (buttons, toggles) own their own sema. ARIA grouping semantics
(<code>role="group"</code>, <code>role="toolbar"</code>, <code>role="radiogroup"</code>)
belong to the consumer who knows what kind of cluster this is.
</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/group/group.css</code> and layers the
<code>grow</code> and <code>attached</code> behaviours on top of the Flex recipe.
</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-group]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker.</td>
</tr>
<tr>
<td class="name"><code>[data-group][data-grow] &gt; *</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Apply <code>flex: 1 1 0</code> to every direct child.</td>
</tr>
<tr>
<td class="name"><code>[data-group][data-attached] &gt; *</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Squash inner radii and negative-margin overlap to absorb adjacent borders.</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">Role</td><td>None implicit. Group is shape-agnostic — assign <code>role="group"</code>, <code>role="toolbar"</code> or <code>role="radiogroup"</code> on the Group provider via <code>{`{...rest}`}</code> when the cluster has semantic meaning. Reach for the <a href="/uix/components/toolbar">&lt;Toolbar&gt;</a> primitive for true toolbar semantics.</td></tr>
<tr><td class="name">Label</td><td>Provide <code>aria-label</code> or <code>aria-labelledby</code> on the Group when assigning a grouping role.</td></tr>
<tr><td class="name">Keyboard</td><td>Group itself is not focusable. Tab order follows DOM order. For arrow-key navigation between children use the headless toolbar / radio-group soma.</td></tr>
<tr><td class="name">Reduced motion</td><td>Group does not animate.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.