@titan-design/react-ui 0.0.2 → 0.1.1

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 (206) hide show
  1. package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
  2. package/dist/chunk-MGW37MPO.mjs.map +1 -0
  3. package/dist/index.d.mts +115 -2
  4. package/dist/index.d.ts +115 -2
  5. package/dist/index.js +262 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +225 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme/index.d.mts +124 -109
  10. package/dist/theme/index.d.ts +124 -109
  11. package/dist/theme/index.js +24 -1
  12. package/dist/theme/index.js.map +1 -1
  13. package/dist/theme/index.mjs +1 -1
  14. package/package.json +12 -5
  15. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  16. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  17. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  18. package/src/components/custom/DateTime/index.ts +2 -0
  19. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  20. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  21. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  22. package/src/components/custom/EmptyState/index.ts +2 -0
  23. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  24. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  25. package/src/components/custom/Metric/Metric.tsx +92 -0
  26. package/src/components/custom/Metric/index.ts +2 -0
  27. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  28. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  29. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  30. package/src/components/custom/Sidebar/index.ts +18 -0
  31. package/src/components/custom/Table/Table.stories.tsx +481 -0
  32. package/src/components/custom/Table/Table.test.tsx +531 -0
  33. package/src/components/custom/Table/Table.tsx +696 -0
  34. package/src/components/custom/Table/index.ts +22 -0
  35. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  36. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  37. package/src/components/custom/Typography/Typography.tsx +218 -0
  38. package/src/components/custom/Typography/index.ts +19 -0
  39. package/src/components/custom/index.ts +8 -0
  40. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  41. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  42. package/src/components/custom/stepper/Stepper.tsx +250 -0
  43. package/src/components/custom/stepper/index.ts +10 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  46. package/src/components/ui/alert/Alert.test.tsx +202 -0
  47. package/src/components/ui/alert/Alert.tsx +170 -0
  48. package/src/components/ui/alert/index.ts +8 -0
  49. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  50. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  51. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  52. package/src/components/ui/autocomplete/index.ts +2 -0
  53. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  54. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  55. package/src/components/ui/avatar/Avatar.tsx +168 -0
  56. package/src/components/ui/avatar/index.ts +2 -0
  57. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  58. package/src/components/ui/badge/Badge.test.tsx +86 -0
  59. package/src/components/ui/badge/Badge.tsx +105 -0
  60. package/src/components/ui/badge/index.ts +2 -0
  61. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  62. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  63. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  64. package/src/components/ui/breadcrumbs/index.ts +2 -0
  65. package/src/components/ui/button/Button.stories.tsx +216 -0
  66. package/src/components/ui/button/Button.test.tsx +132 -0
  67. package/src/components/ui/button/Button.tsx +242 -0
  68. package/src/components/ui/button/index.ts +10 -0
  69. package/src/components/ui/card/Card.stories.tsx +443 -0
  70. package/src/components/ui/card/Card.test.tsx +265 -0
  71. package/src/components/ui/card/Card.tsx +380 -0
  72. package/src/components/ui/card/index.ts +20 -0
  73. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  74. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  75. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  76. package/src/components/ui/checkbox/index.ts +2 -0
  77. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  78. package/src/components/ui/chip/Chip.test.tsx +119 -0
  79. package/src/components/ui/chip/Chip.tsx +151 -0
  80. package/src/components/ui/chip/index.ts +2 -0
  81. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  82. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  83. package/src/components/ui/collapse/Collapse.tsx +275 -0
  84. package/src/components/ui/collapse/index.ts +18 -0
  85. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  86. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  87. package/src/components/ui/data-row/DataRow.tsx +29 -0
  88. package/src/components/ui/data-row/index.ts +2 -0
  89. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  90. package/src/components/ui/divider/Divider.test.tsx +55 -0
  91. package/src/components/ui/divider/Divider.tsx +37 -0
  92. package/src/components/ui/divider/index.ts +2 -0
  93. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  94. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  95. package/src/components/ui/drawer/Drawer.tsx +228 -0
  96. package/src/components/ui/drawer/index.ts +9 -0
  97. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  98. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  99. package/src/components/ui/form-field/FormField.tsx +308 -0
  100. package/src/components/ui/form-field/index.ts +2 -0
  101. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  102. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  103. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  104. package/src/components/ui/help-tip/index.ts +2 -0
  105. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  106. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  107. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  108. package/src/components/ui/icon-box/index.ts +1 -0
  109. package/src/components/ui/index.ts +36 -0
  110. package/src/components/ui/input/Input.stories.tsx +192 -0
  111. package/src/components/ui/input/Input.test.tsx +86 -0
  112. package/src/components/ui/input/Input.tsx +273 -0
  113. package/src/components/ui/input/index.ts +2 -0
  114. package/src/components/ui/link/Link.stories.tsx +78 -0
  115. package/src/components/ui/link/Link.test.tsx +94 -0
  116. package/src/components/ui/link/Link.tsx +86 -0
  117. package/src/components/ui/link/index.ts +2 -0
  118. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  119. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  120. package/src/components/ui/list-item/ListItem.tsx +89 -0
  121. package/src/components/ui/list-item/index.ts +14 -0
  122. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  123. package/src/components/ui/menu/Menu.test.tsx +353 -0
  124. package/src/components/ui/menu/Menu.tsx +222 -0
  125. package/src/components/ui/menu/index.ts +2 -0
  126. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  127. package/src/components/ui/modal/Modal.test.tsx +159 -0
  128. package/src/components/ui/modal/Modal.tsx +254 -0
  129. package/src/components/ui/modal/index.ts +19 -0
  130. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  131. package/src/components/ui/popover/Popover.test.tsx +277 -0
  132. package/src/components/ui/popover/Popover.tsx +162 -0
  133. package/src/components/ui/popover/index.ts +2 -0
  134. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  135. package/src/components/ui/progress/Progress.test.tsx +181 -0
  136. package/src/components/ui/progress/Progress.tsx +320 -0
  137. package/src/components/ui/progress/index.ts +9 -0
  138. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  139. package/src/components/ui/radio/Radio.test.tsx +166 -0
  140. package/src/components/ui/radio/Radio.tsx +200 -0
  141. package/src/components/ui/radio/index.ts +2 -0
  142. package/src/components/ui/section/Section.stories.tsx +111 -0
  143. package/src/components/ui/section/Section.test.tsx +121 -0
  144. package/src/components/ui/section/Section.tsx +52 -0
  145. package/src/components/ui/section/index.ts +6 -0
  146. package/src/components/ui/select/Select.stories.tsx +286 -0
  147. package/src/components/ui/select/Select.test.tsx +180 -0
  148. package/src/components/ui/select/Select.tsx +236 -0
  149. package/src/components/ui/select/index.ts +2 -0
  150. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  151. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  152. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  153. package/src/components/ui/skeleton/index.ts +16 -0
  154. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  155. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  156. package/src/components/ui/spinner/Spinner.tsx +72 -0
  157. package/src/components/ui/spinner/index.ts +2 -0
  158. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  159. package/src/components/ui/stack/Stack.test.tsx +184 -0
  160. package/src/components/ui/stack/Stack.tsx +76 -0
  161. package/src/components/ui/stack/index.ts +2 -0
  162. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  163. package/src/components/ui/surface/Surface.test.tsx +63 -0
  164. package/src/components/ui/surface/Surface.tsx +51 -0
  165. package/src/components/ui/surface/index.ts +1 -0
  166. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  167. package/src/components/ui/switch/Switch.test.tsx +112 -0
  168. package/src/components/ui/switch/Switch.tsx +103 -0
  169. package/src/components/ui/switch/index.ts +2 -0
  170. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  171. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  172. package/src/components/ui/tabs/Tabs.tsx +253 -0
  173. package/src/components/ui/tabs/index.ts +16 -0
  174. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  175. package/src/components/ui/toast/Toast.test.tsx +207 -0
  176. package/src/components/ui/toast/Toast.tsx +327 -0
  177. package/src/components/ui/toast/index.ts +8 -0
  178. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  179. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  180. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  181. package/src/components/ui/toolbar-button/index.ts +7 -0
  182. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  183. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  184. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  185. package/src/components/ui/tooltip/index.ts +2 -0
  186. package/src/index.ts +10 -0
  187. package/src/test/setup.ts +19 -0
  188. package/src/theme/Colors.stories.tsx +258 -0
  189. package/src/theme/config.ts +193 -0
  190. package/src/theme/elevation.stories.tsx +346 -0
  191. package/src/theme/elevation.test.ts +27 -0
  192. package/src/theme/elevation.ts +567 -0
  193. package/src/theme/index.ts +39 -0
  194. package/src/theme/shadows.stories.tsx +523 -0
  195. package/src/theme/shadows.ts +439 -0
  196. package/src/theme/tokens/index.ts +2 -0
  197. package/src/theme/tokens/primitives.ts +354 -0
  198. package/src/theme/tokens/semantic.ts +381 -0
  199. package/src/types/jest-axe.d.ts +16 -0
  200. package/src/types/nativewind.d.ts +54 -0
  201. package/src/utils/cn.ts +14 -0
  202. package/src/utils/colors.ts +140 -0
  203. package/src/utils/form.ts +188 -0
  204. package/src/utils/index.ts +29 -0
  205. package/src/utils/useTheme.ts +72 -0
  206. package/dist/chunk-NA3EES23.mjs.map +0 -1
