@urbint/cl 1.0.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/.cursor/rules +313 -0
  2. package/.rnstorybook/index.ts +11 -0
  3. package/.rnstorybook/main.ts +8 -0
  4. package/.rnstorybook/preview.tsx +14 -0
  5. package/.rnstorybook/storybook.requires.ts +49 -0
  6. package/.storybook/main.ts +16 -0
  7. package/.storybook/preview.ts +32 -0
  8. package/.storybook/vitest.setup.ts +7 -0
  9. package/App.tsx +422 -0
  10. package/README.md +229 -0
  11. package/app.json +33 -0
  12. package/assets/adaptive-icon.png +0 -0
  13. package/assets/favicon.png +0 -0
  14. package/assets/icon.png +0 -0
  15. package/assets/splash-icon.png +0 -0
  16. package/babel.config.js +16 -0
  17. package/docs/components/CodeBlock.tsx +80 -0
  18. package/docs/components/PropTable.tsx +93 -0
  19. package/docs/components/Sidebar.tsx +199 -0
  20. package/docs/components/index.ts +8 -0
  21. package/docs/data/colorTokens.ts +70 -0
  22. package/docs/data/componentData.tsx +1685 -0
  23. package/docs/data/index.ts +7 -0
  24. package/docs/index.ts +19 -0
  25. package/docs/navigation.ts +94 -0
  26. package/docs/pages/ColorsPage.tsx +226 -0
  27. package/docs/pages/ComponentPage.tsx +235 -0
  28. package/docs/pages/InstallationPage.tsx +232 -0
  29. package/docs/pages/IntroductionPage.tsx +163 -0
  30. package/docs/pages/ThemingPage.tsx +251 -0
  31. package/docs/pages/index.ts +10 -0
  32. package/docs/theme.ts +64 -0
  33. package/docs/types.ts +54 -0
  34. package/index.ts +8 -0
  35. package/llms.txt +1893 -0
  36. package/mcp-config.example.json +10 -0
  37. package/mcp-server/README.md +192 -0
  38. package/mcp-server/package-lock.json +1707 -0
  39. package/mcp-server/package.json +38 -0
  40. package/mcp-server/src/index.ts +1136 -0
  41. package/mcp-server/src/registry/components.ts +1446 -0
  42. package/mcp-server/src/registry/index.ts +3 -0
  43. package/mcp-server/src/registry/tokens.ts +256 -0
  44. package/mcp-server/tsconfig.json +19 -0
  45. package/package.json +92 -0
  46. package/src/components/Accordion/Accordion.stories.tsx +226 -0
  47. package/src/components/Accordion/Accordion.tsx +255 -0
  48. package/src/components/Accordion/index.ts +12 -0
  49. package/src/components/ActionSheet/ActionSheet.stories.tsx +393 -0
  50. package/src/components/ActionSheet/ActionSheet.tsx +258 -0
  51. package/src/components/ActionSheet/index.ts +2 -0
  52. package/src/components/Alert/Alert.stories.tsx +165 -0
  53. package/src/components/Alert/Alert.tsx +164 -0
  54. package/src/components/Alert/index.ts +2 -0
  55. package/src/components/AlertDialog/AlertDialog.stories.tsx +330 -0
  56. package/src/components/AlertDialog/AlertDialog.tsx +234 -0
  57. package/src/components/AlertDialog/index.ts +2 -0
  58. package/src/components/Avatar/Avatar.stories.tsx +154 -0
  59. package/src/components/Avatar/Avatar.tsx +219 -0
  60. package/src/components/Avatar/index.ts +2 -0
  61. package/src/components/Badge/Badge.stories.tsx +146 -0
  62. package/src/components/Badge/Badge.tsx +125 -0
  63. package/src/components/Badge/index.ts +2 -0
  64. package/src/components/Box/Box.stories.tsx +192 -0
  65. package/src/components/Box/Box.tsx +184 -0
  66. package/src/components/Box/index.ts +2 -0
  67. package/src/components/Button/Button.stories.tsx +157 -0
  68. package/src/components/Button/Button.tsx +180 -0
  69. package/src/components/Button/index.ts +2 -0
  70. package/src/components/Card/Card.stories.tsx +145 -0
  71. package/src/components/Card/Card.tsx +169 -0
  72. package/src/components/Card/index.ts +11 -0
  73. package/src/components/Center/Center.stories.tsx +215 -0
  74. package/src/components/Center/Center.tsx +29 -0
  75. package/src/components/Center/index.ts +2 -0
  76. package/src/components/Checkbox/Checkbox.stories.tsx +94 -0
  77. package/src/components/Checkbox/Checkbox.tsx +242 -0
  78. package/src/components/Checkbox/index.ts +2 -0
  79. package/src/components/DatePicker/DatePicker.stories.tsx +623 -0
  80. package/src/components/DatePicker/DatePicker.tsx +1228 -0
  81. package/src/components/DatePicker/index.ts +8 -0
  82. package/src/components/Divider/Divider.stories.tsx +224 -0
  83. package/src/components/Divider/Divider.tsx +73 -0
  84. package/src/components/Divider/index.ts +2 -0
  85. package/src/components/Drawer/Drawer.stories.tsx +414 -0
  86. package/src/components/Drawer/Drawer.tsx +342 -0
  87. package/src/components/Drawer/index.ts +11 -0
  88. package/src/components/Fab/Fab.stories.tsx +360 -0
  89. package/src/components/Fab/Fab.tsx +185 -0
  90. package/src/components/Fab/index.ts +2 -0
  91. package/src/components/FormControl/FormControl.stories.tsx +276 -0
  92. package/src/components/FormControl/FormControl.tsx +185 -0
  93. package/src/components/FormControl/index.ts +12 -0
  94. package/src/components/Grid/Grid.stories.tsx +244 -0
  95. package/src/components/Grid/Grid.tsx +93 -0
  96. package/src/components/Grid/index.ts +2 -0
  97. package/src/components/HStack/HStack.stories.tsx +230 -0
  98. package/src/components/HStack/HStack.tsx +80 -0
  99. package/src/components/HStack/index.ts +2 -0
  100. package/src/components/Heading/Heading.stories.tsx +111 -0
  101. package/src/components/Heading/Heading.tsx +85 -0
  102. package/src/components/Heading/index.ts +2 -0
  103. package/src/components/Icon/Icon.stories.tsx +320 -0
  104. package/src/components/Icon/Icon.tsx +117 -0
  105. package/src/components/Icon/index.ts +2 -0
  106. package/src/components/Image/Image.stories.tsx +357 -0
  107. package/src/components/Image/Image.tsx +168 -0
  108. package/src/components/Image/index.ts +2 -0
  109. package/src/components/Input/Input.stories.tsx +164 -0
  110. package/src/components/Input/Input.tsx +274 -0
  111. package/src/components/Input/index.ts +2 -0
  112. package/src/components/Link/Link.stories.tsx +187 -0
  113. package/src/components/Link/Link.tsx +104 -0
  114. package/src/components/Link/index.ts +2 -0
  115. package/src/components/Menu/Menu.stories.tsx +363 -0
  116. package/src/components/Menu/Menu.tsx +238 -0
  117. package/src/components/Menu/index.ts +2 -0
  118. package/src/components/Modal/Modal.stories.tsx +156 -0
  119. package/src/components/Modal/Modal.tsx +280 -0
  120. package/src/components/Modal/index.ts +11 -0
  121. package/src/components/Popover/Popover.stories.tsx +330 -0
  122. package/src/components/Popover/Popover.tsx +315 -0
  123. package/src/components/Popover/index.ts +11 -0
  124. package/src/components/Portal/Portal.stories.tsx +376 -0
  125. package/src/components/Portal/Portal.tsx +100 -0
  126. package/src/components/Portal/index.ts +2 -0
  127. package/src/components/Pressable/Pressable.stories.tsx +338 -0
  128. package/src/components/Pressable/Pressable.tsx +71 -0
  129. package/src/components/Pressable/index.ts +2 -0
  130. package/src/components/Progress/Progress.stories.tsx +131 -0
  131. package/src/components/Progress/Progress.tsx +219 -0
  132. package/src/components/Progress/index.ts +2 -0
  133. package/src/components/Radio/Radio.stories.tsx +101 -0
  134. package/src/components/Radio/Radio.tsx +234 -0
  135. package/src/components/Radio/index.ts +2 -0
  136. package/src/components/Select/Select.stories.tsx +908 -0
  137. package/src/components/Select/Select.tsx +659 -0
  138. package/src/components/Select/index.ts +8 -0
  139. package/src/components/Skeleton/Skeleton.stories.tsx +154 -0
  140. package/src/components/Skeleton/Skeleton.tsx +192 -0
  141. package/src/components/Skeleton/index.ts +8 -0
  142. package/src/components/Slider/Slider.stories.tsx +363 -0
  143. package/src/components/Slider/Slider.tsx +209 -0
  144. package/src/components/Slider/index.ts +2 -0
  145. package/src/components/Spinner/Spinner.stories.tsx +108 -0
  146. package/src/components/Spinner/Spinner.tsx +121 -0
  147. package/src/components/Spinner/index.ts +2 -0
  148. package/src/components/Switch/Switch.stories.tsx +116 -0
  149. package/src/components/Switch/Switch.tsx +172 -0
  150. package/src/components/Switch/index.ts +2 -0
  151. package/src/components/Table/Table.stories.tsx +417 -0
  152. package/src/components/Table/Table.tsx +233 -0
  153. package/src/components/Table/index.ts +2 -0
  154. package/src/components/Text/Text.stories.tsx +93 -0
  155. package/src/components/Text/Text.tsx +119 -0
  156. package/src/components/Text/index.ts +2 -0
  157. package/src/components/Textarea/Textarea.stories.tsx +280 -0
  158. package/src/components/Textarea/Textarea.tsx +212 -0
  159. package/src/components/Textarea/index.ts +2 -0
  160. package/src/components/Toast/Toast.stories.tsx +446 -0
  161. package/src/components/Toast/Toast.tsx +221 -0
  162. package/src/components/Toast/index.ts +2 -0
  163. package/src/components/Tooltip/Tooltip.stories.tsx +354 -0
  164. package/src/components/Tooltip/Tooltip.tsx +261 -0
  165. package/src/components/Tooltip/index.ts +2 -0
  166. package/src/components/VStack/VStack.stories.tsx +183 -0
  167. package/src/components/VStack/VStack.tsx +76 -0
  168. package/src/components/VStack/index.ts +2 -0
  169. package/src/components/index.ts +62 -0
  170. package/src/hooks/index.ts +7 -0
  171. package/src/hooks/useControllableState.ts +41 -0
  172. package/src/hooks/useDisclosure.ts +51 -0
  173. package/src/index.ts +22 -0
  174. package/src/stories/Button.stories.tsx +53 -0
  175. package/src/stories/Button.tsx +101 -0
  176. package/src/stories/Configure.mdx +364 -0
  177. package/src/stories/Header.stories.tsx +33 -0
  178. package/src/stories/Header.tsx +75 -0
  179. package/src/stories/Page.stories.tsx +25 -0
  180. package/src/stories/Page.tsx +154 -0
  181. package/src/stories/assets/accessibility.png +0 -0
  182. package/src/stories/assets/accessibility.svg +1 -0
  183. package/src/stories/assets/addon-library.png +0 -0
  184. package/src/stories/assets/assets.png +0 -0
  185. package/src/stories/assets/avif-test-image.avif +0 -0
  186. package/src/stories/assets/context.png +0 -0
  187. package/src/stories/assets/discord.svg +1 -0
  188. package/src/stories/assets/docs.png +0 -0
  189. package/src/stories/assets/figma-plugin.png +0 -0
  190. package/src/stories/assets/github.svg +1 -0
  191. package/src/stories/assets/share.png +0 -0
  192. package/src/stories/assets/styling.png +0 -0
  193. package/src/stories/assets/testing.png +0 -0
  194. package/src/stories/assets/theming.png +0 -0
  195. package/src/stories/assets/tutorials.svg +1 -0
  196. package/src/stories/assets/youtube.svg +1 -0
  197. package/src/styles/index.ts +7 -0
  198. package/src/styles/tokens.ts +318 -0
  199. package/src/styles/unistyles.ts +254 -0
  200. package/src/utils/createContext.tsx +25 -0
  201. package/src/utils/index.ts +7 -0
  202. package/src/utils/mergeRefs.ts +21 -0
  203. package/tsconfig.json +26 -0
  204. package/urbint-cl-1.0.0.tgz +0 -0
  205. package/vitest.config.ts +37 -0
  206. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,376 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { Portal, PortalProvider, PortalHost } from './Portal';
