@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,192 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Input, InputGroup, PasswordInput } from './Input'
4
+
5
+ const meta: Meta<typeof Input> = {
6
+ title: 'Components/Input',
7
+ component: Input,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['outline', 'filled', 'underline'],
13
+ },
14
+ size: {
15
+ control: 'select',
16
+ options: ['sm', 'md', 'lg'],
17
+ },
18
+ multiline: { control: 'boolean' },
19
+ isDisabled: { control: 'boolean' },
20
+ isInvalid: { control: 'boolean' },
21
+ isReadOnly: { control: 'boolean' },
22
+ isRequired: { control: 'boolean' },
23
+ },
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Input>
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ placeholder: 'Enter text...',
32
+ },
33
+ }
34
+
35
+ export const WithLabel: Story = {
36
+ args: {
37
+ label: 'Email',
38
+ placeholder: 'Enter your email',
39
+ },
40
+ }
41
+
42
+ export const WithHelperText: Story = {
43
+ args: {
44
+ label: 'Password',
45
+ placeholder: 'Enter your password',
46
+ helperText: 'Must be at least 8 characters',
47
+ },
48
+ }
49
+
50
+ export const WithError: Story = {
51
+ args: {
52
+ label: 'Email',
53
+ placeholder: 'Enter your email',
54
+ isInvalid: true,
55
+ errorMessage: 'Please enter a valid email address',
56
+ defaultValue: 'invalid-email',
57
+ },
58
+ }
59
+
60
+ export const Required: Story = {
61
+ args: {
62
+ label: 'Username',
63
+ placeholder: 'Choose a username',
64
+ isRequired: true,
65
+ },
66
+ }
67
+
68
+ export const Disabled: Story = {
69
+ args: {
70
+ label: 'Disabled Input',
71
+ placeholder: 'This is disabled',
72
+ isDisabled: true,
73
+ defaultValue: 'Disabled value',
74
+ },
75
+ }
76
+
77
+ export const ReadOnly: Story = {
78
+ args: {
79
+ label: 'Read Only',
80
+ isReadOnly: true,
81
+ defaultValue: 'This value cannot be changed',
82
+ },
83
+ }
84
+
85
+ export const AllVariants: Story = {
86
+ render: () => (
87
+ <View style={{ gap: 16, width: 300 }}>
88
+ <Input variant="outline" label="Outline" placeholder="Outline variant" />
89
+ <Input variant="filled" label="Filled" placeholder="Filled variant" />
90
+ <Input variant="underline" label="Underline" placeholder="Underline variant" />
91
+ </View>
92
+ ),
93
+ }
94
+
95
+ export const AllSizes: Story = {
96
+ render: () => (
97
+ <View style={{ gap: 16, width: 300 }}>
98
+ <Input size="sm" placeholder="Small input" />
99
+ <Input size="md" placeholder="Medium input" />
100
+ <Input size="lg" placeholder="Large input" />
101
+ </View>
102
+ ),
103
+ }
104
+
105
+ export const Multiline: Story = {
106
+ args: {
107
+ label: 'Description',
108
+ placeholder: 'Enter a description...',
109
+ multiline: true,
110
+ numberOfLines: 4,
111
+ helperText: 'Maximum 500 characters',
112
+ },
113
+ }
114
+
115
+ export const MultilineSizes: Story = {
116
+ render: () => (
117
+ <View style={{ gap: 16, width: 400 }}>
118
+ <Input size="sm" multiline label="Small" placeholder="Small textarea" />
119
+ <Input size="md" multiline label="Medium" placeholder="Medium textarea" />
120
+ <Input size="lg" multiline label="Large" placeholder="Large textarea" />
121
+ </View>
122
+ ),
123
+ }
124
+
125
+ export const Password: Story = {
126
+ render: () => (
127
+ <View style={{ gap: 16, width: 300 }}>
128
+ <PasswordInput
129
+ label="Password"
130
+ placeholder="Enter your password"
131
+ helperText="Must be at least 8 characters"
132
+ />
133
+ </View>
134
+ ),
135
+ }
136
+
137
+ export const PasswordWithError: Story = {
138
+ render: () => (
139
+ <View style={{ gap: 16, width: 300 }}>
140
+ <PasswordInput
141
+ label="Password"
142
+ placeholder="Enter your password"
143
+ isInvalid
144
+ errorMessage="Password must be at least 8 characters"
145
+ defaultValue="short"
146
+ />
147
+ </View>
148
+ ),
149
+ }
150
+
151
+ // Simple search icon for demo
152
+ function SearchIcon() {
153
+ return (
154
+ <View className="w-4 h-4 items-center justify-center">
155
+ <View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
156
+ <View className="absolute bottom-0 right-0 w-1.5 h-0.5 bg-text-secondary rotate-45 origin-left" />
157
+ </View>
158
+ )
159
+ }
160
+
161
+ export const WithLeftElement: Story = {
162
+ render: () => (
163
+ <View style={{ gap: 16, width: 300 }}>
164
+ <Input
165
+ placeholder="Search..."
166
+ leftElement={<SearchIcon />}
167
+ />
168
+ </View>
169
+ ),
170
+ }
171
+
172
+ export const FormExample: Story = {
173
+ render: () => (
174
+ <InputGroup>
175
+ <Input label="First Name" placeholder="John" isRequired />
176
+ <Input label="Last Name" placeholder="Doe" isRequired />
177
+ <Input label="Email" placeholder="john@example.com" isRequired />
178
+ <PasswordInput
179
+ label="Password"
180
+ placeholder="••••••••"
181
+ helperText="Must be at least 8 characters"
182
+ isRequired
183
+ />
184
+ <Input
185
+ label="Bio"
186
+ placeholder="Tell us about yourself..."
187
+ multiline
188
+ numberOfLines={3}
189
+ />
190
+ </InputGroup>
191
+ ),
192
+ }
@@ -0,0 +1,86 @@
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 { Input } from './Input'
5
+
6
+ describe('Input', () => {
7
+ it('renders correctly', () => {
8
+ render(<Input placeholder="Enter text" />)
9
+ expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
10
+ })
11
+
12
+ it('displays label when provided', () => {
13
+ render(<Input label="Email" placeholder="Enter email" />)
14
+ expect(screen.getByText('Email')).toBeInTheDocument()
15
+ })
16
+
17
+ it('displays required indicator when isRequired', () => {
18
+ render(<Input label="Email" isRequired />)
19
+ expect(screen.getByText('*')).toBeInTheDocument()
20
+ })
21
+
22
+ it('displays helper text when provided', () => {
23
+ render(<Input helperText="This is helper text" />)
24
+ expect(screen.getByText('This is helper text')).toBeInTheDocument()
25
+ })
26
+
27
+ it('displays error message when isInvalid', () => {
28
+ render(
29
+ <Input
30
+ isInvalid
31
+ errorMessage="Invalid input"
32
+ helperText="This should not show"
33
+ />
34
+ )
35
+ expect(screen.getByText('Invalid input')).toBeInTheDocument()
36
+ expect(screen.queryByText('This should not show')).not.toBeInTheDocument()
37
+ })
38
+
39
+ it('handles text changes', () => {
40
+ const onChangeText = vi.fn()
41
+ render(<Input onChangeText={onChangeText} placeholder="Type here" />)
42
+
43
+ const input = screen.getByPlaceholderText('Type here')
44
+ // react-native-web renders as HTML input, use change event
45
+ fireEvent.change(input, { target: { value: 'Hello' } })
46
+
47
+ expect(onChangeText).toHaveBeenCalled()
48
+ })
49
+
50
+ it('respects isDisabled prop', () => {
51
+ render(<Input isDisabled placeholder="Disabled" />)
52
+ const input = screen.getByPlaceholderText('Disabled')
53
+ // react-native-web renders isDisabled as readonly, not disabled
54
+ expect(input).toHaveAttribute('readonly')
55
+ })
56
+
57
+ it('respects isReadOnly prop', () => {
58
+ render(<Input isReadOnly defaultValue="Read only" />)
59
+ const input = screen.getByDisplayValue('Read only')
60
+ expect(input).toHaveAttribute('readonly')
61
+ })
62
+
63
+ describe('accessibility', () => {
64
+ it('has no accessibility violations', async () => {
65
+ const { container } = render(
66
+ <Input label="Email" placeholder="Enter your email" />
67
+ )
68
+
69
+ const results = await axe(container)
70
+ expect(results).toHaveNoViolations()
71
+ })
72
+
73
+ it('associates label with input', () => {
74
+ render(<Input label="Username" placeholder="Enter username" />)
75
+ const input = screen.getByPlaceholderText('Enter username')
76
+ expect(input).toHaveAccessibleName('Username')
77
+ })
78
+
79
+ it('communicates disabled state', () => {
80
+ render(<Input isDisabled placeholder="Disabled input" />)
81
+ const input = screen.getByPlaceholderText('Disabled input')
82
+ // react-native-web renders isDisabled as readonly
83
+ expect(input).toHaveAttribute('readonly')
84
+ })
85
+ })
86
+ })
@@ -0,0 +1,273 @@
1
+ import React, { forwardRef, useState } from 'react'
2
+ import { TextInput, View, Text, Pressable, type TextInputProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type InputSize = 'sm' | 'md' | 'lg'
6
+ export type InputVariant = 'outline' | 'filled' | 'underline'
7
+
8
+ export interface InputProps extends Omit<TextInputProps, 'editable'> {
9
+ /** Input size */
10
+ size?: InputSize
11
+ /** Visual variant */
12
+ variant?: InputVariant
13
+ /** Whether the input is disabled */
14
+ isDisabled?: boolean
15
+ /** Whether the input has an error */
16
+ isInvalid?: boolean
17
+ /** Whether the input is read-only */
18
+ isReadOnly?: boolean
19
+ /** Whether the input is required */
20
+ isRequired?: boolean
21
+ /** Whether this is a multiline textarea */
22
+ multiline?: boolean
23
+ /** Number of visible lines for multiline */
24
+ numberOfLines?: number
25
+ /** Label text */
26
+ label?: string
27
+ /** Helper text shown below input */
28
+ helperText?: string
29
+ /** Error message (shown when isInvalid is true) */
30
+ errorMessage?: string
31
+ /** Left element/icon */
32
+ leftElement?: React.ReactNode
33
+ /** Right element/icon */
34
+ rightElement?: React.ReactNode
35
+ /** Additional className for the container */
36
+ className?: string
37
+ /** Additional className for the input */
38
+ inputClassName?: string
39
+ }
40
+
41
+ const sizeStyles: Record<InputSize, string> = {
42
+ sm: 'h-8 px-3 text-sm',
43
+ md: 'h-10 px-4 text-base',
44
+ lg: 'h-12 px-4 text-lg',
45
+ }
46
+
47
+ const multilineSizeStyles: Record<InputSize, string> = {
48
+ sm: 'min-h-[80px] px-3 py-2 text-sm',
49
+ md: 'min-h-[100px] px-4 py-3 text-base',
50
+ lg: 'min-h-[120px] px-4 py-3 text-lg',
51
+ }
52
+
53
+ const variantStyles: Record<InputVariant, { base: string; focus: string; hover: string; error: string }> = {
54
+ outline: {
55
+ base: 'border border-border-input rounded-md bg-transparent',
56
+ hover: 'web:hover:border-border-input-hover',
57
+ focus: 'border-border-input-focus',
58
+ error: 'border-border-input-error',
59
+ },
60
+ filled: {
61
+ base: 'border border-transparent rounded-md bg-surface-input',
62
+ hover: 'web:hover:border-border-input-hover',
63
+ focus: 'border-border-input-focus bg-transparent',
64
+ error: 'border-border-input-error',
65
+ },
66
+ underline: {
67
+ base: 'border-b border-border-input rounded-none bg-transparent',
68
+ hover: 'web:hover:border-border-input-hover',
69
+ focus: 'border-border-input-focus',
70
+ error: 'border-border-input-error',
71
+ },
72
+ }
73
+
74
+ /**
75
+ * Input component for text entry.
76
+ *
77
+ * @example
78
+ * <Input
79
+ * label="Email"
80
+ * placeholder="Enter your email"
81
+ * helperText="We'll never share your email"
82
+ * />
83
+ *
84
+ * // With error state
85
+ * <Input
86
+ * label="Password"
87
+ * isInvalid
88
+ * errorMessage="Password is required"
89
+ * />
90
+ */
91
+ export const Input = forwardRef<TextInput, InputProps>(function Input(
92
+ {
93
+ size = 'md',
94
+ variant = 'outline',
95
+ isDisabled = false,
96
+ isInvalid = false,
97
+ isReadOnly = false,
98
+ isRequired = false,
99
+ multiline = false,
100
+ numberOfLines = 4,
101
+ label,
102
+ helperText,
103
+ errorMessage,
104
+ leftElement,
105
+ rightElement,
106
+ className,
107
+ inputClassName,
108
+ onFocus,
109
+ onBlur,
110
+ ...props
111
+ },
112
+ ref
113
+ ) {
114
+ const [isFocused, setIsFocused] = useState(false)
115
+
116
+ const handleFocus = (e: any) => {
117
+ setIsFocused(true)
118
+ onFocus?.(e)
119
+ }
120
+
121
+ const handleBlur = (e: any) => {
122
+ setIsFocused(false)
123
+ onBlur?.(e)
124
+ }
125
+
126
+ const showError = isInvalid && errorMessage
127
+ const showHelper = !showError && helperText
128
+
129
+ return (
130
+ <View className={cn('w-full', className)}>
131
+ {label && (
132
+ <Text className="mb-1.5 text-sm font-medium text-text-primary">
133
+ {label}
134
+ {isRequired && <Text className="text-status-error ml-0.5">*</Text>}
135
+ </Text>
136
+ )}
137
+
138
+ <View
139
+ className={cn(
140
+ 'flex-row transition-colors duration-150',
141
+ multiline ? 'items-start' : 'items-center',
142
+ variantStyles[variant].base,
143
+ !isDisabled && !isInvalid && variantStyles[variant].hover,
144
+ isFocused && variantStyles[variant].focus,
145
+ isInvalid && variantStyles[variant].error,
146
+ isDisabled && 'opacity-40 cursor-not-allowed'
147
+ )}
148
+ >
149
+ {leftElement && (
150
+ <View className={cn('pl-3 pr-1', multiline && 'pt-3')}>
151
+ {leftElement}
152
+ </View>
153
+ )}
154
+
155
+ <TextInput
156
+ ref={ref}
157
+ editable={!isDisabled && !isReadOnly}
158
+ accessibilityLabel={label}
159
+ accessibilityState={{
160
+ disabled: isDisabled,
161
+ }}
162
+ multiline={multiline}
163
+ numberOfLines={multiline ? numberOfLines : 1}
164
+ textAlignVertical={multiline ? 'top' : 'center'}
165
+ placeholderTextColor="var(--color-text-tertiary)"
166
+ onFocus={handleFocus}
167
+ onBlur={handleBlur}
168
+ className={cn(
169
+ 'flex-1 text-text-primary',
170
+ multiline ? multilineSizeStyles[size] : sizeStyles[size],
171
+ leftElement && 'pl-1',
172
+ rightElement && 'pr-1',
173
+ inputClassName
174
+ )}
175
+ {...props}
176
+ />
177
+
178
+ {rightElement && (
179
+ <View className={cn('pl-1 pr-3', multiline && 'pt-3')}>
180
+ {rightElement}
181
+ </View>
182
+ )}
183
+ </View>
184
+
185
+ {showHelper && (
186
+ <Text className="mt-1.5 text-xs text-text-secondary">
187
+ {helperText}
188
+ </Text>
189
+ )}
190
+
191
+ {showError && (
192
+ <Text className="mt-1.5 text-xs text-status-error">
193
+ {errorMessage}
194
+ </Text>
195
+ )}
196
+ </View>
197
+ )
198
+ })
199
+
200
+ export interface InputGroupProps {
201
+ children: React.ReactNode
202
+ className?: string
203
+ }
204
+
205
+ /**
206
+ * Container for grouping related inputs.
207
+ */
208
+ export function InputGroup({ children, className }: InputGroupProps) {
209
+ return (
210
+ <View className={cn('flex-col gap-4', className)}>
211
+ {children}
212
+ </View>
213
+ )
214
+ }
215
+
216
+ export interface PasswordInputProps extends Omit<InputProps, 'secureTextEntry' | 'rightElement'> {
217
+ /** Custom show password icon */
218
+ showIcon?: React.ReactNode
219
+ /** Custom hide password icon */
220
+ hideIcon?: React.ReactNode
221
+ }
222
+
223
+ /**
224
+ * Password input with visibility toggle.
225
+ *
226
+ * @example
227
+ * <PasswordInput
228
+ * label="Password"
229
+ * placeholder="Enter your password"
230
+ * />
231
+ */
232
+ export const PasswordInput = forwardRef<TextInput, PasswordInputProps>(function PasswordInput(
233
+ { showIcon, hideIcon, ...props },
234
+ ref
235
+ ) {
236
+ const [isVisible, setIsVisible] = useState(false)
237
+
238
+ const toggleVisibility = () => setIsVisible(!isVisible)
239
+
240
+ // Default eye icons (simple SVG-like views)
241
+ const defaultShowIcon = (
242
+ <View className="w-5 h-5 items-center justify-center">
243
+ <View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
244
+ <View className="absolute w-1.5 h-1.5 rounded-full bg-text-secondary" />
245
+ </View>
246
+ )
247
+
248
+ const defaultHideIcon = (
249
+ <View className="w-5 h-5 items-center justify-center">
250
+ <View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
251
+ <View className="absolute w-1.5 h-1.5 rounded-full bg-text-secondary" />
252
+ <View className="absolute w-5 h-0.5 bg-text-secondary rotate-45" />
253
+ </View>
254
+ )
255
+
256
+ return (
257
+ <Input
258
+ ref={ref}
259
+ secureTextEntry={!isVisible}
260
+ rightElement={
261
+ <Pressable
262
+ onPress={toggleVisibility}
263
+ accessibilityRole="button"
264
+ accessibilityLabel={isVisible ? 'Hide password' : 'Show password'}
265
+ className="p-1 rounded active:opacity-70"
266
+ >
267
+ {isVisible ? (hideIcon || defaultHideIcon) : (showIcon || defaultShowIcon)}
268
+ </Pressable>
269
+ }
270
+ {...props}
271
+ />
272
+ )
273
+ })
@@ -0,0 +1,2 @@
1
+ export { Input, InputGroup, PasswordInput } from './Input'
2
+ export type { InputProps, InputGroupProps, PasswordInputProps, InputSize, InputVariant } from './Input'
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Link } from './Link'
4
+
5
+ const meta: Meta<typeof Link> = {
6
+ title: 'Components/Link',
7
+ component: Link,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ color: {
11
+ control: 'select',
12
+ options: ['default', 'primary', 'secondary', 'inherit'],
13
+ description: 'Link color',
14
+ },
15
+ underline: {
16
+ control: 'select',
17
+ options: ['always', 'hover', 'none'],
18
+ description: 'Underline behavior',
19
+ },
20
+ isExternal: {
21
+ control: 'boolean',
22
+ description: 'Whether the link opens externally',
23
+ },
24
+ isDisabled: {
25
+ control: 'boolean',
26
+ description: 'Whether the link is disabled',
27
+ },
28
+ },
29
+ }
30
+
31
+ export default meta
32
+ type Story = StoryObj<typeof Link>
33
+
34
+ export const Default: Story = {
35
+ args: {
36
+ children: 'Click here',
37
+ color: 'default',
38
+ underline: 'hover',
39
+ onPress: () => {},
40
+ },
41
+ }
42
+
43
+ export const AllColors: Story = {
44
+ render: () => (
45
+ <View style={{ flexDirection: 'row', gap: 16 }}>
46
+ <Link color="default" onPress={() => {}}>Default</Link>
47
+ <Link color="primary" onPress={() => {}}>Primary</Link>
48
+ <Link color="secondary" onPress={() => {}}>Secondary</Link>
49
+ </View>
50
+ ),
51
+ }
52
+
53
+ export const UnderlineStyles: Story = {
54
+ render: () => (
55
+ <View style={{ flexDirection: 'row', gap: 16 }}>
56
+ <Link underline="always" onPress={() => {}}>Always</Link>
57
+ <Link underline="hover" onPress={() => {}}>Hover</Link>
58
+ <Link underline="none" onPress={() => {}}>None</Link>
59
+ </View>
60
+ ),
61
+ }
62
+
63
+ export const External: Story = {
64
+ args: {
65
+ children: 'Visit Example',
66
+ href: 'https://example.com',
67
+ isExternal: true,
68
+ onPress: () => {},
69
+ },
70
+ }
71
+
72
+ export const Disabled: Story = {
73
+ args: {
74
+ children: 'Disabled Link',
75
+ isDisabled: true,
76
+ onPress: () => {},
77
+ },
78
+ }