@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 React, { createContext, useContext } from 'react'
2
+ import { View, Text, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type StepStatus = 'completed' | 'active' | 'upcoming' | 'error'
6
+ export type StepperOrientation = 'horizontal' | 'vertical'
7
+
8
+ interface StepperContextType {
9
+ activeStep: number
10
+ orientation: StepperOrientation
11
+ }
12
+
13
+ const StepperContext = createContext<StepperContextType>({
14
+ activeStep: 0,
15
+ orientation: 'horizontal',
16
+ })
17
+
18
+ export interface StepperProps extends ViewProps {
19
+ /** Current active step (0-indexed) */
20
+ activeStep: number
21
+ /** Stepper orientation */
22
+ orientation?: StepperOrientation
23
+ /** Additional className */
24
+ className?: string
25
+ children?: React.ReactNode
26
+ }
27
+
28
+ /**
29
+ * Stepper component for multi-step flows.
30
+ *
31
+ * @example
32
+ * <Stepper activeStep={1}>
33
+ * <Step>
34
+ * <StepIndicator />
35
+ * <StepLabel>Account</StepLabel>
36
+ * </Step>
37
+ * <Step>
38
+ * <StepIndicator />
39
+ * <StepLabel>Profile</StepLabel>
40
+ * </Step>
41
+ * <Step>
42
+ * <StepIndicator />
43
+ * <StepLabel>Review</StepLabel>
44
+ * </Step>
45
+ * </Stepper>
46
+ */
47
+ export function Stepper({
48
+ activeStep,
49
+ orientation = 'horizontal',
50
+ className,
51
+ children,
52
+ ...props
53
+ }: StepperProps) {
54
+ return (
55
+ <StepperContext.Provider value={{ activeStep, orientation }}>
56
+ <View
57
+ className={cn(
58
+ orientation === 'horizontal' ? 'flex-row items-center' : 'flex-col',
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ {React.Children.map(children, (child, index) => {
64
+ if (React.isValidElement(child)) {
65
+ return (
66
+ <>
67
+ {React.cloneElement(child as React.ReactElement<any>, { index })}
68
+ {index < React.Children.count(children) - 1 && (
69
+ <StepConnector index={index} />
70
+ )}
71
+ </>
72
+ )
73
+ }
74
+ return child
75
+ })}
76
+ </View>
77
+ </StepperContext.Provider>
78
+ )
79
+ }
80
+
81
+ interface StepConnectorProps {
82
+ index: number
83
+ }
84
+
85
+ function StepConnector({ index }: StepConnectorProps) {
86
+ const { activeStep, orientation } = useContext(StepperContext)
87
+ const isCompleted = index < activeStep
88
+
89
+ if (orientation === 'horizontal') {
90
+ return (
91
+ <View
92
+ className={cn(
93
+ 'flex-1 h-0.5 mx-2',
94
+ isCompleted ? 'bg-brand-primary' : 'bg-border-default'
95
+ )}
96
+ />
97
+ )
98
+ }
99
+
100
+ return (
101
+ <View
102
+ className={cn(
103
+ 'w-0.5 h-8 ml-4 my-1',
104
+ isCompleted ? 'bg-brand-primary' : 'bg-border-default'
105
+ )}
106
+ />
107
+ )
108
+ }
109
+
110
+ export interface StepProps {
111
+ /** Step index (injected by Stepper) */
112
+ index?: number
113
+ /** Override step status */
114
+ status?: StepStatus
115
+ /** Additional className */
116
+ className?: string
117
+ children?: React.ReactNode
118
+ }
119
+
120
+ /**
121
+ * Individual step in the stepper.
122
+ */
123
+ export function Step({ index = 0, status, className, children }: StepProps) {
124
+ const { activeStep, orientation } = useContext(StepperContext)
125
+
126
+ const derivedStatus: StepStatus = status || (
127
+ index < activeStep ? 'completed' :
128
+ index === activeStep ? 'active' : 'upcoming'
129
+ )
130
+
131
+ return (
132
+ <View
133
+ className={cn(
134
+ orientation === 'horizontal' ? 'items-center' : 'flex-row items-start',
135
+ className
136
+ )}
137
+ >
138
+ {React.Children.map(children, (child) => {
139
+ if (React.isValidElement(child)) {
140
+ return React.cloneElement(child as React.ReactElement<any>, {
141
+ status: derivedStatus,
142
+ index,
143
+ })
144
+ }
145
+ return child
146
+ })}
147
+ </View>
148
+ )
149
+ }
150
+
151
+ export interface StepIndicatorProps {
152
+ /** Step status (injected by Step) */
153
+ status?: StepStatus
154
+ /** Step index (injected by Step) */
155
+ index?: number
156
+ /** Custom icon */
157
+ icon?: React.ReactNode
158
+ /** Additional className */
159
+ className?: string
160
+ }
161
+
162
+ /**
163
+ * Visual indicator for step status.
164
+ */
165
+ export function StepIndicator({
166
+ status = 'upcoming',
167
+ index = 0,
168
+ icon,
169
+ className,
170
+ }: StepIndicatorProps) {
171
+ const statusStyles: Record<StepStatus, string> = {
172
+ completed: 'bg-brand-primary border-brand-primary',
173
+ active: 'bg-brand-primary border-brand-primary',
174
+ upcoming: 'bg-surface-base border-border-strong',
175
+ error: 'bg-status-error border-status-error',
176
+ }
177
+
178
+ return (
179
+ <View
180
+ className={cn(
181
+ 'w-8 h-8 rounded-full border-2 items-center justify-center',
182
+ statusStyles[status],
183
+ className
184
+ )}
185
+ >
186
+ {icon || (
187
+ <Text
188
+ className={cn(
189
+ 'text-sm font-semibold',
190
+ status === 'upcoming' ? 'text-text-secondary' : 'text-white'
191
+ )}
192
+ >
193
+ {status === 'completed' ? '✓' : index + 1}
194
+ </Text>
195
+ )}
196
+ </View>
197
+ )
198
+ }
199
+
200
+ export interface StepLabelProps {
201
+ /** Step status (injected by Step) */
202
+ status?: StepStatus
203
+ /** Additional className */
204
+ className?: string
205
+ children?: React.ReactNode
206
+ }
207
+
208
+ /**
209
+ * Label for a step.
210
+ */
211
+ export function StepLabel({ status = 'upcoming', className, children }: StepLabelProps) {
212
+ const { orientation } = useContext(StepperContext)
213
+
214
+ return (
215
+ <Text
216
+ className={cn(
217
+ 'text-sm font-medium',
218
+ orientation === 'horizontal' ? 'mt-2' : 'ml-3',
219
+ status === 'active' ? 'text-text-primary' :
220
+ status === 'completed' ? 'text-brand-primary' :
221
+ status === 'error' ? 'text-status-error' :
222
+ 'text-text-secondary',
223
+ className
224
+ )}
225
+ >
226
+ {children}
227
+ </Text>
228
+ )
229
+ }
230
+
231
+ export interface StepContentProps {
232
+ /** Step status (injected by Step) */
233
+ status?: StepStatus
234
+ /** Additional className */
235
+ className?: string
236
+ children?: React.ReactNode
237
+ }
238
+
239
+ /**
240
+ * Content area for vertical stepper.
241
+ */
242
+ export function StepContent({ status = 'upcoming', className, children }: StepContentProps) {
243
+ if (status !== 'active') return null
244
+
245
+ return (
246
+ <View className={cn('ml-11 mt-2 pb-4', className)}>
247
+ {children}
248
+ </View>
249
+ )
250
+ }
@@ -0,0 +1,10 @@
1
+ export { Stepper, Step as StepperStep, StepIndicator, StepLabel, StepContent } from './Stepper'
2
+ export type {
3
+ StepperProps,
4
+ StepProps as StepperStepProps,
5
+ StepIndicatorProps,
6
+ StepLabelProps,
7
+ StepContentProps,
8
+ StepStatus,
9
+ StepperOrientation,
10
+ } from './Stepper'
@@ -0,0 +1,5 @@
1
+ // Re-export all UI components
2
+ export * from './ui'
3
+
4
+ // Re-export custom components
5
+ export * from './custom'
@@ -0,0 +1,154 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Alert, AlertTitle, AlertDescription } from './Alert'
4
+
5
+ const meta: Meta<typeof Alert> = {
6
+ title: 'Components/Alert',
7
+ component: Alert,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ status: {
11
+ control: 'select',
12
+ options: ['success', 'info', 'warning', 'error'],
13
+ },
14
+ variant: {
15
+ control: 'select',
16
+ options: ['subtle', 'outline', 'solid'],
17
+ },
18
+ showIcon: { control: 'boolean' },
19
+ },
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof Alert>
24
+
25
+ export const Default: Story = {
26
+ args: {
27
+ status: 'info',
28
+ variant: 'subtle',
29
+ },
30
+ render: (args) => (
31
+ <Alert {...args}>
32
+ <AlertTitle>Information</AlertTitle>
33
+ <AlertDescription>This is an informational alert message.</AlertDescription>
34
+ </Alert>
35
+ ),
36
+ }
37
+
38
+ export const Success: Story = {
39
+ render: () => (
40
+ <Alert status="success">
41
+ <AlertTitle>Success!</AlertTitle>
42
+ <AlertDescription>Your changes have been saved successfully.</AlertDescription>
43
+ </Alert>
44
+ ),
45
+ }
46
+
47
+ export const Warning: Story = {
48
+ render: () => (
49
+ <Alert status="warning">
50
+ <AlertTitle>Warning</AlertTitle>
51
+ <AlertDescription>Please review before proceeding.</AlertDescription>
52
+ </Alert>
53
+ ),
54
+ }
55
+
56
+ export const Error: Story = {
57
+ render: () => (
58
+ <Alert status="error">
59
+ <AlertTitle>Error</AlertTitle>
60
+ <AlertDescription>Something went wrong. Please try again.</AlertDescription>
61
+ </Alert>
62
+ ),
63
+ }
64
+
65
+ export const WithCloseButton: Story = {
66
+ render: () => (
67
+ <Alert status="info" onClose={() => console.log('closed')}>
68
+ <AlertTitle>Dismissable Alert</AlertTitle>
69
+ <AlertDescription>Click the X to close this alert.</AlertDescription>
70
+ </Alert>
71
+ ),
72
+ }
73
+
74
+ export const AllStatuses: Story = {
75
+ render: () => (
76
+ <View style={{ gap: 12 }}>
77
+ <Alert status="success">
78
+ <AlertTitle>Success</AlertTitle>
79
+ <AlertDescription>Operation completed successfully.</AlertDescription>
80
+ </Alert>
81
+ <Alert status="info">
82
+ <AlertTitle>Info</AlertTitle>
83
+ <AlertDescription>Here is some useful information.</AlertDescription>
84
+ </Alert>
85
+ <Alert status="warning">
86
+ <AlertTitle>Warning</AlertTitle>
87
+ <AlertDescription>Please be careful with this action.</AlertDescription>
88
+ </Alert>
89
+ <Alert status="error">
90
+ <AlertTitle>Error</AlertTitle>
91
+ <AlertDescription>An error occurred.</AlertDescription>
92
+ </Alert>
93
+ </View>
94
+ ),
95
+ }
96
+
97
+ export const AllVariants: Story = {
98
+ render: () => (
99
+ <View style={{ gap: 12 }}>
100
+ <Alert status="info" variant="subtle">
101
+ <AlertTitle>Subtle Variant</AlertTitle>
102
+ <AlertDescription>Light background with colored text.</AlertDescription>
103
+ </Alert>
104
+ <Alert status="info" variant="outline">
105
+ <AlertTitle>Outline Variant</AlertTitle>
106
+ <AlertDescription>Border with transparent background.</AlertDescription>
107
+ </Alert>
108
+ <Alert status="info" variant="solid">
109
+ <AlertTitle>Solid Variant</AlertTitle>
110
+ <AlertDescription>Solid background color.</AlertDescription>
111
+ </Alert>
112
+ </View>
113
+ ),
114
+ }
115
+
116
+ export const SolidVariants: Story = {
117
+ render: () => (
118
+ <View style={{ gap: 12 }}>
119
+ <Alert status="success" variant="solid">
120
+ <AlertDescription>Success solid alert</AlertDescription>
121
+ </Alert>
122
+ <Alert status="info" variant="solid">
123
+ <AlertDescription>Info solid alert</AlertDescription>
124
+ </Alert>
125
+ <Alert status="warning" variant="solid">
126
+ <AlertDescription>Warning solid alert</AlertDescription>
127
+ </Alert>
128
+ <Alert status="error" variant="solid">
129
+ <AlertDescription>Error solid alert</AlertDescription>
130
+ </Alert>
131
+ </View>
132
+ ),
133
+ }
134
+
135
+ export const WithoutIcon: Story = {
136
+ args: {
137
+ showIcon: false,
138
+ status: 'info',
139
+ },
140
+ render: (args) => (
141
+ <Alert {...args}>
142
+ <AlertTitle>No Icon</AlertTitle>
143
+ <AlertDescription>This alert has no icon.</AlertDescription>
144
+ </Alert>
145
+ ),
146
+ }
147
+
148
+ export const DescriptionOnly: Story = {
149
+ render: () => (
150
+ <Alert status="info">
151
+ <AlertDescription>Simple alert with description only, no title.</AlertDescription>
152
+ </Alert>
153
+ ),
154
+ }
@@ -0,0 +1,202 @@
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 { Alert, AlertTitle, AlertDescription } from './Alert'
5
+
6
+ describe('Alert', () => {
7
+ it('renders children correctly', () => {
8
+ render(
9
+ <Alert>
10
+ <AlertTitle>Alert Title</AlertTitle>
11
+ <AlertDescription>Alert message</AlertDescription>
12
+ </Alert>
13
+ )
14
+ expect(screen.getByText('Alert Title')).toBeInTheDocument()
15
+ expect(screen.getByText('Alert message')).toBeInTheDocument()
16
+ })
17
+
18
+ it('has alert accessibility role', () => {
19
+ render(
20
+ <Alert>
21
+ <AlertDescription>Info message</AlertDescription>
22
+ </Alert>
23
+ )
24
+ expect(screen.getByRole('alert')).toBeInTheDocument()
25
+ })
26
+
27
+ describe('status variants', () => {
28
+ it('renders with success status', () => {
29
+ render(
30
+ <Alert status="success">
31
+ <AlertDescription>Success</AlertDescription>
32
+ </Alert>
33
+ )
34
+ expect(screen.getByRole('alert')).toBeInTheDocument()
35
+ })
36
+
37
+ it('renders with info status (default)', () => {
38
+ render(
39
+ <Alert>
40
+ <AlertDescription>Info</AlertDescription>
41
+ </Alert>
42
+ )
43
+ expect(screen.getByRole('alert')).toBeInTheDocument()
44
+ })
45
+
46
+ it('renders with warning status', () => {
47
+ render(
48
+ <Alert status="warning">
49
+ <AlertDescription>Warning</AlertDescription>
50
+ </Alert>
51
+ )
52
+ expect(screen.getByRole('alert')).toBeInTheDocument()
53
+ })
54
+
55
+ it('renders with error status', () => {
56
+ render(
57
+ <Alert status="error">
58
+ <AlertDescription>Error</AlertDescription>
59
+ </Alert>
60
+ )
61
+ expect(screen.getByRole('alert')).toBeInTheDocument()
62
+ })
63
+ })
64
+
65
+ describe('visual variants', () => {
66
+ it('renders with subtle variant (default)', () => {
67
+ render(
68
+ <Alert variant="subtle">
69
+ <AlertDescription>Subtle</AlertDescription>
70
+ </Alert>
71
+ )
72
+ expect(screen.getByRole('alert')).toBeInTheDocument()
73
+ })
74
+
75
+ it('renders with outline variant', () => {
76
+ render(
77
+ <Alert variant="outline">
78
+ <AlertDescription>Outline</AlertDescription>
79
+ </Alert>
80
+ )
81
+ expect(screen.getByRole('alert')).toBeInTheDocument()
82
+ })
83
+
84
+ it('renders with solid variant', () => {
85
+ render(
86
+ <Alert variant="solid">
87
+ <AlertDescription>Solid</AlertDescription>
88
+ </Alert>
89
+ )
90
+ expect(screen.getByRole('alert')).toBeInTheDocument()
91
+ })
92
+ })
93
+
94
+ describe('icon behavior', () => {
95
+ it('shows default icon by default', () => {
96
+ render(
97
+ <Alert status="info">
98
+ <AlertDescription>With icon</AlertDescription>
99
+ </Alert>
100
+ )
101
+ // Default info icon is the info symbol
102
+ expect(screen.getByRole('alert')).toBeInTheDocument()
103
+ })
104
+
105
+ it('hides icon when showIcon is false', () => {
106
+ render(
107
+ <Alert status="success" showIcon={false}>
108
+ <AlertDescription>No icon</AlertDescription>
109
+ </Alert>
110
+ )
111
+ expect(screen.getByRole('alert')).toBeInTheDocument()
112
+ })
113
+
114
+ it('renders custom icon when provided', () => {
115
+ render(
116
+ <Alert icon={<span data-testid="custom-icon">!</span>}>
117
+ <AlertDescription>Custom icon</AlertDescription>
118
+ </Alert>
119
+ )
120
+ expect(screen.getByTestId('custom-icon')).toBeInTheDocument()
121
+ })
122
+ })
123
+
124
+ describe('close button', () => {
125
+ it('renders close button when onClose is provided', () => {
126
+ render(
127
+ <Alert onClose={() => {}}>
128
+ <AlertDescription>Closeable</AlertDescription>
129
+ </Alert>
130
+ )
131
+ expect(screen.getByLabelText('Close alert')).toBeInTheDocument()
132
+ })
133
+
134
+ it('calls onClose when close button is clicked', () => {
135
+ const onClose = vi.fn()
136
+ render(
137
+ <Alert onClose={onClose}>
138
+ <AlertDescription>Closeable</AlertDescription>
139
+ </Alert>
140
+ )
141
+
142
+ fireEvent.click(screen.getByLabelText('Close alert'))
143
+ expect(onClose).toHaveBeenCalledTimes(1)
144
+ })
145
+
146
+ it('does not render close button when onClose is not provided', () => {
147
+ render(
148
+ <Alert>
149
+ <AlertDescription>Not closeable</AlertDescription>
150
+ </Alert>
151
+ )
152
+ expect(screen.queryByLabelText('Close alert')).not.toBeInTheDocument()
153
+ })
154
+ })
155
+
156
+ describe('compound sub-components', () => {
157
+ it('renders AlertTitle', () => {
158
+ render(<AlertTitle>Title Text</AlertTitle>)
159
+ expect(screen.getByText('Title Text')).toBeInTheDocument()
160
+ })
161
+
162
+ it('renders AlertDescription', () => {
163
+ render(<AlertDescription>Description text</AlertDescription>)
164
+ expect(screen.getByText('Description text')).toBeInTheDocument()
165
+ })
166
+
167
+ it('accepts custom className on sub-components', () => {
168
+ render(
169
+ <Alert className="custom-alert">
170
+ <AlertTitle className="custom-title">Title</AlertTitle>
171
+ <AlertDescription className="custom-desc">Desc</AlertDescription>
172
+ </Alert>
173
+ )
174
+ expect(screen.getByText('Title')).toBeInTheDocument()
175
+ expect(screen.getByText('Desc')).toBeInTheDocument()
176
+ })
177
+ })
178
+
179
+ describe('accessibility', () => {
180
+ it('has no accessibility violations', async () => {
181
+ const { container } = render(
182
+ <Alert status="info">
183
+ <AlertTitle>Info Alert</AlertTitle>
184
+ <AlertDescription>This is an informational message.</AlertDescription>
185
+ </Alert>
186
+ )
187
+ const results = await axe(container)
188
+ expect(results).toHaveNoViolations()
189
+ })
190
+
191
+ it('has no accessibility violations with close button', async () => {
192
+ const { container } = render(
193
+ <Alert status="error" onClose={() => {}}>
194
+ <AlertTitle>Error</AlertTitle>
195
+ <AlertDescription>Something went wrong.</AlertDescription>
196
+ </Alert>
197
+ )
198
+ const results = await axe(container)
199
+ expect(results).toHaveNoViolations()
200
+ })
201
+ })
202
+ })