diff --git a/src/uix/eidos/components/_layout/index.ts b/src/uix/eidos/components/_layout/index.ts new file mode 100644 index 000000000..315bc7535 --- /dev/null +++ b/src/uix/eidos/components/_layout/index.ts @@ -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'; + * … + * … + * + * 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'; diff --git a/src/uix/eidos/components/_layout/shared.ts b/src/uix/eidos/components/_layout/shared.ts new file mode 100644 index 000000000..a30d1666f --- /dev/null +++ b/src/uix/eidos/components/_layout/shared.ts @@ -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}`; +} diff --git a/src/uix/eidos/components/box/box.css b/src/uix/eidos/components/box/box.css new file mode 100644 index 000000000..83d6b1466 --- /dev/null +++ b/src/uix/eidos/components/box/box.css @@ -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); +} diff --git a/src/uix/eidos/components/box/box.svelte b/src/uix/eidos/components/box/box.svelte new file mode 100644 index 000000000..a79e3a621 --- /dev/null +++ b/src/uix/eidos/components/box/box.svelte @@ -0,0 +1,129 @@ + + +
+ {@render children?.()} +
diff --git a/src/uix/eidos/components/box/index.ts b/src/uix/eidos/components/box/index.ts new file mode 100644 index 000000000..1f96a6ffa --- /dev/null +++ b/src/uix/eidos/components/box/index.ts @@ -0,0 +1,10 @@ +// Box — universal box-model utility. +// +// import { Box } from '$uix/eidos/components/box'; +// +// … +import Box from './box.svelte'; + +export { Box }; +export default Box; +export type { BoxProps } from './types'; diff --git a/src/uix/eidos/components/box/types.ts b/src/uix/eidos/components/box/types.ts new file mode 100644 index 000000000..b404a0307 --- /dev/null +++ b/src/uix/eidos/components/box/types.ts @@ -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, 'style' | 'children'> & { + /** `display`. */ + display?: ResponsiveProp; + /** `width`. Number -> px, string passes through. */ + width?: ResponsiveProp; + /** `min-width`. */ + minWidth?: ResponsiveProp; + /** `max-width`. */ + maxWidth?: ResponsiveProp; + /** `height`. */ + height?: ResponsiveProp; + /** `min-height`. */ + minHeight?: ResponsiveProp; + /** `max-height`. */ + maxHeight?: ResponsiveProp; + /** `padding`. Number -> `--space-N`, string passes through. */ + padding?: ResponsiveProp; + /** `padding-inline` (left+right). */ + paddingX?: ResponsiveProp; + /** `padding-block` (top+bottom). */ + paddingY?: ResponsiveProp; + paddingTop?: ResponsiveProp; + paddingRight?: ResponsiveProp; + paddingBottom?: ResponsiveProp; + paddingLeft?: ResponsiveProp; + /** `margin`. */ + margin?: ResponsiveProp; + /** `margin-inline`. */ + marginX?: ResponsiveProp; + /** `margin-block`. */ + marginY?: ResponsiveProp; + marginTop?: ResponsiveProp; + marginRight?: ResponsiveProp; + marginBottom?: ResponsiveProp; + marginLeft?: ResponsiveProp; + /** `gap` (flex/grid). */ + gap?: ResponsiveProp; + /** `position`. */ + position?: ResponsiveProp; + top?: ResponsiveProp; + right?: ResponsiveProp; + bottom?: ResponsiveProp; + left?: ResponsiveProp; + /** Shorthand for all four insets. */ + inset?: ResponsiveProp; + /** `inset-inline` (left+right). */ + insetX?: ResponsiveProp; + /** `inset-block` (top+bottom). */ + insetY?: ResponsiveProp; + /** `overflow`. */ + overflow?: ResponsiveProp; + overflowX?: ResponsiveProp; + overflowY?: ResponsiveProp; + /** Flex item: `flex` shorthand. */ + flex?: ResponsiveProp; + /** Flex item: `flex-grow`. */ + grow?: ResponsiveProp; + /** Flex item: `flex-shrink`. */ + shrink?: ResponsiveProp; + /** Flex item: `flex-basis`. */ + basis?: ResponsiveProp; + /** Flex/grid item: `order`. */ + order?: ResponsiveProp; + /** Flex/grid item: `align-self`. */ + alignSelf?: ResponsiveProp; + /** Grid item: `justify-self`. */ + justifySelf?: ResponsiveProp; + /** Extra inline style. Merged after the box's own CSS variable declarations. */ + style?: string; + /** Extra class names. */ + class?: string; + children?: Snippet; +}; diff --git a/src/uix/eidos/components/container/container.css b/src/uix/eidos/components/container/container.css new file mode 100644 index 000000000..eb9a9a2b2 --- /dev/null +++ b/src/uix/eidos/components/container/container.css @@ -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)); +} diff --git a/src/uix/eidos/components/container/container.svelte b/src/uix/eidos/components/container/container.svelte new file mode 100644 index 000000000..0cec2663b --- /dev/null +++ b/src/uix/eidos/components/container/container.svelte @@ -0,0 +1,65 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/container/index.ts b/src/uix/eidos/components/container/index.ts new file mode 100644 index 000000000..73f229df7 --- /dev/null +++ b/src/uix/eidos/components/container/index.ts @@ -0,0 +1,10 @@ +// Container — max-width centered content container. +// +// import { Container } from '$uix/eidos/components/container'; +// +// … +import Container from './container.svelte'; + +export { Container }; +export default Container; +export type { ContainerProps, ContainerSize, ContainerAlign } from './types'; diff --git a/src/uix/eidos/components/container/types.ts b/src/uix/eidos/components/container/types.ts new file mode 100644 index 000000000..a883194a1 --- /dev/null +++ b/src/uix/eidos/components/container/types.ts @@ -0,0 +1,16 @@ +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; +import type { BoxProps } from '../box/types'; + +export type ContainerSize = Extract; +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; + /** Horizontal alignment within the parent. @default 'center' */ + align?: ResponsiveProp; +}; diff --git a/src/uix/eidos/components/flex/flex.css b/src/uix/eidos/components/flex/flex.css new file mode 100644 index 000000000..570010fbc --- /dev/null +++ b/src/uix/eidos/components/flex/flex.css @@ -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)); +} diff --git a/src/uix/eidos/components/flex/flex.svelte b/src/uix/eidos/components/flex/flex.svelte new file mode 100644 index 000000000..e1b20ce71 --- /dev/null +++ b/src/uix/eidos/components/flex/flex.svelte @@ -0,0 +1,55 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/flex/index.ts b/src/uix/eidos/components/flex/index.ts new file mode 100644 index 000000000..e3659fbab --- /dev/null +++ b/src/uix/eidos/components/flex/index.ts @@ -0,0 +1,10 @@ +// Flex — display:flex container. +// +// import { Flex } from '$uix/eidos/components/flex'; +// +// … +import Flex from './flex.svelte'; + +export { Flex }; +export default Flex; +export type { FlexProps } from './types'; diff --git a/src/uix/eidos/components/flex/types.ts b/src/uix/eidos/components/flex/types.ts new file mode 100644 index 000000000..9a206cf07 --- /dev/null +++ b/src/uix/eidos/components/flex/types.ts @@ -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 & { + /** Use `inline-flex` instead of `flex`. @default false */ + inline?: boolean; + /** `flex-direction`. @default 'row' */ + direction?: ResponsiveProp; + /** `align-items`. */ + align?: ResponsiveProp; + /** `justify-content`. */ + justify?: ResponsiveProp; + /** `flex-wrap`. @default 'nowrap' */ + wrap?: ResponsiveProp; + /** `row-gap`. Overrides `gap` for the row axis. */ + rowGap?: ResponsiveProp; + /** `column-gap`. Overrides `gap` for the column axis. */ + columnGap?: ResponsiveProp; +}; diff --git a/src/uix/eidos/components/grid/grid.css b/src/uix/eidos/components/grid/grid.css new file mode 100644 index 000000000..99299e802 --- /dev/null +++ b/src/uix/eidos/components/grid/grid.css @@ -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)); +} diff --git a/src/uix/eidos/components/grid/grid.svelte b/src/uix/eidos/components/grid/grid.svelte new file mode 100644 index 000000000..01c3399e5 --- /dev/null +++ b/src/uix/eidos/components/grid/grid.svelte @@ -0,0 +1,63 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/grid/index.ts b/src/uix/eidos/components/grid/index.ts new file mode 100644 index 000000000..6eabb3ed3 --- /dev/null +++ b/src/uix/eidos/components/grid/index.ts @@ -0,0 +1,10 @@ +// Grid — display:grid container. +// +// import { Grid } from '$uix/eidos/components/grid'; +// +// … +import Grid from './grid.svelte'; + +export { Grid }; +export default Grid; +export type { GridProps } from './types'; diff --git a/src/uix/eidos/components/grid/types.ts b/src/uix/eidos/components/grid/types.ts new file mode 100644 index 000000000..b1e4bad0f --- /dev/null +++ b/src/uix/eidos/components/grid/types.ts @@ -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 & { + /** `grid-template-columns`. */ + templateColumns?: ResponsiveProp; + /** `grid-template-rows`. */ + templateRows?: ResponsiveProp; + /** `grid-auto-columns`. */ + autoColumns?: ResponsiveProp; + /** `grid-auto-rows`. */ + autoRows?: ResponsiveProp; + /** `grid-auto-flow`. */ + autoFlow?: ResponsiveProp; + /** `column-gap`. */ + columnGap?: ResponsiveProp; + /** `row-gap`. */ + rowGap?: ResponsiveProp; + /** Shorthand for both `row-gap` and `column-gap`. */ + gap?: ResponsiveProp; + /** `align-items`. */ + align?: ResponsiveProp; + /** `justify-content`. */ + justify?: ResponsiveProp; + /** `place-items` shorthand. */ + placeItems?: ResponsiveProp; + /** `place-content` shorthand. */ + placeContent?: ResponsiveProp; + /** Grid item: `grid-column`. */ + gridColumn?: ResponsiveProp; + /** Grid item: `grid-row`. */ + gridRow?: ResponsiveProp; + /** Grid item: `grid-area`. */ + gridArea?: ResponsiveProp; +}; diff --git a/src/uix/eidos/components/group/group.css b/src/uix/eidos/components/group/group.css new file mode 100644 index 000000000..8bfec804a --- /dev/null +++ b/src/uix/eidos/components/group/group.css @@ -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; +} diff --git a/src/uix/eidos/components/group/group.svelte b/src/uix/eidos/components/group/group.svelte new file mode 100644 index 000000000..ecfe4e048 --- /dev/null +++ b/src/uix/eidos/components/group/group.svelte @@ -0,0 +1,43 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/group/index.ts b/src/uix/eidos/components/group/index.ts new file mode 100644 index 000000000..a328cd476 --- /dev/null +++ b/src/uix/eidos/components/group/index.ts @@ -0,0 +1,12 @@ +// Group — inline cluster (flex row by default). +// +// import { Group } from '$uix/eidos/components/group'; +// +// … +// … +// … +import Group from './group.svelte'; + +export { Group }; +export default Group; +export type { GroupProps } from './types'; diff --git a/src/uix/eidos/components/group/types.ts b/src/uix/eidos/components/group/types.ts new file mode 100644 index 000000000..d18c96c84 --- /dev/null +++ b/src/uix/eidos/components/group/types.ts @@ -0,0 +1,19 @@ +import type { ResponsiveProp } from '$uix/eidos/lib/types'; +import type { FlexProps } from '../flex/types'; + +export type GroupProps = Omit & { + /** + * 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'>; +}; diff --git a/src/uix/eidos/components/section/index.ts b/src/uix/eidos/components/section/index.ts new file mode 100644 index 000000000..ab4ac2643 --- /dev/null +++ b/src/uix/eidos/components/section/index.ts @@ -0,0 +1,10 @@ +// Section — top-level section with block-axis padding. +// +// import { Section } from '$uix/eidos/components/section'; +// +//
…
+import Section from './section.svelte'; + +export { Section }; +export default Section; +export type { SectionProps, SectionSize } from './types'; diff --git a/src/uix/eidos/components/section/section.css b/src/uix/eidos/components/section/section.css new file mode 100644 index 000000000..8ef0ada75 --- /dev/null +++ b/src/uix/eidos/components/section/section.css @@ -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%; +} diff --git a/src/uix/eidos/components/section/section.svelte b/src/uix/eidos/components/section/section.svelte new file mode 100644 index 000000000..fb754af59 --- /dev/null +++ b/src/uix/eidos/components/section/section.svelte @@ -0,0 +1,49 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/section/types.ts b/src/uix/eidos/components/section/types.ts new file mode 100644 index 000000000..cb2de4744 --- /dev/null +++ b/src/uix/eidos/components/section/types.ts @@ -0,0 +1,14 @@ +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; +import type { BoxProps } from '../box/types'; + +export type SectionSize = Extract; + +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; +}; diff --git a/src/uix/eidos/components/stack/index.ts b/src/uix/eidos/components/stack/index.ts new file mode 100644 index 000000000..88017bfee --- /dev/null +++ b/src/uix/eidos/components/stack/index.ts @@ -0,0 +1,11 @@ +// Stack — vertical (default) or horizontal stack. +// +// import { Stack } from '$uix/eidos/components/stack'; +// +// … +// … +import Stack from './stack.svelte'; + +export { Stack }; +export default Stack; +export type { StackProps, StackDirection } from './types'; diff --git a/src/uix/eidos/components/stack/stack.css b/src/uix/eidos/components/stack/stack.css new file mode 100644 index 000000000..b01d118f4 --- /dev/null +++ b/src/uix/eidos/components/stack/stack.css @@ -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); +} diff --git a/src/uix/eidos/components/stack/stack.svelte b/src/uix/eidos/components/stack/stack.svelte new file mode 100644 index 000000000..0b8396885 --- /dev/null +++ b/src/uix/eidos/components/stack/stack.svelte @@ -0,0 +1,27 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/stack/types.ts b/src/uix/eidos/components/stack/types.ts new file mode 100644 index 000000000..a199c94ce --- /dev/null +++ b/src/uix/eidos/components/stack/types.ts @@ -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; + gap?: ResponsiveProp; + align?: ResponsiveProp; + justify?: ResponsiveProp; +}; diff --git a/src/uix/eidos/components/wrap/index.ts b/src/uix/eidos/components/wrap/index.ts new file mode 100644 index 000000000..ce5b40ee4 --- /dev/null +++ b/src/uix/eidos/components/wrap/index.ts @@ -0,0 +1,10 @@ +// Wrap — flex row that always wraps. +// +// import { Wrap } from '$uix/eidos/components/wrap'; +// +// {#each tags as t}{t}{/each} +import Wrap from './wrap.svelte'; + +export { Wrap }; +export default Wrap; +export type { WrapProps } from './types'; diff --git a/src/uix/eidos/components/wrap/types.ts b/src/uix/eidos/components/wrap/types.ts new file mode 100644 index 000000000..43294c256 --- /dev/null +++ b/src/uix/eidos/components/wrap/types.ts @@ -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 & { + gap?: ResponsiveProp; + rowGap?: ResponsiveProp; + columnGap?: ResponsiveProp; + align?: ResponsiveProp; + justify?: ResponsiveProp; +}; diff --git a/src/uix/eidos/components/wrap/wrap.css b/src/uix/eidos/components/wrap/wrap.css new file mode 100644 index 000000000..60827cb4f --- /dev/null +++ b/src/uix/eidos/components/wrap/wrap.css @@ -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; +} diff --git a/src/uix/eidos/components/wrap/wrap.svelte b/src/uix/eidos/components/wrap/wrap.svelte new file mode 100644 index 000000000..2b755b67d --- /dev/null +++ b/src/uix/eidos/components/wrap/wrap.svelte @@ -0,0 +1,36 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 586dddf40..21abbfce0 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -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'; diff --git a/web/routes/uix/components/box/+page.svelte b/web/routes/uix/components/box/+page.svelte new file mode 100644 index 000000000..2a2aa1aec --- /dev/null +++ b/web/routes/uix/components/box/+page.svelte @@ -0,0 +1,133 @@ + + +
+
+
Layout · Box
+

Box

+

+ Universal box-model utility. Every prop maps to a CSS custom property; unset + props fall through to the normal cascade. +

+
+ +
+

Live example

+ + + Cell A + + + Cell B + + + Cell C + + +
+ +
+

Props

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropTypeNotes
displayLayoutDisplayblock · inline · flex · grid · …
width / minWidth / maxWidthnumber | stringnumber → px, string passes through
height / minHeight / maxHeightnumber | stringsame as above
padding / paddingX / paddingYnumber | stringnumber → var(--space-N)
+ paddingTop / paddingRight / + paddingBottom / paddingLeft + number | stringper-side overrides
margin / marginX / marginY / sidesnumber | stringsame mapping as padding
gapnumber | stringfor flex/grid containers
+ position / top / right / + bottom / left + variousposition + insets
inset / insetX / insetYnumber | stringshorthand insets
overflow / overflowX / overflowYLayoutOverflowvisible · hidden · clip · scroll · auto
+ flex / grow / shrink / basis / + order / alignSelf / justifySelf + variousitem-side flex/grid props
stylestringextra inline style; merged after Box vars
+
+ +
+

Reference

+
    +
  • + radix-themes Box — same idea: token-driven shorthand for the + box model; we mirror the responsive prop shape. +
  • +
  • + chakra-ui Box — origin of the per-side / shorthand prop split + (paddingX, paddingY, …). +
  • +
+
+
diff --git a/web/routes/uix/components/container/+page.svelte b/web/routes/uix/components/container/+page.svelte new file mode 100644 index 000000000..b178ae533 --- /dev/null +++ b/web/routes/uix/components/container/+page.svelte @@ -0,0 +1,82 @@ + + +
+
+
Layout · Container
+

Container

+

+ Max-width content container. Caps content at the + --container-width-{'{size}'} token and centers it. +

+
+ +
+

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. +
  • +
+
+
diff --git a/web/routes/uix/components/flex/+page.svelte b/web/routes/uix/components/flex/+page.svelte new file mode 100644 index 000000000..4fa80021d --- /dev/null +++ b/web/routes/uix/components/flex/+page.svelte @@ -0,0 +1,95 @@ + + +
+
+
Layout · Flex
+

Flex

+

+ display:flex container with direction, wrap, alignment and gap. +

+
+ +
+

Live example

+ + Start + Middle + End + +
+ +
+

Props

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropDefaultNotes
inlinefalseswitch to inline-flex
direction'row'row · row-reverse · column · column-reverse
wrap'nowrap'nowrap · wrap · wrap-reverse
align—stretch · start · center · end · baseline
justify—start · center · end · space-between · space-around · space-evenly
gap / rowGap / columnGap—row/columnGap override gap per axis
… plus every BoxProps prop except display
+
+ +
+

Reference

+
    +
  • + radix-themes Flex — same surface (direction, + align, justify, wrap, gap). +
  • +
  • + chakra-ui Flex — origin of the responsive Flex container API; + our prop names match. +
  • +
+
+
diff --git a/web/routes/uix/components/grid/+page.svelte b/web/routes/uix/components/grid/+page.svelte new file mode 100644 index 000000000..49873bff2 --- /dev/null +++ b/web/routes/uix/components/grid/+page.svelte @@ -0,0 +1,92 @@ + + +
+
+
Layout · Grid
+

Grid

+

+ display:grid container with track and placement props. +

+
+ +
+

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). +
  • +
+
+
diff --git a/web/routes/uix/components/group/+page.svelte b/web/routes/uix/components/group/+page.svelte new file mode 100644 index 000000000..11fde0e07 --- /dev/null +++ b/web/routes/uix/components/group/+page.svelte @@ -0,0 +1,94 @@ + + +
+
+
Layout · Group
+

Group

+

+ Inline cluster of children. Supports grow (equal-width children) + and attached (edge-merged children). +

+
+ +
+

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. +
  • +
+
+
diff --git a/web/routes/uix/components/section/+page.svelte b/web/routes/uix/components/section/+page.svelte new file mode 100644 index 000000000..6cfa68009 --- /dev/null +++ b/web/routes/uix/components/section/+page.svelte @@ -0,0 +1,97 @@ + + +
+
+
Layout · Section
+

Section

+

+ Top-level page section with consistent block-axis padding scaled by + size. +

+
+ +
+

Live example

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

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

+
+
+ +
+

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. +
  • +
+
+
diff --git a/web/routes/uix/components/stack/+page.svelte b/web/routes/uix/components/stack/+page.svelte new file mode 100644 index 000000000..11c4aac6b --- /dev/null +++ b/web/routes/uix/components/stack/+page.svelte @@ -0,0 +1,92 @@ + + +
+
+
Layout · Stack
+

Stack

+

+ Vertical (default) or horizontal stack. Thin alias over Flex with + direction='column'. +

+
+ +
+

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. +
  • +
+
+
diff --git a/web/routes/uix/components/wrap/+page.svelte b/web/routes/uix/components/wrap/+page.svelte new file mode 100644 index 000000000..98b878e5e --- /dev/null +++ b/web/routes/uix/components/wrap/+page.svelte @@ -0,0 +1,81 @@ + + +
+
+
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. +

+
+ +
+

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