feat(eidos): port 8 layout primitives from air (box, flex, grid, stack, group, wrap, container, section)

Visual-only primitives translated from the legacy `air/components/layout`
on the morfo-runtime branch. Architectural adjustments for the eidos
target: drop the `air-` class prefix, target `[data-{name}]` directly,
rename CSS vars to `--{name}-*`, replace `var(--air-space-N)` with the
eidos foundation `var(--space-N)` token, resolve responsive props via
`ActiveEidos.resolve(prop)`.

- `box` — universal box-model utility (size, padding, margin, gap,
  position, overflow, flex/grid item props). Every prop maps to a
  CSS custom property; unset props fall through via `revert-layer`.
- `flex` / `grid` / `stack` / `group` / `wrap` — layout containers
  that render through Box (single `<div>` per primitive, no
  semantic-tag prop in this batch). Markers stack as
  `[data-box][data-{name}]`.
- `container` — max-width centered (sm/md/lg/xl/full).
- `section` — semantic-padding block with background tone variants.

Shared helpers at `_layout/shared.ts` (formatLayoutSpace /
formatLayoutLength / formatLayoutRaw / pushStyleVar / composeStyle)
plus an optional `Layout.*` namespace barrel for consumers who prefer
that style; the individual `import { Box }` from each component dir
also works.

Each primitive ships a demo at `/uix/components/{name}/` with a live
example, a props table, and a reference comparison row (radix-themes /
chakra-ui / mantine).

svelte-check: 0 errors / 0 new warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 20709caf9c
commit 9ec2a57a85

@ -0,0 +1,45 @@
/**
* Layout namespace — re-exports the 8 layout primitives as a single
* group for consumers who prefer the dotted style:
*
* import { Layout } from '$uix/eidos/components/_layout';
* <Layout.Box>…</Layout.Box>
* <Layout.Flex>…</Layout.Flex>
*
* The individual barrels still work and are the preferred form for
* tree-shaken imports:
*
* import { Box } from '$uix/eidos/components/box';
*/
export { default as Box } from '../box';
export { default as Flex } from '../flex';
export { default as Grid } from '../grid';
export { default as Stack } from '../stack';
export { default as Group } from '../group';
export { default as Wrap } from '../wrap';
export { default as Container } from '../container';
export { default as Section } from '../section';
export type { BoxProps } from '../box';
export type { FlexProps } from '../flex';
export type { GridProps } from '../grid';
export type { StackProps, StackDirection } from '../stack';
export type { GroupProps } from '../group';
export type { WrapProps } from '../wrap';
export type { ContainerProps, ContainerSize, ContainerAlign } from '../container';
export type { SectionProps, SectionSize } from '../section';
export type {
LayoutSpaceValue,
LayoutLengthValue,
LayoutDisplay,
LayoutOverflow,
LayoutPosition,
LayoutAlignSelf,
LayoutJustifySelf,
LayoutFlexDirection,
LayoutFlexWrap,
LayoutAlignItems,
LayoutJustifyContent,
LayoutGridAutoFlow
} from './shared';

@ -0,0 +1,128 @@
/**
* Shared layout helpers for eidos layout primitives.
*
* Ported from the legacy `air/components/layout/shared.ts` with one
* architectural shift: numeric space values now emit `var(--space-N)`
* tokens from the eidos foundation (`src/uix/eidos/generated/base.css`),
* not the retired `--air-space-N` aliases.
*
* formatLayoutSpace(2) -> 'var(--space-2)'
* formatLayoutSpace('1rem') -> '1rem'
* formatLayoutLength(120) -> '120px'
* formatLayoutLength('50%') -> '50%'
* formatLayoutRaw(3) -> '3'
*
* The shape is identical to air's helpers so the per-primitive Svelte
* components below stay tiny — they just push CSS custom properties
* into a declarations list and join.
*/
export type LayoutSpaceValue = number | string;
export type LayoutLengthValue = number | string;
export type LayoutDisplay =
| 'block'
| 'inline'
| 'inline-block'
| 'flex'
| 'inline-flex'
| 'grid'
| 'inline-grid'
| 'contents'
| 'none';
export type LayoutOverflow = 'visible' | 'hidden' | 'clip' | 'scroll' | 'auto';
export type LayoutPosition = 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
export type LayoutAlignSelf =
| 'auto'
| 'start'
| 'end'
| 'center'
| 'stretch'
| 'flex-start'
| 'flex-end'
| 'baseline';
export type LayoutJustifySelf = 'auto' | 'start' | 'end' | 'center' | 'stretch';
export type LayoutFlexDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type LayoutFlexWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
export type LayoutAlignItems =
| 'stretch'
| 'start'
| 'end'
| 'center'
| 'baseline'
| 'flex-start'
| 'flex-end';
export type LayoutJustifyContent =
| 'start'
| 'end'
| 'center'
| 'stretch'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'flex-start'
| 'flex-end';
export type LayoutGridAutoFlow = 'row' | 'column' | 'dense' | 'row dense' | 'column dense';
function parseNumberish(value: number | string): number | undefined {
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
if (typeof value !== 'string') return undefined;
const normalized = value.trim();
if (!normalized) return undefined;
if (!/^-?\d+(\.\d+)?$/.test(normalized)) return undefined;
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : undefined;
}
/**
* Format a space-scale value (gap, padding, margin). Numbers map to the
* `--space-N` token; strings are emitted verbatim so consumers can pass
* `'1rem'`, `'auto'`, or a custom `var(--…)` chain.
*/
export function formatLayoutSpace(value: LayoutSpaceValue | undefined): string | undefined {
if (value === undefined || value === null || value === '') return undefined;
const parsed = parseNumberish(value);
return parsed !== undefined ? `var(--space-${parsed})` : String(value);
}
/**
* Format a length value (width, height, inset). Numbers become px;
* strings pass through.
*/
export function formatLayoutLength(value: LayoutLengthValue | undefined): string | undefined {
if (value === undefined || value === null || value === '') return undefined;
const parsed = parseNumberish(value);
return parsed !== undefined ? `${parsed}px` : String(value);
}
/**
* Format a raw value (flex, grow, shrink, order). Always stringifies.
*/
export function formatLayoutRaw(value: string | number | undefined): string | undefined {
if (value === undefined || value === null || value === '') return undefined;
return String(value);
}
/**
* Append a CSS custom-property declaration to the list, but only when the
* value is defined. Keeps the resulting `style=""` attribute compact —
* primitives don't emit dozens of `--…: ;` no-ops.
*/
export function pushStyleVar(
declarations: string[],
name: `--${string}`,
value: string | undefined
): void {
if (!value) return;
declarations.push(`${name}:${value}`);
}
/**
* Compose a final style string from a list of `--var:value` declarations
* and an optional caller-provided `style` prop.
*/
export function composeStyle(declarations: string[], style: string | undefined): string {
const own = declarations.join(';');
if (!style) return own;
if (!own) return style;
return `${own};${style}`;
}