4
+ import { Button } from '../Button';
5
+ import { VStack } from '../VStack';
6
+ import { HStack } from '../HStack';
7
+ import { Text } from '../Text';
8
+ import { Box } from '../Box';
9
+ import { colors, spacing, borderRadius } from '../../styles/tokens';
10
+
11
+ const meta: Meta<typeof Portal> = {
12
+ title: 'Utility/Portal',
13
+ component: Portal,
14
+ decorators: [
15
+ (Story) => (
16
+ <PortalProvider>
17
+ <Story />
18
+ </PortalProvider>
19
+ ),
20
+ ],
21
+ };
22
+
23
+ export default meta;
24
+
25
+ type Story = StoryObj<typeof Portal>;
26
+
27
+ const BasicPortalDemo = () => {
28
+ const [showPortal, setShowPortal] = useState(false);
29
+
30
+ return (
31
+ <VStack space={spacing.lg}>
32
+ <Text weight="semiBold">Basic Portal</Text>
33
+ <Text size="sm" color={colors.text.secondary}>
34
+ Portals render content outside the normal component hierarchy.
35
+ </Text>
36
+ <Button onPress={() => setShowPortal(!showPortal)}>
37
+ {showPortal ? 'Hide Portal' : 'Show Portal'}
38
+ </Button>
39
+ {showPortal && (
40
+ <Portal>
41
+ <Box
42
+ position="absolute"
43
+ style={{ top: 100, right: 20 }}
44
+ p={spacing.lg}
45
+ bg={colors.brand.blue}
46
+ rounded="lg"
47
+ shadow="20"
48
+ >
49
+ <Text style={{ color: 'white' }}>I'm rendered in a Portal!</Text>
50
+ </Box>
51
+ </Portal>
52
+ )}
53
+ </VStack>
54
+ );
55
+ };
56
+
57
+ export const Default: Story = {
58
+ render: () => <BasicPortalDemo />,
59
+ };
60
+
61
+ const OverlayDemo = () => {
62
+ const [showOverlay, setShowOverlay] = useState(false);
63
+
64
+ return (
65
+ <VStack space={spacing.lg}>
66
+ <Text weight="semiBold">Portal Overlay</Text>
67
+ <Text size="sm" color={colors.text.secondary}>
68
+ Portals are commonly used for overlays, modals, and dropdowns.
69
+ </Text>
70
+ <Button onPress={() => setShowOverlay(true)}>Show Overlay</Button>
71
+ {showOverlay && (
72
+ <Portal>
73
+ <Box
74
+ position="absolute"
75
+ style={{ top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)' }}
76
+ alignItems="center"
77
+ justifyContent="center"
78
+ >
79
+ <Box p={24} bg={colors.white} rounded="lg" shadow="40" m={spacing.lg}>
80
+ <VStack space={spacing.lg}>
81
+ <Text weight="semiBold" size="lg">Overlay Content</Text>
82
+ <Text color={colors.text.secondary}>
83
+ This content is rendered through a Portal,
84
+ appearing above all other content.
85
+ </Text>
86
+ <Button onPress={() => setShowOverlay(false)}>Close</Button>
87
+ </VStack>
88
+ </Box>
89
+ </Box>
90
+ </Portal>
91
+ )}
92
+ </VStack>
93
+ );
94
+ };
95
+
96
+ export const Overlay: Story = {
97
+ render: () => <OverlayDemo />,
98
+ };
99
+
100
+ const MultiplePortalsDemo = () => {
101
+ const [portals, setPortals] = useState<number[]>([]);
102
+
103
+ const addPortal = () => {
104
+ setPortals([...portals, Date.now()]);
105
+ };
106
+
107
+ const removePortal = (id: number) => {
108
+ setPortals(portals.filter((p) => p !== id));
109
+ };
110
+
111
+ return (
112
+ <VStack space={spacing.lg}>
113
+ <Text weight="semiBold">Multiple Portals</Text>
114
+ <Text size="sm" color={colors.text.secondary}>
115
+ You can have multiple portals rendered simultaneously.
116
+ </Text>
117
+ <HStack space={8}>
118
+ <Button onPress={addPortal}>Add Portal</Button>
119
+ <Button variant="secondary" onPress={() => setPortals([])}>
120
+ Clear All
121
+ </Button>
122
+ </HStack>
123
+ <Text size="sm">Active portals: {portals.length}</Text>
124
+ {portals.map((id, index) => (
125
+ <Portal key={id}>
126
+ <Box
127
+ position="absolute"
128
+ style={{
129
+ top: 100 + index * 60,
130
+ right: 20 + index * 20,
131
+ }}
132
+ p={spacing.md}
133
+ bg={[colors.brand.blue, colors.feedback.success.content, colors.feedback.warning.content, colors.feedback.error.content, colors.badge.purple][index % 5]}
134
+ rounded="lg"
135
+ shadow="20"
136
+ >
137
+ <HStack space={spacing.sm} alignItems="center">
138
+ <Text style={{ color: 'white' }}>Portal {index + 1}</Text>
139
+ <Button
140
+ size="sm"
141
+ variant="ghost"
142
+ onPress={() => removePortal(id)}
143
+ >
144
+ <Text style={{ color: 'white' }}>×</Text>
145
+ </Button>
146
+ </HStack>
147
+ </Box>
148
+ </Portal>
149
+ ))}
150
+ </VStack>
151
+ );
152
+ };
153
+
154
+ export const Multiple: Story = {
155
+ render: () => <MultiplePortalsDemo />,
156
+ };
157
+
158
+ const ToastNotificationDemo = () => {
159
+ const [toasts, setToasts] = useState<{ id: number; message: string; type: 'success' | 'error' | 'info' }[]>([]);
160
+
161
+ const addToast = (type: 'success' | 'error' | 'info') => {
162
+ const messages = {
163
+ success: 'Operation completed successfully!',
164
+ error: 'Something went wrong.',
165
+ info: 'Here is some information.',
166
+ };
167
+ const newToast = { id: Date.now(), message: messages[type], type };
168
+ setToasts([...toasts, newToast]);
169
+ setTimeout(() => {
170
+ setToasts((current) => current.filter((t) => t.id !== newToast.id));
171
+ }, 3000);
172
+ };
173
+
174
+ const toastColors = {
175
+ success: colors.feedback.success.content,
176
+ error: colors.feedback.error.content,
177
+ info: colors.brand.blue,
178
+ };
179
+
180
+ return (
181
+ <VStack space={spacing.lg}>
182
+ <Text weight="semiBold">Toast Notifications with Portal</Text>
183
+ <Text size="sm" color={colors.text.secondary}>
184
+ Portals are perfect for toast notifications that need to appear above everything.
185
+ </Text>
186
+ <HStack space={spacing.sm}>
187
+ <Button onPress={() => addToast('success')}>Success</Button>
188
+ <Button variant="danger" onPress={() => addToast('error')}>Error</Button>
189
+ <Button variant="secondary" onPress={() => addToast('info')}>Info</Button>
190
+ </HStack>
191
+ {toasts.map((toast, index) => (
192
+ <Portal key={toast.id}>
193
+ <Box
194
+ position="absolute"
195
+ style={{
196
+ top: 20 + index * 60,
197
+ left: 20,
198
+ right: 20,
199
+ }}
200
+ p={spacing.lg}
201
+ bg={toastColors[toast.type]}
202
+ rounded="lg"
203
+ shadow="30"
204
+ >
205
+ <Text style={{ color: 'white' }}>{toast.message}</Text>
206
+ </Box>
207
+ </Portal>
208
+ ))}
209
+ </VStack>
210
+ );
211
+ };
212
+
213
+ export const Toasts: Story = {
214
+ render: () => <ToastNotificationDemo />,
215
+ };
216
+
217
+ const DropdownDemo = () => {
218
+ const [showDropdown, setShowDropdown] = useState(false);
219
+
220
+ return (
221
+ <VStack space={spacing.lg}>
222
+ <Text weight="semiBold">Dropdown with Portal</Text>
223
+ <Text size="sm" color={colors.text.secondary}>
224
+ Dropdowns use portals to escape overflow hidden containers.
225
+ </Text>
226
+ <Box
227
+ p={spacing.lg}
228
+ bg={colors.background.secondary}
229
+ rounded="lg"
230
+ overflow="hidden"
231
+ style={{ maxHeight: 100 }}
232
+ >
233
+ <Text size="sm" color={colors.text.secondary} style={{ marginBottom: spacing.sm }}>
234
+ Container with overflow: hidden
235
+ </Text>
236
+ <Box position="relative">
237
+ <Button
238
+ variant="outline"
239
+ onPress={() => setShowDropdown(!showDropdown)}
240
+ >
241
+ Open Dropdown ▼
242
+ </Button>
243
+ {showDropdown && (
244
+ <Portal>
245
+ <Box
246
+ position="absolute"
247
+ style={{ top: 180, left: 36 }}
248
+ bg={colors.white}
249
+ rounded="lg"
250
+ shadow="20"
251
+ w={200}
252
+ >
253
+ {['Option 1', 'Option 2', 'Option 3', 'Option 4'].map((opt, i) => (
254
+ <Box
255
+ key={opt}
256
+ p={spacing.md}
257
+ borderBottomWidth={i < 3 ? 1 : 0}
258
+ borderColor={colors.border.default}
259
+ >
260
+ <Text>{opt}</Text>
261
+ </Box>
262
+ ))}
263
+ </Box>
264
+ </Portal>
265
+ )}
266
+ </Box>
267
+ </Box>
268
+ {showDropdown && (
269
+ <Button variant="ghost" onPress={() => setShowDropdown(false)}>
270
+ Close Dropdown
271
+ </Button>
272
+ )}
273
+ </VStack>
274
+ );
275
+ };
276
+
277
+ export const Dropdown: Story = {
278
+ render: () => <DropdownDemo />,
279
+ };
280
+
281
+ const LoadingOverlayDemo = () => {
282
+ const [isLoading, setIsLoading] = useState(false);
283
+
284
+ const simulateLoading = () => {
285
+ setIsLoading(true);
286
+ setTimeout(() => setIsLoading(false), 2000);
287
+ };
288
+
289
+ return (
290
+ <VStack space={spacing.lg}>
291
+ <Text weight="semiBold">Loading Overlay</Text>
292
+ <Text size="sm" color={colors.text.secondary}>
293
+ Use portals for full-screen loading states.
294
+ </Text>
295
+ <Button onPress={simulateLoading}>Start Loading (2s)</Button>
296
+ {isLoading && (
297
+ <Portal>
298
+ <Box
299
+ position="absolute"
300
+ style={{ top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(255,255,255,0.9)' }}
301
+ alignItems="center"
302
+ justifyContent="center"
303
+ >
304
+ <VStack space={spacing.lg} alignItems="center">
305
+ <Box
306
+ w={48}
307
+ h={48}
308
+ rounded="full"
309
+ bg={colors.brand.blue}
310
+ alignItems="center"
311
+ justifyContent="center"
312
+ >
313
+ <Text style={{ color: 'white', fontSize: 20 }}>⏳</Text>
314
+ </Box>
315
+ <Text weight="semiBold">Loading...</Text>
316
+ <Text size="sm" color={colors.text.secondary}>Please wait</Text>
317
+ </VStack>
318
+ </Box>
319
+ </Portal>
320
+ )}
321
+ </VStack>
322
+ );
323
+ };
324
+
325
+ export const Loading: Story = {
326
+ render: () => <LoadingOverlayDemo />,
327
+ };
328
+
329
+ const ConfirmationDialogDemo = () => {
330
+ const [showConfirm, setShowConfirm] = useState(false);
331
+
332
+ return (
333
+ <VStack space={spacing.lg}>
334
+ <Text weight="semiBold">Confirmation Dialog</Text>
335
+ <Text size="sm" color={colors.text.secondary}>
336
+ Portals ensure dialogs appear above all content.
337
+ </Text>
338
+ <Button variant="danger" onPress={() => setShowConfirm(true)}>
339
+ Delete Item
340
+ </Button>
341
+ {showConfirm && (
342
+ <Portal>
343
+ <Box
344
+ position="absolute"
345
+ style={{ top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)' }}
346
+ alignItems="center"
347
+ justifyContent="center"
348
+ p={16}
349
+ >
350
+ <Box p={24} bg={colors.white} rounded="lg" shadow="40" w="100%" maxW={400}>
351
+ <VStack space={spacing.lg}>
352
+ <Text weight="semiBold" size="lg">Confirm Delete</Text>
353
+ <Text color={colors.text.secondary}>
354
+ Are you sure you want to delete this item? This action cannot be undone.
355
+ </Text>
356
+ <HStack space={spacing.md} justifyContent="flex-end">
357
+ <Button variant="ghost" onPress={() => setShowConfirm(false)}>
358
+ Cancel
359
+ </Button>
360
+ <Button variant="danger" onPress={() => setShowConfirm(false)}>
361
+ Delete
362
+ </Button>
363
+ </HStack>
364
+ </VStack>
365
+ </Box>
366
+ </Box>
367
+ </Portal>
368
+ )}
369
+ </VStack>
370
+ );
371
+ };
372
+
373
+ export const ConfirmDialog: Story = {
374
+ render: () => <ConfirmationDialogDemo />,
375
+ };
376
+
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Portal Component
3
+ * Renders children into a different part of the DOM tree
4
+ */
5
+
6
+ import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
7
+ import { View, ViewProps, StyleSheet } from 'react-native';
8
+ import { zIndex } from '../../styles/tokens';
9
+
10
+ interface PortalContextValue {
11
+ register: (name: string, element: React.ReactNode) => void;
12
+ unregister: (name: string) => void;
13
+ }
14
+
15
+ const PortalContext = createContext<PortalContextValue | null>(null);
16
+
17
+ export interface PortalProviderProps {
18
+ children: React.ReactNode;
19
+ }
20
+
21
+ export const PortalProvider: React.FC<PortalProviderProps> = ({ children }) => {
22
+ const [portals, setPortals] = useState<Map<string, React.ReactNode>>(new Map());
23
+
24
+ const register = useCallback((name: string, element: React.ReactNode) => {
25
+ setPortals((prev) => new Map(prev).set(name, element));
26
+ }, []);
27
+
28
+ const unregister = useCallback((name: string) => {
29
+ setPortals((prev) => {
30
+ const next = new Map(prev);
31
+ next.delete(name);
32
+ return next;
33
+ });
34
+ }, []);
35
+
36
+ return (
37
+ <PortalContext.Provider value={{ register, unregister }}>
38
+ {children}
39
+ <View style={styles.portalContainer} pointerEvents="box-none">
40
+ {Array.from(portals.values())}
41
+ </View>
42
+ </PortalContext.Provider>
43
+ );
44
+ };
45
+
46
+ export interface PortalProps {
47
+ /** Portal name for management */
48
+ name?: string;
49
+ children: React.ReactNode;
50
+ }
51
+
52
+ export const Portal: React.FC<PortalProps> = ({ name, children }) => {
53
+ const context = useContext(PortalContext);
54
+ const portalName = useRef(name || `portal-${Date.now()}-${Math.random()}`);
55
+
56
+ useEffect(() => {
57
+ if (!context) {
58
+ console.warn('Portal requires a PortalProvider to be present in the component tree');
59
+ return;
60
+ }
61
+
62
+ context.register(portalName.current, children);
63
+
64
+ return () => {
65
+ context.unregister(portalName.current);
66
+ };
67
+ }, [context, children]);
68
+
69
+ useEffect(() => {
70
+ if (context) {
71
+ context.register(portalName.current, children);
72
+ }
73
+ }, [context, children]);
74
+
75
+ return null;
76
+ };
77
+
78
+ Portal.displayName = 'Portal';
79
+
80
+ export interface PortalHostProps extends ViewProps {
81
+ /** Host name */
82
+ name?: string;
83
+ }
84
+
85
+ export const PortalHost: React.FC<PortalHostProps> = ({ name, children, ...props }) => {
86
+ return <View {...props}>{children}</View>;
87
+ };
88
+
89
+ PortalHost.displayName = 'PortalHost';
90
+
91
+ const styles = StyleSheet.create({
92
+ portalContainer: {
93
+ position: 'absolute',
94
+ top: 0,
95
+ left: 0,
96
+ right: 0,
97
+ bottom: 0,
98
+ zIndex: zIndex.modal,
99
+ },
100
+ });
@@ -0,0 +1,2 @@
1
+ export { Portal, PortalProvider, PortalHost, type PortalProps, type PortalProviderProps, type PortalHostProps } from './Portal';
2
+