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

402 lines
16 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 { Stack, type StackProps, type StackDirection } from '$uix/eidos/components/stack';
import { Box } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { stackMorfo } from '@/uix/morfo/components/stack';
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 — Stack container props ────────────────────────────────
let direction = $state<StackDirection>('column');
let align = $state<'stretch' | 'start' | 'center' | 'end' | 'baseline'>('stretch');
let justify = $state<
'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch'
>('start');
let gap = $state<number>(3);
let padding = $state<number>(4);
const directionOptions = ['row', 'row-reverse', 'column', 'column-reverse'] 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 stackProps = $derived<Partial<StackProps>>({
direction,
align,
justify,
gap,
padding
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(stackMorfo);
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 { Stack } from '$uix/eidos/components/stack';",
'</' + 'script>',
'',
'<Stack',
direction !== 'column' && ` direction="${direction}"`,
align !== 'stretch' && ` align="${align}"`,
justify !== 'start' && ` justify="${justify}"`,
gap > 0 && ` gap={${gap}}`,
padding > 0 && ` padding={${padding}}`,
'>',
' …',
'</Stack>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Stack</div>
<h1 data-uix-page-title>Stack</h1>
<p data-uix-page-lede>
Direction-controlled flex stack — vertical by default, horizontal when
<code>direction="row"</code>. Thin specialization of
<a href="/uix/components/flex">&lt;Flex&gt;</a> with locked nowrap and 4 ergonomic props
(<code>direction</code>, <code>gap</code>, <code>align</code>, <code>justify</code>). For
row-only clusters with edge-to-edge behaviour use
<a href="/uix/components/group">&lt;Group&gt;</a>; for the full flex API (including
<code>wrap</code> and per-axis gaps) 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}>
<Stack
{...stackProps}
style="border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); min-block-size: 10rem; min-inline-size: 24rem;"
>
<Box
padding={2}
style="border: 1px solid var(--color-primary-border); border-radius: var(--radius-sm); background: var(--color-primary-track); color: var(--color-primary-text);"
>Row A</Box>
<Box
padding={2}
style="border: 1px solid var(--color-affirm-border); border-radius: var(--radius-sm); background: var(--color-affirm-track); color: var(--color-affirm-text);"
>Row B</Box>
<Box
padding={2}
style="border: 1px solid var(--color-risk-border); border-radius: var(--radius-sm); background: var(--color-risk-track); color: var(--color-risk-text);"
>Row C</Box>
</Stack>
</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>gap</span> {gap} ·
<span data-uix-stage-trace-key>align</span> {align}
</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>4</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>
Stack is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The
4 container props below pass through to the underlying <code>&lt;Flex&gt;</code>.
Stack also inherits every <a href="/uix/components/box">Box</a> prop (padding,
margin, size, position) via Flex composition.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · stack 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)</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>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 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 · 4 ergonomic 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>
Stack is a thin, opinionated specialization of <a href="/uix/components/flex"
><code>&lt;Flex&gt;</code></a
>: it locks <code>wrap</code> to nowrap, defaults <code>direction</code> to
<code>column</code>, and exposes only the four most-used container props. Drop down to
<code>&lt;Flex&gt;</code> when you need <code>wrap</code>, <code>rowGap</code>,
<code>columnGap</code> or <code>inline</code>.
</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 | row-reverse | column | column-reverse</td><td class="default">'column'</td><td><code>flex-direction</code>.</td></tr>
<tr><td class="name">gap</td><td class="type">number | string</td><td class="default empty">—</td><td>Number → <code>var(--space-N)</code>.</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>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>top/right/bottom/left</code>, <code>overflow*</code>, plus
item-side props (<code>alignSelf</code>, <code>gridColumn</code>, …).
</p>
<div data-uix-subsection-head>Not on Stack (use Flex instead)</div>
<p data-uix-section-desc>
<code>wrap</code>, <code>inline</code>, <code>rowGap</code>, <code>columnGap</code> —
Stack intentionally omits these. Use <a href="/uix/components/flex"
><code>&lt;Flex&gt;</code></a
> when you need them.
</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 direction="column"&gt;</code></td><td>Radix doesn't ship a separate Stack — they use Flex with direction. UIX names the direction-locked variant for the most common case.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;Stack&gt;</code> / <code>&lt;VStack&gt;</code> / <code>&lt;HStack&gt;</code></td><td>Chakra exposes three components (Stack/VStack/HStack). UIX collapses to a single Stack with a <code>direction</code> prop — <a href="/uix/components/group">&lt;Group&gt;</a> covers the row-cluster case.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;Stack&gt;</code> / <code>&lt;Group&gt;</code></td><td>Mantine has the same split: vertical Stack + horizontal Group. UIX matches Mantine here.</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>{stackMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{stackMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{stackMorfo.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);">
Stack 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-stack]</code> shell. Recipe lives at
<code>src/uix/eidos/components/stack/stack.css</code> — currently just the marker; all
behaviour comes from the Flex layer.
</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>
Stack declares no semantic events. As a passive layout primitive, it does not commit,
emerge, or react to anything — it just arranges its children. Components that animate
on appearance should compose Stack with an interactive primitive (popover, drawer,
collapsible) that owns the relevant sema verbs.
</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/stack/stack.css</code> — currently
declaration-only (the <code>[data-stack]</code> marker exists so consumers can target
stacks specifically). All visual behaviour layers 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-stack]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Used by consumers for targeting; behaviour inherited from Flex.</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. Default element is <code>&lt;div&gt;</code> — semantically neutral. Wrap Stack in a landmark (<code>&lt;nav&gt;</code>, <code>&lt;section&gt;</code>, <code>&lt;ul role="list"&gt;</code>) when content needs one.</td></tr>
<tr><td class="name">Label</td><td>Not applicable — Stack has no content semantics. Labels belong to the interactive children.</td></tr>
<tr><td class="name">Keyboard</td><td>Stack is not focusable. Tab order follows DOM order even when <code>direction</code> visually reverses (<code>row-reverse</code> / <code>column-reverse</code>). Verify visual order matches focus order for keyboard users.</td></tr>
<tr><td class="name">Reduced motion</td><td>Stack does not animate.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.