@ -0,0 +1,83 @@
/*
* Box recipe — universal box-model utility. Every CSS property falls back
* to `revert-layer` so unset Box props inherit the normal cascade rather
* than forcing an initial value.
*
* Cascade pattern (padding example):
* padding-top = --box-padding-top
* || --box-padding-block
* || --box-padding
* || revert-layer
*
* The shorthand props (`padding`, `paddingX`, `paddingY`, `margin`,
* `marginX`, `marginY`, `inset`, `insetX`, `insetY`) supply default
* values for the per-side props at the CSS layer, not in JS — so a
* caller passing both `padding={4}` and `paddingTop={6}` gets exactly
* the expected behavior without `box.svelte` needing to know about
* shorthand resolution.
*/
[data-box] {
box-sizing: border-box;
min-width: 0;
display: var(--box-display, revert-layer);
position: var(--box-position, revert-layer);
width: var(--box-width, revert-layer);
min-width: var(--box-min-width, 0);
max-width: var(--box-max-width, revert-layer);
height: var(--box-height, revert-layer);
min-height: var(--box-min-height, revert-layer);
max-height: var(--box-max-height, revert-layer);
padding-top: var(
--box-padding-top,
var(--box-padding-block, var(--box-padding, revert-layer))
);
padding-right: var(
--box-padding-right,
var(--box-padding-inline, var(--box-padding, revert-layer))
);
padding-bottom: var(
--box-padding-bottom,
var(--box-padding-block, var(--box-padding, revert-layer))
);
padding-left: var(
--box-padding-left,
var(--box-padding-inline, var(--box-padding, revert-layer))
);
margin-top: var(--box-margin-top, var(--box-margin-block, var(--box-margin, revert-layer)));
margin-right: var(
--box-margin-right,
var(--box-margin-inline, var(--box-margin, revert-layer))
);
margin-bottom: var(
--box-margin-bottom,
var(--box-margin-block, var(--box-margin, revert-layer))
);
margin-left: var(--box-margin-left, var(--box-margin-inline, var(--box-margin, revert-layer)));
gap: var(--box-gap, revert-layer);
top: var(--box-top, revert-layer);
right: var(--box-right, revert-layer);
bottom: var(--box-bottom, revert-layer);
left: var(--box-left, revert-layer);
inset-block-start: var(--box-top, var(--box-inset-block, var(--box-inset, revert-layer)));
inset-inline-end: var(--box-right, var(--box-inset-inline, var(--box-inset, revert-layer)));
inset-block-end: var(--box-bottom, var(--box-inset-block, var(--box-inset, revert-layer)));
inset-inline-start: var(--box-left, var(--box-inset-inline, var(--box-inset, revert-layer)));
overflow: var(--box-overflow, revert-layer);
overflow-x: var(--box-overflow-x, var(--box-overflow, revert-layer));
overflow-y: var(--box-overflow-y, var(--box-overflow, revert-layer));
flex: var(--box-flex, revert-layer);
flex-grow: var(--box-grow, revert-layer);
flex-shrink: var(--box-shrink, revert-layer);
flex-basis: var(--box-basis, revert-layer);
order: var(--box-order, revert-layer);
align-self: var(--box-align-self, revert-layer);
justify-self: var(--box-justify-self, revert-layer);
}

@ -0,0 +1,129 @@
<script lang="ts">
/**
* Eidos `<Box>` — universal box-model utility. Visual-only primitive:
* every prop maps to a `--box-*` custom property on a single `<div
* data-box>` root, and the recipe in `box.css` consumes those vars
* with `revert-layer` fallbacks so unset props inherit normal cascade.
*
* <Box padding={4} maxWidth={600} display="flex" gap={3}>…</Box>
*
* `Box` is the foundation that Flex, Grid, Stack, Group, Wrap,
* Container and Section all build on. It owns no behavior and no
* morfo — it just translates props to CSS variables.
*/
import { ActiveEidos } from '$uix/eidos';
import type { BoxProps } from './types';
import {
composeStyle,
formatLayoutLength,
formatLayoutRaw,
formatLayoutSpace,
pushStyleVar
} from '../_layout/shared';
let {
display,
width,
minWidth,
maxWidth,
height,
minHeight,
maxHeight,
padding,
paddingX,
paddingY,
paddingTop,
paddingRight,
paddingBottom,
paddingLeft,
margin,
marginX,
marginY,
marginTop,
marginRight,
marginBottom,
marginLeft,
gap,
position,
top,
right,
bottom,
left,
inset,
insetX,
insetY,
overflow,
overflowX,
overflowY,
flex,
grow,
shrink,
basis,
order,
alignSelf,
justifySelf,
style,
class: className,
children,
...restProps
}: BoxProps = $props();
const eidos = ActiveEidos.require();
const resolvedStyle = $derived.by(() => {
const decls: string[] = [];
pushStyleVar(decls, '--box-display', eidos.resolve(display));
pushStyleVar(decls, '--box-width', formatLayoutLength(eidos.resolve(width)));
pushStyleVar(decls, '--box-min-width', formatLayoutLength(eidos.resolve(minWidth)));
pushStyleVar(decls, '--box-max-width', formatLayoutLength(eidos.resolve(maxWidth)));
pushStyleVar(decls, '--box-height', formatLayoutLength(eidos.resolve(height)));
pushStyleVar(decls, '--box-min-height', formatLayoutLength(eidos.resolve(minHeight)));
pushStyleVar(decls, '--box-max-height', formatLayoutLength(eidos.resolve(maxHeight)));
pushStyleVar(decls, '--box-padding', formatLayoutSpace(eidos.resolve(padding)));
pushStyleVar(decls, '--box-padding-inline', formatLayoutSpace(eidos.resolve(paddingX)));
pushStyleVar(decls, '--box-padding-block', formatLayoutSpace(eidos.resolve(paddingY)));
pushStyleVar(decls, '--box-padding-top', formatLayoutSpace(eidos.resolve(paddingTop)));
pushStyleVar(decls, '--box-padding-right', formatLayoutSpace(eidos.resolve(paddingRight)));
pushStyleVar(decls, '--box-padding-bottom', formatLayoutSpace(eidos.resolve(paddingBottom)));
pushStyleVar(decls, '--box-padding-left', formatLayoutSpace(eidos.resolve(paddingLeft)));
pushStyleVar(decls, '--box-margin', formatLayoutSpace(eidos.resolve(margin)));
pushStyleVar(decls, '--box-margin-inline', formatLayoutSpace(eidos.resolve(marginX)));
pushStyleVar(decls, '--box-margin-block', formatLayoutSpace(eidos.resolve(marginY)));
pushStyleVar(decls, '--box-margin-top', formatLayoutSpace(eidos.resolve(marginTop)));
pushStyleVar(decls, '--box-margin-right', formatLayoutSpace(eidos.resolve(marginRight)));
pushStyleVar(decls, '--box-margin-bottom', formatLayoutSpace(eidos.resolve(marginBottom)));
pushStyleVar(decls, '--box-margin-left', formatLayoutSpace(eidos.resolve(marginLeft)));
pushStyleVar(decls, '--box-gap', formatLayoutSpace(eidos.resolve(gap)));
pushStyleVar(decls, '--box-position', eidos.resolve(position));
pushStyleVar(decls, '--box-top', formatLayoutLength(eidos.resolve(top)));
pushStyleVar(decls, '--box-right', formatLayoutLength(eidos.resolve(right)));
pushStyleVar(decls, '--box-bottom', formatLayoutLength(eidos.resolve(bottom)));
pushStyleVar(decls, '--box-left', formatLayoutLength(eidos.resolve(left)));
pushStyleVar(decls, '--box-inset', formatLayoutLength(eidos.resolve(inset)));
pushStyleVar(decls, '--box-inset-inline', formatLayoutLength(eidos.resolve(insetX)));
pushStyleVar(decls, '--box-inset-block', formatLayoutLength(eidos.resolve(insetY)));
pushStyleVar(decls, '--box-overflow', eidos.resolve(overflow));
pushStyleVar(decls, '--box-overflow-x', eidos.resolve(overflowX));
pushStyleVar(decls, '--box-overflow-y', eidos.resolve(overflowY));
pushStyleVar(decls, '--box-flex', formatLayoutRaw(eidos.resolve(flex)));
pushStyleVar(decls, '--box-grow', formatLayoutRaw(eidos.resolve(grow)));
pushStyleVar(decls, '--box-shrink', formatLayoutRaw(eidos.resolve(shrink)));
pushStyleVar(decls, '--box-basis', formatLayoutLength(eidos.resolve(basis)));
pushStyleVar(decls, '--box-order', formatLayoutRaw(eidos.resolve(order)));
pushStyleVar(decls, '--box-align-self', eidos.resolve(alignSelf));
pushStyleVar(decls, '--box-justify-self', eidos.resolve(justifySelf));
return composeStyle(decls, style);
});
</script>
<div {...restProps} data-box="" class={className} style={resolvedStyle}>
{@render children?.()}
</div>

