Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { inputRegistry } from './input.js';
import { chartsRegistry } from './charts/index.js';
import { alertDialogRegistry } from './alert-dialog.js';
import { avoidKeyboardRegistry } from './avoid-keyboard.js';
import { stackRegistry } from './stack.js';

export const REGISTRY: Record<string, ComponentRegistry> = {
...accordionRegistry,
Expand Down Expand Up @@ -115,6 +116,7 @@ export const REGISTRY: Record<string, ComponentRegistry> = {
...hooksRegistry,
...themeRegistry,
...chartsRegistry,
...stackRegistry,
};

/// Helper functions for component registry
Expand Down
39 changes: 39 additions & 0 deletions src/registry/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Registry configuration for stack (XStack, YStack, ZStack) layout components

export const stackRegistry = {
// Main stack component
stack: {
name: 'stack',
description:
'Layout primitives for horizontal (XStack), vertical (YStack), and overlay (ZStack) stacking with gap, alignment, and shorthand props.',
type: 'registry:ui',
dependencies: [],
registryDependencies: [],
hooks: [],
theme: [],
files: [
{
type: 'registry:ui',
path: 'templates/components/ui/stack.tsx',
target: 'components/ui/stack.tsx',
},
],
},

// Default demo
'stack-demo': {
name: 'stack-demo',
description: 'Basic examples demonstrating XStack, YStack, and ZStack usage',
type: 'registry:example',
registryDependencies: ['stack', 'view', 'text'],
hooks: [],
theme: [],
files: [
{
type: 'registry:example',
path: 'templates/demo/stack/stack-demo.tsx',
target: 'components/demo/stack/stack-demo.tsx',
},
],
},
};
278 changes: 278 additions & 0 deletions templates/components/ui/stack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// stack.tsx
import * as React from "react";
import {
View,
StyleSheet,
type ViewProps,
type ViewStyle,
type StyleProp,
} from "react-native";
type Direction = "row" | "column";

export type BaseStackProps = Omit<ViewProps, "style"> & {
/** Spacing between children (in dp). */
gap?: number;
/** Reverse the main-axis direction (row-reverse/column-reverse). */
reverse?: boolean;
/** Shorthand: center both axes. */
center?: boolean;
/** Shorthand: fill available space (true → flex: 1). */
flex?: number | boolean;
/** Container style. */
style?: StyleProp<ViewStyle>;

// Native padding properties
padding?: ViewStyle["padding"];
paddingTop?: ViewStyle["paddingTop"];
paddingBottom?: ViewStyle["paddingBottom"];
paddingLeft?: ViewStyle["paddingLeft"];
paddingRight?: ViewStyle["paddingRight"];
paddingHorizontal?: ViewStyle["paddingHorizontal"];
paddingVertical?: ViewStyle["paddingVertical"];

// Basic style props that can be passed directly
height?: ViewStyle["height"];
width?: ViewStyle["width"];
backgroundColor?: ViewStyle["backgroundColor"];
borderTopWidth?: ViewStyle["borderTopWidth"];
borderBottomWidth?: ViewStyle["borderBottomWidth"];
borderLeftWidth?: ViewStyle["borderLeftWidth"];
borderRightWidth?: ViewStyle["borderRightWidth"];
borderWidth?: ViewStyle["borderWidth"];
borderColor?: ViewStyle["borderColor"];
borderTopColor?: ViewStyle["borderTopColor"];
borderBottomColor?: ViewStyle["borderBottomColor"];
borderLeftColor?: ViewStyle["borderLeftColor"];
borderRightColor?: ViewStyle["borderRightColor"];
borderRadius?: ViewStyle["borderRadius"];
borderTopLeftRadius?: ViewStyle["borderTopLeftRadius"];
borderTopRightRadius?: ViewStyle["borderTopRightRadius"];
borderBottomLeftRadius?: ViewStyle["borderBottomLeftRadius"];
borderBottomRightRadius?: ViewStyle["borderBottomRightRadius"];
margin?: ViewStyle["margin"];
marginTop?: ViewStyle["marginTop"];
marginBottom?: ViewStyle["marginBottom"];
marginLeft?: ViewStyle["marginLeft"];
marginRight?: ViewStyle["marginRight"];
marginHorizontal?: ViewStyle["marginHorizontal"];
marginVertical?: ViewStyle["marginVertical"];
minHeight?: ViewStyle["minHeight"];
minWidth?: ViewStyle["minWidth"];
maxHeight?: ViewStyle["maxHeight"];
maxWidth?: ViewStyle["maxWidth"];
opacity?: ViewStyle["opacity"];
overflow?: ViewStyle["overflow"];
position?: ViewStyle["position"];
top?: ViewStyle["top"];
bottom?: ViewStyle["bottom"];
left?: ViewStyle["left"];
right?: ViewStyle["right"];
zIndex?: ViewStyle["zIndex"];
alignItems?: ViewStyle["alignItems"];
justifyContent?: ViewStyle["justifyContent"];

};

type InternalProps = BaseStackProps & {
__direction: Direction;
};

const BaseStack = React.forwardRef<View, InternalProps>(function BaseStack(
{
__direction,
gap = 0,
reverse,
center,
flex,
style,
children,
// Native padding properties
padding,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
paddingHorizontal,
paddingVertical,
// Basic style props
height,
width,
backgroundColor,
borderTopWidth,
borderBottomWidth,
borderLeftWidth,
borderRightWidth,
borderWidth,
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
borderRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
margin,
marginTop,
marginBottom,
marginLeft,
marginRight,
marginHorizontal,
marginVertical,
minHeight,
minWidth,
maxHeight,
maxWidth,
opacity,
overflow,
position,
top,
bottom,
left,
right,
zIndex,
alignItems,
justifyContent,
...viewProps
},
ref
) {
const dir = reverse
? __direction === "row"
? "row-reverse"
: "column-reverse"
: __direction;

const resolvedFlex: number | undefined =
typeof flex === "boolean" ? (flex ? 1 : undefined) : flex;


// Direct style props
const directStyleProps: ViewStyle = {
height,
width,
backgroundColor,
borderTopWidth,
borderBottomWidth,
borderLeftWidth,
borderRightWidth,
borderWidth,
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
borderRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
margin,
marginTop,
marginBottom,
marginLeft,
marginRight,
marginHorizontal,
marginVertical,
minHeight,
minWidth,
maxHeight,
maxWidth,
opacity,
overflow,
position,
top,
bottom,
left,
right,
zIndex,
// Native padding properties
padding,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
paddingHorizontal,
paddingVertical,
};

// Base container style
const containerStyle: ViewStyle = {
flexDirection: dir,
alignItems: center ? "center" : alignItems,
justifyContent: center ? "center" : justifyContent,
flex: resolvedFlex,
...directStyleProps,
};

// Apply a gap if it's not 0
if (gap != null) {
containerStyle.gap = gap;
}

// Normalize children
const kids = React.Children.toArray(children).filter(
(c) => c !== null && c !== undefined && !!c
);

return (
<View ref={ref} style={[containerStyle, style]} {...viewProps}>
{kids}
</View>
);
});

export type XStackProps = BaseStackProps;
export type YStackProps = BaseStackProps;
export type ZStackProps = BaseStackProps;

export const XStack = React.forwardRef<View, XStackProps>(function XStack(
props,
ref
) {
return <BaseStack ref={ref} __direction="row" {...props} />;
});

export const YStack = React.forwardRef<View, YStackProps>(function YStack(
props,
ref
) {
return <BaseStack ref={ref} __direction="column" {...props} />;
});

export const ZStack = React.forwardRef<View, ZStackProps>(function ZStack(
{ children, style, gap, ...rest },
ref
) {
// Normalize children
const kids = React.Children.toArray(children).filter(
(c) => c !== null && c !== undefined && !!c
);

// Render: first child in normal flow, later children absolutely fill
return (
<BaseStack
ref={ref}
__direction="column"
// Force no gap for overlay layout to ensure perfect stacking
gap={0}
{...rest}
style={[{ position: "relative" }, style]}
>
{kids.map((child, idx) =>
idx === 0 ? (
child as React.ReactElement
) : (
<View
key={(child as any)?.key ?? `z-${idx}`}
pointerEvents="box-none"
style={StyleSheet.absoluteFillObject}
>
{child}
</View>
)
)}
</BaseStack>
);
});
35 changes: 35 additions & 0 deletions templates/demo/stack/stack-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { View } from '@/components/ui/view';
import { Text } from '@/components/ui/text';
import { XStack, YStack, ZStack } from '@/components/ui/stack';

export default function StackDemo() {
return (
<YStack gap={16} padding={16}>
<Text style={{ fontWeight: '600', fontSize: 16 }}>XStack (row)</Text>
<XStack gap={8} center>
<View style={{ width: 40, height: 40, backgroundColor: '#0ea5e9', borderRadius: 8 }} />
<View style={{ width: 40, height: 40, backgroundColor: '#22c55e', borderRadius: 8 }} />
<View style={{ width: 40, height: 40, backgroundColor: '#f59e0b', borderRadius: 8 }} />
</XStack>

<Text style={{ fontWeight: '600', fontSize: 16, marginTop: 8 }}>YStack (column)</Text>
<YStack gap={8}>
<View style={{ height: 24, backgroundColor: '#e5e7eb', borderRadius: 6 }} />
<View style={{ height: 24, backgroundColor: '#d1d5db', borderRadius: 6 }} />
<View style={{ height: 24, backgroundColor: '#9ca3af', borderRadius: 6 }} />
</YStack>

<Text style={{ fontWeight: '600', fontSize: 16, marginTop: 8 }}>ZStack (overlay)</Text>
<ZStack height={100} borderRadius={10} overflow="hidden">
<View style={{ flex: 1, backgroundColor: '#93c5fd' }} />
<View style={{ flex: 1, backgroundColor: '#00000020' }} />
<View
pointerEvents="none"
style={{ position: 'absolute', bottom: 8, right: 8, backgroundColor: '#111827cc', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6 }}
>
<Text style={{ color: 'white', fontSize: 12 }}>Overlay</Text>
</View>
</ZStack>
</YStack>
);
}