diff --git a/src/uix/morfo/components/container.ts b/src/uix/morfo/components/container.ts new file mode 100644 index 000000000..d80b24587 --- /dev/null +++ b/src/uix/morfo/components/container.ts @@ -0,0 +1,30 @@ +import type { Morfo } from '../types'; + +/** + * Container — max-width centred content shell (layout primitive). + * + * Eidos-native: caps content width at the `--container-width-{size}` + * token and centers via auto inline margins. The provider is a single + * `
` shell — Container composes through + * ``. + * + * No semantic events — Container is a passive container that constrains + * its children. See `box.ts` for the same 0-event justification. + */ +export const containerMorfo = { + name: 'Container', + kebab: 'container', + scope: ['eidos'], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'public', + defaultElement: 'div', + optional: false, + data: [], + aria: [] + } + ] +} as const satisfies Morfo; diff --git a/src/uix/morfo/components/grid.ts b/src/uix/morfo/components/grid.ts new file mode 100644 index 000000000..d2e47ca84 --- /dev/null +++ b/src/uix/morfo/components/grid.ts @@ -0,0 +1,30 @@ +import type { Morfo } from '../types'; + +/** + * Grid — `display:grid` container (layout primitive). + * + * Eidos-native: every container-side grid prop maps to a `--grid-*` CSS + * variable; the provider is a single `
` shell — + * Grid composes through ``. Item-side placement props (`gridColumn`, + * `gridRow`, `gridArea`, `placeSelf`) live on `` — see `box.ts`. + * + * No semantic events — Grid is a passive container that arranges its + * children. See `box.ts` for the same 0-event justification. + */ +export const gridMorfo = { + name: 'Grid', + kebab: 'grid', + scope: ['eidos'], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'public', + defaultElement: 'div', + optional: false, + data: [], + aria: [] + } + ] +} as const satisfies Morfo; diff --git a/src/uix/morfo/components/group.ts b/src/uix/morfo/components/group.ts new file mode 100644 index 000000000..44bfb235a --- /dev/null +++ b/src/uix/morfo/components/group.ts @@ -0,0 +1,30 @@ +import type { Morfo } from '../types'; + +/** + * Group — inline cluster (layout primitive). + * + * Eidos-native: a flex row by default with two cluster-specific + * behaviors (`grow` for equal-width children, `attached` for edge-to-edge + * segmented controls). The provider is a single `
` shell — Group composes through ``. + * + * No semantic events — Group is a passive container that arranges its + * children. See `box.ts` for the same 0-event justification. + */ +export const groupMorfo = { + name: 'Group', + kebab: 'group', + scope: ['eidos'], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'public', + defaultElement: 'div', + optional: false, + data: [], + aria: [] + } + ] +} as const satisfies Morfo; diff --git a/src/uix/morfo/components/section.ts b/src/uix/morfo/components/section.ts new file mode 100644 index 000000000..83fc1239a --- /dev/null +++ b/src/uix/morfo/components/section.ts @@ -0,0 +1,31 @@ +import type { Morfo } from '../types'; + +/** + * Section — block-axis padded region (layout primitive). + * + * Eidos-native: applies a `padding-block` keyed off `size` (sm/md/lg/xl). + * Currently renders as `
` — until an `as` prop is added, consumers + * who need a real `
` landmark wrap or nest one. The provider is + * a single `
` shell — Section composes through + * ``. + * + * No semantic events — Section is a passive container that pads its + * children. See `box.ts` for the same 0-event justification. + */ +export const sectionMorfo = { + name: 'Section', + kebab: 'section', + scope: ['eidos'], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'public', + defaultElement: 'div', + optional: false, + data: [], + aria: [] + } + ] +} as const satisfies Morfo; diff --git a/src/uix/morfo/components/stack.ts b/src/uix/morfo/components/stack.ts new file mode 100644 index 000000000..b6624c31a --- /dev/null +++ b/src/uix/morfo/components/stack.ts @@ -0,0 +1,30 @@ +import type { Morfo } from '../types'; + +/** + * Stack — direction-controlled flex stack (layout primitive). + * + * Eidos-native: a thin specialization of `` that defaults + * `direction` to `column` and routes `gap`/`align`/`justify` through the + * Flex container channel. The provider is a single `
` shell — Stack composes through ``. + * + * No semantic events — Stack is a passive container that arranges its + * children. See `box.ts` for the same 0-event justification. + */ +export const stackMorfo = { + name: 'Stack', + kebab: 'stack', + scope: ['eidos'], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'public', + defaultElement: 'div', + optional: false, + data: [], + aria: [] + } + ] +} as const satisfies Morfo; diff --git a/src/uix/morfo/components/wrap.ts b/src/uix/morfo/components/wrap.ts new file mode 100644 index 000000000..501a62ca9 --- /dev/null +++ b/src/uix/morfo/components/wrap.ts @@ -0,0 +1,30 @@ +import type { Morfo } from '../types'; + +/** + * Wrap — always-wrapping flex row (layout primitive). + * + * Eidos-native: a flex row that locks `flex-wrap: wrap`. Use for tag + * clusters, chip rows or anything that should reflow on narrow viewports. + * The provider is a single `
` shell — + * Wrap composes through ``. + * + * No semantic events — Wrap is a passive container that arranges its + * children. See `box.ts` for the same 0-event justification. + */ +export const wrapMorfo = { + name: 'Wrap', + kebab: 'wrap', + scope: ['eidos'], + parts: [ + { + name: 'Provider', + kebab: 'provider', + archetype: 'provider', + kind: 'public', + defaultElement: 'div', + optional: false, + data: [], + aria: [] + } + ] +} as const satisfies Morfo; diff --git a/web/routes/uix/components/container/+page.svelte b/web/routes/uix/components/container/+page.svelte index b178ae533..6ab9c426f 100644 --- a/web/routes/uix/components/container/+page.svelte +++ b/web/routes/uix/components/container/+page.svelte @@ -1,8 +1,101 @@
@@ -10,73 +103,313 @@
Layout · Container