@ -0,0 +1,10 @@
// Box — universal box-model utility.
//
// import { Box } from '$uix/eidos/components/box';
//
// <Box padding={4} maxWidth={600} display="flex" gap={3}>…</Box>
import Box from './box.svelte';
export { Box };
export default Box;
export type { BoxProps } from './types';

@ -0,0 +1,86 @@
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type {
LayoutAlignSelf,
LayoutDisplay,
LayoutJustifySelf,
LayoutLengthValue,
LayoutOverflow,
LayoutPosition,
LayoutSpaceValue
} from '../_layout/shared';
export type BoxProps = Omit<HTMLAttributes<HTMLElement>, 'style' | 'children'> & {
/** `display`. */
display?: ResponsiveProp<LayoutDisplay>;
/** `width`. Number -> px, string passes through. */
width?: ResponsiveProp<LayoutLengthValue>;
/** `min-width`. */
minWidth?: ResponsiveProp<LayoutLengthValue>;
/** `max-width`. */
maxWidth?: ResponsiveProp<LayoutLengthValue>;
/** `height`. */
height?: ResponsiveProp<LayoutLengthValue>;
/** `min-height`. */
minHeight?: ResponsiveProp<LayoutLengthValue>;
/** `max-height`. */
maxHeight?: ResponsiveProp<LayoutLengthValue>;
/** `padding`. Number -> `--space-N`, string passes through. */
padding?: ResponsiveProp<LayoutSpaceValue>;
/** `padding-inline` (left+right). */
paddingX?: ResponsiveProp<LayoutSpaceValue>;
/** `padding-block` (top+bottom). */
paddingY?: ResponsiveProp<LayoutSpaceValue>;
paddingTop?: ResponsiveProp<LayoutSpaceValue>;
paddingRight?: ResponsiveProp<LayoutSpaceValue>;
paddingBottom?: ResponsiveProp<LayoutSpaceValue>;
paddingLeft?: ResponsiveProp<LayoutSpaceValue>;
/** `margin`. */
margin?: ResponsiveProp<LayoutSpaceValue>;
/** `margin-inline`. */
marginX?: ResponsiveProp<LayoutSpaceValue>;
/** `margin-block`. */
marginY?: ResponsiveProp<LayoutSpaceValue>;
marginTop?: ResponsiveProp<LayoutSpaceValue>;
marginRight?: ResponsiveProp<LayoutSpaceValue>;
marginBottom?: ResponsiveProp<LayoutSpaceValue>;
marginLeft?: ResponsiveProp<LayoutSpaceValue>;
/** `gap` (flex/grid). */
gap?: ResponsiveProp<LayoutSpaceValue>;
/** `position`. */
position?: ResponsiveProp<LayoutPosition>;
top?: ResponsiveProp<LayoutLengthValue>;
right?: ResponsiveProp<LayoutLengthValue>;
bottom?: ResponsiveProp<LayoutLengthValue>;
left?: ResponsiveProp<LayoutLengthValue>;
/** Shorthand for all four insets. */
inset?: ResponsiveProp<LayoutLengthValue>;
/** `inset-inline` (left+right). */
insetX?: ResponsiveProp<LayoutLengthValue>;
/** `inset-block` (top+bottom). */
insetY?: ResponsiveProp<LayoutLengthValue>;
/** `overflow`. */
overflow?: ResponsiveProp<LayoutOverflow>;
overflowX?: ResponsiveProp<LayoutOverflow>;
overflowY?: ResponsiveProp<LayoutOverflow>;
/** Flex item: `flex` shorthand. */
flex?: ResponsiveProp<string | number>;
/** Flex item: `flex-grow`. */
grow?: ResponsiveProp<string | number>;
/** Flex item: `flex-shrink`. */
shrink?: ResponsiveProp<string | number>;
/** Flex item: `flex-basis`. */
basis?: ResponsiveProp<LayoutLengthValue>;
/** Flex/grid item: `order`. */
order?: ResponsiveProp<string | number>;
/** Flex/grid item: `align-self`. */
alignSelf?: ResponsiveProp<LayoutAlignSelf>;
/** Grid item: `justify-self`. */
justifySelf?: ResponsiveProp<LayoutJustifySelf>;
/** Extra inline style. Merged after the box's own CSS variable declarations. */
style?: string;
/** Extra class names. */
class?: string;
children?: Snippet;
};

@ -0,0 +1,13 @@
/*
* Container recipe — all sizing/alignment is driven via the Box prop
* channel (maxWidth, marginLeft/Right). The dedicated selector exists
* so consumers can target `[data-container]` for archetype-level
* adjustments without needing to know the Box marker.
*
* Width and padding fall back to foundation tokens when the JS layer
* hasn't pushed them (e.g. SSR snapshots before hydration).
*/
[data-box][data-container] {
max-width: var(--box-max-width, var(--container-width-xl));
}

@ -0,0 +1,65 @@
<script lang="ts">
/**
* Eidos `<Container>` — max-width content container.
*
* Caps content width at the `--container-width-{size}` token from the
* eidos foundation, applies a horizontal padding so text doesn't run
* to the viewport edge on small screens, and centers the content
* (`align='center'`) by default.
*
* <Container size="lg">…</Container>
* <Container size="xl" align="left">…</Container>
*/
import { ActiveEidos } from '$uix/eidos';
import Box from '../box/box.svelte';
import type { ContainerAlign, ContainerProps, ContainerSize } from './types';
const SIZE_TO_WIDTH: Record<ContainerSize, string> = {
sm: 'var(--container-width-sm)',
md: 'var(--container-width-md)',
lg: 'var(--container-width-lg)',
xl: 'var(--container-width-xl)',
xxl: 'var(--container-width-xxl)',
full: 'var(--container-width-full)'
};
const ALIGN_TO_MARGINS: Record<ContainerAlign, { left: string; right: string }> = {
left: { left: '0', right: 'auto' },
center: { left: 'auto', right: 'auto' },
right: { left: 'auto', right: '0' }
};
let {
size = 'xl',
align = 'center',
paddingX,
class: className,
children,
...restProps
}: ContainerProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'xl'));
const resolvedAlign = $derived(eidos.resolve(align, 'center'));
const resolvedMaxWidth = $derived(SIZE_TO_WIDTH[resolvedSize]);
const resolvedMargins = $derived(ALIGN_TO_MARGINS[resolvedAlign]);
// Default horizontal padding comes from the foundation token; caller
// can override via the BoxProps `paddingX` prop.
const resolvedPaddingX = $derived(paddingX ?? 'var(--container-padding-inline)');
</script>
<Box
{...restProps}
width="100%"
maxWidth={resolvedMaxWidth}
paddingX={resolvedPaddingX}
marginLeft={resolvedMargins.left}
marginRight={resolvedMargins.right}
class={className}
data-container=""
data-size={resolvedSize}
data-align={resolvedAlign}
>
{@render children?.()}
</Box>

