;
+type RowComponent = React.ComponentType
;
+
+const rowTypes = new Set();
+
+const markAsRow = (Component: T) => {
+ rowTypes.add(Component);
+ return Component;
+};
+
+export const isRowType = (type: unknown): type is RowComponent => {
+ return typeof type === 'function' && rowTypes.has(type as RowComponent);
+};
+
+export const Row = markAsRow((_props: Props): React.ReactElement => {
+ throw new Error('Row must be a direct child of Rows.');
+}) as RowComponent & {
+ from: (Component: T) => T;
+};
+
+Row.from = markAsRow;
+Row.displayName = 'Row';
diff --git a/packages/stacks/src/components/Rows.tsx b/packages/stacks/src/components/Rows.tsx
new file mode 100644
index 00000000..15451ff2
--- /dev/null
+++ b/packages/stacks/src/components/Rows.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react';
+
+import type { AxisX, AxisY, Flex, ResponsiveProp, Space } from '../types';
+import { Box } from './Box';
+import { isRowType, Row } from './Row';
+
+type BoxProps = Omit, 'alignX' | 'alignY' | 'direction' | 'gapX' | 'gapY'>;
+type RowProps = React.ComponentProps;
+type RowElement = React.ReactElement;
+
+type RowsProps = {
+ readonly children?: RowElement | readonly RowElement[];
+ readonly defaultFlex?: ResponsiveProp;
+ readonly alignX?: ResponsiveProp;
+ readonly alignY?: ResponsiveProp;
+};
+
+type Props = BoxProps & RowsProps;
+
+const getRowProps = (child: React.ReactNode) => {
+ if (React.isValidElement(child) && isRowType(child.type)) {
+ return child.props;
+ }
+
+ return undefined;
+};
+
+const resolveRowFlex = (props: RowProps, defaultFlex: ResponsiveProp) => {
+ return props.height === undefined ? (props.flex ?? defaultFlex) : 'content';
+};
+
+export const Rows = (props: Props) => {
+ const { children, defaultFlex = 'fluid', ...rest } = props;
+
+ return (
+
+ {React.Children.map(children as React.ReactNode, (child) => {
+ const rowProps = getRowProps(child);
+
+ if (rowProps === undefined) {
+ throw new Error('Rows accepts only Row components as direct children.');
+ }
+
+ if (React.isValidElement(child) && child.type !== Row) {
+ return React.cloneElement(child, {
+ flex: resolveRowFlex(rowProps, defaultFlex),
+ });
+ }
+
+ const { children, ...rest } = rowProps;
+
+ return (
+
+ {children}
+
+ );
+ })}
+
+ );
+};
+
+Rows.displayName = 'Rows';
diff --git a/packages/stacks/src/components/Stack.tsx b/packages/stacks/src/components/Stack.tsx
new file mode 100644
index 00000000..73e9b89b
--- /dev/null
+++ b/packages/stacks/src/components/Stack.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import type { ViewStyle } from 'react-native';
+import { StyleSheet } from 'react-native-unistyles';
+
+import type { AxisX, AxisY, ResponsiveProp } from '../types';
+import { flattenChildren, intersperse, resolveFlexAlignment, resolveResponsiveStyle } from '../utils';
+import { Box } from './Box';
+
+type BoxProps = Omit, 'alignX' | 'alignY' | 'direction' | 'gapX' | 'gapY'>;
+
+type StackProps = {
+ readonly horizontal?: ResponsiveProp;
+ readonly align?: ResponsiveProp;
+ readonly divider?: React.ReactElement;
+};
+
+export type Props = BoxProps & StackProps;
+
+export const Stack = (props: Props) => {
+ const { children, flex = 'content', horizontal, align, divider, style, ...rest } = props;
+
+ return (
+
+ {React.isValidElement(divider)
+ ? flattenChildren(intersperse(React.Children.toArray(children), divider))
+ : children}
+
+ );
+};
+
+Stack.displayName = 'Stack';
+
+const styles = StyleSheet.create({
+ from: (props: Omit) => {
+ const { align, horizontal } = props;
+ const { alignItems, justifyContent } = resolveFlexAlignment({
+ alignX: align,
+ alignY: align,
+ horizontal,
+ });
+
+ return {
+ flexDirection: resolveResponsiveStyle(horizontal, {
+ false: 'column',
+ true: 'row',
+ }),
+ alignItems,
+ justifyContent,
+ };
+ },
+});
diff --git a/packages/stacks/src/components/Tiles.tsx b/packages/stacks/src/components/Tiles.tsx
new file mode 100644
index 00000000..d70973c0
--- /dev/null
+++ b/packages/stacks/src/components/Tiles.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react';
+import type { ViewStyle } from 'react-native';
+import { StyleSheet } from 'react-native-unistyles';
+
+import type { AxisY, ResponsiveProp, Space } from '../types';
+import { resolveFlexBasis, resolveResponsiveStyle } from '../utils';
+import { Box } from './Box';
+
+type BoxProps = Omit, 'alignX' | 'alignY' | 'direction' | 'wrap'>;
+
+type TilesProps = {
+ readonly columns?: ResponsiveProp;
+ readonly fill?: ResponsiveProp;
+ readonly alignY?: ResponsiveProp;
+};
+
+type Props = BoxProps & TilesProps;
+
+export const Tiles = (props: Props) => {
+ const { children, columns = 1, fill = false, ...rest } = props;
+
+ return (
+
+ {React.Children.toArray(children).map((child, index) => {
+ return (
+
+ {child}
+
+ );
+ })}
+
+ );
+};
+
+Tiles.displayName = 'Tiles';
+
+const styles = StyleSheet.create({
+ tile: (props: Omit) => {
+ const { columns, fill } = props;
+
+ return {
+ flexBasis: resolveFlexBasis(columns),
+ flexGrow: resolveResponsiveStyle(fill, {
+ false: 0,
+ true: 1,
+ }),
+ flexShrink: 0,
+ };
+ },
+});
diff --git a/src/components/index.ts b/packages/stacks/src/components/index.ts
similarity index 76%
rename from src/components/index.ts
rename to packages/stacks/src/components/index.ts
index 43500fe2..27f95c01 100644
--- a/src/components/index.ts
+++ b/packages/stacks/src/components/index.ts
@@ -1,13 +1,11 @@
+export * from './AbsoluteBox';
export * from './Bleed';
export * from './Box';
export * from './Column';
export * from './Columns';
-export * from './FloatBox';
-export * from './Grid';
-export * from './Hidden';
export * from './Inline';
export * from './Inset';
-export * from './Rows';
export * from './Row';
+export * from './Rows';
export * from './Stack';
export * from './Tiles';
diff --git a/src/index.ts b/packages/stacks/src/index.ts
similarity index 68%
rename from src/index.ts
rename to packages/stacks/src/index.ts
index 2103701b..195d95f5 100644
--- a/src/index.ts
+++ b/packages/stacks/src/index.ts
@@ -1,3 +1,2 @@
export * from './components';
-export * from './hooks';
export * from './types';
diff --git a/packages/stacks/src/polymorphic.ts b/packages/stacks/src/polymorphic.ts
new file mode 100644
index 00000000..46ae7916
--- /dev/null
+++ b/packages/stacks/src/polymorphic.ts
@@ -0,0 +1,31 @@
+import type * as React from 'react';
+
+type Merge = Omit & P2;
+
+type ComponentProps = React.ComponentPropsWithRef;
+
+type PolymorphicProps = Merge<
+ Omit, 'as' | 'style'>,
+ Omit & {
+ readonly as?: ComponentType;
+ style?: ComponentProps['style'];
+ }
+>;
+
+export interface Polymorphic<
+ DefaultComponentType extends React.ElementType,
+ OwnProps = object,
+ AllowedComponentType extends React.ElementType = React.ElementType,
+> {
+ displayName: string;
+ (props: PolymorphicProps): React.ReactElement | null;
+ (
+ props: Merge<
+ Omit, 'as' | 'style'>,
+ Omit & {
+ readonly as: As;
+ style?: ComponentProps['style'];
+ }
+ >,
+ ): React.ReactElement | null;
+}
diff --git a/packages/stacks/src/types.ts b/packages/stacks/src/types.ts
new file mode 100644
index 00000000..43e6662d
--- /dev/null
+++ b/packages/stacks/src/types.ts
@@ -0,0 +1,46 @@
+import type { UnistylesBreakpoints } from 'react-native-unistyles';
+
+export type Breakpoint = keyof UnistylesBreakpoints;
+// biome-ignore lint/suspicious/noEmptyInterface: augmented by design-system consumers
+export interface StacksColorPalette {}
+// biome-ignore lint/suspicious/noEmptyInterface: augmented by design-system consumers
+export interface StacksBorderRadius {}
+// biome-ignore lint/suspicious/noEmptyInterface: augmented by design-system consumers
+export interface StacksGap {}
+// biome-ignore lint/suspicious/noEmptyInterface: augmented by design-system consumers
+export interface StacksMargin {}
+// biome-ignore lint/suspicious/noEmptyInterface: augmented by design-system consumers
+export interface StacksPadding {}
+
+type AnyString = string & {};
+type AnyNumber = number & {};
+
+export type StacksConfig = {
+ colorPaletteKey?: string;
+ gap: (value?: Gap) => number;
+ borderRadius: (value?: BorderRadius) => number;
+ margin: (value?: Margin) => number;
+ padding: (value?: Padding) => number;
+};
+
+export type AxisX = 'left' | 'center' | 'right';
+export type AxisY = 'top' | 'center' | 'bottom';
+export type Stretch = 'stretch';
+export type Space = 'between' | 'around' | 'evenly';
+export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse';
+export type Wrap = 'wrap' | 'no-wrap' | 'wrap-reverse';
+export type PointerEvent = 'auto' | 'box-none' | 'box-only' | 'none';
+export type Overflow = 'visible' | 'hidden';
+export type BackfaceVisibility = 'visible' | 'hidden';
+export type OutlineStyle = 'solid' | 'dotted' | 'dashed';
+export type BorderCurve = 'circular' | 'continuous';
+
+export type Flex = 'content' | 'fluid' | '1/2' | '1/3' | '2/3' | '1/4' | '3/4' | '1/5' | '2/5' | '3/5' | '4/5';
+export type Alignment = AxisX | AxisY | Space | Stretch;
+export type Color = keyof StacksColorPalette | AnyString;
+export type BorderRadius = keyof StacksBorderRadius | AnyNumber;
+export type Gap = keyof StacksGap | AnyNumber;
+export type Margin = keyof StacksMargin | AnyNumber;
+export type Padding = keyof StacksPadding | AnyNumber;
+
+export type ResponsiveProp = T | readonly T[];
diff --git a/packages/stacks/src/utils.ts b/packages/stacks/src/utils.ts
new file mode 100644
index 00000000..a23eba34
--- /dev/null
+++ b/packages/stacks/src/utils.ts
@@ -0,0 +1,556 @@
+import * as React from 'react';
+import type { DimensionValue } from 'react-native';
+import { UnistylesRuntime, type UnistylesThemes } from 'react-native-unistyles';
+
+import type { Alignment, Breakpoint, Color, Direction, Flex, ResponsiveProp, StacksConfig, Wrap } from './types';
+
+type Number = T extends number ? number : T;
+type BooleanMap = { true?: True; false?: False };
+type Boolean = T extends BooleanMap ? False | True : never;
+
+const dual: {
+ ) => any, DataFirst extends (...args: Array) => any>(
+ arity: Parameters['length'],
+ body: DataFirst,
+ ): DataLast & DataFirst;
+ ) => any, DataFirst extends (...args: Array) => any>(
+ isDataFirst: (args: IArguments) => boolean,
+ body: DataFirst,
+ ): DataLast & DataFirst;
+ // biome-ignore lint/complexity/useArrowFunction: ignore
+} = function (arity, body) {
+ if (typeof arity === 'function') {
+ return function (this: any) {
+ // biome-ignore lint/complexity/noArguments: ignore
+ return arity(arguments) ? body.apply(this, arguments as any) : (((self: any) => body(self, ...arguments)) as any);
+ };
+ }
+
+ switch (arity) {
+ case 0:
+ case 1:
+ throw new RangeError(`Invalid arity ${arity}`);
+
+ case 2:
+ return function (a, b) {
+ // biome-ignore lint/complexity/noArguments: ignore
+ if (arguments.length >= 2) {
+ return body(a, b);
+ }
+ // biome-ignore lint/complexity/useArrowFunction: ignore
+ return function (self: any) {
+ return body(self, a);
+ };
+ };
+
+ case 3:
+ return function (a, b, c) {
+ // biome-ignore lint/complexity/noArguments: ignore
+ if (arguments.length >= 3) {
+ return body(a, b, c);
+ }
+ // biome-ignore lint/complexity/useArrowFunction: ignore
+ return function (self: any) {
+ return body(self, a, b);
+ };
+ };
+
+ default:
+ return function () {
+ // biome-ignore lint/complexity/noArguments: ignore
+ if (arguments.length >= arity) {
+ // @ts-expect-error
+ // biome-ignore lint/complexity/noArguments: ignore
+ return body.apply(this, arguments);
+ }
+ // biome-ignore lint/complexity/noArguments: ignore
+ const args = arguments;
+ // biome-ignore lint/complexity/useArrowFunction: ignore
+ return function (self: any) {
+ return body(self, ...args);
+ };
+ };
+ }
+};
+
+export const flattenChildren = (children: React.ReactNode): ReturnType => {
+ return React.Children.toArray(children).reduce(
+ (acc: ReturnType, child) => {
+ if (React.isValidElement(child) && child.type === React.Fragment) {
+ return acc.concat(flattenChildren(child.props.children));
+ }
+ acc.push(child);
+ return acc;
+ },
+ [] as ReturnType,
+ );
+};
+
+const reduceWithIndex = (arr: readonly A[], initialValue: B, fn: (acc: B, element: A, index: number) => B): B => {
+ let e = initialValue;
+
+ for (let t = 0, v = arr.length; t < v; ++t) {
+ e = fn(e, arr[t] as A, t);
+ }
+
+ return e;
+};
+
+export const intersperse = (arr: readonly A[], delimiter: A) => {
+ return reduceWithIndex(arr, [] as A[], (acc, element, index) => {
+ if (((arr.length - 1) | 0) === index) {
+ acc.push(element);
+ } else {
+ acc.push(element, delimiter);
+ }
+ return acc;
+ });
+};
+
+const normalizeResponsiveProp = (responsiveProp: ResponsiveProp): readonly T[] => {
+ if (typeof responsiveProp === 'string' || typeof responsiveProp === 'number' || typeof responsiveProp === 'boolean') {
+ return [responsiveProp];
+ }
+
+ if (responsiveProp && Array.isArray(responsiveProp) && responsiveProp.length > 0) {
+ return responsiveProp;
+ }
+
+ if (__DEV__) {
+ // biome-ignore lint/suspicious/noConsole: warn about invalid layout input in development
+ console.warn(`Invalid ResponsiveProp: ${JSON.stringify(responsiveProp)}`);
+ }
+
+ return [];
+};
+
+const getResponsiveValue = dual<
+ (index: number) => (responsiveProp?: ResponsiveProp) => T | undefined,
+ (responsiveProp: ResponsiveProp | undefined, index: number) => T | undefined
+>(2, (responsiveProp: ResponsiveProp | undefined, index: number) => {
+ if (typeof responsiveProp === 'undefined') {
+ return undefined;
+ }
+
+ const normalized = normalizeResponsiveProp(responsiveProp);
+ const last = normalized[normalized.length - 1];
+
+ return normalized[index] ?? last;
+});
+
+const resolveWithBreakpointEntries = (mapFn: (breakpoints: readonly [string, number][]) => T) => {
+ const breakpoints = Object.entries(UnistylesRuntime.breakpoints).sort(([, first], [, second]) => {
+ return first - second;
+ });
+ return mapFn(breakpoints);
+};
+
+const resolveWithBreakpointKeys = (mapFn: (key: Breakpoint, index: number) => [Breakpoint, T]) => {
+ return resolveWithBreakpointEntries((breakpoints) => {
+ const keys = breakpoints.map(([key]) => {
+ return key as Breakpoint;
+ });
+
+ return Object.fromEntries(keys.map(mapFn)) as Partial>;
+ });
+};
+
+const resolveWithBreakpointMap = (mapFn: (key: Breakpoint, index: number) => T) => {
+ return resolveWithBreakpointKeys((key, index) => {
+ return [key, mapFn(key, index)];
+ });
+};
+
+const resolveResponsiveValue = (
+ responsiveProp: ResponsiveProp | undefined,
+ transform: (value: In) => Out,
+) => {
+ if (typeof responsiveProp === 'undefined') {
+ return undefined;
+ }
+
+ return resolveWithBreakpointMap((_key, index) => {
+ const value = getResponsiveValue(responsiveProp, index);
+ return transform(value as In);
+ });
+};
+
+export function resolveResponsiveStyle>>(
+ responsiveProp: ResponsiveProp,
+ to: To,
+): Partial | undefined>>;
+export function resolveResponsiveStyle>>(
+ responsiveProp: ResponsiveProp | undefined,
+ to: To,
+): Partial | undefined>> | undefined;
+export function resolveResponsiveStyle(
+ responsiveProp: ResponsiveProp,
+ to: To,
+): Partial | undefined>>;
+export function resolveResponsiveStyle(
+ responsiveProp: ResponsiveProp | undefined,
+ to: To,
+): Partial | undefined>> | undefined;
+export function resolveResponsiveStyle(
+ responsiveProp: ResponsiveProp,
+): Partial>;
+export function resolveResponsiveStyle(
+ responsiveProp: ResponsiveProp | undefined,
+): Partial> | undefined;
+
+export function resolveResponsiveStyle(
+ responsiveProp: ResponsiveProp | undefined,
+ to?: Partial>,
+) {
+ const resolved = resolveResponsiveValue(responsiveProp, (value) => {
+ return to ? to[String(value) as PropertyKey] : value;
+ });
+
+ if (resolved && to) {
+ return Object.entries(resolved).reduce>((result, [breakpoint, value]) => {
+ if (isObject(value)) {
+ return Object.entries(value).reduce((result, [key, propValue]) => {
+ const value = Object.assign(isObject(result[key]) ? result[key] : {}, {
+ [breakpoint]: propValue,
+ });
+
+ return Object.assign(result, {
+ [key]: value,
+ });
+ }, result);
+ }
+
+ return Object.assign(result, {
+ [breakpoint]: value,
+ });
+ }, {});
+ }
+
+ return resolved;
+}
+
+export function resolveResponsiveProp>(
+ responsiveProp: ResponsiveProp | undefined,
+ to: To,
+): To[keyof To] | undefined;
+export function resolveResponsiveProp(responsiveProp: ResponsiveProp): T | undefined;
+export function resolveResponsiveProp(responsiveProp: ResponsiveProp | undefined): T | undefined;
+export function resolveResponsiveProp(
+ responsiveProp: ResponsiveProp | undefined,
+ to?: Record,
+) {
+ if (typeof responsiveProp === 'undefined') {
+ return undefined;
+ }
+
+ return resolveWithBreakpointEntries((breakpoints) => {
+ const index = breakpoints.findIndex(([breakpoint]) => {
+ return breakpoint === UnistylesRuntime.breakpoint;
+ });
+ const value = getResponsiveValue(responsiveProp, index);
+ return to && value !== undefined ? to[value as keyof typeof to] : value;
+ });
+}
+
+type CollapseOptions = {
+ collapseBelow?: Breakpoint;
+ expanded: T;
+ collapsed: T;
+};
+
+type CollapsedResponsivePropOptions = {
+ collapseBelow?: Breakpoint;
+ expanded?: ResponsiveProp;
+ collapsed?: ResponsiveProp;
+};
+
+type Theme = UnistylesThemes[keyof UnistylesThemes] & {
+ stacks: StacksConfig;
+};
+
+type AlignmentOptions = {
+ alignX?: ResponsiveProp;
+ alignY?: ResponsiveProp;
+ direction?: ResponsiveProp;
+ horizontal?: ResponsiveProp;
+ collapseBelow?: Breakpoint;
+};
+
+type FlexSizingOptions = {
+ flex?: ResponsiveProp;
+ direction?: ResponsiveProp;
+ width?: ResponsiveProp;
+ height?: ResponsiveProp;
+};
+
+type FlexDirectionOptions = {
+ direction?: ResponsiveProp;
+ reverse?: ResponsiveProp;
+};
+
+const isBreakpointBelow = (currentBreakpoint: Breakpoint, breakpoint?: Breakpoint) => {
+ if (breakpoint === undefined) {
+ return false;
+ }
+
+ const currentValue = UnistylesRuntime.breakpoints[currentBreakpoint];
+ const breakpointValue = UnistylesRuntime.breakpoints[breakpoint];
+
+ return typeof currentValue === 'number' && typeof breakpointValue === 'number' && currentValue < breakpointValue;
+};
+
+export const resolveCollapse = (options: CollapseOptions) => {
+ const { collapseBelow, expanded, collapsed } = options;
+
+ return resolveWithBreakpointMap((key) => {
+ return isBreakpointBelow(key, collapseBelow) ? collapsed : expanded;
+ });
+};
+
+export const resolveResponsiveStyleByCollapse = (options: CollapsedResponsivePropOptions) => {
+ const { collapseBelow, expanded, collapsed } = options;
+
+ return resolveWithBreakpointMap((key, index) => {
+ const responsiveProp = isBreakpointBelow(key, collapseBelow) ? collapsed : expanded;
+ return getResponsiveValue(responsiveProp, index);
+ });
+};
+
+const isObject = (value: unknown): value is Record => {
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+};
+
+const CSS_VARIABLE = /^var\((--[^),]+)\)$/;
+
+const resolveThemeString = (value: unknown) => {
+ if (typeof value !== 'string') {
+ return undefined;
+ }
+
+ const match = CSS_VARIABLE.exec(value.trim());
+
+ if (match) {
+ // @ts-expect-error: ignore
+ if (typeof document === 'undefined') {
+ return undefined;
+ }
+ // @ts-expect-error: ignore
+ return getComputedStyle(document.documentElement).getPropertyValue(match[1]).trim();
+ }
+
+ return value;
+};
+
+const getColorPalette = (theme: Theme): Record | undefined => {
+ const paletteKey = resolveThemeString(theme.stacks.colorPaletteKey);
+ const palette = paletteKey ? theme[paletteKey as keyof Theme] : undefined;
+
+ return isObject(palette) ? palette : undefined;
+};
+
+export const resolveColor = (theme: Theme) => {
+ return (color?: ResponsiveProp