Container

- Max-width content container. Caps content at the - --container-width-{'{size}'} token and centers it. + Max-width centred content shell. Caps content at one of six tokens + (sm · md · lg · xl · xxl · full), applies a default horizontal padding via + --container-padding-inline, and centres via auto inline margins (or + align="left" / align="right"). For arbitrary + max-width values use <Box> directly; + for semantic block padding use <Section>.

+
+ + parts{compiled.parts.order.length} + + + events0 + + + sizes{sizeOptions.length} + + + scopeeidos + +
-
-

Live example

-
- {#each sizes as s (s)} - - {/each} -
- +
+
-

Container content (size: {size}).

- -
- -
-

Props

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropDefaultNotes
size'xl'sm · md · lg · xl · xxl · full
align'center'left · center · right
paddingXvar(--container-padding-inline)override per-instance
… plus every BoxProps prop
-
- -
-

Reference

-
    -
  • - radix-themes Container — same surface; sizes map to fixed - pixel widths via tokens. -
  • -
  • - chakra-ui Container — origin of the centered max-width pattern. -
  • -
-
+ + + Content (max-width: var(--container-width-{size})) + + +
+
+ trace + {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} + · + size + {size} + + align {align} · + paddingY {paddingY} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Container is eidos-native — no soma split. + The hatched background of the stage shows the parent's width; the bordered region is + what Container caps. full removes the cap entirely. Container also + inherits every Box prop (padding, margin) via + composition. +

+ +
+ eidos props · width & alignment +
+
+ + + + +
+ + +
+
+ soma + n/a · container is eidos-native — equivalent markup shown + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · size token + auto inline margins + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Container layers max-inline-size, padding-inline and + margin-inline rules on top of <Box>. Sizes are tokens defined in the eidos foundation; full removes the cap + (max-width: none). For arbitrary max-width values pass + maxWidth on Box directly. +

+ +
Width & alignment
+
+ + + + + + +
PropTypeDefaultNotes
sizesm | md | lg | xl | xxl | full'xl'Maps to --container-width-{`{size}`} token. full uncaps.
alignleft | center | right'center'Drives margin-inline: auto/0 combinations.
+
+ +
Padding
+
+ + + + + + +
PropTypeDefaultNotes
paddingXnumber | string'var(--container-padding-inline)'Default comes from the foundation token; override per-instance.
paddingYnumber | string—Inherited from Box; default is 0. Use <Section> if you want token-keyed block padding.
+
+ +
Inherited from Box
+

+ Every BoxProps field is forwarded: + padding*, margin*, width (locked to 100% by + Container), height, position, overflow*. Note + that Container sets marginLeft / marginRight from + align, so manual marginX overrides override the alignment. +

+ +
Reference comparison
+
+ + + + + + + +
LibraryClosest equivalentDifference
radix-themes<Container>Radix exposes 4 sizes (1..4); UIX uses 6 named tokens (sm..xxl + full). Same centred-by-default shape.
chakra-ui<Container>Chakra defaults to maxW="60ch" regardless of theme; UIX wires through to foundation tokens for project-wide consistency.
mantine<Container>Mantine takes size="xs|sm|md|lg|xl" or a raw number; UIX matches the named scale and adds full to opt out.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+ +
+ + + + + + + + + +
FieldValue
name{containerMorfo.name}
kebab{containerMorfo.kebab}
scope{containerMorfo.scope.join(', ')}
parts{partsList.length}
events0
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
kebabmarkerelementarchetypeoptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.archetype}{part.optional ? 'yes' : 'no'}
+
+ +

