@astacinco/rn-primitives 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +195 -0
  2. package/__tests__/Button.test.tsx +114 -0
  3. package/__tests__/Card.test.tsx +73 -0
  4. package/__tests__/Container.test.tsx +71 -0
  5. package/__tests__/Divider.test.tsx +41 -0
  6. package/__tests__/Input.test.tsx +69 -0
  7. package/__tests__/Stack.test.tsx +85 -0
  8. package/__tests__/Text.test.tsx +64 -0
  9. package/__tests__/__snapshots__/Button.test.tsx.snap +143 -0
  10. package/__tests__/__snapshots__/Card.test.tsx.snap +47 -0
  11. package/__tests__/__snapshots__/Container.test.tsx.snap +51 -0
  12. package/__tests__/__snapshots__/Divider.test.tsx.snap +37 -0
  13. package/__tests__/__snapshots__/Input.test.tsx.snap +73 -0
  14. package/__tests__/__snapshots__/Stack.test.tsx.snap +101 -0
  15. package/__tests__/__snapshots__/Text.test.tsx.snap +39 -0
  16. package/package.json +47 -0
  17. package/src/Button/Button.tsx +119 -0
  18. package/src/Button/index.ts +2 -0
  19. package/src/Button/types.ts +43 -0
  20. package/src/Card/Card.tsx +62 -0
  21. package/src/Card/index.ts +2 -0
  22. package/src/Card/types.ts +21 -0
  23. package/src/Container/Container.tsx +42 -0
  24. package/src/Container/index.ts +2 -0
  25. package/src/Container/types.ts +23 -0
  26. package/src/Divider/Divider.tsx +40 -0
  27. package/src/Divider/index.ts +2 -0
  28. package/src/Divider/types.ts +21 -0
  29. package/src/Input/Input.tsx +103 -0
  30. package/src/Input/index.ts +2 -0
  31. package/src/Input/types.ts +23 -0
  32. package/src/Stack/Stack.tsx +77 -0
  33. package/src/Stack/index.ts +2 -0
  34. package/src/Stack/types.ts +30 -0
  35. package/src/Text/Text.tsx +50 -0
  36. package/src/Text/index.ts +2 -0
  37. package/src/Text/types.ts +21 -0
  38. package/src/index.ts +51 -0
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # @astacinco/rn-primitives
2
+
3
+ Theme-aware UI primitives for React Native. All components automatically adapt to light/dark modes.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @astacinco/rn-primitives @astacinco/rn-theming
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { ThemeProvider } from '@astacinco/rn-theming';
15
+ import { Text, Button, Card, VStack } from '@astacinco/rn-primitives';
16
+
17
+ function App() {
18
+ return (
19
+ <ThemeProvider mode="auto">
20
+ <VStack spacing="md">
21
+ <Text variant="title">Welcome</Text>
22
+ <Card>
23
+ <Text>This is a themed card</Text>
24
+ </Card>
25
+ <Button label="Get Started" onPress={() => {}} />
26
+ </VStack>
27
+ </ThemeProvider>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ## Components
33
+
34
+ ### Text
35
+
36
+ Themed text with variants.
37
+
38
+ ```typescript
39
+ <Text variant="title">Title Text</Text>
40
+ <Text variant="subtitle">Subtitle Text</Text>
41
+ <Text variant="body">Body Text</Text>
42
+ <Text variant="caption">Caption Text</Text>
43
+ <Text variant="label">Label Text</Text>
44
+ ```
45
+
46
+ **Props:**
47
+ - `variant` - `'title' | 'subtitle' | 'body' | 'caption' | 'label'` (default: `'body'`)
48
+ - `color` - Override text color (uses theme colors by default)
49
+ - All standard `Text` props from React Native
50
+
51
+ ### Button
52
+
53
+ Themed button with variants.
54
+
55
+ ```typescript
56
+ <Button label="Primary" variant="primary" onPress={handlePress} />
57
+ <Button label="Secondary" variant="secondary" onPress={handlePress} />
58
+ <Button label="Outline" variant="outline" onPress={handlePress} />
59
+ <Button label="Ghost" variant="ghost" onPress={handlePress} />
60
+ <Button label="Disabled" disabled onPress={handlePress} />
61
+ ```
62
+
63
+ **Props:**
64
+ - `label` - Button text (required)
65
+ - `variant` - `'primary' | 'secondary' | 'outline' | 'ghost'` (default: `'primary'`)
66
+ - `size` - `'sm' | 'md' | 'lg'` (default: `'md'`)
67
+ - `disabled` - Disable the button
68
+ - `loading` - Show loading state
69
+ - `onPress` - Press handler (required)
70
+
71
+ ### Card
72
+
73
+ Themed card container.
74
+
75
+ ```typescript
76
+ <Card>
77
+ <Text>Card content</Text>
78
+ </Card>
79
+
80
+ <Card variant="outlined">
81
+ <Text>Outlined card</Text>
82
+ </Card>
83
+
84
+ <Card variant="elevated">
85
+ <Text>Elevated card with shadow</Text>
86
+ </Card>
87
+ ```
88
+
89
+ **Props:**
90
+ - `variant` - `'filled' | 'outlined' | 'elevated'` (default: `'filled'`)
91
+ - `padding` - Override padding
92
+
93
+ ### Stack (VStack, HStack)
94
+
95
+ Layout components for vertical and horizontal stacking.
96
+
97
+ ```typescript
98
+ <VStack spacing="md" align="center">
99
+ <Text>Item 1</Text>
100
+ <Text>Item 2</Text>
101
+ </VStack>
102
+
103
+ <HStack spacing="sm" justify="space-between">
104
+ <Button label="Cancel" variant="ghost" />
105
+ <Button label="Save" />
106
+ </HStack>
107
+ ```
108
+
109
+ **Props:**
110
+ - `spacing` - Gap between items: `'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'`
111
+ - `align` - Align items: `'start' | 'center' | 'end' | 'stretch'`
112
+ - `justify` - Justify content: `'start' | 'center' | 'end' | 'space-between' | 'space-around'`
113
+
114
+ ### Container
115
+
116
+ Themed container with padding and optional max width.
117
+
118
+ ```typescript
119
+ <Container>
120
+ <Text>Full width content</Text>
121
+ </Container>
122
+
123
+ <Container maxWidth={600} centered>
124
+ <Text>Centered content with max width</Text>
125
+ </Container>
126
+ ```
127
+
128
+ **Props:**
129
+ - `padding` - Override padding
130
+ - `maxWidth` - Maximum width in pixels
131
+ - `centered` - Center the container horizontally
132
+
133
+ ### Input
134
+
135
+ Themed text input.
136
+
137
+ ```typescript
138
+ <Input
139
+ label="Email"
140
+ placeholder="Enter your email"
141
+ value={email}
142
+ onChangeText={setEmail}
143
+ />
144
+
145
+ <Input
146
+ label="Password"
147
+ secureTextEntry
148
+ value={password}
149
+ onChangeText={setPassword}
150
+ error="Password is required"
151
+ />
152
+ ```
153
+
154
+ **Props:**
155
+ - `label` - Input label
156
+ - `placeholder` - Placeholder text
157
+ - `error` - Error message to display
158
+ - `disabled` - Disable the input
159
+ - All standard `TextInput` props
160
+
161
+ ### Divider
162
+
163
+ Themed horizontal divider.
164
+
165
+ ```typescript
166
+ <Divider />
167
+ <Divider variant="thick" />
168
+ <Divider color="#FF0000" />
169
+ ```
170
+
171
+ **Props:**
172
+ - `variant` - `'thin' | 'thick'` (default: `'thin'`)
173
+ - `color` - Override divider color
174
+
175
+ ## Theme Integration
176
+
177
+ All components use the `useTheme()` hook from `@astacinco/rn-theming`. They automatically:
178
+
179
+ - Adapt colors to light/dark mode
180
+ - Use consistent spacing tokens
181
+ - Apply proper typography styles
182
+ - Support scoped theming
183
+
184
+ ## Testing
185
+
186
+ Use `@astacinco/rn-testing` for testing primitives:
187
+
188
+ ```typescript
189
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
190
+ import { Button } from '@astacinco/rn-primitives';
191
+
192
+ describe('Button', () => {
193
+ createThemeSnapshot(<Button label="Click" onPress={() => {}} />);
194
+ });
195
+ ```
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
4
+ import { Button } from '../src/Button';
5
+
6
+ describe('Button', () => {
7
+ const mockOnPress = jest.fn();
8
+
9
+ beforeEach(() => {
10
+ mockOnPress.mockClear();
11
+ });
12
+
13
+ // Snapshot tests for both themes
14
+ createThemeSnapshot(<Button label="Click Me" onPress={() => {}} testID="button" />);
15
+
16
+ describe('variants', () => {
17
+ it('renders_primary_variant_byDefault', () => {
18
+ const { getByTestId } = renderWithTheme(
19
+ <Button label="Primary" onPress={mockOnPress} testID="button" />
20
+ );
21
+ expect(getByTestId('button')).toBeTruthy();
22
+ });
23
+
24
+ it('renders_secondary_variant', () => {
25
+ const { getByTestId } = renderWithTheme(
26
+ <Button label="Secondary" variant="secondary" onPress={mockOnPress} testID="button" />
27
+ );
28
+ expect(getByTestId('button')).toBeTruthy();
29
+ });
30
+
31
+ it('renders_outline_variant', () => {
32
+ const { getByTestId } = renderWithTheme(
33
+ <Button label="Outline" variant="outline" onPress={mockOnPress} testID="button" />
34
+ );
35
+ expect(getByTestId('button')).toBeTruthy();
36
+ });
37
+
38
+ it('renders_ghost_variant', () => {
39
+ const { getByTestId } = renderWithTheme(
40
+ <Button label="Ghost" variant="ghost" onPress={mockOnPress} testID="button" />
41
+ );
42
+ expect(getByTestId('button')).toBeTruthy();
43
+ });
44
+ });
45
+
46
+ describe('sizes', () => {
47
+ it('renders_sm_size', () => {
48
+ const { getByTestId } = renderWithTheme(
49
+ <Button label="Small" size="sm" onPress={mockOnPress} testID="button" />
50
+ );
51
+ expect(getByTestId('button')).toBeTruthy();
52
+ });
53
+
54
+ it('renders_md_size_byDefault', () => {
55
+ const { getByTestId } = renderWithTheme(
56
+ <Button label="Medium" onPress={mockOnPress} testID="button" />
57
+ );
58
+ expect(getByTestId('button')).toBeTruthy();
59
+ });
60
+
61
+ it('renders_lg_size', () => {
62
+ const { getByTestId } = renderWithTheme(
63
+ <Button label="Large" size="lg" onPress={mockOnPress} testID="button" />
64
+ );
65
+ expect(getByTestId('button')).toBeTruthy();
66
+ });
67
+ });
68
+
69
+ describe('interactions', () => {
70
+ it('calls_onPress_whenPressed', () => {
71
+ const { getByTestId } = renderWithTheme(
72
+ <Button label="Press Me" onPress={mockOnPress} testID="button" />
73
+ );
74
+
75
+ fireEvent.press(getByTestId('button'));
76
+ expect(mockOnPress).toHaveBeenCalledTimes(1);
77
+ });
78
+
79
+ it('does_notCall_onPress_whenDisabled', () => {
80
+ const { getByTestId } = renderWithTheme(
81
+ <Button label="Disabled" disabled onPress={mockOnPress} testID="button" />
82
+ );
83
+
84
+ fireEvent.press(getByTestId('button'));
85
+ expect(mockOnPress).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('does_notCall_onPress_whenLoading', () => {
89
+ const { getByTestId } = renderWithTheme(
90
+ <Button label="Loading" loading onPress={mockOnPress} testID="button" />
91
+ );
92
+
93
+ fireEvent.press(getByTestId('button'));
94
+ expect(mockOnPress).not.toHaveBeenCalled();
95
+ });
96
+ });
97
+
98
+ describe('theming', () => {
99
+ it('uses_different_colors_inDarkMode', () => {
100
+ const lightResult = renderWithTheme(
101
+ <Button label="Test" onPress={mockOnPress} testID="button" />,
102
+ 'light'
103
+ );
104
+ const darkResult = renderWithTheme(
105
+ <Button label="Test" onPress={mockOnPress} testID="button" />,
106
+ 'dark'
107
+ );
108
+
109
+ // Both should render without errors
110
+ expect(lightResult.getByTestId('button')).toBeTruthy();
111
+ expect(darkResult.getByTestId('button')).toBeTruthy();
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
4
+ import { Card } from '../src/Card';
5
+
6
+ describe('Card', () => {
7
+ // Snapshot tests for both themes
8
+ createThemeSnapshot(
9
+ <Card testID="card">
10
+ <Text>Card content</Text>
11
+ </Card>
12
+ );
13
+
14
+ describe('variants', () => {
15
+ it('renders_filled_variant_byDefault', () => {
16
+ const { getByTestId } = renderWithTheme(
17
+ <Card testID="card">
18
+ <Text>Content</Text>
19
+ </Card>
20
+ );
21
+ expect(getByTestId('card')).toBeTruthy();
22
+ });
23
+
24
+ it('renders_outlined_variant', () => {
25
+ const { getByTestId } = renderWithTheme(
26
+ <Card variant="outlined" testID="card">
27
+ <Text>Content</Text>
28
+ </Card>
29
+ );
30
+ expect(getByTestId('card')).toBeTruthy();
31
+ });
32
+
33
+ it('renders_elevated_variant', () => {
34
+ const { getByTestId } = renderWithTheme(
35
+ <Card variant="elevated" testID="card">
36
+ <Text>Content</Text>
37
+ </Card>
38
+ );
39
+ expect(getByTestId('card')).toBeTruthy();
40
+ });
41
+ });
42
+
43
+ describe('theming', () => {
44
+ it('uses_different_colors_inDarkMode', () => {
45
+ const lightResult = renderWithTheme(
46
+ <Card testID="card">
47
+ <Text>Test</Text>
48
+ </Card>,
49
+ 'light'
50
+ );
51
+ const darkResult = renderWithTheme(
52
+ <Card testID="card">
53
+ <Text>Test</Text>
54
+ </Card>,
55
+ 'dark'
56
+ );
57
+
58
+ const lightBg = lightResult.getByTestId('card').props.style[1].backgroundColor;
59
+ const darkBg = darkResult.getByTestId('card').props.style[1].backgroundColor;
60
+
61
+ expect(lightBg).not.toBe(darkBg);
62
+ });
63
+ });
64
+
65
+ it('accepts_custom_padding', () => {
66
+ const { getByTestId } = renderWithTheme(
67
+ <Card padding={32} testID="card">
68
+ <Text>Content</Text>
69
+ </Card>
70
+ );
71
+ expect(getByTestId('card').props.style[1].padding).toBe(32);
72
+ });
73
+ });
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
4
+ import { Container } from '../src/Container';
5
+
6
+ describe('Container', () => {
7
+ // Snapshot tests for both themes
8
+ createThemeSnapshot(
9
+ <Container testID="container">
10
+ <Text>Content</Text>
11
+ </Container>
12
+ );
13
+
14
+ it('renders_children', () => {
15
+ const { getByText } = renderWithTheme(
16
+ <Container testID="container">
17
+ <Text>Hello</Text>
18
+ </Container>
19
+ );
20
+ expect(getByText('Hello')).toBeTruthy();
21
+ });
22
+
23
+ it('applies_maxWidth', () => {
24
+ const { getByTestId } = renderWithTheme(
25
+ <Container maxWidth={600} testID="container">
26
+ <Text>Content</Text>
27
+ </Container>
28
+ );
29
+ expect(getByTestId('container').props.style[1].maxWidth).toBe(600);
30
+ });
31
+
32
+ it('applies_custom_padding', () => {
33
+ const { getByTestId } = renderWithTheme(
34
+ <Container padding={32} testID="container">
35
+ <Text>Content</Text>
36
+ </Container>
37
+ );
38
+ expect(getByTestId('container').props.style[1].padding).toBe(32);
39
+ });
40
+
41
+ it('centers_whenCenteredProp', () => {
42
+ const { getByTestId } = renderWithTheme(
43
+ <Container centered testID="container">
44
+ <Text>Content</Text>
45
+ </Container>
46
+ );
47
+ expect(getByTestId('container').props.style[1].alignSelf).toBe('center');
48
+ });
49
+
50
+ describe('theming', () => {
51
+ it('uses_different_colors_inDarkMode', () => {
52
+ const lightResult = renderWithTheme(
53
+ <Container testID="container">
54
+ <Text>Test</Text>
55
+ </Container>,
56
+ 'light'
57
+ );
58
+ const darkResult = renderWithTheme(
59
+ <Container testID="container">
60
+ <Text>Test</Text>
61
+ </Container>,
62
+ 'dark'
63
+ );
64
+
65
+ const lightBg = lightResult.getByTestId('container').props.style[1].backgroundColor;
66
+ const darkBg = darkResult.getByTestId('container').props.style[1].backgroundColor;
67
+
68
+ expect(lightBg).not.toBe(darkBg);
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
3
+ import { Divider } from '../src/Divider';
4
+
5
+ describe('Divider', () => {
6
+ // Snapshot tests for both themes
7
+ createThemeSnapshot(<Divider testID="divider" />);
8
+
9
+ describe('variants', () => {
10
+ it('renders_thin_variant_byDefault', () => {
11
+ const { getByTestId } = renderWithTheme(<Divider testID="divider" />);
12
+ expect(getByTestId('divider').props.style[1].height).toBe(1);
13
+ });
14
+
15
+ it('renders_thick_variant', () => {
16
+ const { getByTestId } = renderWithTheme(
17
+ <Divider variant="thick" testID="divider" />
18
+ );
19
+ expect(getByTestId('divider').props.style[1].height).toBe(2);
20
+ });
21
+ });
22
+
23
+ describe('theming', () => {
24
+ it('uses_different_colors_inDarkMode', () => {
25
+ const lightResult = renderWithTheme(<Divider testID="divider" />, 'light');
26
+ const darkResult = renderWithTheme(<Divider testID="divider" />, 'dark');
27
+
28
+ const lightColor = lightResult.getByTestId('divider').props.style[1].backgroundColor;
29
+ const darkColor = darkResult.getByTestId('divider').props.style[1].backgroundColor;
30
+
31
+ expect(lightColor).not.toBe(darkColor);
32
+ });
33
+ });
34
+
35
+ it('accepts_custom_color', () => {
36
+ const { getByTestId } = renderWithTheme(
37
+ <Divider color="#FF0000" testID="divider" />
38
+ );
39
+ expect(getByTestId('divider').props.style[1].backgroundColor).toBe('#FF0000');
40
+ });
41
+ });
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
4
+ import { Input } from '../src/Input';
5
+
6
+ describe('Input', () => {
7
+ // Snapshot tests for both themes
8
+ createThemeSnapshot(
9
+ <Input testID="input" placeholder="Enter text" />
10
+ );
11
+
12
+ it('renders_withLabel', () => {
13
+ const { getByText } = renderWithTheme(
14
+ <Input label="Email" testID="input" />
15
+ );
16
+ expect(getByText('Email')).toBeTruthy();
17
+ });
18
+
19
+ it('renders_withError', () => {
20
+ const { getByText } = renderWithTheme(
21
+ <Input error="This field is required" testID="input" />
22
+ );
23
+ expect(getByText('This field is required')).toBeTruthy();
24
+ });
25
+
26
+ it('renders_withPlaceholder', () => {
27
+ const { getByTestId } = renderWithTheme(
28
+ <Input placeholder="Enter email" testID="input" />
29
+ );
30
+ expect(getByTestId('input').props.placeholder).toBe('Enter email');
31
+ });
32
+
33
+ describe('interactions', () => {
34
+ it('calls_onChangeText_whenTyping', () => {
35
+ const mockOnChangeText = jest.fn();
36
+ const { getByTestId } = renderWithTheme(
37
+ <Input onChangeText={mockOnChangeText} testID="input" />
38
+ );
39
+
40
+ fireEvent.changeText(getByTestId('input'), 'hello');
41
+ expect(mockOnChangeText).toHaveBeenCalledWith('hello');
42
+ });
43
+
44
+ it('is_notEditable_whenDisabled', () => {
45
+ const { getByTestId } = renderWithTheme(
46
+ <Input disabled testID="input" />
47
+ );
48
+ expect(getByTestId('input').props.editable).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe('theming', () => {
53
+ it('uses_different_colors_inDarkMode', () => {
54
+ const lightResult = renderWithTheme(
55
+ <Input testID="input" />,
56
+ 'light'
57
+ );
58
+ const darkResult = renderWithTheme(
59
+ <Input testID="input" />,
60
+ 'dark'
61
+ );
62
+
63
+ const lightBorder = lightResult.getByTestId('input').props.style[1].borderColor;
64
+ const darkBorder = darkResult.getByTestId('input').props.style[1].borderColor;
65
+
66
+ expect(lightBorder).not.toBe(darkBorder);
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
4
+ import { VStack, HStack } from '../src/Stack';
5
+
6
+ describe('VStack', () => {
7
+ // Snapshot tests for both themes
8
+ createThemeSnapshot(
9
+ <VStack testID="vstack" spacing="md">
10
+ <Text>Item 1</Text>
11
+ <Text>Item 2</Text>
12
+ </VStack>,
13
+ 'VStack'
14
+ );
15
+
16
+ it('renders_children_vertically', () => {
17
+ const { getByTestId } = renderWithTheme(
18
+ <VStack testID="vstack">
19
+ <Text>Item 1</Text>
20
+ <Text>Item 2</Text>
21
+ </VStack>
22
+ );
23
+ expect(getByTestId('vstack').props.style[1].flexDirection).toBe('column');
24
+ });
25
+
26
+ it('applies_spacing', () => {
27
+ const { getByTestId } = renderWithTheme(
28
+ <VStack testID="vstack" spacing="md">
29
+ <Text>Item 1</Text>
30
+ <Text>Item 2</Text>
31
+ </VStack>
32
+ );
33
+ expect(getByTestId('vstack').props.style[1].gap).toBe(16); // md = 16
34
+ });
35
+
36
+ it('applies_alignment', () => {
37
+ const { getByTestId } = renderWithTheme(
38
+ <VStack testID="vstack" align="center">
39
+ <Text>Item</Text>
40
+ </VStack>
41
+ );
42
+ expect(getByTestId('vstack').props.style[1].alignItems).toBe('center');
43
+ });
44
+
45
+ it('applies_justification', () => {
46
+ const { getByTestId } = renderWithTheme(
47
+ <VStack testID="vstack" justify="space-between">
48
+ <Text>Item 1</Text>
49
+ <Text>Item 2</Text>
50
+ </VStack>
51
+ );
52
+ expect(getByTestId('vstack').props.style[1].justifyContent).toBe('space-between');
53
+ });
54
+ });
55
+
56
+ describe('HStack', () => {
57
+ // Snapshot tests for both themes
58
+ createThemeSnapshot(
59
+ <HStack testID="hstack" spacing="sm">
60
+ <Text>Left</Text>
61
+ <Text>Right</Text>
62
+ </HStack>,
63
+ 'HStack'
64
+ );
65
+
66
+ it('renders_children_horizontally', () => {
67
+ const { getByTestId } = renderWithTheme(
68
+ <HStack testID="hstack">
69
+ <Text>Left</Text>
70
+ <Text>Right</Text>
71
+ </HStack>
72
+ );
73
+ expect(getByTestId('hstack').props.style[1].flexDirection).toBe('row');
74
+ });
75
+
76
+ it('applies_spacing', () => {
77
+ const { getByTestId } = renderWithTheme(
78
+ <HStack testID="hstack" spacing="lg">
79
+ <Text>Left</Text>
80
+ <Text>Right</Text>
81
+ </HStack>
82
+ );
83
+ expect(getByTestId('hstack').props.style[1].gap).toBe(24); // lg = 24
84
+ });
85
+ });