@ -0,0 +1,10 @@
// Container — max-width centered content container.
//
// import { Container } from '$uix/eidos/components/container';
//
// <Container size="lg">…</Container>
import Container from './container.svelte';
export { Container };
export default Container;
export type { ContainerProps, ContainerSize, ContainerAlign } from './types';

@ -0,0 +1,16 @@
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
export type ContainerSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'full'>;
export type ContainerAlign = 'left' | 'center' | 'right';
export type ContainerProps = BoxProps & {
/**
* Max content width. Maps to the `--container-width-{size}` tokens
* defined in the eidos foundation. `full` removes the cap.
* @default 'xl'
*/
size?: ResponsiveProp<ContainerSize>;
/** Horizontal alignment within the parent. @default 'center' */
align?: ResponsiveProp<ContainerAlign>;
};

@ -0,0 +1,14 @@
/*
* Flex recipe — additional CSS variables layered on top of the Box
* recipe. `row-gap` and `column-gap` fall back to the Box-level `--box-gap`
* so callers can use `gap={N}` to set both axes at once.
*/
[data-box][data-flex] {
flex-direction: var(--flex-direction, row);
align-items: var(--flex-align, stretch);
justify-content: var(--flex-justify, flex-start);
flex-wrap: var(--flex-wrap, nowrap);
row-gap: var(--flex-row-gap, var(--box-gap, revert-layer));
column-gap: var(--flex-column-gap, var(--box-gap, revert-layer));
}

@ -0,0 +1,55 @@
<script lang="ts">
/**
* Eidos `<Flex>` — `display:flex` container. Wraps `<Box>` and adds the
* flex-axis vars (`direction`, `align`, `justify`, `wrap`, row/column
* gap). Marker is `data-flex` and the resolved direction is exposed
* as `data-direction` for selector-driven recipes (Group needs it to
* compute attached overlap on the correct axis).
*
* <Flex direction="row" gap={3} align="center" justify="space-between">…</Flex>
*/
import { ActiveEidos } from '$uix/eidos';
import Box from '../box/box.svelte';
import { composeStyle, formatLayoutSpace, pushStyleVar } from '../_layout/shared';
import type { FlexProps } from './types';
let {
inline = false,
direction = 'row',
align,
justify,
wrap = 'nowrap',
rowGap,
columnGap,
style,
class: className,
children,
...restProps
}: FlexProps = $props();
const eidos = ActiveEidos.require();
const resolvedStyle = $derived.by(() => {
const decls: string[] = [];
pushStyleVar(decls, '--flex-direction', eidos.resolve(direction));
pushStyleVar(decls, '--flex-align', eidos.resolve(align));
pushStyleVar(decls, '--flex-justify', eidos.resolve(justify));
pushStyleVar(decls, '--flex-wrap', eidos.resolve(wrap));
pushStyleVar(decls, '--flex-row-gap', formatLayoutSpace(eidos.resolve(rowGap)));
pushStyleVar(decls, '--flex-column-gap', formatLayoutSpace(eidos.resolve(columnGap)));
return composeStyle(decls, style);
});
const resolvedDirection = $derived(eidos.resolve(direction) ?? 'row');
</script>
<Box
{...restProps}
display={inline ? 'inline-flex' : 'flex'}
class={className}
style={resolvedStyle}
data-flex=""
data-direction={resolvedDirection}
>
{@render children?.()}
</Box>

@ -0,0 +1,10 @@
// Flex — display:flex container.
//
// import { Flex } from '$uix/eidos/components/flex';
//
// <Flex direction="row" gap={3} align="center">…</Flex>
import Flex from './flex.svelte';
export { Flex };
export default Flex;
export type { FlexProps } from './types';

@ -0,0 +1,26 @@
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
import type {
LayoutAlignItems,
LayoutFlexDirection,
LayoutFlexWrap,
LayoutJustifyContent,
LayoutSpaceValue
} from '../_layout/shared';
export type FlexProps = Omit<BoxProps, 'display'> & {
/** Use `inline-flex` instead of `flex`. @default false */
inline?: boolean;
/** `flex-direction`. @default 'row' */
direction?: ResponsiveProp<LayoutFlexDirection>;
/** `align-items`. */
align?: ResponsiveProp<LayoutAlignItems>;
/** `justify-content`. */
justify?: ResponsiveProp<LayoutJustifyContent>;
/** `flex-wrap`. @default 'nowrap' */
wrap?: ResponsiveProp<LayoutFlexWrap>;
/** `row-gap`. Overrides `gap` for the row axis. */
rowGap?: ResponsiveProp<LayoutSpaceValue>;
/** `column-gap`. Overrides `gap` for the column axis. */
columnGap?: ResponsiveProp<LayoutSpaceValue>;
};

@ -0,0 +1,23 @@
/*
* Grid recipe — additional CSS variables layered on top of the Box
* recipe. Track and placement props (`place-items`, `place-content`,
* `grid-column`, `grid-row`, `grid-area`) fall through to `revert-layer`
* so they only apply when explicitly set.
*/
[data-box][data-grid] {
grid-template-columns: var(--grid-template-columns, none);
grid-template-rows: var(--grid-template-rows, none);
grid-auto-columns: var(--grid-auto-columns, auto);
grid-auto-rows: var(--grid-auto-rows, auto);
grid-auto-flow: var(--grid-auto-flow, row);
align-items: var(--grid-align, stretch);
justify-content: var(--grid-justify, start);
place-items: var(--grid-place-items, revert-layer);
place-content: var(--grid-place-content, revert-layer);
grid-column: var(--grid-column, revert-layer);
grid-row: var(--grid-row, revert-layer);
grid-area: var(--grid-area, revert-layer);
row-gap: var(--grid-row-gap, var(--grid-gap, revert-layer));
column-gap: var(--grid-column-gap, var(--grid-gap, revert-layer));
}

@ -0,0 +1,63 @@
<script lang="ts">
/**
* Eidos `<Grid>` — `display:grid` container. Adds the grid-template /
* auto-flow / placement vars on top of `<Box>`. The `gap` prop here
* lives in the grid layer so it can be authored independently from
* the Box-level `gap` if a caller mixes Box-as-Grid-item with the
* Grid container's own gap.
*
* <Grid templateColumns="repeat(3, 1fr)" gap={3}>…</Grid>
*/
import { ActiveEidos } from '$uix/eidos';
import Box from '../box/box.svelte';
import { composeStyle, formatLayoutSpace, pushStyleVar } from '../_layout/shared';
import type { GridProps } from './types';
let {
templateColumns,
templateRows,
autoColumns,
autoRows,
autoFlow,
columnGap,
rowGap,
gap,
align,
justify,
placeItems,
placeContent,
gridColumn,
gridRow,
gridArea,
style,
class: className,
children,
...restProps
}: GridProps = $props();
const eidos = ActiveEidos.require();
const resolvedStyle = $derived.by(() => {
const decls: string[] = [];
pushStyleVar(decls, '--grid-template-columns', eidos.resolve(templateColumns));
pushStyleVar(decls, '--grid-template-rows', eidos.resolve(templateRows));
pushStyleVar(decls, '--grid-auto-columns', eidos.resolve(autoColumns));
pushStyleVar(decls, '--grid-auto-rows', eidos.resolve(autoRows));
pushStyleVar(decls, '--grid-auto-flow', eidos.resolve(autoFlow));
pushStyleVar(decls, '--grid-align', eidos.resolve(align));
pushStyleVar(decls, '--grid-justify', eidos.resolve(justify));
pushStyleVar(decls, '--grid-place-items', eidos.resolve(placeItems));
pushStyleVar(decls, '--grid-place-content', eidos.resolve(placeContent));
pushStyleVar(decls, '--grid-column', eidos.resolve(gridColumn));
pushStyleVar(decls, '--grid-row', eidos.resolve(gridRow));
pushStyleVar(decls, '--grid-area', eidos.resolve(gridArea));
pushStyleVar(decls, '--grid-gap', formatLayoutSpace(eidos.resolve(gap)));
pushStyleVar(decls, '--grid-column-gap', formatLayoutSpace(eidos.resolve(columnGap)));
pushStyleVar(decls, '--grid-row-gap', formatLayoutSpace(eidos.resolve(rowGap)));
return composeStyle(decls, style);
});
</script>
<Box {...restProps} display="grid" class={className} style={resolvedStyle} data-grid="">
{@render children?.()}
</Box>