+ Container composes through <Box>, so the rendered DOM is a single + div[data-box][data-container] shell with data-size and + data-align attributes for selector targeting. +

+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events +

+

+ Container declares no semantic events. As a passive layout primitive, it does not + commit, emerge, or react to anything — it just constrains its children's width. + Content inside the container owns its own sema. +

+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Container reads --container-width-{`{size}`} and + --container-padding-inline from the eidos foundation. The Svelte + component computes the resolved width and margins inline; the recipe at + src/uix/eidos/components/container/container.css mostly carries the + marker. +

+
+ + + + + + + + + + + + + + +
SelectorOwnerPurpose
[data-container]morfoProvider marker.
[data-container][data-size='{`{size}`}']eidosPer-size hooks (currently used by the component's inline style, available for consumer overrides).
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + +
ConcernContract
RoleNone implicit. Default element is <div> — semantically neutral. Container is a layout wrapper; landmarks (<main>, <article>) belong on a separate element.
LabelNot applicable.
KeyboardContainer is not focusable. Tab order follows DOM order.
Reduced motionContainer does not animate.
+
+
+ {/if}
diff --git a/web/routes/uix/components/grid/+page.svelte b/web/routes/uix/components/grid/+page.svelte index 49873bff2..0ca2bb544 100644 --- a/web/routes/uix/components/grid/+page.svelte +++ b/web/routes/uix/components/grid/+page.svelte @@ -1,9 +1,114 @@
@@ -11,82 +116,364 @@
Layout · Grid

Grid

- display:grid container with track and placement props. + display:grid container. Owns track templates + (templateColumns, templateRows, + autoColumns, autoRows, autoFlow), container alignment + (align, justify, placeItems, + placeContent) and the row/column gap. Item placement props + (gridColumn, gridRow, gridArea, + placeSelf) live on the child <Box>. + For flex layouts use <Flex>.

+
+ + parts{compiled.parts.order.length} + + + events0 + + + extendsBox + + + scopeeidos + +
-
-

Live example

- - 1 - 2 - 3 - 4 - 5 - 6 - -
- -
-

Props

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropNotes
templateColumns / templateRowsraw grid-template-* strings
autoColumns / autoRowsimplicit track sizes
autoFlowrow · column · dense · row dense · column dense
gap / rowGap / columnGaprow/columnGap override gap per axis
align / justifyalign-items / justify-content
placeItems / placeContentraw shorthand strings
gridColumn / gridRow / gridAreaper-item placement
… plus every BoxProps prop except display
-
- -
-

Reference

-
    -
  • - CSS Grid spec — props are 1:1 with the CSS properties they wrap. -
  • -
  • - radix-themes Grid — same prop names (columns is - called templateColumns here, for symmetry with templateRows). -
  • -
-
+ +
+
+ + 1 + 2 + 3 + 4 + 5 + 6 + +
+
+ trace + {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} + · + templateColumns + {templateColumns} + + gap {gap} · + autoFlow {autoFlow} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Grid is eidos-native — no soma split. The + container props below map to --grid-* CSS variables. Grid also inherits + every Box prop (padding, margin, size, position) by + composition. Item-side placement (gridColumn, gridRow, + gridArea, placeSelf) is set on the child Box. +

+ +
+ eidos props · container behaviour +
+
+ + + + + + +
+ + +
+
+ soma + n/a · grid is eidos-native — equivalent markup shown + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · container props mapped to --grid-* CSS variables + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Grid adds container-side grid props on top of every BoxProps field. Track + templates accept any CSS grid-template-* string. Item placement + props (gridColumn, gridRow, gridArea, + placeSelf) live on the child <Box>. +

+ +
Track templates
+
+ + + + + + + + + +
PropTypeDefaultNotes
templateColumnsstring—grid-template-columns. e.g. "repeat(3, 1fr)".
templateRowsstring—grid-template-rows.
autoColumnsstring'auto'grid-auto-columns.
autoRowsstring'auto'grid-auto-rows.
autoFlowrow | column | dense | row dense | column dense'row'grid-auto-flow.
+
+ +
Alignment
+
+ + + + + + + + +
PropTypeDefaultNotes
alignstretch | start | center | end | baseline | flex-start | flex-end'stretch'align-items.
justifystart | center | end | stretch | space-between | space-around | space-evenly | flex-start | flex-end'start'justify-content.
placeItemsstring—Shorthand for align-items + justify-items.
placeContentstring—Shorthand for align-content + justify-content.
+
+ +
Gap
+
+ + + + + + + +
PropTypeDefaultNotes
gapnumber | string—Shorthand for both axes. Number → var(--space-N).
rowGapnumber | string—Per-axis override; takes precedence over gap.
columnGapnumber | string—Per-axis override; takes precedence over gap.
+
+ +
Inherited from Box
+

+ Every BoxProps field is forwarded (except + display, which Grid locks to grid): + padding*, margin*, width, height, + position, top/right/bottom/left, overflow*. +

+ +
Item placement (on child Box)
+

+ Item-side props are not on Grid. Set them on the child Box: + gridColumn, gridRow, gridArea, + placeSelf, alignSelf, justifySelf, order. + See <Box> → "Flex / grid item". +

+ +
Reference comparison
+
+ + + + + + + +
LibraryClosest equivalentDifference
radix-themes<Grid>Same shape: container props on Grid, item props on Box. Radix accepts columns="3" shorthand; UIX uses raw templateColumns="repeat(3, 1fr)".
chakra-ui<Grid> / <GridItem>Chakra ships a separate <GridItem> for placement; UIX uses Box on the child instead — one primitive, less indirection.
mantine<SimpleGrid> / <Grid>Mantine splits into SimpleGrid (auto-fit responsive) and Grid (12-col span); UIX keeps a single Grid that accepts arbitrary CSS Grid templates.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+ +
+ + + + + + + + + +
FieldValue
name{gridMorfo.name}
kebab{gridMorfo.kebab}
scope{gridMorfo.scope.join(', ')}
parts{partsList.length}
events0
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
kebabmarkerelementarchetypeoptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.archetype}{part.optional ? 'yes' : 'no'}
+
+ +

