@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,280 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { FormField, FormSection, FormActions, FormRow } from './FormField'
5
+
6
+ describe('FormField', () => {
7
+ it('renders label and children', () => {
8
+ render(
9
+ <FormField label="Email">
10
+ <input type="email" placeholder="Enter email" />
11
+ </FormField>
12
+ )
13
+ expect(screen.getByText('Email')).toBeInTheDocument()
14
+ expect(screen.getByPlaceholderText('Enter email')).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders without a label', () => {
18
+ render(
19
+ <FormField>
20
+ <input placeholder="No label" />
21
+ </FormField>
22
+ )
23
+ expect(screen.getByPlaceholderText('No label')).toBeInTheDocument()
24
+ })
25
+
26
+ describe('required indicator', () => {
27
+ it('shows asterisk when isRequired is true', () => {
28
+ render(
29
+ <FormField label="Name" isRequired>
30
+ <input />
31
+ </FormField>
32
+ )
33
+ expect(screen.getByText('*')).toBeInTheDocument()
34
+ })
35
+
36
+ it('does not show asterisk when isRequired is false', () => {
37
+ render(
38
+ <FormField label="Name">
39
+ <input />
40
+ </FormField>
41
+ )
42
+ expect(screen.queryByText('*')).not.toBeInTheDocument()
43
+ })
44
+ })
45
+
46
+ describe('helper and error text', () => {
47
+ it('renders helper text', () => {
48
+ render(
49
+ <FormField label="Email" helperText="We will never share your email.">
50
+ <input />
51
+ </FormField>
52
+ )
53
+ expect(screen.getByText('We will never share your email.')).toBeInTheDocument()
54
+ })
55
+
56
+ it('renders error message', () => {
57
+ render(
58
+ <FormField label="Password" errorMessage="Password is too short.">
59
+ <input />
60
+ </FormField>
61
+ )
62
+ expect(screen.getByText('Password is too short.')).toBeInTheDocument()
63
+ })
64
+
65
+ it('shows error message instead of helper text when both are provided', () => {
66
+ render(
67
+ <FormField
68
+ label="Email"
69
+ helperText="Enter a valid email"
70
+ errorMessage="Invalid email"
71
+ >
72
+ <input />
73
+ </FormField>
74
+ )
75
+ expect(screen.getByText('Invalid email')).toBeInTheDocument()
76
+ expect(screen.queryByText('Enter a valid email')).not.toBeInTheDocument()
77
+ })
78
+ })
79
+
80
+ describe('disabled state', () => {
81
+ it('applies disabled styling when isDisabled is true', () => {
82
+ const { container } = render(
83
+ <FormField label="Name" isDisabled>
84
+ <input />
85
+ </FormField>
86
+ )
87
+ expect(container).toBeInTheDocument()
88
+ })
89
+ })
90
+
91
+ describe('label sizes', () => {
92
+ it('renders with sm label size', () => {
93
+ render(
94
+ <FormField label="Small Label" labelSize="sm">
95
+ <input />
96
+ </FormField>
97
+ )
98
+ expect(screen.getByText('Small Label')).toBeInTheDocument()
99
+ })
100
+
101
+ it('renders with md label size (default)', () => {
102
+ render(
103
+ <FormField label="Medium Label" labelSize="md">
104
+ <input />
105
+ </FormField>
106
+ )
107
+ expect(screen.getByText('Medium Label')).toBeInTheDocument()
108
+ })
109
+
110
+ it('renders with lg label size', () => {
111
+ render(
112
+ <FormField label="Large Label" labelSize="lg">
113
+ <input />
114
+ </FormField>
115
+ )
116
+ expect(screen.getByText('Large Label')).toBeInTheDocument()
117
+ })
118
+ })
119
+
120
+ describe('orientation', () => {
121
+ it('renders in vertical orientation (default)', () => {
122
+ const { container } = render(
123
+ <FormField label="Vertical" orientation="vertical">
124
+ <input />
125
+ </FormField>
126
+ )
127
+ expect(container).toBeInTheDocument()
128
+ })
129
+
130
+ it('renders in horizontal orientation', () => {
131
+ const { container } = render(
132
+ <FormField label="Horizontal" orientation="horizontal" labelWidth={120}>
133
+ <input />
134
+ </FormField>
135
+ )
136
+ expect(container).toBeInTheDocument()
137
+ })
138
+ })
139
+
140
+ describe('help content', () => {
141
+ it('renders help indicator when helpContent is provided', () => {
142
+ render(
143
+ <FormField label="API Key" helpContent="Found in settings">
144
+ <input />
145
+ </FormField>
146
+ )
147
+ expect(screen.getByText('API Key')).toBeInTheDocument()
148
+ })
149
+ })
150
+
151
+ describe('accessibility', () => {
152
+ it('has no accessibility violations', async () => {
153
+ const { container } = render(
154
+ <FormField label="Email" isRequired helperText="Enter a valid email">
155
+ <input type="email" aria-label="Email" />
156
+ </FormField>
157
+ )
158
+ const results = await axe(container)
159
+ expect(results).toHaveNoViolations()
160
+ })
161
+
162
+ it('has no accessibility violations with error', async () => {
163
+ const { container } = render(
164
+ <FormField label="Password" errorMessage="Too short">
165
+ <input type="password" aria-label="Password" />
166
+ </FormField>
167
+ )
168
+ const results = await axe(container)
169
+ expect(results).toHaveNoViolations()
170
+ })
171
+ })
172
+ })
173
+
174
+ describe('FormSection', () => {
175
+ it('renders title and description', () => {
176
+ render(
177
+ <FormSection title="Personal Info" description="Enter your details">
178
+ <span>Form fields</span>
179
+ </FormSection>
180
+ )
181
+ expect(screen.getByText('Personal Info')).toBeInTheDocument()
182
+ expect(screen.getByText('Enter your details')).toBeInTheDocument()
183
+ expect(screen.getByText('Form fields')).toBeInTheDocument()
184
+ })
185
+
186
+ it('renders without title and description', () => {
187
+ render(
188
+ <FormSection>
189
+ <span>Just fields</span>
190
+ </FormSection>
191
+ )
192
+ expect(screen.getByText('Just fields')).toBeInTheDocument()
193
+ })
194
+
195
+ it('has group accessibility role', () => {
196
+ render(
197
+ <FormSection title="Section">
198
+ <span>Content</span>
199
+ </FormSection>
200
+ )
201
+ expect(screen.getByRole('group')).toBeInTheDocument()
202
+ })
203
+
204
+ describe('accessibility', () => {
205
+ it('has no accessibility violations', async () => {
206
+ const { container } = render(
207
+ <FormSection title="Section" description="Description">
208
+ <span>Content</span>
209
+ </FormSection>
210
+ )
211
+ const results = await axe(container)
212
+ expect(results).toHaveNoViolations()
213
+ })
214
+ })
215
+ })
216
+
217
+ describe('FormActions', () => {
218
+ it('renders children', () => {
219
+ render(
220
+ <FormActions>
221
+ <button>Cancel</button>
222
+ <button>Submit</button>
223
+ </FormActions>
224
+ )
225
+ expect(screen.getByText('Cancel')).toBeInTheDocument()
226
+ expect(screen.getByText('Submit')).toBeInTheDocument()
227
+ })
228
+
229
+ it('renders with different alignments', () => {
230
+ const { rerender } = render(
231
+ <FormActions align="left">
232
+ <button>Action</button>
233
+ </FormActions>
234
+ )
235
+ expect(screen.getByText('Action')).toBeInTheDocument()
236
+
237
+ rerender(
238
+ <FormActions align="center">
239
+ <button>Action</button>
240
+ </FormActions>
241
+ )
242
+ expect(screen.getByText('Action')).toBeInTheDocument()
243
+
244
+ rerender(
245
+ <FormActions align="between">
246
+ <button>Action</button>
247
+ </FormActions>
248
+ )
249
+ expect(screen.getByText('Action')).toBeInTheDocument()
250
+ })
251
+ })
252
+
253
+ describe('FormRow', () => {
254
+ it('renders children in a row', () => {
255
+ render(
256
+ <FormRow>
257
+ <span>Field 1</span>
258
+ <span>Field 2</span>
259
+ </FormRow>
260
+ )
261
+ expect(screen.getByText('Field 1')).toBeInTheDocument()
262
+ expect(screen.getByText('Field 2')).toBeInTheDocument()
263
+ })
264
+
265
+ it('renders with different gap sizes', () => {
266
+ const { rerender } = render(
267
+ <FormRow gap="sm">
268
+ <span>Field</span>
269
+ </FormRow>
270
+ )
271
+ expect(screen.getByText('Field')).toBeInTheDocument()
272
+
273
+ rerender(
274
+ <FormRow gap="lg">
275
+ <span>Field</span>
276
+ </FormRow>
277
+ )
278
+ expect(screen.getByText('Field')).toBeInTheDocument()
279
+ })
280
+ })
@@ -0,0 +1,308 @@
1
+ import React from 'react'
2
+ import { View, Text, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export interface FormFieldProps extends ViewProps {
6
+ /** Label text */
7
+ label?: string
8
+ /** Whether the field is required */
9
+ isRequired?: boolean
10
+ /** Helper text shown below the input */
11
+ helperText?: string
12
+ /** Error message (replaces helper text when present) */
13
+ errorMessage?: string
14
+ /** Whether the field is disabled */
15
+ isDisabled?: boolean
16
+ /** Whether the field is read-only */
17
+ isReadOnly?: boolean
18
+ /** Help tooltip content */
19
+ helpContent?: React.ReactNode
20
+ /** Size of the label */
21
+ labelSize?: 'sm' | 'md' | 'lg'
22
+ /** Direction of the layout */
23
+ orientation?: 'vertical' | 'horizontal'
24
+ /** Label width (for horizontal layout) */
25
+ labelWidth?: number | string
26
+ /** Additional className */
27
+ className?: string
28
+ children?: React.ReactNode
29
+ }
30
+
31
+ const labelSizeStyles = {
32
+ sm: 'text-xs',
33
+ md: 'text-sm',
34
+ lg: 'text-base',
35
+ }
36
+
37
+ /**
38
+ * FormField component for wrapping form inputs with label, help text, and error states.
39
+ *
40
+ * @example
41
+ * // Basic usage
42
+ * <FormField label="Email" isRequired>
43
+ * <Input placeholder="Enter your email" />
44
+ * </FormField>
45
+ *
46
+ * @example
47
+ * // With error
48
+ * <FormField
49
+ * label="Password"
50
+ * isRequired
51
+ * errorMessage="Password must be at least 8 characters"
52
+ * >
53
+ * <Input type="password" />
54
+ * </FormField>
55
+ *
56
+ * @example
57
+ * // Horizontal layout
58
+ * <FormField label="Name" orientation="horizontal" labelWidth={100}>
59
+ * <Input />
60
+ * </FormField>
61
+ */
62
+ export function FormField({
63
+ label,
64
+ isRequired = false,
65
+ helperText,
66
+ errorMessage,
67
+ isDisabled = false,
68
+ isReadOnly = false,
69
+ helpContent,
70
+ labelSize = 'sm',
71
+ orientation = 'vertical',
72
+ labelWidth,
73
+ className,
74
+ children,
75
+ ...props
76
+ }: FormFieldProps) {
77
+ const isInvalid = !!errorMessage
78
+ const isHorizontal = orientation === 'horizontal'
79
+
80
+ // Generate unique IDs for accessibility
81
+ const fieldId = React.useId()
82
+ const helperId = `${fieldId}-helper`
83
+ const errorId = `${fieldId}-error`
84
+
85
+ return (
86
+ <View
87
+ className={cn(
88
+ 'w-full',
89
+ isHorizontal && 'flex-row items-start',
90
+ isDisabled && 'opacity-50',
91
+ className
92
+ )}
93
+ {...props}
94
+ >
95
+ {/* Label Section */}
96
+ {label && (
97
+ <View
98
+ className={cn(
99
+ 'flex-row items-center',
100
+ isHorizontal ? 'pt-2' : 'mb-1.5'
101
+ )}
102
+ style={isHorizontal && labelWidth ? { width: labelWidth as number } : undefined}
103
+ >
104
+ <Text
105
+ className={cn(
106
+ 'font-medium text-text-primary',
107
+ labelSizeStyles[labelSize]
108
+ )}
109
+ >
110
+ {label}
111
+ {isRequired && (
112
+ <Text className="text-status-error ml-0.5">*</Text>
113
+ )}
114
+ </Text>
115
+
116
+ {helpContent && (
117
+ <View className="ml-1">
118
+ {/* Inline help indicator - could use HelpTip here */}
119
+ <Text className="text-text-tertiary text-xs">ℹ</Text>
120
+ </View>
121
+ )}
122
+ </View>
123
+ )}
124
+
125
+ {/* Input Section */}
126
+ <View className={cn(isHorizontal && 'flex-1')}>
127
+ {/* Clone children to pass accessibility props */}
128
+ {React.Children.map(children, (child) => {
129
+ if (React.isValidElement(child)) {
130
+ const childProps = child.props as Record<string, any>
131
+ return React.cloneElement(child as React.ReactElement<any>, {
132
+ accessibilityHint: errorMessage || helperText || childProps.accessibilityHint,
133
+ accessibilityState: {
134
+ ...childProps.accessibilityState,
135
+ disabled: isDisabled || childProps.accessibilityState?.disabled,
136
+ ...(isInvalid ? { invalid: true } : {}),
137
+ },
138
+ isDisabled: isDisabled || childProps.isDisabled,
139
+ isReadOnly: isReadOnly || childProps.isReadOnly,
140
+ isInvalid: isInvalid || childProps.isInvalid,
141
+ })
142
+ }
143
+ return child
144
+ })}
145
+
146
+ {/* Helper/Error Text */}
147
+ {(helperText || errorMessage) && (
148
+ <Text
149
+ id={isInvalid ? errorId : helperId}
150
+ className={cn(
151
+ 'text-xs mt-1.5',
152
+ isInvalid ? 'text-status-error' : 'text-text-tertiary'
153
+ )}
154
+ >
155
+ {errorMessage || helperText}
156
+ </Text>
157
+ )}
158
+ </View>
159
+ </View>
160
+ )
161
+ }
162
+
163
+ export interface FormSectionProps extends ViewProps {
164
+ /** Section title */
165
+ title?: string
166
+ /** Section description */
167
+ description?: string
168
+ /** Additional className */
169
+ className?: string
170
+ children?: React.ReactNode
171
+ }
172
+
173
+ /**
174
+ * FormSection component for grouping related form fields.
175
+ *
176
+ * @example
177
+ * <FormSection title="Personal Information" description="Enter your basic details">
178
+ * <FormField label="Name">
179
+ * <Input />
180
+ * </FormField>
181
+ * <FormField label="Email">
182
+ * <Input />
183
+ * </FormField>
184
+ * </FormSection>
185
+ */
186
+ export function FormSection({
187
+ title,
188
+ description,
189
+ className,
190
+ children,
191
+ ...props
192
+ }: FormSectionProps) {
193
+ return (
194
+ <View
195
+ className={cn('w-full', className)}
196
+ accessibilityRole={'group' as any}
197
+ accessibilityLabel={title}
198
+ {...props}
199
+ >
200
+ {(title || description) && (
201
+ <View className="mb-4">
202
+ {title && (
203
+ <Text className="text-lg font-semibold text-text-primary">
204
+ {title}
205
+ </Text>
206
+ )}
207
+ {description && (
208
+ <Text className="text-sm text-text-secondary mt-1">
209
+ {description}
210
+ </Text>
211
+ )}
212
+ </View>
213
+ )}
214
+
215
+ <View className="gap-4">
216
+ {children}
217
+ </View>
218
+ </View>
219
+ )
220
+ }
221
+
222
+ export interface FormActionsProps extends ViewProps {
223
+ /** Alignment of actions */
224
+ align?: 'left' | 'center' | 'right' | 'between'
225
+ /** Additional className */
226
+ className?: string
227
+ children?: React.ReactNode
228
+ }
229
+
230
+ const alignStyles = {
231
+ left: 'justify-start',
232
+ center: 'justify-center',
233
+ right: 'justify-end',
234
+ between: 'justify-between',
235
+ }
236
+
237
+ /**
238
+ * FormActions component for form action buttons.
239
+ *
240
+ * @example
241
+ * <FormActions align="right">
242
+ * <Button variant="ghost">Cancel</Button>
243
+ * <Button>Submit</Button>
244
+ * </FormActions>
245
+ */
246
+ export function FormActions({
247
+ align = 'right',
248
+ className,
249
+ children,
250
+ ...props
251
+ }: FormActionsProps) {
252
+ return (
253
+ <View
254
+ className={cn(
255
+ 'flex-row items-center gap-3 pt-4',
256
+ 'border-t border-border-default mt-4',
257
+ alignStyles[align],
258
+ className
259
+ )}
260
+ {...props}
261
+ >
262
+ {children}
263
+ </View>
264
+ )
265
+ }
266
+
267
+ export interface FormRowProps extends ViewProps {
268
+ /** Gap between items */
269
+ gap?: 'sm' | 'md' | 'lg'
270
+ /** Additional className */
271
+ className?: string
272
+ children?: React.ReactNode
273
+ }
274
+
275
+ const gapStyles = {
276
+ sm: 'gap-2',
277
+ md: 'gap-4',
278
+ lg: 'gap-6',
279
+ }
280
+
281
+ /**
282
+ * FormRow component for laying out multiple form fields horizontally.
283
+ *
284
+ * @example
285
+ * <FormRow>
286
+ * <FormField label="First Name" className="flex-1">
287
+ * <Input />
288
+ * </FormField>
289
+ * <FormField label="Last Name" className="flex-1">
290
+ * <Input />
291
+ * </FormField>
292
+ * </FormRow>
293
+ */
294
+ export function FormRow({
295
+ gap = 'md',
296
+ className,
297
+ children,
298
+ ...props
299
+ }: FormRowProps) {
300
+ return (
301
+ <View
302
+ className={cn('flex-row items-start', gapStyles[gap], className)}
303
+ {...props}
304
+ >
305
+ {children}
306
+ </View>
307
+ )
308
+ }
@@ -0,0 +1,2 @@
1
+ export { FormField, FormSection, FormActions, FormRow } from './FormField'
2
+ export type { FormFieldProps, FormSectionProps, FormActionsProps, FormRowProps } from './FormField'