@@ -0,0 +1,277 @@
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 {
5
+ Popover,
6
+ PopoverTrigger,
7
+ PopoverContent,
8
+ PopoverCloseButton,
9
+ } from './Popover'
10
+
11
+ describe('Popover', () => {
12
+ it('renders trigger element', () => {
13
+ render(
14
+ <Popover>
15
+ <PopoverTrigger>
16
+ <button>Open</button>
17
+ </PopoverTrigger>
18
+ <PopoverContent>
19
+ <span>Content</span>
20
+ </PopoverContent>
21
+ </Popover>
22
+ )
23
+ expect(screen.getByText('Open')).toBeInTheDocument()
24
+ })
25
+
26
+ it('does not show content by default', () => {
27
+ render(
28
+ <Popover>
29
+ <PopoverTrigger>
30
+ <button>Open</button>
31
+ </PopoverTrigger>
32
+ <PopoverContent>
33
+ <span>Hidden content</span>
34
+ </PopoverContent>
35
+ </Popover>
36
+ )
37
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument()
38
+ })
39
+
40
+ it('opens popover when trigger is clicked', () => {
41
+ render(
42
+ <Popover>
43
+ <PopoverTrigger>
44
+ <button>Open</button>
45
+ </PopoverTrigger>
46
+ <PopoverContent>
47
+ <span>Visible content</span>
48
+ </PopoverContent>
49
+ </Popover>
50
+ )
51
+
52
+ fireEvent.click(screen.getByText('Open'))
53
+ expect(screen.getByText('Visible content')).toBeInTheDocument()
54
+ })
55
+
56
+ it('closes popover when trigger is clicked again', () => {
57
+ render(
58
+ <Popover>
59
+ <PopoverTrigger>
60
+ <button>Toggle</button>
61
+ </PopoverTrigger>
62
+ <PopoverContent>
63
+ <span>Content</span>
64
+ </PopoverContent>
65
+ </Popover>
66
+ )
67
+
68
+ fireEvent.click(screen.getByText('Toggle'))
69
+ expect(screen.getByText('Content')).toBeInTheDocument()
70
+
71
+ fireEvent.click(screen.getByText('Toggle'))
72
+ expect(screen.queryByText('Content')).not.toBeInTheDocument()
73
+ })
74
+
75
+ describe('controlled state', () => {
76
+ it('shows content when isOpen is true', () => {
77
+ render(
78
+ <Popover isOpen>
79
+ <PopoverTrigger>
80
+ <button>Trigger</button>
81
+ </PopoverTrigger>
82
+ <PopoverContent>
83
+ <span>Open content</span>
84
+ </PopoverContent>
85
+ </Popover>
86
+ )
87
+ expect(screen.getByText('Open content')).toBeInTheDocument()
88
+ })
89
+
90
+ it('hides content when isOpen is false', () => {
91
+ render(
92
+ <Popover isOpen={false}>
93
+ <PopoverTrigger>
94
+ <button>Trigger</button>
95
+ </PopoverTrigger>
96
+ <PopoverContent>
97
+ <span>Closed content</span>
98
+ </PopoverContent>
99
+ </Popover>
100
+ )
101
+ expect(screen.queryByText('Closed content')).not.toBeInTheDocument()
102
+ })
103
+
104
+ it('calls onOpenChange when trigger is clicked', () => {
105
+ const onOpenChange = vi.fn()
106
+ render(
107
+ <Popover onOpenChange={onOpenChange}>
108
+ <PopoverTrigger>
109
+ <button>Trigger</button>
110
+ </PopoverTrigger>
111
+ <PopoverContent>
112
+ <span>Content</span>
113
+ </PopoverContent>
114
+ </Popover>
115
+ )
116
+
117
+ fireEvent.click(screen.getByText('Trigger'))
118
+ expect(onOpenChange).toHaveBeenCalledWith(true)
119
+ })
120
+ })
121
+
122
+ describe('placement', () => {
123
+ it('renders with bottom placement (default)', () => {
124
+ render(
125
+ <Popover isOpen placement="bottom">
126
+ <PopoverTrigger>
127
+ <button>Trigger</button>
128
+ </PopoverTrigger>
129
+ <PopoverContent>
130
+ <span>Bottom</span>
131
+ </PopoverContent>
132
+ </Popover>
133
+ )
134
+ expect(screen.getByText('Bottom')).toBeInTheDocument()
135
+ })
136
+
137
+ it('renders with top placement', () => {
138
+ render(
139
+ <Popover isOpen placement="top">
140
+ <PopoverTrigger>
141
+ <button>Trigger</button>
142
+ </PopoverTrigger>
143
+ <PopoverContent>
144
+ <span>Top</span>
145
+ </PopoverContent>
146
+ </Popover>
147
+ )
148
+ expect(screen.getByText('Top')).toBeInTheDocument()
149
+ })
150
+
151
+ it('renders with left placement', () => {
152
+ render(
153
+ <Popover isOpen placement="left">
154
+ <PopoverTrigger>
155
+ <button>Trigger</button>
156
+ </PopoverTrigger>
157
+ <PopoverContent>
158
+ <span>Left</span>
159
+ </PopoverContent>
160
+ </Popover>
161
+ )
162
+ expect(screen.getByText('Left')).toBeInTheDocument()
163
+ })
164
+
165
+ it('renders with right placement', () => {
166
+ render(
167
+ <Popover isOpen placement="right">
168
+ <PopoverTrigger>
169
+ <button>Trigger</button>
170
+ </PopoverTrigger>
171
+ <PopoverContent>
172
+ <span>Right</span>
173
+ </PopoverContent>
174
+ </Popover>
175
+ )
176
+ expect(screen.getByText('Right')).toBeInTheDocument()
177
+ })
178
+ })
179
+
180
+ describe('PopoverCloseButton', () => {
181
+ it('closes popover when clicked', () => {
182
+ render(
183
+ <Popover>
184
+ <PopoverTrigger>
185
+ <button>Open</button>
186
+ </PopoverTrigger>
187
+ <PopoverContent>
188
+ <span>Content</span>
189
+ <PopoverCloseButton>
190
+ <span>Close</span>
191
+ </PopoverCloseButton>
192
+ </PopoverContent>
193
+ </Popover>
194
+ )
195
+
196
+ fireEvent.click(screen.getByText('Open'))
197
+ expect(screen.getByText('Content')).toBeInTheDocument()
198
+
199
+ fireEvent.click(screen.getByLabelText('Close popover'))
200
+ expect(screen.queryByText('Content')).not.toBeInTheDocument()
201
+ })
202
+
203
+ it('has correct accessibility label', () => {
204
+ render(
205
+ <Popover isOpen>
206
+ <PopoverTrigger>
207
+ <button>Open</button>
208
+ </PopoverTrigger>
209
+ <PopoverContent>
210
+ <PopoverCloseButton>
211
+ <span>X</span>
212
+ </PopoverCloseButton>
213
+ </PopoverContent>
214
+ </Popover>
215
+ )
216
+ expect(screen.getByLabelText('Close popover')).toBeInTheDocument()
217
+ })
218
+ })
219
+
220
+ describe('PopoverTrigger', () => {
221
+ it('communicates expanded state', () => {
222
+ render(
223
+ <Popover>
224
+ <PopoverTrigger>
225
+ <span>Trigger</span>
226
+ </PopoverTrigger>
227
+ <PopoverContent>
228
+ <span>Content</span>
229
+ </PopoverContent>
230
+ </Popover>
231
+ )
232
+
233
+ const trigger = screen.getByRole('button', { name: 'Trigger' })
234
+ // react-native-web does not render aria-expanded for accessibilityState.expanded
235
+ expect(trigger).toBeInTheDocument()
236
+
237
+ fireEvent.click(trigger)
238
+ // Verify popover opens by checking for content
239
+ expect(screen.getByText('Content')).toBeInTheDocument()
240
+ })
241
+ })
242
+
243
+ describe('accessibility', () => {
244
+ it('has no accessibility violations when closed', async () => {
245
+ const { container } = render(
246
+ <Popover>
247
+ <PopoverTrigger>
248
+ <span>Open popover</span>
249
+ </PopoverTrigger>
250
+ <PopoverContent>
251
+ <span>Popover content</span>
252
+ </PopoverContent>
253
+ </Popover>
254
+ )
255
+ const results = await axe(container)
256
+ expect(results).toHaveNoViolations()
257
+ })
258
+
259
+ it('has no accessibility violations when open', async () => {
260
+ const { container } = render(
261
+ <Popover isOpen>
262
+ <PopoverTrigger>
263
+ <span>Open popover</span>
264
+ </PopoverTrigger>
265
+ <PopoverContent>
266
+ <span>Popover content</span>
267
+ <PopoverCloseButton>
268
+ <span>Close</span>
269
+ </PopoverCloseButton>
270
+ </PopoverContent>
271
+ </Popover>
272
+ )
273
+ const results = await axe(container)
274
+ expect(results).toHaveNoViolations()
275
+ })
276
+ })
277
+ })
@@ -0,0 +1,162 @@
1
+ import React, { useState, 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
+ }
12
+
13
+ const PopoverContext = createContext<PopoverContextType>({
14
+ isOpen: false,
15
+ setIsOpen: () => {},
16
+ placement: 'bottom',
17
+ })
18
+
19
+ export interface PopoverProps extends ViewProps {
20
+ /** Placement of the popover */
21
+ placement?: PopoverPlacement
22
+ /** Controlled open state */
23
+ isOpen?: boolean
24
+ /** Callback when open state changes */
25
+ onOpenChange?: (isOpen: boolean) => void
26
+ /** Whether clicking outside closes the popover */
27
+ closeOnClickOutside?: boolean
28
+ /** Additional className */
29
+ className?: string
30
+ children?: React.ReactNode
31
+ }
32
+
33
+ /**
34
+ * Popover component for displaying floating content.
35
+ *
36
+ * @example
37
+ * <Popover>
38
+ * <PopoverTrigger>
39
+ * <Button><ButtonText>Open</ButtonText></Button>
40
+ * </PopoverTrigger>
41
+ * <PopoverContent>
42
+ * <Text>Popover content</Text>
43
+ * </PopoverContent>
44
+ * </Popover>
45
+ */
46
+ export function Popover({
47
+ placement = 'bottom',
48
+ isOpen: controlledIsOpen,
49
+ onOpenChange,
50
+ closeOnClickOutside = true,
51
+ className,
52
+ children,
53
+ ...props
54
+ }: PopoverProps) {
55
+ const [internalIsOpen, setInternalIsOpen] = useState(false)
56
+ const isOpen = controlledIsOpen ?? internalIsOpen
57
+
58
+ const setIsOpen = (open: boolean) => {
59
+ if (controlledIsOpen === undefined) {
60
+ setInternalIsOpen(open)
61
+ }
62
+ onOpenChange?.(open)
63
+ }
64
+
65
+ return (
66
+ <PopoverContext.Provider value={{ isOpen, setIsOpen, placement }}>
67
+ <View className={cn('relative', className)} {...props}>
68
+ {children}
69
+ </View>
70
+ </PopoverContext.Provider>
71
+ )
72
+ }
73
+
74
+ export interface PopoverTriggerProps {
75
+ children: React.ReactNode
76
+ className?: string
77
+ }
78
+
79
+ /**
80
+ * Trigger element for the popover.
81
+ */
82
+ export function PopoverTrigger({ children, className }: PopoverTriggerProps) {
83
+ const { isOpen, setIsOpen } = useContext(PopoverContext)
84
+
85
+ return (
86
+ <Pressable
87
+ onPress={() => setIsOpen(!isOpen)}
88
+ accessibilityRole="button"
89
+ accessibilityState={{ expanded: isOpen }}
90
+ className={className}
91
+ >
92
+ {children}
93
+ </Pressable>
94
+ )
95
+ }
96
+
97
+ export interface PopoverContentProps {
98
+ children?: React.ReactNode
99
+ className?: string
100
+ }
101
+
102
+ /**
103
+ * Content container for the popover.
104
+ */
105
+ export function PopoverContent({ children, className }: PopoverContentProps) {
106
+ const { isOpen, setIsOpen, placement } = useContext(PopoverContext)
107
+
108
+ if (!isOpen) return null
109
+
110
+ const placementStyles = {
111
+ top: 'bottom-full left-0 mb-2',
112
+ bottom: 'top-full left-0 mt-2',
113
+ left: 'right-full top-0 mr-2',
114
+ right: 'left-full top-0 ml-2',
115
+ }
116
+
117
+ return (
118
+ <>
119
+ {/* Backdrop */}
120
+ <Pressable
121
+ onPress={() => setIsOpen(false)}
122
+ className="fixed inset-0 z-40"
123
+ style={{ position: 'absolute' }}
124
+ />
125
+ {/* Content */}
126
+ <View
127
+ className={cn(
128
+ 'absolute z-50 min-w-[200px]',
129
+ 'bg-surface-elevated rounded-lg shadow-lg border border-border-default',
130
+ 'p-4',
131
+ placementStyles[placement],
132
+ className
133
+ )}
134
+ >
135
+ {children}
136
+ </View>
137
+ </>
138
+ )
139
+ }
140
+
141
+ export interface PopoverCloseButtonProps {
142
+ children?: React.ReactNode
143
+ className?: string
144
+ }
145
+
146
+ /**
147
+ * Button to close the popover.
148
+ */
149
+ export function PopoverCloseButton({ children, className }: PopoverCloseButtonProps) {
150
+ const { setIsOpen } = useContext(PopoverContext)
151
+
152
+ return (
153
+ <Pressable
154
+ onPress={() => setIsOpen(false)}
155
+ accessibilityRole="button"
156
+ accessibilityLabel="Close popover"
157
+ className={cn('p-1', className)}
158
+ >
159
+ {children}
160
+ </Pressable>
161
+ )
162
+ }
@@ -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
+ }