+ Grid composes through <Box>, so the rendered DOM is a single + div[data-box][data-grid] shell. Recipe lives at + src/uix/eidos/components/grid/grid.css and layers on top of the Box recipe. +

+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events +

+

+ Grid 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 Grid with an interactive primitive (popover, drawer, + collapsible) that owns the relevant sema verbs. +

+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Recipe lives in src/uix/eidos/components/grid/grid.css and layers on top of + the Box recipe. The provider element carries both data-box and + data-grid markers because Grid renders through Box. +

+
+ + + + + + + + + + + + + + +
SelectorOwnerPurpose
[data-grid]morfoProvider marker.
[data-box][data-grid] {`{ grid-template-columns: var(--grid-template-columns, none); … }`}eidosLayer container-side grid properties on the Box recipe.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + +
ConcernContract
RoleNone implicit. Default element is <div> — semantically neutral. Wrap Grid in a landmark (<main>, <nav>, <section>) when content needs one.
LabelNot applicable — Grid has no content semantics. Labels belong to the interactive children.
KeyboardGrid is not focusable. Tab order follows DOM order even when CSS Grid reflows visual order (e.g. order, grid-area, autoFlow="dense"). Verify visual order matches focus order for keyboard users.
Reduced motionGrid does not animate. Layout shifts on resize follow CSS defaults.
+
+
+ {/if}
diff --git a/web/routes/uix/components/group/+page.svelte b/web/routes/uix/components/group/+page.svelte index 11fde0e07..0dccecd0b 100644 --- a/web/routes/uix/components/group/+page.svelte +++ b/web/routes/uix/components/group/+page.svelte @@ -1,8 +1,115 @@
@@ -10,85 +117,342 @@
Layout · Group

Group

- Inline cluster of children. Supports grow (equal-width children) - and attached (edge-merged children). + Inline cluster — flex row by default, intended for toolbars, button groups and + segmented controls. Adds two cluster-specific behaviours on top of + <Flex>: grow (every child stretches + to equal width via flex: 1 1 0) and attached (children share an + edge — used for segmented controls). For vertical clusters use + <Stack>; for chips that should wrap across lines + use <Wrap>; for the full flex API drop down to + <Flex>.

+
+ + parts{compiled.parts.order.length} + + + events0 + + + extendsFlex + + + scopeeidos + +
-
-

Default

- - - - - -
- -
-

Attached (segmented control)

- - - - - -
- -
-

Grow (equal widths)

- - - - -
- -
-

Props

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropDefaultNotes
direction'row'row · column
growfalsechildren get flex: 1 1 0
attachedfalsecollapses inner radii + overlaps 1px to merge borders
gap0 if attacheddefaults to 0 when attached is on
… plus FlexProps minus wrap
-
- -
-

Reference

-
    -
  • - mantine Group — same name and intent. -
  • -
  • - chakra-ui ButtonGroup — origin of the - attached behavior, generalized here. -
  • -
-
+ +
+
+ + One + Two + Three + +
+
+ trace + {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} + · + direction + {direction} + + grow {String(grow)} · + attached {String(attached)} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Group is eidos-native — no soma split. Five + container props plus two cluster-specific booleans (grow, + attached) on top of <Flex>. Wrap is intentionally + disabled — use <Wrap> when you need wrapping. +

+ +
+ eidos props · cluster behaviour +
+
+ + + + + + + +
+ + +
+
+ soma + n/a · group is eidos-native — equivalent markup shown + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · cluster props on top of Flex + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Group narrows <Flex>'s + direction to row | column (no reverse variants), drops + wrap entirely (use <Wrap> for that), and adds two cluster-specific booleans. +

