@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,330 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { Popover, PopoverHeader, PopoverBody, PopoverFooter } from './Popover';
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 { Input } from '../Input';
10
+ import { colors, spacing, borderRadius } from '../../styles/tokens';
11
+
12
+ const meta: Meta<typeof Popover> = {
13
+ title: 'Overlay/Popover',
14
+ component: Popover,
15
+ argTypes: {
16
+ placement: {
17
+ control: 'select',
18
+ options: ['top', 'bottom', 'left', 'right'],
19
+ },
20
+ closeOnBlur: { control: 'boolean' },
21
+ hasArrow: { control: 'boolean' },
22
+ },
23
+ args: {
24
+ placement: 'bottom',
25
+ closeOnBlur: true,
26
+ hasArrow: true,
27
+ },
28
+ };
29
+
30
+ export default meta;
31
+
32
+ type Story = StoryObj<typeof Popover>;
33
+
34
+ export const Default: Story = {
35
+ render: () => (
36
+ <Box p={50} alignItems="center">
37
+ <Popover
38
+ trigger={<Button>Open Popover</Button>}
39
+ >
40
+ <PopoverHeader>Popover Title</PopoverHeader>
41
+ <PopoverBody>
42
+ <Text>This is the popover content. You can put any content here.</Text>
43
+ </PopoverBody>
44
+ </Popover>
45
+ </Box>
46
+ ),
47
+ };
48
+
49
+ const PlacementDemo = () => (
50
+ <VStack space={spacing.lg}>
51
+ <Text weight="semiBold">Popover Placement</Text>
52
+ <Box p={80} alignItems="center">
53
+ <VStack space={spacing.lg} alignItems="center">
54
+ <Popover
55
+ trigger={<Button size="sm">Top</Button>}
56
+ placement="top"
57
+ >
58
+ <PopoverBody>
59
+ <Text>Popover on top</Text>
60
+ </PopoverBody>
61
+ </Popover>
62
+
63
+ <HStack space={100}>
64
+ <Popover
65
+ trigger={<Button size="sm">Left</Button>}
66
+ placement="left"
67
+ >
68
+ <PopoverBody>
69
+ <Text>Popover on left</Text>
70
+ </PopoverBody>
71
+ </Popover>
72
+
73
+ <Popover
74
+ trigger={<Button size="sm">Right</Button>}
75
+ placement="right"
76
+ >
77
+ <PopoverBody>
78
+ <Text>Popover on right</Text>
79
+ </PopoverBody>
80
+ </Popover>
81
+ </HStack>
82
+
83
+ <Popover
84
+ trigger={<Button size="sm">Bottom</Button>}
85
+ placement="bottom"
86
+ >
87
+ <PopoverBody>
88
+ <Text>Popover on bottom</Text>
89
+ </PopoverBody>
90
+ </Popover>
91
+ </VStack>
92
+ </Box>
93
+ </VStack>
94
+ );
95
+
96
+ export const Placement: Story = {
97
+ render: () => <PlacementDemo />,
98
+ };
99
+
100
+ const WithHeaderAndFooter = () => (
101
+ <VStack space={spacing.lg}>
102
+ <Text weight="semiBold">Full Popover with Header and Footer</Text>
103
+ <Box p={50} alignItems="center">
104
+ <Popover
105
+ trigger={<Button>Open Popover</Button>}
106
+ >
107
+ <PopoverHeader>Confirmation</PopoverHeader>
108
+ <PopoverBody>
109
+ <Text>Are you sure you want to proceed with this action?</Text>
110
+ </PopoverBody>
111
+ <PopoverFooter>
112
+ <Button size="sm" variant="ghost">Cancel</Button>
113
+ <Button size="sm">Confirm</Button>
114
+ </PopoverFooter>
115
+ </Popover>
116
+ </Box>
117
+ </VStack>
118
+ );
119
+
120
+ export const HeaderAndFooter: Story = {
121
+ render: () => <WithHeaderAndFooter />,
122
+ };
123
+
124
+ const FormPopover = () => (
125
+ <VStack space={spacing.lg}>
126
+ <Text weight="semiBold">Popover with Form</Text>
127
+ <Box p={50} alignItems="center">
128
+ <Popover
129
+ trigger={<Button>Edit</Button>}
130
+ >
131
+ <PopoverHeader>Edit Name</PopoverHeader>
132
+ <PopoverBody>
133
+ <VStack space={spacing.md}>
134
+ <Input label="First Name" placeholder="Enter first name" />
135
+ <Input label="Last Name" placeholder="Enter last name" />
136
+ </VStack>
137
+ </PopoverBody>
138
+ <PopoverFooter>
139
+ <Button size="sm" variant="ghost">Cancel</Button>
140
+ <Button size="sm">Save</Button>
141
+ </PopoverFooter>
142
+ </Popover>
143
+ </Box>
144
+ </VStack>
145
+ );
146
+
147
+ export const WithForm: Story = {
148
+ render: () => <FormPopover />,
149
+ };
150
+
151
+ const InfoPopover = () => (
152
+ <VStack space={spacing.lg}>
153
+ <Text weight="semiBold">Information Popover</Text>
154
+ <HStack space={spacing.sm} alignItems="center">
155
+ <Text>What is this?</Text>
156
+ <Popover
157
+ trigger={
158
+ <Box
159
+ w={20}
160
+ h={20}
161
+ rounded="full"
162
+ bg={colors.brand.blue + '20'}
163
+ alignItems="center"
164
+ justifyContent="center"
165
+ >
166
+ <Text variant="small">?</Text>
167
+ </Box>
168
+ }
169
+ placement="right"
170
+ >
171
+ <PopoverBody>
172
+ <VStack space={spacing.sm}>
173
+ <Text weight="semiBold">Information</Text>
174
+ <Text variant="small" color={colors.text.secondary}>
175
+ This is additional information that helps explain the feature.
176
+ Click anywhere outside to close.
177
+ </Text>
178
+ </VStack>
179
+ </PopoverBody>
180
+ </Popover>
181
+ </HStack>
182
+ </VStack>
183
+ );
184
+
185
+ export const Info: Story = {
186
+ render: () => <InfoPopover />,
187
+ };
188
+
189
+ const ProfilePopover = () => (
190
+ <VStack space={spacing.lg}>
191
+ <Text weight="semiBold">Profile Card Popover</Text>
192
+ <Box p={50}>
193
+ <Popover
194
+ trigger={
195
+ <HStack space={spacing.sm} alignItems="center">
196
+ <Box
197
+ w={32}
198
+ h={32}
199
+ rounded="full"
200
+ bg={colors.brand.blue + '30'}
201
+ alignItems="center"
202
+ justifyContent="center"
203
+ >
204
+ <Text weight="semiBold">JD</Text>
205
+ </Box>
206
+ <Text>John Doe</Text>
207
+ </HStack>
208
+ }
209
+ >
210
+ <PopoverBody>
211
+ <VStack space={spacing.md}>
212
+ <HStack space={spacing.md} alignItems="center">
213
+ <Box
214
+ w={48}
215
+ h={48}
216
+ rounded="full"
217
+ bg={colors.brand.blue + '30'}
218
+ alignItems="center"
219
+ justifyContent="center"
220
+ >
221
+ <Text weight="bold">JD</Text>
222
+ </Box>
223
+ <VStack space={spacing.xs}>
224
+ <Text weight="semiBold">John Doe</Text>
225
+ <Text variant="small" color={colors.text.secondary}>john@example.com</Text>
226
+ </VStack>
227
+ </HStack>
228
+ <Box h={1} bg={colors.border.default} />
229
+ <VStack space={spacing.xs}>
230
+ <Text variant="small" color={colors.text.secondary}>Role: Senior Developer</Text>
231
+ <Text variant="small" color={colors.text.secondary}>Location: San Francisco</Text>
232
+ <Text variant="small" color={colors.text.secondary}>Joined: Jan 2024</Text>
233
+ </VStack>
234
+ </VStack>
235
+ </PopoverBody>
236
+ <PopoverFooter>
237
+ <Button size="sm" variant="ghost">View Profile</Button>
238
+ <Button size="sm" variant="secondary">Message</Button>
239
+ </PopoverFooter>
240
+ </Popover>
241
+ </Box>
242
+ </VStack>
243
+ );
244
+
245
+ export const Profile: Story = {
246
+ render: () => <ProfilePopover />,
247
+ };
248
+
249
+ const ControlledPopover = () => {
250
+ const [isOpen, setIsOpen] = useState(false);
251
+
252
+ return (
253
+ <VStack space={spacing.lg}>
254
+ <Text weight="semiBold">Controlled Popover</Text>
255
+ <HStack space={spacing.sm}>
256
+ <Button onPress={() => setIsOpen(true)}>Open Programmatically</Button>
257
+ <Button variant="secondary" onPress={() => setIsOpen(false)}>Close Programmatically</Button>
258
+ </HStack>
259
+ <Box p={50} alignItems="center">
260
+ <Popover
261
+ trigger={<Button variant="outline">Click to Toggle</Button>}
262
+ isOpen={isOpen}
263
+ onOpen={() => setIsOpen(true)}
264
+ onClose={() => setIsOpen(false)}
265
+ >
266
+ <PopoverBody>
267
+ <Text>This popover is controlled externally.</Text>
268
+ </PopoverBody>
269
+ </Popover>
270
+ </Box>
271
+ </VStack>
272
+ );
273
+ };
274
+
275
+ export const Controlled: Story = {
276
+ render: () => <ControlledPopover />,
277
+ };
278
+
279
+ const DatePickerPopover = () => (
280
+ <VStack space={spacing.lg}>
281
+ <Text weight="semiBold">Date Selection Popover</Text>
282
+ <Box p={50}>
283
+ <Popover
284
+ trigger={
285
+ <Button variant="outline">Select Date</Button>
286
+ }
287
+ >
288
+ <PopoverHeader>Select Date</PopoverHeader>
289
+ <PopoverBody>
290
+ <VStack space={spacing.md}>
291
+ <Text variant="small" color={colors.text.secondary} align="center">January 2024</Text>
292
+ <HStack justifyContent="space-around">
293
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
294
+ <Box key={i} w={30} alignItems="center">
295
+ <Text variant="small" color={colors.text.secondary}>{day}</Text>
296
+ </Box>
297
+ ))}
298
+ </HStack>
299
+ {[[null, 1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12, 13]].map((week, i) => (
300
+ <HStack key={i} justifyContent="space-around">
301
+ {week.map((day, j) => (
302
+ <Box
303
+ key={j}
304
+ w={30}
305
+ h={30}
306
+ rounded="full"
307
+ bg={day === 10 ? colors.brand.blue : 'transparent'}
308
+ alignItems="center"
309
+ justifyContent="center"
310
+ >
311
+ {day && (
312
+ <Text variant="small" color={day === 10 ? 'white' : undefined}>
313
+ {day}
314
+ </Text>
315
+ )}
316
+ </Box>
317
+ ))}
318
+ </HStack>
319
+ ))}
320
+ </VStack>
321
+ </PopoverBody>
322
+ </Popover>
323
+ </Box>
324
+ </VStack>
325
+ );
326
+
327
+ export const DatePicker: Story = {
328
+ render: () => <DatePickerPopover />,
329
+ };
330
+
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Popover Component
3
+ * Floating content panel anchored to a trigger
4
+ */
5
+
6
+ import React, { forwardRef, useState, useRef, useCallback } from 'react';
7
+ import {
8
+ View,
9
+ ViewProps,
10
+ Pressable,
11
+ Text,
12
+ Modal,
13
+ Animated,
14
+ LayoutRectangle,
15
+ Dimensions,
16
+ StyleSheet,
17
+ Platform,
18
+ } from 'react-native';
19
+ import { colors, spacing, borderRadius, typography, elevation } from '../../styles/tokens';
20
+
21
+ const getScreenDimensions = () => Dimensions.get('window');
22
+
23
+ export interface PopoverProps extends ViewProps {
24
+ /** Popover trigger element */
25
+ trigger: React.ReactElement;
26
+ /** Is open (controlled) */
27
+ isOpen?: boolean;
28
+ /** On open handler */
29
+ onOpen?: () => void;
30
+ /** On close handler */
31
+ onClose?: () => void;
32
+ /** Placement */
33
+ placement?: 'top' | 'bottom' | 'left' | 'right';
34
+ /** Close on click outside */
35
+ closeOnBlur?: boolean;
36
+ /** Show arrow */
37
+ hasArrow?: boolean;
38
+ }
39
+
40
+ export const Popover = forwardRef<View, PopoverProps>(
41
+ (
42
+ {
43
+ style,
44
+ trigger,
45
+ isOpen: controlledIsOpen,
46
+ onOpen,
47
+ onClose,
48
+ placement = 'bottom',
49
+ closeOnBlur = true,
50
+ hasArrow = true,
51
+ children,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
56
+ const [internalIsOpen, setInternalIsOpen] = useState(false);
57
+ const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
58
+ const triggerRef = useRef<View>(null);
59
+ const opacityAnim = useRef(new Animated.Value(0)).current;
60
+ const scaleAnim = useRef(new Animated.Value(0.95)).current;
61
+
62
+ const isControlled = controlledIsOpen !== undefined;
63
+ const isOpen = isControlled ? controlledIsOpen : internalIsOpen;
64
+
65
+ // Measure trigger position when opening
66
+ const measureTrigger = useCallback((): Promise<LayoutRectangle> => {
67
+ return new Promise((resolve) => {
68
+ if (Platform.OS === 'web') {
69
+ // On web, try to access DOM element directly
70
+ const node = triggerRef.current as any;
71
+ if (node) {
72
+ // In React Native Web, the ref is the DOM element
73
+ try {
74
+ const rect = node.getBoundingClientRect?.() ||
75
+ node._nativeTag?.getBoundingClientRect?.();
76
+ if (rect) {
77
+ resolve({ x: rect.left, y: rect.top, width: rect.width, height: rect.height });
78
+ return;
79
+ }
80
+ } catch (e) {
81
+ // Fall through to measure
82
+ }
83
+
84
+ // Fallback to measure
85
+ node.measure?.((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
86
+ resolve({ x: pageX || 0, y: pageY || 0, width: width || 100, height: height || 40 });
87
+ });
88
+
89
+ // If measure doesn't callback, resolve with defaults after timeout
90
+ setTimeout(() => resolve({ x: 0, y: 0, width: 100, height: 40 }), 100);
91
+ } else {
92
+ resolve({ x: 0, y: 0, width: 100, height: 40 });
93
+ }
94
+ } else {
95
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
96
+ resolve({ x: x || 0, y: y || 0, width: width || 100, height: height || 40 });
97
+ });
98
+ }
99
+ });
100
+ }, []);
101
+
102
+ const handleOpen = useCallback(() => {
103
+ measureTrigger().then((layout) => {
104
+ setTriggerLayout(layout);
105
+
106
+ if (!isControlled) {
107
+ setInternalIsOpen(true);
108
+ }
109
+ onOpen?.();
110
+
111
+ Animated.parallel([
112
+ Animated.timing(opacityAnim, {
113
+ toValue: 1,
114
+ duration: 150,
115
+ useNativeDriver: true,
116
+ }),
117
+ Animated.spring(scaleAnim, {
118
+ toValue: 1,
119
+ useNativeDriver: true,
120
+ tension: 100,
121
+ friction: 8,
122
+ }),
123
+ ]).start();
124
+ });
125
+ }, [isControlled, onOpen, measureTrigger]);
126
+
127
+ const handleClose = useCallback(() => {
128
+ Animated.parallel([
129
+ Animated.timing(opacityAnim, {
130
+ toValue: 0,
131
+ duration: 100,
132
+ useNativeDriver: true,
133
+ }),
134
+ Animated.timing(scaleAnim, {
135
+ toValue: 0.95,
136
+ duration: 100,
137
+ useNativeDriver: true,
138
+ }),
139
+ ]).start(() => {
140
+ if (!isControlled) {
141
+ setInternalIsOpen(false);
142
+ }
143
+ onClose?.();
144
+ });
145
+ }, [isControlled, onClose]);
146
+
147
+ const handleToggle = () => {
148
+ if (isOpen) {
149
+ handleClose();
150
+ } else {
151
+ handleOpen();
152
+ }
153
+ };
154
+
155
+ const getPopoverPosition = () => {
156
+ if (!triggerLayout) return {};
157
+
158
+ const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = getScreenDimensions();
159
+ const offset = 8;
160
+
161
+ switch (placement) {
162
+ case 'top':
163
+ return {
164
+ bottom: SCREEN_HEIGHT - triggerLayout.y + offset,
165
+ left: triggerLayout.x,
166
+ minWidth: triggerLayout.width,
167
+ };
168
+ case 'bottom':
169
+ return {
170
+ top: triggerLayout.y + triggerLayout.height + offset,
171
+ left: triggerLayout.x,
172
+ minWidth: triggerLayout.width,
173
+ };
174
+ case 'left':
175
+ return {
176
+ top: triggerLayout.y,
177
+ right: SCREEN_WIDTH - triggerLayout.x + offset,
178
+ };
179
+ case 'right':
180
+ return {
181
+ top: triggerLayout.y,
182
+ left: triggerLayout.x + triggerLayout.width + offset,
183
+ };
184
+ default:
185
+ return {};
186
+ }
187
+ };
188
+
189
+ // Clone trigger and inject onPress handler
190
+ const triggerWithHandler = React.cloneElement(trigger, {
191
+ onPress: (e: any) => {
192
+ // Call original onPress if exists
193
+ (trigger.props as any)?.onPress?.(e);
194
+ // Then toggle popover
195
+ handleToggle();
196
+ },
197
+ } as any);
198
+
199
+ return (
200
+ <View ref={ref} {...props}>
201
+ <View ref={triggerRef} collapsable={false}>
202
+ {triggerWithHandler}
203
+ </View>
204
+ <Modal
205
+ visible={isOpen}
206
+ transparent
207
+ animationType="none"
208
+ onRequestClose={handleClose}
209
+ >
210
+ <Pressable
211
+ style={styles.overlay}
212
+ onPress={closeOnBlur ? handleClose : undefined}
213
+ >
214
+ <Animated.View
215
+ style={[
216
+ styles.popover,
217
+ getPopoverPosition(),
218
+ {
219
+ opacity: opacityAnim,
220
+ transform: [{ scale: scaleAnim }],
221
+ },
222
+ style,
223
+ ]}
224
+ >
225
+ <Pressable>
226
+ {children}
227
+ </Pressable>
228
+ </Animated.View>
229
+ </Pressable>
230
+ </Modal>
231
+ </View>
232
+ );
233
+ }
234
+ );
235
+
236
+ Popover.displayName = 'Popover';
237
+
238
+ export interface PopoverHeaderProps extends ViewProps {}
239
+ export interface PopoverBodyProps extends ViewProps {}
240
+ export interface PopoverFooterProps extends ViewProps {}
241
+
242
+ export const PopoverHeader = forwardRef<View, PopoverHeaderProps>(
243
+ ({ style, children, ...props }, ref) => {
244
+ return (
245
+ <View ref={ref} style={[styles.header, style]} {...props}>
246
+ {typeof children === 'string' ? (
247
+ <Text style={styles.headerText}>{children}</Text>
248
+ ) : (
249
+ children
250
+ )}
251
+ </View>
252
+ );
253
+ }
254
+ );
255
+
256
+ PopoverHeader.displayName = 'PopoverHeader';
257
+
258
+ export const PopoverBody = forwardRef<View, PopoverBodyProps>(
259
+ ({ style, children, ...props }, ref) => {
260
+ return (
261
+ <View ref={ref} style={[styles.body, style]} {...props}>
262
+ {children}
263
+ </View>
264
+ );
265
+ }
266
+ );
267
+
268
+ PopoverBody.displayName = 'PopoverBody';
269
+
270
+ export const PopoverFooter = forwardRef<View, PopoverFooterProps>(
271
+ ({ style, children, ...props }, ref) => {
272
+ return (
273
+ <View ref={ref} style={[styles.footer, style]} {...props}>
274
+ {children}
275
+ </View>
276
+ );
277
+ }
278
+ );
279
+
280
+ PopoverFooter.displayName = 'PopoverFooter';
281
+
282
+ const styles = StyleSheet.create({
283
+ overlay: {
284
+ flex: 1,
285
+ },
286
+ popover: {
287
+ position: 'absolute',
288
+ backgroundColor: colors.background.default,
289
+ borderRadius: borderRadius.lg,
290
+ minWidth: 200,
291
+ maxWidth: 320,
292
+ ...elevation['30'],
293
+ },
294
+ header: {
295
+ padding: spacing['3x'],
296
+ borderBottomWidth: 1,
297
+ borderBottomColor: colors.border.disabled,
298
+ },
299
+ headerText: {
300
+ fontSize: typography.fontSize.body,
301
+ fontWeight: typography.fontWeight.semiBold,
302
+ color: colors.text.default,
303
+ },
304
+ body: {
305
+ padding: spacing['3x'],
306
+ },
307
+ footer: {
308
+ flexDirection: 'row',
309
+ justifyContent: 'flex-end',
310
+ padding: spacing['3x'],
311
+ borderTopWidth: 1,
312
+ borderTopColor: colors.border.disabled,
313
+ gap: spacing['2x'],
314
+ },
315
+ });
@@ -0,0 +1,11 @@
1
+ export {
2
+ Popover,
3
+ PopoverHeader,
4
+ PopoverBody,
5
+ PopoverFooter,
6
+ type PopoverProps,
7
+ type PopoverHeaderProps,
8
+ type PopoverBodyProps,
9
+ type PopoverFooterProps,
10
+ } from './Popover';
11
+