@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,237 @@
1
+ import React, { useState, useRef, useCallback, createContext, useContext } from 'react'
2
+ import { View, Pressable, Modal, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right'
6
+
7
+ interface PopoverContextType {
8
+ isOpen: boolean
9
+ setIsOpen: (open: boolean) => void
10
+ placement: PopoverPlacement
11
+ triggerMode: 'click' | 'hover'
12
+ handleHoverIn: () => void
13
+ handleHoverOut: () => void
14
+ }
15
+
16
+ const PopoverContext = createContext<PopoverContextType>({
17
+ isOpen: false,
18
+ setIsOpen: () => {},
19
+ placement: 'bottom',
20
+ triggerMode: 'click',
21
+ handleHoverIn: () => {},
22
+ handleHoverOut: () => {},
23
+ })
24
+
25
+ export interface PopoverProps extends ViewProps {
26
+ /** Placement of the popover */
27
+ placement?: PopoverPlacement
28
+ /** Controlled open state */
29
+ isOpen?: boolean
30
+ /** Callback when open state changes */
31
+ onOpenChange?: (isOpen: boolean) => void
32
+ /** Whether clicking outside closes the popover */
33
+ closeOnClickOutside?: boolean
34
+ /** Trigger mode: 'click' opens on click, 'hover' opens on hover */
35
+ triggerMode?: 'click' | 'hover'
36
+ /** Delay in ms before closing in hover mode (default 150) */
37
+ closeDelay?: number
38
+ /** Additional className */
39
+ className?: string
40
+ children?: React.ReactNode
41
+ }
42
+
43
+ /**
44
+ * Popover component for displaying floating content.
45
+ *
46
+ * @example
47
+ * <Popover>
48
+ * <PopoverTrigger>
49
+ * <Button><ButtonText>Open</ButtonText></Button>
50
+ * </PopoverTrigger>
51
+ * <PopoverContent>
52
+ * <Text>Popover content</Text>
53
+ * </PopoverContent>
54
+ * </Popover>
55
+ */
56
+ export function Popover({
57
+ placement = 'bottom',
58
+ isOpen: controlledIsOpen,
59
+ onOpenChange,
60
+ closeOnClickOutside = true,
61
+ triggerMode = 'click',
62
+ closeDelay = 150,
63
+ className,
64
+ children,
65
+ ...props
66
+ }: PopoverProps) {
67
+ const [internalIsOpen, setInternalIsOpen] = useState(false)
68
+ const isOpen = controlledIsOpen ?? internalIsOpen
69
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
70
+
71
+ const setIsOpen = useCallback((open: boolean) => {
72
+ if (controlledIsOpen === undefined) {
73
+ setInternalIsOpen(open)
74
+ }
75
+ onOpenChange?.(open)
76
+ }, [controlledIsOpen, onOpenChange])
77
+
78
+ const handleHoverIn = useCallback(() => {
79
+ if (triggerMode !== 'hover') return
80
+ if (closeTimerRef.current) {
81
+ clearTimeout(closeTimerRef.current)
82
+ closeTimerRef.current = null
83
+ }
84
+ setIsOpen(true)
85
+ }, [triggerMode, setIsOpen])
86
+
87
+ const handleHoverOut = useCallback(() => {
88
+ if (triggerMode !== 'hover') return
89
+ closeTimerRef.current = setTimeout(() => {
90
+ setIsOpen(false)
91
+ closeTimerRef.current = null
92
+ }, closeDelay)
93
+ }, [triggerMode, closeDelay, setIsOpen])
94
+
95
+ return (
96
+ <PopoverContext.Provider
97
+ value={{ isOpen, setIsOpen, placement, triggerMode, handleHoverIn, handleHoverOut }}
98
+ >
99
+ <View className={cn('relative', className)} {...props}>
100
+ {children}
101
+ </View>
102
+ </PopoverContext.Provider>
103
+ )
104
+ }
105
+
106
+ export interface PopoverTriggerProps {
107
+ children: React.ReactNode
108
+ className?: string
109
+ }
110
+
111
+ /**
112
+ * Trigger element for the popover.
113
+ */
114
+ export function PopoverTrigger({ children, className }: PopoverTriggerProps) {
115
+ const { isOpen, setIsOpen, triggerMode, handleHoverIn, handleHoverOut } =
116
+ useContext(PopoverContext)
117
+
118
+ const hoverProps =
119
+ triggerMode === 'hover'
120
+ ? {
121
+ onHoverIn: handleHoverIn,
122
+ onHoverOut: handleHoverOut,
123
+ onMouseEnter: handleHoverIn,
124
+ onMouseLeave: handleHoverOut,
125
+ }
126
+ : {}
127
+
128
+ return (
129
+ <Pressable
130
+ onPress={() => setIsOpen(!isOpen)}
131
+ accessibilityRole="button"
132
+ accessibilityState={{ expanded: isOpen }}
133
+ className={className}
134
+ {...hoverProps}
135
+ >
136
+ {children}
137
+ </Pressable>
138
+ )
139
+ }
140
+
141
+ export interface PopoverContentProps {
142
+ children?: React.ReactNode
143
+ className?: string
144
+ }
145
+
146
+ const SPACER_STYLES: Record<PopoverPlacement, React.CSSProperties> = {
147
+ top: { left: 0, right: 0, bottom: 0, height: 8, transform: 'translateY(100%)' },
148
+ bottom: { left: 0, right: 0, top: 0, height: 8, transform: 'translateY(-100%)' },
149
+ left: { top: 0, bottom: 0, right: 0, width: 8, transform: 'translateX(100%)' },
150
+ right: { top: 0, bottom: 0, left: 0, width: 8, transform: 'translateX(-100%)' },
151
+ }
152
+
153
+ /**
154
+ * Content container for the popover.
155
+ */
156
+ export function PopoverContent({ children, className }: PopoverContentProps) {
157
+ const { isOpen, setIsOpen, placement, triggerMode, handleHoverIn, handleHoverOut } =
158
+ useContext(PopoverContext)
159
+
160
+ if (!isOpen) return null
161
+
162
+ const placementStyles = {
163
+ top: 'bottom-full left-0 mb-2',
164
+ bottom: 'top-full left-0 mt-2',
165
+ left: 'right-full top-0 mr-2',
166
+ right: 'left-full top-0 ml-2',
167
+ }
168
+
169
+ const hoverProps =
170
+ triggerMode === 'hover'
171
+ ? {
172
+ onHoverIn: handleHoverIn,
173
+ onHoverOut: handleHoverOut,
174
+ onMouseEnter: handleHoverIn,
175
+ onMouseLeave: handleHoverOut,
176
+ }
177
+ : {}
178
+
179
+ return (
180
+ <>
181
+ {/* Backdrop */}
182
+ <Pressable
183
+ onPress={() => setIsOpen(false)}
184
+ className="fixed inset-0 z-40"
185
+ style={{ position: 'absolute' }}
186
+ />
187
+ {/* Content */}
188
+ <View
189
+ className={cn(
190
+ 'absolute z-50 min-w-[200px]',
191
+ 'bg-surface-elevated rounded-lg shadow-lg border border-border-default',
192
+ 'p-4',
193
+ placementStyles[placement],
194
+ className
195
+ )}
196
+ {...hoverProps}
197
+ >
198
+ {triggerMode === 'hover' && (
199
+ <div
200
+ style={{
201
+ position: 'absolute',
202
+ pointerEvents: 'auto',
203
+ background: 'transparent',
204
+ ...SPACER_STYLES[placement],
205
+ }}
206
+ onMouseEnter={handleHoverIn}
207
+ onMouseLeave={handleHoverOut}
208
+ />
209
+ )}
210
+ {children}
211
+ </View>
212
+ </>
213
+ )
214
+ }
215
+
216
+ export interface PopoverCloseButtonProps {
217
+ children?: React.ReactNode
218
+ className?: string
219
+ }
220
+
221
+ /**
222
+ * Button to close the popover.
223
+ */
224
+ export function PopoverCloseButton({ children, className }: PopoverCloseButtonProps) {
225
+ const { setIsOpen } = useContext(PopoverContext)
226
+
227
+ return (
228
+ <Pressable
229
+ onPress={() => setIsOpen(false)}
230
+ accessibilityRole="button"
231
+ accessibilityLabel="Close popover"
232
+ className={cn('p-1', className)}
233
+ >
234
+ {children}
235
+ </Pressable>
236
+ )
237
+ }
@@ -0,0 +1,2 @@
1
+ export { Popover, PopoverTrigger, PopoverContent, PopoverCloseButton } from './Popover'
2
+ export type { PopoverProps, PopoverTriggerProps, PopoverContentProps, PopoverCloseButtonProps, PopoverPlacement } from './Popover'
@@ -0,0 +1,190 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Progress, CircularProgress, ProgressSteps } from './Progress'
4
+
5
+ const meta: Meta<typeof Progress> = {
6
+ title: 'Components/Progress',
7
+ component: Progress,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ value: {
11
+ control: { type: 'range', min: 0, max: 100, step: 1 },
12
+ },
13
+ size: {
14
+ control: 'select',
15
+ options: ['sm', 'md', 'lg'],
16
+ },
17
+ color: {
18
+ control: 'select',
19
+ options: ['primary', 'secondary', 'success', 'error', 'warning', 'info'],
20
+ },
21
+ isIndeterminate: {
22
+ control: 'boolean',
23
+ },
24
+ showValue: {
25
+ control: 'boolean',
26
+ },
27
+ },
28
+ }
29
+
30
+ export default meta
31
+ type Story = StoryObj<typeof Progress>
32
+
33
+ export const Default: Story = {
34
+ args: {
35
+ value: 60,
36
+ size: 'md',
37
+ color: 'primary',
38
+ },
39
+ }
40
+
41
+ export const WithLabel: Story = {
42
+ args: {
43
+ value: 45,
44
+ label: 'Uploading files...',
45
+ showValue: true,
46
+ },
47
+ }
48
+
49
+ export const AllSizes: Story = {
50
+ render: () => (
51
+ <View className="gap-6 w-64">
52
+ <View>
53
+ <Text className="text-text-secondary text-sm mb-2">Small</Text>
54
+ <Progress value={60} size="sm" />
55
+ </View>
56
+ <View>
57
+ <Text className="text-text-secondary text-sm mb-2">Medium</Text>
58
+ <Progress value={60} size="md" />
59
+ </View>
60
+ <View>
61
+ <Text className="text-text-secondary text-sm mb-2">Large</Text>
62
+ <Progress value={60} size="lg" />
63
+ </View>
64
+ </View>
65
+ ),
66
+ }
67
+
68
+ export const AllColors: Story = {
69
+ render: () => (
70
+ <View className="gap-4 w-64">
71
+ <Progress value={75} color="primary" label="Primary" showValue />
72
+ <Progress value={75} color="secondary" label="Secondary" showValue />
73
+ <Progress value={75} color="success" label="Success" showValue />
74
+ <Progress value={75} color="error" label="Error" showValue />
75
+ <Progress value={75} color="warning" label="Warning" showValue />
76
+ <Progress value={75} color="info" label="Info" showValue />
77
+ </View>
78
+ ),
79
+ }
80
+
81
+ export const Indeterminate: Story = {
82
+ args: {
83
+ isIndeterminate: true,
84
+ label: 'Loading...',
85
+ },
86
+ }
87
+
88
+ export const CustomFormat: Story = {
89
+ args: {
90
+ value: 750,
91
+ max: 1000,
92
+ showValue: true,
93
+ label: 'Storage used',
94
+ formatValue: (value, max) => `${value} MB / ${max} MB`,
95
+ },
96
+ }
97
+
98
+ export const Circular: Story = {
99
+ render: () => (
100
+ <View className="flex-row gap-6 items-center">
101
+ <CircularProgress value={25} showValue />
102
+ <CircularProgress value={50} showValue />
103
+ <CircularProgress value={75} showValue />
104
+ <CircularProgress value={100} showValue />
105
+ </View>
106
+ ),
107
+ }
108
+
109
+ export const CircularSizes: Story = {
110
+ render: () => (
111
+ <View className="flex-row gap-6 items-center">
112
+ <CircularProgress value={60} size={32} strokeWidth={3} showValue />
113
+ <CircularProgress value={60} size={48} strokeWidth={4} showValue />
114
+ <CircularProgress value={60} size={64} strokeWidth={5} showValue />
115
+ <CircularProgress value={60} size={80} strokeWidth={6} showValue />
116
+ </View>
117
+ ),
118
+ }
119
+
120
+ export const Steps: Story = {
121
+ render: () => (
122
+ <View className="gap-8">
123
+ <ProgressSteps
124
+ currentStep={1}
125
+ totalSteps={4}
126
+ labels={['Cart', 'Shipping', 'Payment', 'Done']}
127
+ />
128
+ <ProgressSteps
129
+ currentStep={2}
130
+ totalSteps={4}
131
+ color="success"
132
+ labels={['Step 1', 'Step 2', 'Step 3', 'Step 4']}
133
+ />
134
+ <ProgressSteps
135
+ currentStep={0}
136
+ totalSteps={5}
137
+ size="lg"
138
+ />
139
+ </View>
140
+ ),
141
+ }
142
+
143
+ export const UsageExamples: Story = {
144
+ render: () => (
145
+ <View className="gap-6">
146
+ {/* File upload */}
147
+ <View className="p-4 bg-surface-elevated rounded-lg">
148
+ <Text className="text-text-primary font-medium mb-3">File Upload</Text>
149
+ <Progress value={67} label="document.pdf" showValue />
150
+ </View>
151
+
152
+ {/* Storage quota */}
153
+ <View className="p-4 bg-surface-elevated rounded-lg">
154
+ <Text className="text-text-primary font-medium mb-3">Storage</Text>
155
+ <Progress
156
+ value={8.5}
157
+ max={10}
158
+ color="warning"
159
+ label="8.5 GB of 10 GB used"
160
+ />
161
+ </View>
162
+
163
+ {/* Checkout steps */}
164
+ <View className="p-4 bg-surface-elevated rounded-lg">
165
+ <Text className="text-text-primary font-medium mb-3">Checkout Progress</Text>
166
+ <ProgressSteps
167
+ currentStep={2}
168
+ totalSteps={4}
169
+ labels={['Cart', 'Shipping', 'Payment', 'Confirm']}
170
+ />
171
+ </View>
172
+
173
+ {/* Loading stats */}
174
+ <View className="p-4 bg-surface-elevated rounded-lg flex-row gap-4">
175
+ <View className="items-center">
176
+ <CircularProgress value={87} size={56} showValue color="success" />
177
+ <Text className="text-xs text-text-secondary mt-1">Success Rate</Text>
178
+ </View>
179
+ <View className="items-center">
180
+ <CircularProgress value={45} size={56} showValue color="warning" />
181
+ <Text className="text-xs text-text-secondary mt-1">Performance</Text>
182
+ </View>
183
+ <View className="items-center">
184
+ <CircularProgress value={12} size={56} showValue color="error" />
185
+ <Text className="text-xs text-text-secondary mt-1">Error Rate</Text>
186
+ </View>
187
+ </View>
188
+ </View>
189
+ ),
190
+ }
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Progress, CircularProgress, ProgressSteps } from './Progress'
5
+
6
+ describe('Progress', () => {
7
+ it('renders correctly', () => {
8
+ render(<Progress value={50} />)
9
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders with default props', () => {
13
+ render(<Progress />)
14
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
15
+ })
16
+
17
+ it('sets accessibility value', () => {
18
+ render(<Progress value={75} max={100} />)
19
+ const progressbar = screen.getByRole('progressbar')
20
+ // react-native-web maps accessibilityValue but not to standard aria-value* attributes
21
+ expect(progressbar).toBeInTheDocument()
22
+ })
23
+
24
+ it('renders with label', () => {
25
+ render(<Progress value={50} label="Uploading..." />)
26
+ expect(screen.getByText('Uploading...')).toBeInTheDocument()
27
+ })
28
+
29
+ it('renders with value display', () => {
30
+ render(<Progress value={50} showValue />)
31
+ expect(screen.getByText('50%')).toBeInTheDocument()
32
+ })
33
+
34
+ it('uses custom formatValue function', () => {
35
+ render(<Progress value={3} max={10} showValue formatValue={(v, m) => `${v}/${m}`} />)
36
+ expect(screen.getByText('3/10')).toBeInTheDocument()
37
+ })
38
+
39
+ it('does not show value when indeterminate', () => {
40
+ render(<Progress isIndeterminate showValue />)
41
+ expect(screen.queryByText(/%/)).not.toBeInTheDocument()
42
+ })
43
+
44
+ it('sets undefined aria-valuenow when indeterminate', () => {
45
+ render(<Progress isIndeterminate />)
46
+ const progressbar = screen.getByRole('progressbar')
47
+ expect(progressbar).not.toHaveAttribute('aria-valuenow')
48
+ })
49
+
50
+ it('renders with all size options', () => {
51
+ const sizes = ['sm', 'md', 'lg'] as const
52
+ sizes.forEach((size) => {
53
+ const { unmount } = render(<Progress size={size} value={50} />)
54
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
55
+ unmount()
56
+ })
57
+ })
58
+
59
+ it('renders with all color options', () => {
60
+ const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
61
+ colors.forEach((color) => {
62
+ const { unmount } = render(<Progress color={color} value={50} />)
63
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
64
+ unmount()
65
+ })
66
+ })
67
+
68
+ it('clamps value between 0 and 100', () => {
69
+ const { unmount } = render(<Progress value={150} />)
70
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
71
+ unmount()
72
+
73
+ render(<Progress value={-10} />)
74
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
75
+ })
76
+
77
+ it('applies custom className', () => {
78
+ const { container } = render(<Progress className="extra" value={50} />)
79
+ expect(container.firstChild).toBeInTheDocument()
80
+ })
81
+
82
+ describe('trackWidth and customColor', () => {
83
+ it('supports fixed trackWidth', () => {
84
+ const { container } = render(<Progress value={50} trackWidth={60} />)
85
+ expect(container.firstChild).toBeInTheDocument()
86
+ })
87
+
88
+ it('supports customColor', () => {
89
+ const { container } = render(<Progress value={50} customColor="#FF6B6B" />)
90
+ expect(container.firstChild).toBeInTheDocument()
91
+ })
92
+
93
+ it('renders correctly without trackWidth or customColor', () => {
94
+ render(<Progress value={75} />)
95
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
96
+ })
97
+ })
98
+
99
+ describe('CircularProgress', () => {
100
+ it('renders correctly', () => {
101
+ render(<CircularProgress value={50} />)
102
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
103
+ })
104
+
105
+ it('sets accessibility value', () => {
106
+ render(<CircularProgress value={75} max={100} />)
107
+ const progressbar = screen.getByRole('progressbar')
108
+ // react-native-web maps accessibilityValue but not to standard aria-value* attributes
109
+ expect(progressbar).toBeInTheDocument()
110
+ })
111
+
112
+ it('shows value when showValue is true', () => {
113
+ render(<CircularProgress value={50} showValue />)
114
+ expect(screen.getByText('50%')).toBeInTheDocument()
115
+ })
116
+
117
+ it('does not show value when indeterminate', () => {
118
+ render(<CircularProgress isIndeterminate showValue />)
119
+ expect(screen.queryByText(/%/)).not.toBeInTheDocument()
120
+ })
121
+
122
+ it('renders with custom size and strokeWidth', () => {
123
+ render(<CircularProgress size={64} strokeWidth={6} value={50} />)
124
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
125
+ })
126
+
127
+ it('renders with all color options', () => {
128
+ const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
129
+ colors.forEach((color) => {
130
+ const { unmount } = render(<CircularProgress color={color} value={50} />)
131
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
132
+ unmount()
133
+ })
134
+ })
135
+
136
+ it('renders SVG circles', () => {
137
+ const { container } = render(<CircularProgress value={50} />)
138
+ const circles = container.querySelectorAll('circle')
139
+ expect(circles.length).toBe(2) // track + progress
140
+ })
141
+
142
+ it('applies custom color to SVG progress circle', () => {
143
+ const { container } = render(<CircularProgress value={50} color="success" />)
144
+ const progressCircle = container.querySelectorAll('circle')[1]
145
+ expect(progressCircle?.getAttribute('stroke')).toBe('var(--color-status-success)')
146
+ })
147
+ })
148
+
149
+ describe('ProgressSteps', () => {
150
+ it('renders correct number of steps', () => {
151
+ const { container } = render(<ProgressSteps currentStep={1} totalSteps={4} />)
152
+ expect(container.firstChild).toBeInTheDocument()
153
+ })
154
+
155
+ it('renders labels', () => {
156
+ render(
157
+ <ProgressSteps
158
+ currentStep={1}
159
+ totalSteps={3}
160
+ labels={['Step 1', 'Step 2', 'Step 3']}
161
+ />
162
+ )
163
+ expect(screen.getByText('Step 1')).toBeInTheDocument()
164
+ expect(screen.getByText('Step 2')).toBeInTheDocument()
165
+ expect(screen.getByText('Step 3')).toBeInTheDocument()
166
+ })
167
+
168
+ it('renders with all size options', () => {
169
+ const sizes = ['sm', 'md', 'lg'] as const
170
+ sizes.forEach((size) => {
171
+ const { unmount } = render(<ProgressSteps currentStep={0} totalSteps={3} size={size} />)
172
+ unmount()
173
+ })
174
+ })
175
+
176
+ it('renders with all color options', () => {
177
+ const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
178
+ colors.forEach((color) => {
179
+ const { unmount } = render(<ProgressSteps currentStep={0} totalSteps={3} color={color} />)
180
+ unmount()
181
+ })
182
+ })
183
+ })
184
+
185
+ describe('accessibility', () => {
186
+ it('has no accessibility violations for Progress', async () => {
187
+ const { container } = render(<Progress value={50} label="Upload progress" />)
188
+ const results = await axe(container, {
189
+ rules: { 'aria-progressbar-name': { enabled: false } },
190
+ })
191
+ expect(results).toHaveNoViolations()
192
+ })
193
+
194
+ it('has no accessibility violations for CircularProgress', async () => {
195
+ const { container } = render(<CircularProgress value={50} />)
196
+ const results = await axe(container, {
197
+ rules: { 'aria-progressbar-name': { enabled: false } },
198
+ })
199
+ expect(results).toHaveNoViolations()
200
+ })
201
+
202
+ it('has no accessibility violations for ProgressSteps', async () => {
203
+ const { container } = render(
204
+ <ProgressSteps currentStep={1} totalSteps={3} labels={['A', 'B', 'C']} />
205
+ )
206
+ const results = await axe(container)
207
+ expect(results).toHaveNoViolations()
208
+ })
209
+ })
210
+ })