+ +
Container props
+
+ + + + + + + + +
PropTypeDefaultNotes
directionrow | column'row'Narrowed from Flex's full direction union.
gapnumber | stringattached ? 0 : —Number → var(--space-N). attached implies gap=0; explicit prop wins.
alignstretch | start | center | end | baseline | flex-start | flex-end—align-items — cross-axis.
justifystart | center | end | space-between | space-around | space-evenly | stretch | flex-start | flex-end—justify-content — main-axis.
+
+ +
Cluster behaviour
+
+ + + + + + +
PropTypeDefaultNotes
growbooleanfalseEvery child gets flex: 1 1 0 — fills the row evenly.
attachedbooleanfalseAdjacent children share an edge: inner radii squared, 1px overlap absorbs doubled borders. Implies gap=0 unless explicitly set.
+
+ +
Inherited from Box
+

+ Every BoxProps field is forwarded through Flex: + padding*, margin*, width, height, + position, overflow*, plus item-side props. +

+ +
Reference comparison
+
+ + + + + + + +
LibraryClosest equivalentDifference
radix-themes<Flex>Radix doesn't ship a separate Group — they recommend Flex with gap. UIX adds attached for segmented controls and grow for equal-width toolbars.
chakra-ui<ButtonGroup> / <HStack>Chakra splits attached behaviour into ButtonGroup (button-specific). UIX's Group is element-agnostic — works for any cluster.
mantine<Group>Mantine has the same name and shape; grow maps 1:1. Mantine doesn't have attached — UIX adds it.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+ +
+ + + + + + + + + +
FieldValue
name{groupMorfo.name}
kebab{groupMorfo.kebab}
scope{groupMorfo.scope.join(', ')}
parts{partsList.length}
events0
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
kebabmarkerelementarchetypeoptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.archetype}{part.optional ? 'yes' : 'no'}
+
+ +

+ Group composes through <Flex> which composes through + <Box>, so the rendered DOM is a single + div[data-box][data-flex][data-group] shell with optional + data-grow / data-attached attributes for the cluster + behaviours. +

+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events +

+

+ 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 + (role="group", role="toolbar", role="radiogroup") + belong to the consumer who knows what kind of cluster this is. +

+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Recipe lives in src/uix/eidos/components/group/group.css and layers the + grow and attached behaviours on top of the Flex recipe. +

+
+ + + + + + + + + + + + + + + + + + + +
SelectorOwnerPurpose
[data-group]morfoProvider marker.
[data-group][data-grow] > *eidosApply flex: 1 1 0 to every direct child.
[data-group][data-attached] > *eidosSquash inner radii and negative-margin overlap to absorb adjacent borders.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + +
ConcernContract
RoleNone implicit. Group is shape-agnostic — assign role="group", role="toolbar" or role="radiogroup" on the Group provider via {`{...rest}`} when the cluster has semantic meaning. Reach for the <Toolbar> primitive for true toolbar semantics.
LabelProvide aria-label or aria-labelledby on the Group when assigning a grouping role.
KeyboardGroup itself is not focusable. Tab order follows DOM order. For arrow-key navigation between children use the headless toolbar / radio-group soma.
Reduced motionGroup does not animate.
+
+
+ {/if}
diff --git a/web/routes/uix/components/section/+page.svelte b/web/routes/uix/components/section/+page.svelte index 6cfa68009..550c61634 100644 --- a/web/routes/uix/components/section/+page.svelte +++ b/web/routes/uix/components/section/+page.svelte @@ -1,8 +1,91 @@
@@ -10,88 +93,294 @@
Layout · Section

Section

- Top-level page section with consistent block-axis padding scaled by - size. + Top-level page section with consistent block-axis padding. Four size tokens + (sm · md · lg · xl) map to --section-padding-block-{`{size}`}. + Despite the name, Section currently renders as <div> — there is no + as prop yet, so when you need a real <section> landmark, + wrap or nest one. For centred max-width content use + <Container>; for non-semantic padding use + <Box>.

+
+ + parts{compiled.parts.order.length} + + + events0 + + + sizes{sizeOptions.length} + + + scopeeidos + +
-
-

Live example