@ -0,0 +1,10 @@
// Grid — display:grid container.
//
// import { Grid } from '$uix/eidos/components/grid';
//
// <Grid templateColumns="repeat(3, 1fr)" gap={3}>…</Grid>
import Grid from './grid.svelte';
export { Grid };
export default Grid;
export type { GridProps } from './types';

@ -0,0 +1,41 @@
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
import type {
LayoutAlignItems,
LayoutGridAutoFlow,
LayoutJustifyContent,
LayoutSpaceValue
} from '../_layout/shared';
export type GridProps = Omit<BoxProps, 'display'> & {
/** `grid-template-columns`. */
templateColumns?: ResponsiveProp<string>;
/** `grid-template-rows`. */
templateRows?: ResponsiveProp<string>;
/** `grid-auto-columns`. */
autoColumns?: ResponsiveProp<string>;
/** `grid-auto-rows`. */
autoRows?: ResponsiveProp<string>;
/** `grid-auto-flow`. */
autoFlow?: ResponsiveProp<LayoutGridAutoFlow>;
/** `column-gap`. */
columnGap?: ResponsiveProp<LayoutSpaceValue>;
/** `row-gap`. */
rowGap?: ResponsiveProp<LayoutSpaceValue>;
/** Shorthand for both `row-gap` and `column-gap`. */
gap?: ResponsiveProp<LayoutSpaceValue>;
/** `align-items`. */
align?: ResponsiveProp<LayoutAlignItems>;
/** `justify-content`. */
justify?: ResponsiveProp<LayoutJustifyContent>;
/** `place-items` shorthand. */
placeItems?: ResponsiveProp<string>;
/** `place-content` shorthand. */
placeContent?: ResponsiveProp<string>;
/** Grid item: `grid-column`. */
gridColumn?: ResponsiveProp<string>;
/** Grid item: `grid-row`. */
gridRow?: ResponsiveProp<string>;
/** Grid item: `grid-area`. */
gridArea?: ResponsiveProp<string>;
};

@ -0,0 +1,49 @@
/*
* Group recipe — cluster behavior driven by `data-grow` and
* `data-attached`. The attached overlap defaults to 1px so adjacent
* 1px borders collapse cleanly; consumers can override via
* `--group-attached-overlap`.
*/
[data-box][data-flex][data-group] {
--group-attached-overlap: 1px;
}
[data-box][data-flex][data-group][data-grow] > * {
flex: 1 1 0;
min-width: 0;
}
/* ── Attached: row direction ───────────────────────────────────────── */
[data-box][data-flex][data-group][data-attached]:not([data-direction='column']):not(
[data-direction='column-reverse']
)
> :not(:first-child) {
margin-inline-start: calc(var(--group-attached-overlap) * -1);
border-start-start-radius: 0 !important;
border-end-start-radius: 0 !important;
}
[data-box][data-flex][data-group][data-attached]:not([data-direction='column']):not(
[data-direction='column-reverse']
)
> :not(:last-child) {
border-start-end-radius: 0 !important;
border-end-end-radius: 0 !important;
}
/* ── Attached: column direction ────────────────────────────────────── */
[data-box][data-flex][data-group][data-attached][data-direction='column'] > :not(:first-child),
[data-box][data-flex][data-group][data-attached][data-direction='column-reverse']
> :not(:first-child) {
margin-block-start: calc(var(--group-attached-overlap) * -1);
border-start-start-radius: 0 !important;
border-start-end-radius: 0 !important;
}
[data-box][data-flex][data-group][data-attached][data-direction='column'] > :not(:last-child),
[data-box][data-flex][data-group][data-attached][data-direction='column-reverse']
> :not(:last-child) {
border-end-start-radius: 0 !important;
border-end-end-radius: 0 !important;
}

@ -0,0 +1,43 @@
<script lang="ts">
/**
* Eidos `<Group>` — inline cluster. Flex row by default, with two
* cluster-specific behaviors:
*
* `grow` — every child stretches to equal width (`flex: 1 1 0`)
* `attached` — adjacent children share an edge: the inner radii are
* squared, and a 1px overlap absorbs doubled borders.
* Use for segmented controls and button groups.
*
* <Group gap={2}>…</Group>
* <Group attached>…</Group>
* <Group grow>…</Group>
*/
import Flex from '../flex/flex.svelte';
import type { GroupProps } from './types';
let {
grow = false,
attached = false,
direction = 'row',
gap,
class: className,
children,
...restProps
}: GroupProps = $props();
// `attached` implies `gap=0`; an explicit `gap` prop still wins.
const effectiveGap = $derived(gap !== undefined ? gap : attached ? 0 : undefined);
const toBoolAttr = (value: boolean) => (value ? '' : undefined);
</script>
<Flex
{...restProps}
{direction}
gap={effectiveGap}
class={className}
data-group=""
data-grow={toBoolAttr(grow)}
data-attached={toBoolAttr(attached)}
>
{@render children?.()}
</Flex>

@ -0,0 +1,12 @@
// Group — inline cluster (flex row by default).
//
// import { Group } from '$uix/eidos/components/group';
//
// <Group gap={2}>…</Group>
// <Group attached>…</Group>
// <Group grow>…</Group>
import Group from './group.svelte';
export { Group };
export default Group;
export type { GroupProps } from './types';

@ -0,0 +1,19 @@
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { FlexProps } from '../flex/types';
export type GroupProps = Omit<FlexProps, 'wrap'> & {
/**
* Make every child flex to equal width (`flex: 1 1 0`). Useful for
* toolbars where buttons should fill the row evenly.
* @default false
*/
grow?: boolean;
/**
* Attach children edge-to-edge, hiding adjacent borders/radii to
* produce a single visual unit (segmented control, button group).
* @default false
*/
attached?: boolean;
/** @default 'row' */
direction?: ResponsiveProp<'row' | 'column'>;
};

@ -0,0 +1,10 @@
// Section — top-level section with block-axis padding.
//
// import { Section } from '$uix/eidos/components/section';
//
// <Section size="lg">…</Section>
import Section from './section.svelte';
export { Section };
export default Section;
export type { SectionProps, SectionSize } from './types';

@ -0,0 +1,20 @@
/*
* Section recipe — defines the size-keyed padding tokens locally. These
* are intentionally not in the generated foundation: Section is the
* sole consumer and the values live with the component that owns the
* scale.
*
* sm -> 32px
* md -> 48px
* lg -> 64px
* xl -> 96px
*/
[data-box][data-section] {
--section-padding-block-sm: var(--space-8);
--section-padding-block-md: var(--space-12);
--section-padding-block-lg: var(--space-16);
--section-padding-block-xl: calc(var(--space-16) * 1.5);
inline-size: 100%;
}

