@titan-design/react-ui 0.1.0 → 0.2.5

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 (236) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/package.json +22 -6
  16. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  17. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  18. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  19. package/src/components/custom/DateTime/index.ts +2 -0
  20. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  21. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  22. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  23. package/src/components/custom/EmptyState/index.ts +2 -0
  24. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  25. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  26. package/src/components/custom/Metric/Metric.tsx +92 -0
  27. package/src/components/custom/Metric/index.ts +2 -0
  28. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  29. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  30. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  31. package/src/components/custom/Sidebar/index.ts +18 -0
  32. package/src/components/custom/Table/Table.stories.tsx +481 -0
  33. package/src/components/custom/Table/Table.test.tsx +531 -0
  34. package/src/components/custom/Table/Table.tsx +696 -0
  35. package/src/components/custom/Table/index.ts +22 -0
  36. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  37. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  38. package/src/components/custom/Typography/Typography.tsx +218 -0
  39. package/src/components/custom/Typography/index.ts +19 -0
  40. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  41. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  42. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  43. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  44. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  45. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  46. package/src/components/custom/Workout/index.ts +2 -0
  47. package/src/components/custom/index.ts +9 -0
  48. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  49. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  50. package/src/components/custom/stepper/Stepper.tsx +250 -0
  51. package/src/components/custom/stepper/index.ts +10 -0
  52. package/src/components/index.ts +5 -0
  53. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  54. package/src/components/ui/alert/Alert.test.tsx +202 -0
  55. package/src/components/ui/alert/Alert.tsx +170 -0
  56. package/src/components/ui/alert/index.ts +8 -0
  57. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  58. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  59. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  60. package/src/components/ui/autocomplete/index.ts +2 -0
  61. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  62. package/src/components/ui/avatar/Avatar.test.tsx +152 -0
  63. package/src/components/ui/avatar/Avatar.tsx +176 -0
  64. package/src/components/ui/avatar/index.ts +2 -0
  65. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  66. package/src/components/ui/badge/Badge.test.tsx +103 -0
  67. package/src/components/ui/badge/Badge.tsx +119 -0
  68. package/src/components/ui/badge/index.ts +3 -0
  69. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  70. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  71. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  72. package/src/components/ui/breadcrumbs/index.ts +2 -0
  73. package/src/components/ui/button/Button.stories.tsx +216 -0
  74. package/src/components/ui/button/Button.test.tsx +132 -0
  75. package/src/components/ui/button/Button.tsx +297 -0
  76. package/src/components/ui/button/index.ts +10 -0
  77. package/src/components/ui/card/Card.stories.tsx +443 -0
  78. package/src/components/ui/card/Card.test.tsx +294 -0
  79. package/src/components/ui/card/Card.tsx +401 -0
  80. package/src/components/ui/card/index.ts +20 -0
  81. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  82. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  83. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  84. package/src/components/ui/checkbox/index.ts +2 -0
  85. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  86. package/src/components/ui/chip/Chip.test.tsx +119 -0
  87. package/src/components/ui/chip/Chip.tsx +151 -0
  88. package/src/components/ui/chip/index.ts +2 -0
  89. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  90. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  91. package/src/components/ui/collapse/Collapse.tsx +275 -0
  92. package/src/components/ui/collapse/index.ts +18 -0
  93. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  94. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  95. package/src/components/ui/data-row/DataRow.tsx +29 -0
  96. package/src/components/ui/data-row/index.ts +2 -0
  97. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  98. package/src/components/ui/divider/Divider.test.tsx +55 -0
  99. package/src/components/ui/divider/Divider.tsx +37 -0
  100. package/src/components/ui/divider/index.ts +2 -0
  101. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  102. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  103. package/src/components/ui/drawer/Drawer.tsx +228 -0
  104. package/src/components/ui/drawer/index.ts +9 -0
  105. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  106. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  107. package/src/components/ui/form-field/FormField.tsx +308 -0
  108. package/src/components/ui/form-field/index.ts +2 -0
  109. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  110. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  111. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  112. package/src/components/ui/help-tip/index.ts +2 -0
  113. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  114. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  115. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  116. package/src/components/ui/icon-box/index.ts +1 -0
  117. package/src/components/ui/index.ts +38 -0
  118. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  119. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  120. package/src/components/ui/indicator/Indicator.tsx +73 -0
  121. package/src/components/ui/indicator/index.ts +2 -0
  122. package/src/components/ui/input/Input.stories.tsx +192 -0
  123. package/src/components/ui/input/Input.test.tsx +86 -0
  124. package/src/components/ui/input/Input.tsx +273 -0
  125. package/src/components/ui/input/index.ts +2 -0
  126. package/src/components/ui/link/Link.stories.tsx +78 -0
  127. package/src/components/ui/link/Link.test.tsx +94 -0
  128. package/src/components/ui/link/Link.tsx +86 -0
  129. package/src/components/ui/link/index.ts +2 -0
  130. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  131. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  132. package/src/components/ui/list-item/ListItem.tsx +89 -0
  133. package/src/components/ui/list-item/index.ts +14 -0
  134. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  135. package/src/components/ui/menu/Menu.test.tsx +353 -0
  136. package/src/components/ui/menu/Menu.tsx +222 -0
  137. package/src/components/ui/menu/index.ts +2 -0
  138. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  139. package/src/components/ui/modal/Modal.test.tsx +159 -0
  140. package/src/components/ui/modal/Modal.tsx +254 -0
  141. package/src/components/ui/modal/index.ts +19 -0
  142. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  143. package/src/components/ui/pill/Pill.test.tsx +69 -0
  144. package/src/components/ui/pill/Pill.tsx +104 -0
  145. package/src/components/ui/pill/index.ts +2 -0
  146. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  147. package/src/components/ui/popover/Popover.test.tsx +419 -0
  148. package/src/components/ui/popover/Popover.tsx +237 -0
  149. package/src/components/ui/popover/index.ts +2 -0
  150. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  151. package/src/components/ui/progress/Progress.test.tsx +210 -0
  152. package/src/components/ui/progress/Progress.tsx +339 -0
  153. package/src/components/ui/progress/index.ts +9 -0
  154. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  155. package/src/components/ui/radio/Radio.test.tsx +166 -0
  156. package/src/components/ui/radio/Radio.tsx +200 -0
  157. package/src/components/ui/radio/index.ts +2 -0
  158. package/src/components/ui/section/Section.stories.tsx +111 -0
  159. package/src/components/ui/section/Section.test.tsx +121 -0
  160. package/src/components/ui/section/Section.tsx +52 -0
  161. package/src/components/ui/section/index.ts +6 -0
  162. package/src/components/ui/select/Select.stories.tsx +286 -0
  163. package/src/components/ui/select/Select.test.tsx +200 -0
  164. package/src/components/ui/select/Select.tsx +243 -0
  165. package/src/components/ui/select/index.ts +2 -0
  166. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  167. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  168. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  169. package/src/components/ui/skeleton/index.ts +16 -0
  170. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  171. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  172. package/src/components/ui/spinner/Spinner.tsx +72 -0
  173. package/src/components/ui/spinner/index.ts +2 -0
  174. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  175. package/src/components/ui/stack/Stack.test.tsx +184 -0
  176. package/src/components/ui/stack/Stack.tsx +76 -0
  177. package/src/components/ui/stack/index.ts +2 -0
  178. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  179. package/src/components/ui/surface/Surface.test.tsx +63 -0
  180. package/src/components/ui/surface/Surface.tsx +51 -0
  181. package/src/components/ui/surface/index.ts +1 -0
  182. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  183. package/src/components/ui/switch/Switch.test.tsx +112 -0
  184. package/src/components/ui/switch/Switch.tsx +103 -0
  185. package/src/components/ui/switch/index.ts +2 -0
  186. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  187. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  188. package/src/components/ui/tabs/Tabs.tsx +253 -0
  189. package/src/components/ui/tabs/index.ts +16 -0
  190. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  191. package/src/components/ui/toast/Toast.test.tsx +207 -0
  192. package/src/components/ui/toast/Toast.tsx +327 -0
  193. package/src/components/ui/toast/index.ts +9 -0
  194. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  195. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  196. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  197. package/src/components/ui/toolbar-button/index.ts +7 -0
  198. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  199. package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
  200. package/src/components/ui/tooltip/Tooltip.tsx +234 -0
  201. package/src/components/ui/tooltip/index.ts +2 -0
  202. package/src/index.ts +10 -0
  203. package/src/stories/PresetShowcase.stories.tsx +462 -0
  204. package/src/stories/ThemePresets.stories.tsx +85 -0
  205. package/src/test/setup.ts +19 -0
  206. package/src/theme/Colors.stories.tsx +258 -0
  207. package/src/theme/config.ts +193 -0
  208. package/src/theme/elevation.stories.tsx +346 -0
  209. package/src/theme/elevation.test.ts +27 -0
  210. package/src/theme/elevation.ts +567 -0
  211. package/src/theme/global.css +129 -1
  212. package/src/theme/index.ts +40 -0
  213. package/src/theme/presets/apply.ts +97 -0
  214. package/src/theme/presets/audiobook.ts +93 -0
  215. package/src/theme/presets/default.ts +6 -0
  216. package/src/theme/presets/index.ts +4 -0
  217. package/src/theme/presets/presets.test.ts +51 -0
  218. package/src/theme/presets/types.ts +76 -0
  219. package/src/theme/shadows.stories.tsx +523 -0
  220. package/src/theme/shadows.ts +439 -0
  221. package/src/theme/tokens/index.ts +2 -0
  222. package/src/theme/tokens/primitives.ts +354 -0
  223. package/src/theme/tokens/semantic.ts +381 -0
  224. package/src/types/jest-axe.d.ts +16 -0
  225. package/src/types/nativewind.d.ts +54 -0
  226. package/src/utils/avatar-color.test.ts +30 -0
  227. package/src/utils/avatar-color.ts +20 -0
  228. package/src/utils/cn.ts +14 -0
  229. package/src/utils/colors.ts +140 -0
  230. package/src/utils/form.ts +188 -0
  231. package/src/utils/index.ts +29 -0
  232. package/src/utils/useTheme.ts +72 -0
  233. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  234. package/src/web-jsx/jsx-runtime.ts +41 -0
  235. package/tailwind.config.js +53 -3
  236. package/dist/chunk-MGW37MPO.mjs.map +0 -1
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Stack, HStack, VStack } from './Stack'
5
+
6
+ describe('Stack', () => {
7
+ it('renders children correctly', () => {
8
+ render(
9
+ <Stack>
10
+ <span>Item 1</span>
11
+ <span>Item 2</span>
12
+ </Stack>
13
+ )
14
+ expect(screen.getByText('Item 1')).toBeInTheDocument()
15
+ expect(screen.getByText('Item 2')).toBeInTheDocument()
16
+ })
17
+
18
+ it('renders with testID', () => {
19
+ render(
20
+ <Stack testID="my-stack">
21
+ <span>Child</span>
22
+ </Stack>
23
+ )
24
+ expect(screen.getByTestId('my-stack')).toBeInTheDocument()
25
+ })
26
+
27
+ it('renders with gap prop without crashing', () => {
28
+ const { container } = render(
29
+ <Stack gap={4}>
30
+ <span>A</span>
31
+ <span>B</span>
32
+ </Stack>
33
+ )
34
+ expect(container.firstChild).toBeTruthy()
35
+ expect(screen.getByText('A')).toBeInTheDocument()
36
+ expect(screen.getByText('B')).toBeInTheDocument()
37
+ })
38
+
39
+ it('renders with align and justify props without crashing', () => {
40
+ const { container } = render(
41
+ <Stack align="center" justify="between">
42
+ <span>Child</span>
43
+ </Stack>
44
+ )
45
+ expect(container.firstChild).toBeTruthy()
46
+ })
47
+
48
+ it('renders with wrap prop without crashing', () => {
49
+ const { container } = render(
50
+ <Stack wrap>
51
+ <span>Child</span>
52
+ </Stack>
53
+ )
54
+ expect(container.firstChild).toBeTruthy()
55
+ })
56
+
57
+ it('renders with className prop without crashing', () => {
58
+ const { container } = render(
59
+ <Stack className="p-4">
60
+ <span>Child</span>
61
+ </Stack>
62
+ )
63
+ expect(container.firstChild).toBeTruthy()
64
+ })
65
+
66
+ it('renders with all props combined', () => {
67
+ render(
68
+ <Stack gap={8} align="center" justify="between" wrap className="mt-2" testID="full-stack">
69
+ <span>One</span>
70
+ <span>Two</span>
71
+ <span>Three</span>
72
+ </Stack>
73
+ )
74
+ expect(screen.getByTestId('full-stack')).toBeInTheDocument()
75
+ expect(screen.getByText('One')).toBeInTheDocument()
76
+ expect(screen.getByText('Two')).toBeInTheDocument()
77
+ expect(screen.getByText('Three')).toBeInTheDocument()
78
+ })
79
+
80
+ describe('accessibility', () => {
81
+ it('has no accessibility violations', async () => {
82
+ const { container } = render(
83
+ <Stack gap={4}>
84
+ <span>Item 1</span>
85
+ <span>Item 2</span>
86
+ </Stack>
87
+ )
88
+ const results = await axe(container)
89
+ expect(results).toHaveNoViolations()
90
+ })
91
+ })
92
+ })
93
+
94
+ describe('HStack', () => {
95
+ it('renders children correctly', () => {
96
+ render(
97
+ <HStack>
98
+ <span>Left</span>
99
+ <span>Right</span>
100
+ </HStack>
101
+ )
102
+ expect(screen.getByText('Left')).toBeInTheDocument()
103
+ expect(screen.getByText('Right')).toBeInTheDocument()
104
+ })
105
+
106
+ it('renders with gap and alignment props', () => {
107
+ render(
108
+ <HStack gap={2} align="center" testID="hstack">
109
+ <span>A</span>
110
+ <span>B</span>
111
+ </HStack>
112
+ )
113
+ expect(screen.getByTestId('hstack')).toBeInTheDocument()
114
+ expect(screen.getByText('A')).toBeInTheDocument()
115
+ })
116
+
117
+ it('merges custom className without crashing', () => {
118
+ render(
119
+ <HStack className="bg-red-500" testID="styled-hstack">
120
+ <span>Child</span>
121
+ </HStack>
122
+ )
123
+ expect(screen.getByTestId('styled-hstack')).toBeInTheDocument()
124
+ })
125
+
126
+ describe('accessibility', () => {
127
+ it('has no accessibility violations', async () => {
128
+ const { container } = render(
129
+ <HStack gap={4}>
130
+ <span>Left</span>
131
+ <span>Right</span>
132
+ </HStack>
133
+ )
134
+ const results = await axe(container)
135
+ expect(results).toHaveNoViolations()
136
+ })
137
+ })
138
+ })
139
+
140
+ describe('VStack', () => {
141
+ it('renders children correctly', () => {
142
+ render(
143
+ <VStack>
144
+ <span>Top</span>
145
+ <span>Bottom</span>
146
+ </VStack>
147
+ )
148
+ expect(screen.getByText('Top')).toBeInTheDocument()
149
+ expect(screen.getByText('Bottom')).toBeInTheDocument()
150
+ })
151
+
152
+ it('renders with gap and alignment props', () => {
153
+ render(
154
+ <VStack gap={6} justify="center" testID="vstack">
155
+ <span>A</span>
156
+ <span>B</span>
157
+ </VStack>
158
+ )
159
+ expect(screen.getByTestId('vstack')).toBeInTheDocument()
160
+ expect(screen.getByText('A')).toBeInTheDocument()
161
+ })
162
+
163
+ it('merges custom className without crashing', () => {
164
+ render(
165
+ <VStack className="mt-4" testID="styled-vstack">
166
+ <span>Child</span>
167
+ </VStack>
168
+ )
169
+ expect(screen.getByTestId('styled-vstack')).toBeInTheDocument()
170
+ })
171
+
172
+ describe('accessibility', () => {
173
+ it('has no accessibility violations', async () => {
174
+ const { container } = render(
175
+ <VStack gap={4}>
176
+ <span>Top</span>
177
+ <span>Bottom</span>
178
+ </VStack>
179
+ )
180
+ const results = await axe(container)
181
+ expect(results).toHaveNoViolations()
182
+ })
183
+ })
184
+ })
@@ -0,0 +1,76 @@
1
+ import React from 'react'
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12
6
+
7
+ const gapClasses: Record<Gap, string> = {
8
+ 0: 'gap-0',
9
+ 1: 'gap-1',
10
+ 2: 'gap-2',
11
+ 3: 'gap-3',
12
+ 4: 'gap-4',
13
+ 5: 'gap-5',
14
+ 6: 'gap-6',
15
+ 8: 'gap-8',
16
+ 10: 'gap-10',
17
+ 12: 'gap-12',
18
+ }
19
+
20
+ export interface StackProps extends ViewProps {
21
+ gap?: Gap
22
+ align?: 'start' | 'center' | 'end' | 'stretch'
23
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around'
24
+ wrap?: boolean
25
+ className?: string
26
+ children: React.ReactNode
27
+ }
28
+
29
+ const alignMap = {
30
+ start: 'items-start',
31
+ center: 'items-center',
32
+ end: 'items-end',
33
+ stretch: 'items-stretch',
34
+ }
35
+
36
+ const justifyMap = {
37
+ start: 'justify-start',
38
+ center: 'justify-center',
39
+ end: 'justify-end',
40
+ between: 'justify-between',
41
+ around: 'justify-around',
42
+ }
43
+
44
+ export function Stack({
45
+ gap = 0,
46
+ align,
47
+ justify,
48
+ wrap,
49
+ className,
50
+ children,
51
+ ...props
52
+ }: StackProps) {
53
+ return (
54
+ <View
55
+ className={cn(
56
+ 'flex',
57
+ gapClasses[gap],
58
+ align && alignMap[align],
59
+ justify && justifyMap[justify],
60
+ wrap && 'flex-wrap',
61
+ className,
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ </View>
67
+ )
68
+ }
69
+
70
+ export function HStack(props: StackProps) {
71
+ return <Stack {...props} className={cn('flex-row', props.className)} />
72
+ }
73
+
74
+ export function VStack(props: StackProps) {
75
+ return <Stack {...props} className={cn('flex-col', props.className)} />
76
+ }
@@ -0,0 +1,2 @@
1
+ export { Stack, HStack, VStack } from './Stack'
2
+ export type { StackProps } from './Stack'
@@ -0,0 +1,91 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Surface } from './Surface'
4
+ import type { ElevationLevel } from '../../../theme/elevation'
5
+
6
+ const meta: Meta<typeof Surface> = {
7
+ title: 'Components/Surface',
8
+ component: Surface,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ elevation: {
12
+ control: { type: 'range', min: -2, max: 5, step: 1 },
13
+ description: 'Elevation level (-2 to 5)',
14
+ },
15
+ theme: {
16
+ control: 'select',
17
+ options: ['light', 'dark'],
18
+ description: 'Color theme',
19
+ },
20
+ glowColor: {
21
+ control: 'color',
22
+ description: 'Optional glow color (hex)',
23
+ },
24
+ glowIntensity: {
25
+ control: 'select',
26
+ options: ['subtle', 'medium', 'strong'],
27
+ description: 'Glow intensity level',
28
+ },
29
+ },
30
+ }
31
+
32
+ export default meta
33
+ type Story = StoryObj<typeof Surface>
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ elevation: 2,
38
+ theme: 'dark',
39
+ },
40
+ render: (args) => (
41
+ <Surface {...args} style={{ padding: 24 }}>
42
+ <Text style={{ color: '#fff' }}>Default Surface</Text>
43
+ </Surface>
44
+ ),
45
+ }
46
+
47
+ export const ElevationLevels: Story = {
48
+ render: () => (
49
+ <View style={{ gap: 16, padding: 24 }}>
50
+ {([-2, -1, 0, 1, 2, 3, 4, 5] as ElevationLevel[]).map((level) => (
51
+ <Surface key={level} elevation={level} style={{ padding: 16 }}>
52
+ <Text style={{ color: '#fff' }}>Elevation {level}</Text>
53
+ </Surface>
54
+ ))}
55
+ </View>
56
+ ),
57
+ }
58
+
59
+ export const WithGlow: Story = {
60
+ render: () => (
61
+ <View style={{ gap: 16, padding: 24 }}>
62
+ <Surface elevation={2} glowColor="#FF7900" glowIntensity="subtle" style={{ padding: 16 }}>
63
+ <Text style={{ color: '#fff' }}>Subtle Orange Glow</Text>
64
+ </Surface>
65
+ <Surface elevation={2} glowColor="#FF7900" glowIntensity="medium" style={{ padding: 16 }}>
66
+ <Text style={{ color: '#fff' }}>Medium Orange Glow</Text>
67
+ </Surface>
68
+ <Surface elevation={2} glowColor="#FF7900" glowIntensity="strong" style={{ padding: 16 }}>
69
+ <Text style={{ color: '#fff' }}>Strong Orange Glow</Text>
70
+ </Surface>
71
+ <Surface elevation={2} glowColor="#22C55E" glowIntensity="medium" style={{ padding: 16 }}>
72
+ <Text style={{ color: '#fff' }}>Green Glow (Success)</Text>
73
+ </Surface>
74
+ <Surface elevation={2} glowColor="#EF4444" glowIntensity="medium" style={{ padding: 16 }}>
75
+ <Text style={{ color: '#fff' }}>Red Glow (Error)</Text>
76
+ </Surface>
77
+ </View>
78
+ ),
79
+ }
80
+
81
+ export const LightTheme: Story = {
82
+ render: () => (
83
+ <View style={{ gap: 16, padding: 24, backgroundColor: '#F3F4F6' }}>
84
+ {([0, 1, 2, 3, 4, 5] as ElevationLevel[]).map((level) => (
85
+ <Surface key={level} elevation={level} theme="light" style={{ padding: 16 }}>
86
+ <Text style={{ color: '#111' }}>Light Elevation {level}</Text>
87
+ </Surface>
88
+ ))}
89
+ </View>
90
+ ),
91
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Surface } from './Surface'
5
+
6
+ describe('Surface', () => {
7
+ it('renders children', () => {
8
+ render(
9
+ <Surface>
10
+ <span>Hello</span>
11
+ </Surface>
12
+ )
13
+ expect(screen.getByText('Hello')).toBeInTheDocument()
14
+ })
15
+
16
+ it('accepts elevation prop without crashing', () => {
17
+ render(
18
+ <Surface elevation={3}>
19
+ <span>Elevated</span>
20
+ </Surface>
21
+ )
22
+ expect(screen.getByText('Elevated')).toBeInTheDocument()
23
+ })
24
+
25
+ it('accepts glow props without crashing', () => {
26
+ render(
27
+ <Surface glowColor="#FF7900" glowIntensity="strong">
28
+ <span>Glowing</span>
29
+ </Surface>
30
+ )
31
+ expect(screen.getByText('Glowing')).toBeInTheDocument()
32
+ })
33
+
34
+ it('passes className through', () => {
35
+ render(
36
+ <Surface className="p-4" testID="surface">
37
+ <span>Styled</span>
38
+ </Surface>
39
+ )
40
+ expect(screen.getByTestId('surface')).toBeInTheDocument()
41
+ })
42
+
43
+ it('passes additional ViewProps', () => {
44
+ render(
45
+ <Surface testID="my-surface">
46
+ <span>Content</span>
47
+ </Surface>
48
+ )
49
+ expect(screen.getByTestId('my-surface')).toBeInTheDocument()
50
+ })
51
+
52
+ describe('accessibility', () => {
53
+ it('has no accessibility violations', async () => {
54
+ const { container } = render(
55
+ <Surface>
56
+ <span>Accessible surface</span>
57
+ </Surface>
58
+ )
59
+ const results = await axe(container)
60
+ expect(results).toHaveNoViolations()
61
+ })
62
+ })
63
+ })
@@ -0,0 +1,51 @@
1
+ import React from 'react'
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+ import {
5
+ type ElevationLevel,
6
+ getElevationSurface,
7
+ getElevationShadow,
8
+ getBaseSurfaceColor,
9
+ type GlowIntensity,
10
+ getGlowShadow,
11
+ } from '../../../theme/elevation'
12
+
13
+ export interface SurfaceProps extends ViewProps {
14
+ elevation?: ElevationLevel
15
+ glowColor?: string
16
+ glowIntensity?: GlowIntensity
17
+ theme?: 'light' | 'dark'
18
+ className?: string
19
+ children: React.ReactNode
20
+ }
21
+
22
+ export function Surface({
23
+ elevation = 0,
24
+ glowColor,
25
+ glowIntensity,
26
+ theme = 'dark',
27
+ className,
28
+ style,
29
+ children,
30
+ ...props
31
+ }: SurfaceProps) {
32
+ const baseColor = getBaseSurfaceColor(theme)
33
+ const surfaceColor = getElevationSurface(baseColor, elevation, theme)
34
+ const shadowStyle = getElevationShadow(baseColor, elevation, theme)
35
+ const glowStyle = glowColor ? getGlowShadow(glowColor, glowIntensity) : {}
36
+
37
+ return (
38
+ <View
39
+ className={cn('rounded-2xl', className)}
40
+ style={[
41
+ { backgroundColor: surfaceColor },
42
+ shadowStyle,
43
+ glowStyle,
44
+ style,
45
+ ]}
46
+ {...props}
47
+ >
48
+ {children}
49
+ </View>
50
+ )
51
+ }
@@ -0,0 +1 @@
1
+ export { Surface, type SurfaceProps } from './Surface'
@@ -0,0 +1,111 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Switch } from './Switch'
4
+
5
+ const meta: Meta<typeof Switch> = {
6
+ title: 'Components/Switch',
7
+ component: Switch,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ isChecked: {
11
+ control: 'boolean',
12
+ description: 'Whether the switch is on',
13
+ },
14
+ isDisabled: {
15
+ control: 'boolean',
16
+ description: 'Whether the switch is disabled',
17
+ },
18
+ size: {
19
+ control: 'select',
20
+ options: ['sm', 'md', 'lg'],
21
+ description: 'Switch size',
22
+ },
23
+ label: {
24
+ control: 'text',
25
+ description: 'Label text',
26
+ },
27
+ labelPosition: {
28
+ control: 'select',
29
+ options: ['left', 'right'],
30
+ description: 'Position of the label relative to the switch',
31
+ },
32
+ },
33
+ }
34
+
35
+ export default meta
36
+ type Story = StoryObj<typeof Switch>
37
+
38
+ export const Default: Story = {
39
+ args: {
40
+ label: 'Enable notifications',
41
+ size: 'md',
42
+ },
43
+ render: (args) => <Switch {...args} />,
44
+ }
45
+
46
+ export const On: Story = {
47
+ args: {
48
+ isChecked: true,
49
+ label: 'Switch is on',
50
+ size: 'md',
51
+ },
52
+ render: (args) => <Switch {...args} />,
53
+ }
54
+
55
+ export const Off: Story = {
56
+ args: {
57
+ isChecked: false,
58
+ label: 'Switch is off',
59
+ size: 'md',
60
+ },
61
+ render: (args) => <Switch {...args} />,
62
+ }
63
+
64
+ export const AllSizes: Story = {
65
+ render: () => (
66
+ <View style={{ gap: 16 }}>
67
+ <Switch isChecked size="sm" label="Small" />
68
+ <Switch isChecked size="md" label="Medium" />
69
+ <Switch isChecked size="lg" label="Large" />
70
+ </View>
71
+ ),
72
+ }
73
+
74
+ export const LabelLeft: Story = {
75
+ args: {
76
+ isChecked: true,
77
+ label: 'Label on the left',
78
+ labelPosition: 'left',
79
+ size: 'md',
80
+ },
81
+ render: (args) => <Switch {...args} />,
82
+ }
83
+
84
+ export const LabelRight: Story = {
85
+ args: {
86
+ isChecked: true,
87
+ label: 'Label on the right',
88
+ labelPosition: 'right',
89
+ size: 'md',
90
+ },
91
+ render: (args) => <Switch {...args} />,
92
+ }
93
+
94
+ export const Disabled: Story = {
95
+ render: () => (
96
+ <View style={{ gap: 16 }}>
97
+ <Switch isDisabled label="Disabled off" />
98
+ <Switch isDisabled isChecked label="Disabled on" />
99
+ </View>
100
+ ),
101
+ }
102
+
103
+ export const WithoutLabel: Story = {
104
+ render: () => (
105
+ <View style={{ flexDirection: 'row', gap: 16, alignItems: 'center' }}>
106
+ <Switch size="sm" />
107
+ <Switch isChecked size="md" />
108
+ <Switch size="lg" />
109
+ </View>
110
+ ),
111
+ }
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Switch } from './Switch'
5
+
6
+ describe('Switch', () => {
7
+ it('renders correctly', () => {
8
+ render(<Switch label="Toggle" />)
9
+ expect(screen.getByRole('switch')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders label text', () => {
13
+ render(<Switch label="Enable notifications" />)
14
+ expect(screen.getByText('Enable notifications')).toBeInTheDocument()
15
+ })
16
+
17
+ it('handles checked state change', () => {
18
+ const onCheckedChange = vi.fn()
19
+ render(<Switch label="Toggle" onCheckedChange={onCheckedChange} />)
20
+ fireEvent.click(screen.getByRole('switch'))
21
+ expect(onCheckedChange).toHaveBeenCalledWith(true)
22
+ })
23
+
24
+ it('toggles from checked to unchecked', () => {
25
+ const onCheckedChange = vi.fn()
26
+ render(<Switch label="Toggle" isChecked onCheckedChange={onCheckedChange} />)
27
+ fireEvent.click(screen.getByRole('switch'))
28
+ expect(onCheckedChange).toHaveBeenCalledWith(false)
29
+ })
30
+
31
+ it('does not fire onCheckedChange when disabled', () => {
32
+ const onCheckedChange = vi.fn()
33
+ render(<Switch label="Toggle" isDisabled onCheckedChange={onCheckedChange} />)
34
+ fireEvent.click(screen.getByRole('switch'))
35
+ expect(onCheckedChange).not.toHaveBeenCalled()
36
+ })
37
+
38
+ it('communicates checked state via aria', () => {
39
+ render(<Switch label="Toggle" isChecked />)
40
+ // react-native-web does not map accessibilityState.checked to aria-checked
41
+ expect(screen.getByRole('switch')).toBeInTheDocument()
42
+ })
43
+
44
+ it('communicates unchecked state via aria', () => {
45
+ render(<Switch label="Toggle" />)
46
+ expect(screen.getByRole('switch')).toBeInTheDocument()
47
+ })
48
+
49
+ it('communicates disabled state', () => {
50
+ render(<Switch label="Toggle" isDisabled />)
51
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true')
52
+ })
53
+
54
+ it('renders label on right by default', () => {
55
+ render(<Switch label="Right label" />)
56
+ expect(screen.getByText('Right label')).toBeInTheDocument()
57
+ })
58
+
59
+ it('renders label on left when labelPosition is left', () => {
60
+ render(<Switch label="Left label" labelPosition="left" />)
61
+ expect(screen.getByText('Left label')).toBeInTheDocument()
62
+ })
63
+
64
+ it('renders without label', () => {
65
+ render(<Switch />)
66
+ expect(screen.getByRole('switch')).toBeInTheDocument()
67
+ })
68
+
69
+ it('renders with all size options', () => {
70
+ const sizes = ['sm', 'md', 'lg'] as const
71
+ sizes.forEach((size) => {
72
+ const { unmount } = render(<Switch size={size} label="Test" />)
73
+ expect(screen.getByRole('switch')).toBeInTheDocument()
74
+ unmount()
75
+ })
76
+ })
77
+
78
+ it('applies custom className', () => {
79
+ render(<Switch className="extra" label="Test" />)
80
+ expect(screen.getByRole('switch')).toBeInTheDocument()
81
+ })
82
+
83
+ it('sets accessibility label from label prop', () => {
84
+ render(<Switch label="My switch" />)
85
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-label', 'My switch')
86
+ })
87
+
88
+ describe('accessibility', () => {
89
+ it('has no accessibility violations', async () => {
90
+ const { container } = render(<Switch label="Enable feature" />)
91
+ // react-native-web does not output aria-checked for accessibilityState.checked,
92
+ // so we disable the aria-required-attr rule that checks for it on role="switch"
93
+ const results = await axe(container, {
94
+ rules: { 'aria-required-attr': { enabled: false } },
95
+ })
96
+ expect(results).toHaveNoViolations()
97
+ })
98
+
99
+ it('has no accessibility violations when checked', async () => {
100
+ const { container } = render(<Switch label="Enabled" isChecked />)
101
+ const results = await axe(container, {
102
+ rules: { 'aria-required-attr': { enabled: false } },
103
+ })
104
+ expect(results).toHaveNoViolations()
105
+ })
106
+
107
+ it('has correct switch role', () => {
108
+ render(<Switch label="Toggle" />)
109
+ expect(screen.getByRole('switch')).toBeInTheDocument()
110
+ })
111
+ })
112
+ })