-
- {#each sizes as s (s)} - - {/each} -
-
+
+
-

- Section content (size: {size}). +

+ + Content (padding-block: var(--section-padding-block-{size})) + +
+
+
+ trace + {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} + · + size + {size} + + paddingY {paddingY || `var(--section-padding-block-${size})`} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Section is eidos-native — no soma split. The + horizontal hatched background shows how the block padding shifts content vertically + as size changes. Note the rendered element is a <div> + — see the A11y tab for guidance on adding a real landmark. +

+ +
+ eidos props · padding +
+
+ + + +
+ + +
+
+ soma + n/a · section is eidos-native — equivalent markup shown + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · size-keyed block padding on top of Box + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Section is a one-prop specialization of <Box>: it sets width: 100%, picks a default + padding-block from size, and renders as <div>. + If you pass paddingY directly, your value wins over the size-keyed + default. +

+ +
Padding
+
+ + + + + + +
PropTypeDefaultNotes
sizesm | md | lg | xl'lg'Maps to --section-padding-block-{`{size}`}: sm→8 / md→12 / lg→16 / xl→24 space steps.
paddingYnumber | stringSIZE_TO_PADDING[size]Override the size-keyed default per-instance.
+
+ +
Inherited from Box
+

+ Every BoxProps field is forwarded: + padding*, margin*, width (locked to 100% by + Section), height, position, overflow*. Pass + paddingX for horizontal padding (no default). +

+ +
Limitations
+

+ Section currently has no as prop — the rendered element is always + <div>. Add a <section> wrapper outside Section + (or nest a <section> as the first child) when landmark semantics + are needed.

-
-
- -
-

Padding scale

- - - - - - - - - - - - - -
SizeBlock padding
smvar(--space-8) = 32px
mdvar(--space-12) = 48px
lgvar(--space-16) = 64px
xlcalc(var(--space-16) * 1.5) = 96px
-
- -
-

Props

- - - - - - - - - - - - - - - - - - - - - - - -
PropDefaultNotes
size'lg'sm · md · lg · xl
paddingY—override the size-keyed default
… plus every BoxProps prop
-
- -
-

Reference

-
    -
  • - radix-themes Section — same intent (size-keyed block padding - for vertical rhythm). -
  • -
  • - HTML <section> — Section is a `<div>` - in batch 1; wrap with a real <section> when you need landmark semantics. -
  • -
-
+ +
Reference comparison
+
+ + + + + + + +
LibraryClosest equivalentDifference
radix-themes<Section>Radix renders a real <section> by default and takes size="1..4"; UIX uses named tokens (sm..xl) and renders <div> pending an as prop.
chakra-uin/aChakra has no dedicated Section — they recommend <Box as="section" py="…">. UIX names the pattern as a primitive for consistency.
mantinen/aMantine has no Section either. Sections are usually authored ad-hoc with Stack + AppShell.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+ +
+ + + + + + + + + +
FieldValue
name{sectionMorfo.name}
kebab{sectionMorfo.kebab}
scope{sectionMorfo.scope.join(', ')}
parts{partsList.length}
events0
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
kebabmarkerelementarchetypeoptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.archetype}{part.optional ? 'yes' : 'no'}
+
+ +

+ Section composes through <Box>, so the rendered DOM is a single + div[data-box][data-section] shell with a data-size + attribute. Recipe lives at + src/uix/eidos/components/section/section.css and declares the + --section-padding-block-* tokens locally. +

+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events +

+

+ Section declares no semantic events. As a passive layout primitive, it does not + commit, emerge, or react to anything — it just pads its children. Content inside the + section owns its own sema. +

+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Recipe lives in src/uix/eidos/components/section/section.css. The + --section-padding-block-* tokens are defined locally on the + [data-section] selector (intentionally not in the foundation — Section + is the sole consumer). +

+
+ + + + + + + + + +
SelectorOwnerPurpose
[data-section]morfoProvider marker. Declares the per-size padding tokens and locks inline-size: 100%.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + +
ConcernContract
RoleNone implicit. Despite the name, Section renders as <div> — there is no implicit region role. Wrap Section in a real <section aria-labelledby="…"> or use a heading inside to provide landmark structure.
LabelNot applicable to Section itself. If you wrap a real <section> around it, label it with aria-labelledby pointing at the heading.
KeyboardSection is not focusable. Tab order follows DOM order.
Reduced motionSection does not animate.
+
+
+ {/if}
diff --git a/web/routes/uix/components/stack/+page.svelte b/web/routes/uix/components/stack/+page.svelte index 11c4aac6b..7e4f993e3 100644 --- a/web/routes/uix/components/stack/+page.svelte +++ b/web/routes/uix/components/stack/+page.svelte @@ -1,8 +1,109 @@
@@ -10,83 +111,316 @@
Layout · Stack

Stack

- Vertical (default) or horizontal stack. Thin alias over Flex with - direction='column'. + Direction-controlled flex stack — vertical by default, horizontal when + direction="row". Thin specialization of + <Flex> with locked nowrap and 4 ergonomic props + (direction, gap, align, justify). For + row-only clusters with edge-to-edge behaviour use + <Group>; for the full flex API (including + wrap and per-axis gaps) drop down to <Flex>.

+
+ + parts{compiled.parts.order.length} + + + events0 + + + extendsFlex + + + scopeeidos + +
-
-

Vertical (default)

- - Row 1 - Row 2 - Row 3 - -
- -
-