@ -0,0 +1,49 @@
<script lang="ts">
/**
* Eidos `<Section>` — top-level page section with consistent
* block-axis padding.
*
* Visually this is a Box with a default `padding-block` keyed off
* `size`. Per the batch-1 simplification we render as a `<div>` — if
* a consumer needs an actual `<section>` element for landmark
* semantics they can wrap or nest one.
*
* <Section size="lg">…</Section>
* <Section size="sm" paddingY={4}>…</Section>
*/
import { ActiveEidos } from '$uix/eidos';
import Box from '../box/box.svelte';
import type { SectionProps, SectionSize } from './types';
const SIZE_TO_PADDING: Record<SectionSize, string> = {
sm: 'var(--section-padding-block-sm)',
md: 'var(--section-padding-block-md)',
lg: 'var(--section-padding-block-lg)',
xl: 'var(--section-padding-block-xl)'
};
let {
size = 'lg',
paddingY,
class: className,
children,
...restProps
}: SectionProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'lg'));
const resolvedPaddingY = $derived(paddingY ?? SIZE_TO_PADDING[resolvedSize]);
</script>
<Box
{...restProps}
display="block"
width="100%"
paddingY={resolvedPaddingY}
class={className}
data-section=""
data-size={resolvedSize}
>
{@render children?.()}
</Box>

@ -0,0 +1,14 @@
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { BoxProps } from '../box/types';
export type SectionSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl'>;
export type SectionProps = BoxProps & {
/**
* Vertical (block-axis) padding. Maps to size-scoped padding tokens
* defined in `section.css`. Override per-instance by passing
* `paddingY` directly.
* @default 'lg'
*/
size?: ResponsiveProp<SectionSize>;
};

@ -0,0 +1,11 @@
// Stack — vertical (default) or horizontal stack.
//
// import { Stack } from '$uix/eidos/components/stack';
//
// <Stack gap={2}>…</Stack>
// <Stack direction="row" gap={3}>…</Stack>
import Stack from './stack.svelte';
export { Stack };
export default Stack;
export type { StackProps, StackDirection } from './types';

@ -0,0 +1,12 @@
/*
* Stack recipe — no per-axis override; Stack reuses the Flex `gap`
* channel directly. Marker selector exists so consumers can target
* `[data-stack]` for archetype-level rules.
*/
[data-box][data-flex][data-stack] {
/* All visual behavior comes from Flex; this rule exists so the marker
* selector matches at least one declaration and isn't dropped by the
* build's empty-rule pruning. */
display: var(--box-display, flex);
}

@ -0,0 +1,27 @@
<script lang="ts">
/**
* Eidos `<Stack>` — vertical (default) or horizontal stack with a `gap`.
* Thin specialization of `<Flex>`: locks `wrap` to nowrap-by-default,
* defaults `direction` to `column`, and routes `gap` through the
* shared Flex gap channel.
*
* <Stack gap={2}>…</Stack>
* <Stack direction="row" gap={3} align="center">…</Stack>
*/
import Flex from '../flex/flex.svelte';
import type { StackProps } from './types';
let {
direction = 'column',
gap,
align,
justify,
class: className,
children,
...restProps
}: StackProps = $props();
</script>
<Flex {...restProps} {direction} {gap} {align} {justify} class={className} data-stack="">
{@render children?.()}
</Flex>

@ -0,0 +1,24 @@
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { FlexProps } from '../flex/types';
import type {
LayoutAlignItems,
LayoutFlexDirection,
LayoutJustifyContent,
LayoutSpaceValue
} from '../_layout/shared';
export type StackDirection = Extract<
LayoutFlexDirection,
'row' | 'row-reverse' | 'column' | 'column-reverse'
>;
export type StackProps = Omit<
FlexProps,
'direction' | 'wrap' | 'inline' | 'rowGap' | 'columnGap'
> & {
/** @default 'column' */
direction?: ResponsiveProp<StackDirection>;
gap?: ResponsiveProp<LayoutSpaceValue>;
align?: ResponsiveProp<LayoutAlignItems>;
justify?: ResponsiveProp<LayoutJustifyContent>;
};

@ -0,0 +1,10 @@
// Wrap — flex row that always wraps.
//
// import { Wrap } from '$uix/eidos/components/wrap';
//
// <Wrap gap={2}>{#each tags as t}<Tag>{t}</Tag>{/each}</Wrap>
import Wrap from './wrap.svelte';
export { Wrap };
export default Wrap;
export type { WrapProps } from './types';

@ -0,0 +1,11 @@
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { FlexProps } from '../flex/types';
import type { LayoutAlignItems, LayoutJustifyContent, LayoutSpaceValue } from '../_layout/shared';
export type WrapProps = Omit<FlexProps, 'direction' | 'wrap'> & {
gap?: ResponsiveProp<LayoutSpaceValue>;
rowGap?: ResponsiveProp<LayoutSpaceValue>;
columnGap?: ResponsiveProp<LayoutSpaceValue>;
align?: ResponsiveProp<LayoutAlignItems>;
justify?: ResponsiveProp<LayoutJustifyContent>;
};

@ -0,0 +1,9 @@
/*
* Wrap recipe — locks `flex-wrap: wrap` over the Flex default. The
* data-wrap attribute also gates any future archetype rules that want
* to distinguish "wrap by intent" from "wrap=wrap configured on a Flex".
*/
[data-box][data-flex][data-wrap] {
flex-wrap: wrap;
}

@ -0,0 +1,36 @@
<script lang="ts">
/**
* Eidos `<Wrap>` — flex row that always wraps. Use when you want a
* cluster of tags / chips / pills that should reflow across multiple
* lines on narrow viewports.
*
* <Wrap gap={2}>{#each tags as t}<Tag>{t}</Tag>{/each}</Wrap>
*/
import Flex from '../flex/flex.svelte';
import type { WrapProps } from './types';
let {
gap,
rowGap,
columnGap,
align,
justify,
class: className,
children,
...restProps
}: WrapProps = $props();
</script>
<Flex
{...restProps}
wrap="wrap"
{gap}
{rowGap}
{columnGap}
{align}
{justify}
class={className}
data-wrap=""
>
{@render children?.()}
</Flex>

@ -60,6 +60,15 @@
@import './events.css';
/* ── Component recipes ──────────────────────────────────────────────────── */
/* Layout primitives load first so component recipes can layer on top of them. */
@import './components/box/box.css';
@import './components/flex/flex.css';
@import './components/grid/grid.css';
@import './components/stack/stack.css';
@import './components/group/group.css';
@import './components/wrap/wrap.css';
@import './components/container/container.css';
@import './components/section/section.css';
@import './components/icon/icon.css';
@import './components/avatar/avatar.css';
@import './components/breadcrumb/breadcrumb.css';

