@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,250 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { HelpTip, LabelWithHelp } from './HelpTip'
4
+
5
+ const meta: Meta<typeof HelpTip> = {
6
+ title: 'Components/HelpTip',
7
+ component: HelpTip,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ size: {
11
+ control: 'select',
12
+ options: ['sm', 'md', 'lg'],
13
+ },
14
+ placement: {
15
+ control: 'select',
16
+ options: ['top', 'bottom', 'left', 'right'],
17
+ },
18
+ icon: {
19
+ control: 'select',
20
+ options: ['help', 'info', 'warning'],
21
+ },
22
+ color: {
23
+ control: 'select',
24
+ options: ['default', 'primary', 'secondary', 'muted'],
25
+ },
26
+ hasArrow: {
27
+ control: 'boolean',
28
+ },
29
+ },
30
+ }
31
+
32
+ export default meta
33
+ type Story = StoryObj<typeof HelpTip>
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ content: 'This is helpful information about the feature.',
38
+ size: 'md',
39
+ placement: 'top',
40
+ icon: 'help',
41
+ },
42
+ }
43
+
44
+ export const AllIcons: Story = {
45
+ render: () => (
46
+ <View className="flex-row gap-8 items-center">
47
+ <View className="items-center gap-2">
48
+ <HelpTip content="Help icon tooltip" icon="help" />
49
+ <Text className="text-xs text-text-secondary">Help</Text>
50
+ </View>
51
+ <View className="items-center gap-2">
52
+ <HelpTip content="Info icon tooltip" icon="info" />
53
+ <Text className="text-xs text-text-secondary">Info</Text>
54
+ </View>
55
+ <View className="items-center gap-2">
56
+ <HelpTip content="Warning icon tooltip" icon="warning" />
57
+ <Text className="text-xs text-text-secondary">Warning</Text>
58
+ </View>
59
+ </View>
60
+ ),
61
+ }
62
+
63
+ export const AllSizes: Story = {
64
+ render: () => (
65
+ <View className="flex-row gap-8 items-center">
66
+ <View className="items-center gap-2">
67
+ <HelpTip content="Small help tip" size="sm" />
68
+ <Text className="text-xs text-text-secondary">Small</Text>
69
+ </View>
70
+ <View className="items-center gap-2">
71
+ <HelpTip content="Medium help tip" size="md" />
72
+ <Text className="text-xs text-text-secondary">Medium</Text>
73
+ </View>
74
+ <View className="items-center gap-2">
75
+ <HelpTip content="Large help tip" size="lg" />
76
+ <Text className="text-xs text-text-secondary">Large</Text>
77
+ </View>
78
+ </View>
79
+ ),
80
+ }
81
+
82
+ export const AllColors: Story = {
83
+ render: () => (
84
+ <View className="flex-row gap-6 items-center">
85
+ <HelpTip content="Default color" color="default" />
86
+ <HelpTip content="Primary color" color="primary" />
87
+ <HelpTip content="Secondary color" color="secondary" />
88
+ <HelpTip content="Muted color" color="muted" />
89
+ </View>
90
+ ),
91
+ }
92
+
93
+ export const AllPlacements: Story = {
94
+ render: () => (
95
+ <View className="gap-8 items-center py-16">
96
+ <View className="flex-row items-center gap-2">
97
+ <Text className="text-text-secondary w-16">Top:</Text>
98
+ <HelpTip content="Tooltip on top" placement="top" />
99
+ </View>
100
+ <View className="flex-row items-center gap-2">
101
+ <Text className="text-text-secondary w-16">Bottom:</Text>
102
+ <HelpTip content="Tooltip on bottom" placement="bottom" />
103
+ </View>
104
+ <View className="flex-row items-center gap-2">
105
+ <Text className="text-text-secondary w-16">Left:</Text>
106
+ <HelpTip content="Tooltip on left" placement="left" />
107
+ </View>
108
+ <View className="flex-row items-center gap-2">
109
+ <Text className="text-text-secondary w-16">Right:</Text>
110
+ <HelpTip content="Tooltip on right" placement="right" />
111
+ </View>
112
+ </View>
113
+ ),
114
+ }
115
+
116
+ export const WithLongContent: Story = {
117
+ args: {
118
+ content: 'This is a longer tooltip that contains more detailed information about the feature. It will wrap to multiple lines if needed, and you can customize the max width.',
119
+ maxWidth: 300,
120
+ },
121
+ }
122
+
123
+ export const WithRichContent: Story = {
124
+ render: () => (
125
+ <HelpTip
126
+ content={
127
+ <View className="gap-1">
128
+ <Text className="text-sm font-semibold text-text-primary">API Rate Limits</Text>
129
+ <Text className="text-xs text-text-secondary">
130
+ • Free tier: 100 requests/hour
131
+ </Text>
132
+ <Text className="text-xs text-text-secondary">
133
+ • Pro tier: 1000 requests/hour
134
+ </Text>
135
+ <Text className="text-xs text-text-secondary">
136
+ • Enterprise: Unlimited
137
+ </Text>
138
+ </View>
139
+ }
140
+ icon="info"
141
+ maxWidth={220}
142
+ />
143
+ ),
144
+ }
145
+
146
+ export const InlineWithLabel: Story = {
147
+ render: () => (
148
+ <View className="gap-4">
149
+ <View className="flex-row items-center gap-1">
150
+ <Text className="text-sm font-medium text-text-primary">Email Address</Text>
151
+ <HelpTip
152
+ content="We'll never share your email with third parties."
153
+ size="sm"
154
+ />
155
+ </View>
156
+
157
+ <View className="flex-row items-center gap-1">
158
+ <Text className="text-sm font-medium text-text-primary">Password</Text>
159
+ <HelpTip
160
+ content="Must be at least 8 characters with one uppercase, one lowercase, and one number."
161
+ size="sm"
162
+ />
163
+ </View>
164
+
165
+ <View className="flex-row items-center gap-1">
166
+ <Text className="text-sm font-medium text-text-primary">API Key</Text>
167
+ <HelpTip
168
+ content="Keep this secret! Anyone with this key can access your account."
169
+ icon="warning"
170
+ size="sm"
171
+ color="primary"
172
+ />
173
+ </View>
174
+ </View>
175
+ ),
176
+ }
177
+
178
+ export const LabelWithHelpComponent: Story = {
179
+ render: () => (
180
+ <View className="gap-4">
181
+ <LabelWithHelp
182
+ label="Username"
183
+ helpContent="Choose a unique username for your account."
184
+ />
185
+
186
+ <LabelWithHelp
187
+ label="Email Address"
188
+ helpContent="We'll use this for account notifications."
189
+ isRequired
190
+ />
191
+
192
+ <LabelWithHelp
193
+ label="Bio"
194
+ helpContent="Tell others a bit about yourself. Max 500 characters."
195
+ helpSize="sm"
196
+ />
197
+ </View>
198
+ ),
199
+ }
200
+
201
+ export const FormExample: Story = {
202
+ render: () => (
203
+ <View className="gap-4 p-4 bg-surface-base rounded-lg max-w-sm">
204
+ <Text className="text-lg font-semibold text-text-primary">Account Settings</Text>
205
+
206
+ <View className="gap-1">
207
+ <LabelWithHelp
208
+ label="Display Name"
209
+ helpContent="This is how your name will appear to other users."
210
+ isRequired
211
+ />
212
+ <View className="border border-border-input rounded-md px-3 py-2 bg-surface-input">
213
+ <Text className="text-text-primary">John Doe</Text>
214
+ </View>
215
+ </View>
216
+
217
+ <View className="gap-1">
218
+ <LabelWithHelp
219
+ label="Timezone"
220
+ helpContent="All dates and times will be displayed in this timezone."
221
+ />
222
+ <View className="border border-border-input rounded-md px-3 py-2 bg-surface-input">
223
+ <Text className="text-text-primary">America/New_York (EST)</Text>
224
+ </View>
225
+ </View>
226
+
227
+ <View className="gap-1">
228
+ <View className="flex-row items-center gap-1">
229
+ <Text className="text-sm font-medium text-text-primary">Two-Factor Authentication</Text>
230
+ <HelpTip
231
+ content="Enable 2FA for additional account security. You'll need an authenticator app."
232
+ icon="info"
233
+ size="sm"
234
+ color="primary"
235
+ />
236
+ </View>
237
+ <View className="border border-border-input rounded-md px-3 py-2 bg-surface-input">
238
+ <Text className="text-status-success">Enabled ✓</Text>
239
+ </View>
240
+ </View>
241
+ </View>
242
+ ),
243
+ }
244
+
245
+ export const NoArrow: Story = {
246
+ args: {
247
+ content: 'This tooltip has no arrow.',
248
+ hasArrow: false,
249
+ },
250
+ }
@@ -0,0 +1,198 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { HelpTip, LabelWithHelp } from './HelpTip'
5
+
6
+ describe('HelpTip', () => {
7
+ it('renders the help icon button', () => {
8
+ render(<HelpTip content="Help text" />)
9
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
10
+ })
11
+
12
+ it('shows tooltip content when pressed', () => {
13
+ render(<HelpTip content="This is helpful" />)
14
+
15
+ fireEvent.click(screen.getByRole('button', { name: 'Show help information' }))
16
+ expect(screen.getByText('This is helpful')).toBeInTheDocument()
17
+ })
18
+
19
+ it('hides tooltip content when pressed again', () => {
20
+ render(<HelpTip content="Toggle me" />)
21
+
22
+ const button = screen.getByRole('button', { name: 'Show help information' })
23
+ fireEvent.click(button)
24
+ expect(screen.getByText('Toggle me')).toBeInTheDocument()
25
+
26
+ fireEvent.click(button)
27
+ expect(screen.queryByText('Toggle me')).not.toBeInTheDocument()
28
+ })
29
+
30
+ it('renders custom ReactNode content', () => {
31
+ render(
32
+ <HelpTip content={<span data-testid="custom-content">Rich content</span>} />
33
+ )
34
+
35
+ fireEvent.click(screen.getByRole('button', { name: 'Show help information' }))
36
+ expect(screen.getByTestId('custom-content')).toBeInTheDocument()
37
+ })
38
+
39
+ describe('controlled state', () => {
40
+ it('shows tooltip when isOpen is true', () => {
41
+ render(<HelpTip content="Always visible" isOpen />)
42
+ expect(screen.getByText('Always visible')).toBeInTheDocument()
43
+ })
44
+
45
+ it('hides tooltip when isOpen is false', () => {
46
+ render(<HelpTip content="Always hidden" isOpen={false} />)
47
+ expect(screen.queryByText('Always hidden')).not.toBeInTheDocument()
48
+ })
49
+
50
+ it('does not toggle on press when controlled', () => {
51
+ render(<HelpTip content="Controlled" isOpen={false} />)
52
+
53
+ fireEvent.click(screen.getByRole('button', { name: 'Show help information' }))
54
+ expect(screen.queryByText('Controlled')).not.toBeInTheDocument()
55
+ })
56
+ })
57
+
58
+ describe('icon types', () => {
59
+ it('renders help icon by default', () => {
60
+ render(<HelpTip content="Help" />)
61
+ expect(screen.getByText('?')).toBeInTheDocument()
62
+ })
63
+
64
+ it('renders info icon', () => {
65
+ render(<HelpTip content="Info" icon="info" />)
66
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
67
+ })
68
+
69
+ it('renders warning icon', () => {
70
+ render(<HelpTip content="Warning" icon="warning" />)
71
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
72
+ })
73
+ })
74
+
75
+ describe('sizes', () => {
76
+ it('renders with sm size', () => {
77
+ render(<HelpTip content="Small" size="sm" />)
78
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
79
+ })
80
+
81
+ it('renders with md size (default)', () => {
82
+ render(<HelpTip content="Medium" size="md" />)
83
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
84
+ })
85
+
86
+ it('renders with lg size', () => {
87
+ render(<HelpTip content="Large" size="lg" />)
88
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
89
+ })
90
+ })
91
+
92
+ describe('colors', () => {
93
+ it('renders with default color', () => {
94
+ render(<HelpTip content="Default" color="default" />)
95
+ expect(screen.getByRole('button')).toBeInTheDocument()
96
+ })
97
+
98
+ it('renders with primary color', () => {
99
+ render(<HelpTip content="Primary" color="primary" />)
100
+ expect(screen.getByRole('button')).toBeInTheDocument()
101
+ })
102
+
103
+ it('renders with secondary color', () => {
104
+ render(<HelpTip content="Secondary" color="secondary" />)
105
+ expect(screen.getByRole('button')).toBeInTheDocument()
106
+ })
107
+
108
+ it('renders with muted color', () => {
109
+ render(<HelpTip content="Muted" color="muted" />)
110
+ expect(screen.getByRole('button')).toBeInTheDocument()
111
+ })
112
+ })
113
+
114
+ describe('placement', () => {
115
+ it('renders with top placement (default)', () => {
116
+ render(<HelpTip content="Top" placement="top" isOpen />)
117
+ expect(screen.getByText('Top')).toBeInTheDocument()
118
+ })
119
+
120
+ it('renders with bottom placement', () => {
121
+ render(<HelpTip content="Bottom" placement="bottom" isOpen />)
122
+ expect(screen.getByText('Bottom')).toBeInTheDocument()
123
+ })
124
+
125
+ it('renders with left placement', () => {
126
+ render(<HelpTip content="Left" placement="left" isOpen />)
127
+ expect(screen.getByText('Left')).toBeInTheDocument()
128
+ })
129
+
130
+ it('renders with right placement', () => {
131
+ render(<HelpTip content="Right" placement="right" isOpen />)
132
+ expect(screen.getByText('Right')).toBeInTheDocument()
133
+ })
134
+ })
135
+
136
+ describe('arrow', () => {
137
+ it('renders with arrow by default', () => {
138
+ render(<HelpTip content="With arrow" isOpen />)
139
+ expect(screen.getByText('With arrow')).toBeInTheDocument()
140
+ })
141
+
142
+ it('renders without arrow when hasArrow is false', () => {
143
+ render(<HelpTip content="No arrow" hasArrow={false} isOpen />)
144
+ expect(screen.getByText('No arrow')).toBeInTheDocument()
145
+ })
146
+ })
147
+
148
+ describe('accessibility', () => {
149
+ it('has no accessibility violations', async () => {
150
+ const { container } = render(<HelpTip content="Accessible help tip" />)
151
+ const results = await axe(container)
152
+ expect(results).toHaveNoViolations()
153
+ })
154
+
155
+ it('has correct accessibility label', () => {
156
+ render(<HelpTip content="Help text" />)
157
+ expect(screen.getByLabelText('Show help information')).toBeInTheDocument()
158
+ })
159
+
160
+ it('sets accessibility hint for string content', () => {
161
+ render(<HelpTip content="This is a hint" />)
162
+ const button = screen.getByRole('button', { name: 'Show help information' })
163
+ expect(button).toBeInTheDocument()
164
+ })
165
+ })
166
+ })
167
+
168
+ describe('LabelWithHelp', () => {
169
+ it('renders label text', () => {
170
+ render(<LabelWithHelp label="API Key" helpContent="Found in settings" />)
171
+ expect(screen.getByText('API Key')).toBeInTheDocument()
172
+ })
173
+
174
+ it('renders help icon alongside label', () => {
175
+ render(<LabelWithHelp label="Field" helpContent="Help text" />)
176
+ expect(screen.getByRole('button', { name: 'Show help information' })).toBeInTheDocument()
177
+ })
178
+
179
+ it('shows required indicator when isRequired is true', () => {
180
+ render(<LabelWithHelp label="Email" helpContent="Help" isRequired />)
181
+ expect(screen.getByText('*')).toBeInTheDocument()
182
+ })
183
+
184
+ it('does not show required indicator by default', () => {
185
+ render(<LabelWithHelp label="Email" helpContent="Help" />)
186
+ expect(screen.queryByText('*')).not.toBeInTheDocument()
187
+ })
188
+
189
+ describe('accessibility', () => {
190
+ it('has no accessibility violations', async () => {
191
+ const { container } = render(
192
+ <LabelWithHelp label="Username" helpContent="Choose a unique username" />
193
+ )
194
+ const results = await axe(container)
195
+ expect(results).toHaveNoViolations()
196
+ })
197
+ })
198
+ })
@@ -0,0 +1,229 @@
1
+ import React, { useState } from 'react'
2
+ import { View, Text, Pressable, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type HelpTipSize = 'sm' | 'md' | 'lg'
6
+ export type HelpTipPlacement = 'top' | 'bottom' | 'left' | 'right'
7
+ export type HelpTipIcon = 'help' | 'info' | 'warning'
8
+
9
+ export interface HelpTipProps extends ViewProps {
10
+ /** Tooltip content */
11
+ content: React.ReactNode
12
+ /** Size of the icon */
13
+ size?: HelpTipSize
14
+ /** Placement of the tooltip */
15
+ placement?: HelpTipPlacement
16
+ /** Icon type */
17
+ icon?: HelpTipIcon
18
+ /** Icon color */
19
+ color?: 'default' | 'primary' | 'secondary' | 'muted'
20
+ /** Whether the tooltip is always visible (controlled) */
21
+ isOpen?: boolean
22
+ /** Whether to show an arrow */
23
+ hasArrow?: boolean
24
+ /** Max width of the tooltip */
25
+ maxWidth?: number
26
+ /** Additional className for the icon */
27
+ iconClassName?: string
28
+ /** Additional className for the tooltip */
29
+ tooltipClassName?: string
30
+ }
31
+
32
+ const sizeStyles: Record<HelpTipSize, { icon: string; text: string }> = {
33
+ sm: { icon: 'w-3.5 h-3.5 text-xs', text: 'text-xs' },
34
+ md: { icon: 'w-4 h-4 text-sm', text: 'text-sm' },
35
+ lg: { icon: 'w-5 h-5 text-base', text: 'text-base' },
36
+ }
37
+
38
+ const colorStyles: Record<string, string> = {
39
+ default: 'text-text-tertiary',
40
+ primary: 'text-brand-primary',
41
+ secondary: 'text-text-secondary',
42
+ muted: 'text-text-tertiary opacity-60',
43
+ }
44
+
45
+ const iconMap: Record<HelpTipIcon, string> = {
46
+ help: '?',
47
+ info: 'ℹ',
48
+ warning: '⚠',
49
+ }
50
+
51
+ const placementStyles: Record<HelpTipPlacement, string> = {
52
+ top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
53
+ bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
54
+ left: 'right-full top-1/2 -translate-y-1/2 mr-2',
55
+ right: 'left-full top-1/2 -translate-y-1/2 ml-2',
56
+ }
57
+
58
+ const arrowStyles: Record<HelpTipPlacement, string> = {
59
+ top: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-full border-t-surface-elevated border-x-transparent border-b-transparent',
60
+ bottom: 'top-0 left-1/2 -translate-x-1/2 -translate-y-full border-b-surface-elevated border-x-transparent border-t-transparent',
61
+ left: 'right-0 top-1/2 -translate-y-1/2 translate-x-full border-l-surface-elevated border-y-transparent border-r-transparent',
62
+ right: 'left-0 top-1/2 -translate-y-1/2 -translate-x-full border-r-surface-elevated border-y-transparent border-l-transparent',
63
+ }
64
+
65
+ /**
66
+ * HelpTip component for displaying contextual help information.
67
+ *
68
+ * A small icon (typically a question mark or info icon) that shows
69
+ * a tooltip with helpful information when hovered or pressed.
70
+ *
71
+ * @example
72
+ * // Basic usage
73
+ * <HelpTip content="This field is required for form submission." />
74
+ *
75
+ * @example
76
+ * // With custom icon and placement
77
+ * <HelpTip
78
+ * content="API rate limits apply to this endpoint."
79
+ * icon="warning"
80
+ * placement="right"
81
+ * color="primary"
82
+ * />
83
+ *
84
+ * @example
85
+ * // Inline with label
86
+ * <View className="flex-row items-center gap-1">
87
+ * <Text>Email Address</Text>
88
+ * <HelpTip content="We'll never share your email with anyone." size="sm" />
89
+ * </View>
90
+ */
91
+ export function HelpTip({
92
+ content,
93
+ size = 'md',
94
+ placement = 'top',
95
+ icon = 'help',
96
+ color = 'default',
97
+ isOpen: controlledIsOpen,
98
+ hasArrow = true,
99
+ maxWidth = 250,
100
+ iconClassName,
101
+ tooltipClassName,
102
+ className,
103
+ ...props
104
+ }: HelpTipProps) {
105
+ const [internalIsOpen, setInternalIsOpen] = useState(false)
106
+ const isOpen = controlledIsOpen ?? internalIsOpen
107
+
108
+ const { icon: iconSize, text: textSize } = sizeStyles[size]
109
+
110
+ const handlePress = () => {
111
+ if (controlledIsOpen === undefined) {
112
+ setInternalIsOpen((prev) => !prev)
113
+ }
114
+ }
115
+
116
+ const handleHoverIn = () => {
117
+ if (controlledIsOpen === undefined) {
118
+ setInternalIsOpen(true)
119
+ }
120
+ }
121
+
122
+ const handleHoverOut = () => {
123
+ if (controlledIsOpen === undefined) {
124
+ setInternalIsOpen(false)
125
+ }
126
+ }
127
+
128
+ return (
129
+ <View className={cn('relative inline-flex', className)} {...props}>
130
+ {/* Icon button */}
131
+ <Pressable
132
+ onPress={handlePress}
133
+ onHoverIn={handleHoverIn}
134
+ onHoverOut={handleHoverOut}
135
+ accessibilityRole="button"
136
+ accessibilityLabel="Show help information"
137
+ accessibilityHint={typeof content === 'string' ? content : undefined}
138
+ className={cn(
139
+ 'items-center justify-center rounded-full',
140
+ 'border border-current',
141
+ colorStyles[color],
142
+ iconSize,
143
+ 'web:hover:opacity-80 web:cursor-help',
144
+ iconClassName
145
+ )}
146
+ >
147
+ <Text className={cn('font-semibold', iconSize, colorStyles[color])}>
148
+ {iconMap[icon]}
149
+ </Text>
150
+ </Pressable>
151
+
152
+ {/* Tooltip */}
153
+ {isOpen && (
154
+ <View
155
+ className={cn(
156
+ 'absolute z-50',
157
+ 'bg-surface-elevated rounded-lg shadow-lg',
158
+ 'border border-border-default',
159
+ 'px-3 py-2',
160
+ placementStyles[placement],
161
+ tooltipClassName
162
+ )}
163
+ style={{ maxWidth }}
164
+ >
165
+ {typeof content === 'string' ? (
166
+ <Text className={cn('text-text-primary', textSize)}>
167
+ {content}
168
+ </Text>
169
+ ) : (
170
+ content
171
+ )}
172
+
173
+ {/* Arrow */}
174
+ {hasArrow && (
175
+ <View
176
+ className={cn(
177
+ 'absolute w-0 h-0',
178
+ 'border-4',
179
+ arrowStyles[placement]
180
+ )}
181
+ />
182
+ )}
183
+ </View>
184
+ )}
185
+ </View>
186
+ )
187
+ }
188
+
189
+ export interface LabelWithHelpProps extends ViewProps {
190
+ /** Label text */
191
+ label: string
192
+ /** Help content */
193
+ helpContent: React.ReactNode
194
+ /** Whether the field is required */
195
+ isRequired?: boolean
196
+ /** Size of the help icon */
197
+ helpSize?: HelpTipSize
198
+ /** Additional className */
199
+ className?: string
200
+ }
201
+
202
+ /**
203
+ * Convenience component combining a label with a help tip.
204
+ *
205
+ * @example
206
+ * <LabelWithHelp
207
+ * label="API Key"
208
+ * helpContent="Your API key can be found in the developer settings."
209
+ * isRequired
210
+ * />
211
+ */
212
+ export function LabelWithHelp({
213
+ label,
214
+ helpContent,
215
+ isRequired = false,
216
+ helpSize = 'sm',
217
+ className,
218
+ ...props
219
+ }: LabelWithHelpProps) {
220
+ return (
221
+ <View className={cn('flex-row items-center gap-1', className)} {...props}>
222
+ <Text className="text-sm font-medium text-text-primary">
223
+ {label}
224
+ {isRequired && <Text className="text-status-error ml-0.5">*</Text>}
225
+ </Text>
226
+ <HelpTip content={helpContent} size={helpSize} />
227
+ </View>
228
+ )
229
+ }
@@ -0,0 +1,2 @@
1
+ export { HelpTip, LabelWithHelp } from './HelpTip'
2
+ export type { HelpTipProps, LabelWithHelpProps, HelpTipSize, HelpTipPlacement, HelpTipIcon } from './HelpTip'