Horizontal

- - A - B - C - -
- -
-

Props

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropDefaultNotes
direction'column'row · row-reverse · column · column-reverse
gap—maps to var(--space-N) when numeric
align / justify—inherited from Flex
… plus FlexProps minus wrap, inline, rowGap, columnGap
-
- -
-

Reference

-
    -
  • - chakra-ui Stack / VStack / HStack — same intent; we collapse - the three into one component with a direction prop. -
  • -
  • - radix-themes Flex direction='column' — equivalent shape; Stack - is the named shortcut. -
  • -
-
+ +
+
+ + Row A + Row B + Row C + +
+
+ trace + {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} + · + direction + {direction} + + gap {gap} · + align {align} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Stack is eidos-native — no soma split. The + 4 container props below pass through to the underlying <Flex>. + Stack also inherits every Box prop (padding, + margin, size, position) via Flex composition. +

+ +
+ eidos props · stack behaviour +
+
+ + + + + +
+ + +
+
+ soma + n/a · stack is eidos-native — equivalent markup shown + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · 4 ergonomic props on top of Flex + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Stack is a thin, opinionated specialization of <Flex>: it locks wrap to nowrap, defaults direction to + column, and exposes only the four most-used container props. Drop down to + <Flex> when you need wrap, rowGap, + columnGap or inline. +

+ +
Container props
+
+ + + + + + + + +
PropTypeDefaultNotes
directionrow | row-reverse | column | column-reverse'column'flex-direction.
gapnumber | string—Number → var(--space-N).
alignstretch | start | center | end | baseline | flex-start | flex-end—align-items — cross-axis.
justifystart | center | end | space-between | space-around | space-evenly | stretch | flex-start | flex-end—justify-content — main-axis.
+
+ +
Inherited from Box
+

+ Every BoxProps field is forwarded through Flex: + padding*, margin*, width, height, + position, top/right/bottom/left, overflow*, plus + item-side props (alignSelf, gridColumn, …). +

+ +
Not on Stack (use Flex instead)
+

+ wrap, inline, rowGap, columnGap — + Stack intentionally omits these. Use <Flex> when you need them. +

+ +
Reference comparison
+
+ + + + + + + +
LibraryClosest equivalentDifference
radix-themes<Flex direction="column">Radix doesn't ship a separate Stack — they use Flex with direction. UIX names the direction-locked variant for the most common case.
chakra-ui<Stack> / <VStack> / <HStack>Chakra exposes three components (Stack/VStack/HStack). UIX collapses to a single Stack with a direction prop — <Group> covers the row-cluster case.
mantine<Stack> / <Group>Mantine has the same split: vertical Stack + horizontal Group. UIX matches Mantine here.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+ +
+ + + + + + + + + +
FieldValue
name{stackMorfo.name}
kebab{stackMorfo.kebab}
scope{stackMorfo.scope.join(', ')}
parts{partsList.length}
events0
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
kebabmarkerelementarchetypeoptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.archetype}{part.optional ? 'yes' : 'no'}
+
+ +

+ Stack composes through <Flex> which composes through + <Box>, so the rendered DOM is a single + div[data-box][data-flex][data-stack] shell. Recipe lives at + src/uix/eidos/components/stack/stack.css — currently just the marker; all + behaviour comes from the Flex layer. +

+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events +

+

+ 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. +

+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Recipe lives in src/uix/eidos/components/stack/stack.css — currently + declaration-only (the [data-stack] marker exists so consumers can target + stacks specifically). All visual behaviour layers on top of the Flex recipe. +

+
+ + + + + + + + + +
SelectorOwnerPurpose
[data-stack]morfoProvider marker. Used by consumers for targeting; behaviour inherited from Flex.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + +
ConcernContract
RoleNone implicit. Default element is <div> — semantically neutral. Wrap Stack in a landmark (<nav>, <section>, <ul role="list">) when content needs one.
LabelNot applicable — Stack has no content semantics. Labels belong to the interactive children.
KeyboardStack is not focusable. Tab order follows DOM order even when direction visually reverses (row-reverse / column-reverse). Verify visual order matches focus order for keyboard users.
Reduced motionStack does not animate.
+
+
+ {/if}
diff --git a/web/routes/uix/components/wrap/+page.svelte b/web/routes/uix/components/wrap/+page.svelte index 98b878e5e..c01c4580b 100644 --- a/web/routes/uix/components/wrap/+page.svelte +++ b/web/routes/uix/components/wrap/+page.svelte @@ -1,21 +1,132 @@
@@ -23,59 +134,327 @@
Layout · Wrap

Wrap

- Flex row that always wraps. Use for tag clouds, chip groups and any - row-of-children that should reflow on narrow viewports. + Flex row that always wraps. Use for tag/chip clusters, filter bars or anything + that should reflow across multiple lines on narrow viewports. Equivalent to + <Flex wrap="wrap"> with two extra ergonomic + gap-axis shorthands (rowGap, columnGap). For nowrap clusters + use <Group>; for arbitrary flex direction use + <Flex>.