@ -0,0 +1,133 @@
<script lang="ts">
import { Box } from '$uix/eidos/components/box';
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Box</div>
<h1 data-uix-page-title>Box</h1>
<p data-uix-page-lede>
Universal box-model utility. Every prop maps to a CSS custom property; unset
props fall through to the normal cascade.
</p>
</header>
<section>
<h2>Live example</h2>
<Box
padding={4}
maxWidth={480}
display="flex"
gap={3}
style="background: var(--color-surface-1); border: 1px solid var(--color-border); border-radius: 8px"
>
<Box padding={2} style="background: var(--color-surface-2); border-radius: 4px">
Cell A
</Box>
<Box padding={2} style="background: var(--color-surface-2); border-radius: 4px">
Cell B
</Box>
<Box padding={2} style="background: var(--color-surface-2); border-radius: 4px">
Cell C
</Box>
</Box>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>display</code></td>
<td>LayoutDisplay</td>
<td>block · inline · flex · grid · …</td>
</tr>
<tr>
<td><code>width</code> / <code>minWidth</code> / <code>maxWidth</code></td>
<td>number | string</td>
<td>number → px, string passes through</td>
</tr>
<tr>
<td><code>height</code> / <code>minHeight</code> / <code>maxHeight</code></td>
<td>number | string</td>
<td>same as above</td>
</tr>
<tr>
<td><code>padding</code> / <code>paddingX</code> / <code>paddingY</code></td>
<td>number | string</td>
<td>number → <code>var(--space-N)</code></td>
</tr>
<tr>
<td>
<code>paddingTop</code> / <code>paddingRight</code> /
<code>paddingBottom</code> / <code>paddingLeft</code>
</td>
<td>number | string</td>
<td>per-side overrides</td>
</tr>
<tr>
<td><code>margin</code> / <code>marginX</code> / <code>marginY</code> / sides</td>
<td>number | string</td>
<td>same mapping as padding</td>
</tr>
<tr>
<td><code>gap</code></td>
<td>number | string</td>
<td>for flex/grid containers</td>
</tr>
<tr>
<td>
<code>position</code> / <code>top</code> / <code>right</code> /
<code>bottom</code> / <code>left</code>
</td>
<td>various</td>
<td>position + insets</td>
</tr>
<tr>
<td><code>inset</code> / <code>insetX</code> / <code>insetY</code></td>
<td>number | string</td>
<td>shorthand insets</td>
</tr>
<tr>
<td><code>overflow</code> / <code>overflowX</code> / <code>overflowY</code></td>
<td>LayoutOverflow</td>
<td>visible · hidden · clip · scroll · auto</td>
</tr>
<tr>
<td>
<code>flex</code> / <code>grow</code> / <code>shrink</code> / <code>basis</code> /
<code>order</code> / <code>alignSelf</code> / <code>justifySelf</code>
</td>
<td>various</td>
<td>item-side flex/grid props</td>
</tr>
<tr>
<td><code>style</code></td>
<td>string</td>
<td>extra inline style; merged after Box vars</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>radix-themes Box</strong> — same idea: token-driven shorthand for the
box model; we mirror the responsive prop shape.
</li>
<li>
<strong>chakra-ui Box</strong> — origin of the per-side / shorthand prop split
(<code>paddingX</code>, <code>paddingY</code>, …).
</li>
</ul>
</section>
</div>

