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

385 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 { Flex, type FlexProps } from '$uix/eidos/components/flex';
import { Box } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { flexMorfo } from '@/uix/morfo/components/flex';
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 — Flex container props ──────────────────────────────────
let direction = $state<'row' | 'row-reverse' | 'column' | 'column-reverse'>('row');
let align = $state<'stretch' | 'start' | 'center' | 'end' | 'baseline'>('center');
let justify = $state<'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch'>('space-between');
let wrap = $state<'nowrap' | 'wrap' | 'wrap-reverse'>('nowrap');
let gap = $state<number>(3);
let padding = $state<number>(4);
let inline = $state<boolean>(false);
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 wrapOptions = ['nowrap', 'wrap', 'wrap-reverse'] as const;
const flexProps = $derived<Partial<FlexProps>>({
direction,
align,
justify,
wrap,
gap,
padding,
inline
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(flexMorfo);
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 { Flex } from '$uix/eidos/components/flex';",
'</' + 'script>',
'',
'<Flex',
direction !== 'row' && ` direction="${direction}"`,
align !== 'stretch' && ` align="${align}"`,
justify !== 'start' && ` justify="${justify}"`,
wrap !== 'nowrap' && ` wrap="${wrap}"`,
gap > 0 && ` gap={${gap}}`,
padding > 0 && ` padding={${padding}}`,
inline && ' inline',
'>',
' …',
'</Flex>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Flex</div>
<h1 data-uix-page-title>Flex</h1>
<p data-uix-page-lede>
<code>display:flex</code> container. Adds <code>direction</code> / <code>align</code> /
<code>justify</code> / <code>wrap</code> / <code>rowGap</code> / <code>columnGap</code>
on top of every <a href="/uix/components/box">&lt;Box&gt;</a> prop. For grid containers
use <a href="/uix/components/grid">&lt;Grid&gt;</a>; for flex/grid <em>item</em> props
(alignSelf, gridColumn, order, basis, …) use <code>&lt;Box&gt;</code> on the child.
</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>Box
</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}>
<Flex {...flexProps} style="border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); min-block-size: 8rem; 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);">Item 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);">Item 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);">Item C</Box>
</Flex>
</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>align</span> {align} ·
<span data-uix-stage-trace-key>justify</span> {justify}
</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>
Flex is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The 7
container-side props below map to <code>--flex-*</code> CSS variables. Flex also inherits
every <a href="/uix/components/box">Box</a> prop (padding, margin, size, position) by
composition.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · container 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>wrap</span>
<span data-uix-chips role="radiogroup">
{#each wrapOptions as opt}
<button data-uix-chip data-active={wrap === opt} onclick={() => (wrap = 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>
<label data-uix-control>
<span data-uix-control-label>inline <span data-uix-control-hint>display:inline-flex</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={inline} />
<span data-uix-switch-label>{inline ? 'on' : 'off'}</span>
</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 · container props mapped to --flex-* CSS variables</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>
Flex adds container-side flex props on top of every <code>BoxProps</code> field. Pass
<code>inline</code> to switch to <code>inline-flex</code>; otherwise the provider renders
with <code>display:flex</code>. Item-side props (<code>flex</code>, <code>grow</code>,
<code>shrink</code>, <code>basis</code>, <code>order</code>, <code>alignSelf</code>,
<code>justifySelf</code>, <code>placeSelf</code>) live on the child
<a href="/uix/components/box"><code>&lt;Box&gt;</code></a>.
</p>
<div data-uix-subsection-head>Container props (Flex-only)</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">inline</td><td class="type">boolean</td><td class="default">false</td><td>Switches to <code>display:inline-flex</code>.</td></tr>
<tr><td class="name">direction</td><td class="type">row | row-reverse | column | column-reverse</td><td class="default">'row'</td><td><code>flex-direction</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>
<tr><td class="name">wrap</td><td class="type">nowrap | wrap | wrap-reverse</td><td class="default">'nowrap'</td><td><code>flex-wrap</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">rowGap / columnGap</td><td class="type">number | string</td><td class="default empty">—</td><td>Per-axis gap override; takes precedence over <code>gap</code>.</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:
<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 that apply when this Flex is itself an item of a parent flex/grid
(<code>alignSelf</code>, <code>gridColumn</code>, …).
</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>Same shape: container props on Flex, item props on Box. Radix uses <code>flexShrink</code> spelling; UIX uses <code>shrink</code>.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;Flex&gt;</code> / <code>&lt;HStack&gt;</code> / <code>&lt;VStack&gt;</code></td><td>Chakra also exposes <code>&lt;HStack&gt;</code>/<code>&lt;VStack&gt;</code> as direction-specific helpers. UIX uses <a href="/uix/components/stack">&lt;Stack&gt;</a> with a <code>direction</code> prop.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;Flex&gt;</code> / <code>&lt;Group&gt;</code></td><td>Mantine splits row-only into <a href="/uix/components/group">&lt;Group&gt;</a>; UIX has the same split.</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>{flexMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{flexMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{flexMorfo.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);">
Flex composes through <code>&lt;Box&gt;</code>, so the rendered DOM is a single
<code>div[data-box][data-flex]</code> shell. Recipe lives at
<code>src/uix/eidos/components/flex/flex.css</code> and layers on top of the Box
recipe.
</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>
Flex 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 Flex 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/flex/flex.css</code> and layers on top
of the Box recipe. The provider element carries both <code>data-box</code> and
<code>data-flex</code> markers because Flex renders through Box.
</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-flex]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker.</td>
</tr>
<tr>
<td class="name"><code>[data-box][data-flex] {`{ display: flex | inline-flex; flex-direction: var(--flex-direction, row); … }`}</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Layer container-side flex properties on the Box recipe.</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. Use a semantic wrapper (<code>&lt;nav&gt;</code>, <code>&lt;header&gt;</code>, <code>&lt;ul role="list"&gt;</code>) around Flex when content needs a landmark or list semantics.</td></tr>
<tr><td class="name">Label</td><td>Not applicable — Flex has no content semantics. Labels belong to the interactive children.</td></tr>
<tr><td class="name">Keyboard</td><td>Flex 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>Flex does not animate. Layout shifts on resize follow CSS defaults — no <code>transition</code> on the recipe.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.