@ -1,9 +1,105 @@
< script lang = "ts" >
import { Flex } from '$uix/eidos/components/flex';
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';
const cellStyle =
'background: var(--color-surface-2); border-radius: 4px; padding: 8px 12px';
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 snippets ─────────────────────────────────────────────────────
const somaSnippet = $derived(
[
'<!-- Flex is eidos - native — no soma layer. --> ',
'<!-- Equivalent semantic markup (not real soma): --> ',
'',
'< div ' ,
' data-box',
' data-flex',
' style="display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: var(--space-3);"',
'>',
' …',
'< / div > '
].join('\n')
);
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 >
@ -11,85 +107,302 @@
< div data-uix-eyebrow > Layout · Flex< / div >
< h1 data-uix-page-title > Flex< / h1 >
< p data-uix-page-lede >
< code > display:flex< / code > container with direction, wrap, alignment and gap.
< 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" > < Box> < / a > prop. For grid containers
use < a href = "/uix/components/grid" > < Grid> < / a > ; for flex/grid < em > item< / em > props
(alignSelf, gridColumn, order, basis, …) use < code > < Box> < / 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 >
< section >
< h2 > Live example< / h2 >
< Flex
direction="row"
gap={ 3 }
align="center"
justify="space-between"
padding={ 4 }
style="background: var(--color-surface-1); border: 1px solid var(--color-border); border-radius: 8px"
>
< Box style = { cellStyle } > Start</Box >
< Box style = { cellStyle } > Middle</Box >
< Box style = { cellStyle } > End</Box >
< / Flex >
< / section >
< section >
< h2 > Props< / h2 >
< table >
< thead >
< tr >
< th > Prop< / th >
< th > Default< / th >
< th > Notes< / th >
< / tr >
< / thead >
< tbody >
< tr >
< td > < code > inline< / code > < / td >
< td > false< / td >
< td > switch to < code > inline-flex< / code > < / td >
< / tr >
< tr >
< td > < code > direction< / code > < / td >
< td > 'row'< / td >
< td > row · row-reverse · column · column-reverse< / td >
< / tr >
< tr >
< td > < code > wrap< / code > < / td >
< td > 'nowrap'< / td >
< td > nowrap · wrap · wrap-reverse< / td >
< / tr >
< tr >
< td > < code > align< / code > < / td >
< td > —< / td >
< td > stretch · start · center · end · baseline< / td >
< / tr >
< tr >
< td > < code > justify< / code > < / td >
< td > —< / td >
< td > start · center · end · space-between · space-around · space-evenly< / td >
< / tr >
< tr >
< td > < code > gap< / code > / < code > rowGap< / code > / < code > columnGap< / code > < / td >
< td > —< / td >
< td > row/columnGap override < code > gap< / code > per axis< / td >
< / tr >
< tr >
< td colspan = "3" > < em > … plus every < code > BoxProps< / code > prop except < code > display< / code > < / em > < / td >
< / tr >
< / tbody >
< / table >
< / section >
< section >
< h2 > Reference< / h2 >
< ul >
< li >
< strong > radix-themes Flex< / strong > — same surface (< code > direction< / code > ,
< code > align< / code > , < code > justify< / code > , < code > wrap< / code > , < code > gap< / code > ).
< / li >
< li >
< strong > chakra-ui Flex< / strong > — origin of the responsive Flex container API;
our prop names match.
< / li >
< / ul >
< / section >
<!-- 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 snippets per layer ─────────────────────────────────── -->
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "soma" > soma< / span >
< span > n/a · flex 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 · 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 > < Box> < / 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 > < Flex> < / 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 > < Flex> < / code > / < code > < HStack> < / code > / < code > < VStack> < / code > < / td > < td > Chakra also exposes < code > < HStack> < / code > /< code > < VStack> < / code > as direction-specific helpers. UIX uses < a href = "/uix/components/stack" > < Stack> < / a > with a < code > direction< / code > prop.< / td > < / tr >
< tr > < td class = "name" > mantine< / td > < td > < code > < Flex> < / code > / < code > < Group> < / code > < / td > < td > Mantine splits row-only into < a href = "/uix/components/group" > < Group> < / 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" > < { part . defaultElement } > </ 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 > < Box> < / 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 > < div> < / code > — semantically neutral. Use a semantic wrapper (< code > < nav> < / code > , < code > < header> < / code > , < code > < ul role="list"> < / 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 >