@ -0,0 +1,82 @@
<script lang="ts">
import { Container, type ContainerSize } from '$uix/eidos/components/container';
const sizes: ContainerSize[] = ['sm', 'md', 'lg', 'xl', 'xxl', 'full'];
let size = $state<ContainerSize>('lg');
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Container</div>
<h1 data-uix-page-title>Container</h1>
<p data-uix-page-lede>
Max-width content container. Caps content at the
<code>--container-width-{'{size}'}</code> token and centers it.
</p>
</header>
<section>
<h2>Live example</h2>
<div style="display: flex; gap: 8px; margin-bottom: 12px">
{#each sizes as s (s)}
<label style="display: inline-flex; gap: 4px; align-items: center">
<input type="radio" name="size" value={s} bind:group={size} />
<span><code>{s}</code></span>
</label>
{/each}
</div>
<Container
{size}
padding={4}
style="background: var(--color-surface-1); border: 1px dashed var(--color-border); border-radius: 8px"
>
<p style="margin: 0">Container content (size: <code>{size}</code>).</p>
</Container>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Default</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>size</code></td>
<td>'xl'</td>
<td>sm · md · lg · xl · xxl · full</td>
</tr>
<tr>
<td><code>align</code></td>
<td>'center'</td>
<td>left · center · right</td>
</tr>
<tr>
<td><code>paddingX</code></td>
<td><code>var(--container-padding-inline)</code></td>
<td>override per-instance</td>
</tr>
<tr>
<td colspan="3"><em>… plus every <code>BoxProps</code> prop</em></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>radix-themes Container</strong> — same surface; sizes map to fixed
pixel widths via tokens.
</li>
<li>
<strong>chakra-ui Container</strong> — origin of the centered max-width pattern.
</li>
</ul>
</section>
</div>

@ -0,0 +1,95 @@
<script lang="ts">
import { Flex } from '$uix/eidos/components/flex';
import { Box } from '$uix/eidos/components/box';
const cellStyle =
'background: var(--color-surface-2); border-radius: 4px; padding: 8px 12px';
</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 with direction, wrap, alignment and gap.
</p>
</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>
</div>

@ -0,0 +1,92 @@
<script lang="ts">
import { Grid } from '$uix/eidos/components/grid';
import { Box } from '$uix/eidos/components/box';
const cellStyle =
'background: var(--color-surface-2); border-radius: 4px; padding: 12px; text-align: center';
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Grid</div>
<h1 data-uix-page-title>Grid</h1>
<p data-uix-page-lede>
<code>display:grid</code> container with track and placement props.
</p>
</header>
<section>
<h2>Live example</h2>
<Grid
templateColumns="repeat(3, 1fr)"
gap={3}
padding={4}
style="background: var(--color-surface-1); border: 1px solid var(--color-border); border-radius: 8px"
>
<Box style={cellStyle}>1</Box>
<Box style={cellStyle}>2</Box>
<Box style={cellStyle}>3</Box>
<Box style={cellStyle}>4</Box>
<Box style={cellStyle}>5</Box>
<Box style={cellStyle}>6</Box>
</Grid>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>templateColumns</code> / <code>templateRows</code></td>
<td>raw <code>grid-template-*</code> strings</td>
</tr>
<tr>
<td><code>autoColumns</code> / <code>autoRows</code></td>
<td>implicit track sizes</td>
</tr>
<tr>
<td><code>autoFlow</code></td>
<td>row · column · dense · row dense · column dense</td>
</tr>
<tr>
<td><code>gap</code> / <code>rowGap</code> / <code>columnGap</code></td>
<td>row/columnGap override <code>gap</code> per axis</td>
</tr>
<tr>
<td><code>align</code> / <code>justify</code></td>
<td><code>align-items</code> / <code>justify-content</code></td>
</tr>
<tr>
<td><code>placeItems</code> / <code>placeContent</code></td>
<td>raw shorthand strings</td>
</tr>
<tr>
<td><code>gridColumn</code> / <code>gridRow</code> / <code>gridArea</code></td>
<td>per-item placement</td>
</tr>
<tr>
<td colspan="2"><em>… plus every <code>BoxProps</code> prop except <code>display</code></em></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>CSS Grid spec</strong> — props are 1:1 with the CSS properties they wrap.
</li>
<li>
<strong>radix-themes Grid</strong> — same prop names (<code>columns</code> is
called <code>templateColumns</code> here, for symmetry with <code>templateRows</code>).
</li>
</ul>
</section>
</div>

@ -0,0 +1,94 @@
<script lang="ts">
import { Group } from '$uix/eidos/components/group';
const btnStyle =
'background: var(--color-surface-2); border: 1px solid var(--color-border); border-radius: 6px; padding: 6px 12px; font: inherit';
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Group</div>
<h1 data-uix-page-title>Group</h1>
<p data-uix-page-lede>
Inline cluster of children. Supports <code>grow</code> (equal-width children)
and <code>attached</code> (edge-merged children).
</p>
</header>
<section>
<h2>Default</h2>
<Group gap={2}>
<button type="button" style={btnStyle}>One</button>
<button type="button" style={btnStyle}>Two</button>
<button type="button" style={btnStyle}>Three</button>
</Group>
</section>
<section>
<h2>Attached (segmented control)</h2>
<Group attached>
<button type="button" style={btnStyle}>Day</button>
<button type="button" style={btnStyle}>Week</button>
<button type="button" style={btnStyle}>Month</button>
</Group>
</section>
<section>
<h2>Grow (equal widths)</h2>
<Group grow gap={2} style="width: 320px">
<button type="button" style={btnStyle}>Cancel</button>
<button type="button" style={btnStyle}>Save</button>
</Group>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Default</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>direction</code></td>
<td>'row'</td>
<td>row · column</td>
</tr>
<tr>
<td><code>grow</code></td>
<td>false</td>
<td>children get <code>flex: 1 1 0</code></td>
</tr>
<tr>
<td><code>attached</code></td>
<td>false</td>
<td>collapses inner radii + overlaps 1px to merge borders</td>
</tr>
<tr>
<td><code>gap</code></td>
<td>0 if <code>attached</code></td>
<td>defaults to <code>0</code> when <code>attached</code> is on</td>
</tr>
<tr>
<td colspan="3"><em>… plus FlexProps minus <code>wrap</code></em></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>mantine Group</strong> — same name and intent.
</li>
<li>
<strong>chakra-ui ButtonGroup</strong> — origin of the
<code>attached</code> behavior, generalized here.
</li>
</ul>
</section>
</div>

@ -0,0 +1,97 @@
<script lang="ts">
import { Section, type SectionSize } from '$uix/eidos/components/section';
const sizes: SectionSize[] = ['sm', 'md', 'lg', 'xl'];
let size = $state<SectionSize>('lg');
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Section</div>
<h1 data-uix-page-title>Section</h1>
<p data-uix-page-lede>
Top-level page section with consistent block-axis padding scaled by
<code>size</code>.
</p>
</header>
<section>
<h2>Live example</h2>
<div style="display: flex; gap: 8px; margin-bottom: 12px">
{#each sizes as s (s)}
<label style="display: inline-flex; gap: 4px; align-items: center">
<input type="radio" name="size" value={s} bind:group={size} />
<span><code>{s}</code></span>
</label>
{/each}
</div>
<Section
{size}
style="background: var(--color-surface-1); border: 1px dashed var(--color-border); border-radius: 8px"
>
<p style="margin: 0; text-align: center">
Section content (size: <code>{size}</code>).
</p>
</Section>
</section>
<section>
<h2>Padding scale</h2>
<table>
<thead>
<tr>
<th>Size</th>
<th>Block padding</th>
</tr>
</thead>
<tbody>
<tr><td><code>sm</code></td><td>var(--space-8) = 32px</td></tr>
<tr><td><code>md</code></td><td>var(--space-12) = 48px</td></tr>
<tr><td><code>lg</code></td><td>var(--space-16) = 64px</td></tr>
<tr><td><code>xl</code></td><td>calc(var(--space-16) * 1.5) = 96px</td></tr>
</tbody>
</table>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Default</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>size</code></td>
<td>'lg'</td>
<td>sm · md · lg · xl</td>
</tr>
<tr>
<td><code>paddingY</code></td>
<td>—</td>
<td>override the size-keyed default</td>
</tr>
<tr>
<td colspan="3"><em>… plus every <code>BoxProps</code> prop</em></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>radix-themes Section</strong> — same intent (size-keyed block padding
for vertical rhythm).
</li>
<li>
<strong>HTML <code>&lt;section&gt;</code></strong> — Section is a `&lt;div&gt;`
in batch 1; wrap with a real <code>&lt;section&gt;</code> when you need landmark semantics.
</li>
</ul>
</section>
</div>

@ -0,0 +1,92 @@
<script lang="ts">
import { Stack } from '$uix/eidos/components/stack';
import { Box } from '$uix/eidos/components/box';
const cellStyle = 'background: var(--color-surface-2); border-radius: 4px; padding: 8px 12px';
</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>
Vertical (default) or horizontal stack. Thin alias over Flex with
<code>direction='column'</code>.
</p>
</header>
<section>
<h2>Vertical (default)</h2>
<Stack
gap={2}
padding={4}
style="background: var(--color-surface-1); border: 1px solid var(--color-border); border-radius: 8px"
>
<Box style={cellStyle}>Row 1</Box>
<Box style={cellStyle}>Row 2</Box>
<Box style={cellStyle}>Row 3</Box>
</Stack>
</section>
<section>
<h2>Horizontal</h2>
<Stack
direction="row"
gap={2}
padding={4}
align="center"
style="background: var(--color-surface-1); border: 1px solid var(--color-border); border-radius: 8px"
>
<Box style={cellStyle}>A</Box>
<Box style={cellStyle}>B</Box>
<Box style={cellStyle}>C</Box>
</Stack>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Default</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>direction</code></td>
<td>'column'</td>
<td>row · row-reverse · column · column-reverse</td>
</tr>
<tr>
<td><code>gap</code></td>
<td>—</td>
<td>maps to <code>var(--space-N)</code> when numeric</td>
</tr>
<tr>
<td><code>align</code> / <code>justify</code></td>
<td>—</td>
<td>inherited from Flex</td>
</tr>
<tr>
<td colspan="3"><em>… plus FlexProps minus <code>wrap</code>, <code>inline</code>, <code>rowGap</code>, <code>columnGap</code></em></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>chakra-ui Stack / VStack / HStack</strong> — same intent; we collapse
the three into one component with a <code>direction</code> prop.
</li>
<li>
<strong>radix-themes Flex direction='column'</strong> — equivalent shape; Stack
is the named shortcut.
</li>
</ul>
</section>
</div>

@ -0,0 +1,81 @@
<script lang="ts">
import { Wrap } from '$uix/eidos/components/wrap';
const tagStyle =
'background: var(--color-surface-2); border: 1px solid var(--color-border); border-radius: 999px; padding: 4px 10px; font-size: 0.875rem';
const tags = [
'design',
'systems',
'reactive',
'svelte',
'typescript',
'css',
'tokens',
'accessibility',
'i18n',
'documentation'
];
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Wrap</div>
<h1 data-uix-page-title>Wrap</h1>
<p data-uix-page-lede>
Flex row that always wraps. Use for tag clouds, chip groups and any
row-of-children that should reflow on narrow viewports.
</p>
</header>
<section>
<h2>Live example</h2>
<Wrap
gap={2}
padding={4}
style="background: var(--color-surface-1); border: 1px solid var(--color-border); border-radius: 8px; max-width: 360px"
>
{#each tags as t (t)}
<span style={tagStyle}>{t}</span>
{/each}
</Wrap>
</section>
<section>
<h2>Props</h2>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>gap</code> / <code>rowGap</code> / <code>columnGap</code></td>
<td>independent row/column gaps</td>
</tr>
<tr>
<td><code>align</code> / <code>justify</code></td>
<td>main- and cross-axis alignment</td>
</tr>
<tr>
<td colspan="2"><em>… plus FlexProps minus <code>direction</code> and <code>wrap</code></em></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Reference</h2>
<ul>
<li>
<strong>mantine Group wrap</strong> — same intent; we name the wrap-locked
variant explicitly to make intent obvious at the call site.
</li>
<li>
<strong>chakra-ui Wrap</strong> — origin of the dedicated wrap component.
</li>
</ul>
</section>
</div>
Loading…
Cancel
Save

Powered by TurnKey Linux.