+
+ + parts{compiled.parts.order.length} + + + events0 + + + extendsFlex + + + scopeeidos + +
-
-

Live example

- - {#each tags as t (t)} - {t} - {/each} - -
- -
-

Props

- - - - - - - - - - - - - - - - - - - - -
PropNotes
gap / rowGap / columnGapindependent row/column gaps
align / justifymain- and cross-axis alignment
… plus FlexProps minus direction and wrap
-
- -
-

Reference

-
    -
  • - mantine Group wrap — same intent; we name the wrap-locked - variant explicitly to make intent obvious at the call site. -
  • -
  • - chakra-ui Wrap — origin of the dedicated wrap component. -
  • -
-
+ +
+
+ + {#each pills as label} + {label} + {/each} + +
+
+ trace + {trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`} + · + items + {pills.length} + + gap {gap} · + justify {justify} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Wrap is eidos-native — no soma split. Locks + flex-wrap: wrap and exposes 5 container props on top of <Flex>. + The stage caps width at 32rem to demonstrate reflow. +

+ +
+ eidos props · wrap behaviour +
+
+ + + + + + + +
+ + +
+
+ soma + n/a · wrap is eidos-native — equivalent markup shown + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · always wraps · per-axis gap shorthands + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Wrap is a narrow specialization of <Flex>: locks wrap to 'wrap', drops direction + (always row), and elevates rowGap / columnGap as + first-class props because per-axis gaps are the typical reason to reach for Wrap. +

+ +
Container props
+
+ + + + + + + + + +
PropTypeDefaultNotes
gapnumber | string—Shorthand for both axes. Number → var(--space-N).
rowGapnumber | string—Gap between wrapped lines.
columnGapnumber | string—Gap between items within a line.
alignstretch | start | center | end | baseline | flex-start | flex-end—align-items per line — cross-axis.
justifystart | center | end | space-between | space-around | space-evenly | stretch | flex-start | flex-end—justify-content — main-axis.
+
+ +
Inherited from Box
+

+ Every BoxProps field is forwarded through Flex: + padding*, margin*, width, height, + position, overflow*, plus item-side props. +

+ +
Not on Wrap (use Flex instead)
+

+ direction (Wrap is always row), wrap (locked to wrap), + inline — Wrap intentionally omits these. Use <Flex> when you need them. +

+ +
Reference comparison
+
+ + + + + + + +
LibraryClosest equivalentDifference
radix-themes<Flex wrap="wrap">Radix doesn't ship a separate Wrap — they recommend Flex with the wrap prop. UIX names it for the common chip-cluster case.
chakra-ui<Wrap> / <WrapItem>Chakra wraps each child in a <WrapItem> internally. UIX has no item wrapper — gap is owned by the container.
mantine<Group wrap="wrap">Mantine collapses both into Group with a prop. UIX splits Group (nowrap) and Wrap (wrap) because their visual intent is different.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

Morfo contract

+ +
+ + + + + + + + + +
FieldValue
name{wrapMorfo.name}
kebab{wrapMorfo.kebab}
scope{wrapMorfo.scope.join(', ')}
parts{partsList.length}
events0
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + {/each} + +
kebabmarkerelementarchetypeoptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.archetype}{part.optional ? 'yes' : 'no'}
+
+ +

+ Wrap composes through <Flex wrap="wrap"> which composes through + <Box>, so the rendered DOM is a single + div[data-box][data-flex][data-wrap] shell. +

+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema · events +

+

+ Wrap declares no semantic events. As a passive layout primitive, it does not commit, + emerge, or react to anything — it just arranges its children. Tags/chips/buttons + inside the Wrap own their own sema. +

+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Recipe lives in src/uix/eidos/components/wrap/wrap.css. The + data-wrap marker is mostly for consumer targeting; the actual + flex-wrap: wrap behaviour is set by the underlying Flex layer because Wrap + renders as <Flex wrap="wrap">. +

+
+ + + + + + + + + +
SelectorOwnerPurpose
[data-wrap]morfoProvider marker. Used by consumers for targeting; flex-wrap comes from the Flex layer.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+
+ + + + + + + + +
ConcernContract
RoleNone implicit. Default element is <div>. When wrapping a list of tags/filters, consider role="list" + role="listitem" on the children for screen-reader semantics.
LabelNot applicable — Wrap has no content semantics.
KeyboardWrap is not focusable. Tab order follows DOM order; wrapped lines do not change the focus sequence. For arrow-key cluster navigation use a tag-group or toolbar soma.
Reduced motionWrap does not animate. Reflow on resize follows CSS defaults.
+
+
+ {/if}