@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,363 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Menu } from './Menu';
3
+ import { Button } from '../Button';
4
+ import { VStack } from '../VStack';
5
+ import { HStack } from '../HStack';
6
+ import { Text } from '../Text';
7
+ import { Box } from '../Box';
8
+ import Svg, { Path } from 'react-native-svg';
9
+ import { colors, spacing, borderRadius, elevation } from '../../styles/tokens';
10
+
11
+ const meta: Meta<typeof Menu> = {
12
+ title: 'Overlay/Menu',
13
+ component: Menu,
14
+ argTypes: {
15
+ placement: {
16
+ control: 'select',
17
+ options: ['bottom-start', 'bottom-end', 'top-start', 'top-end'],
18
+ },
19
+ closeOnSelect: { control: 'boolean' },
20
+ },
21
+ args: {
22
+ placement: 'bottom-start',
23
+ closeOnSelect: true,
24
+ },
25
+ };
26
+
27
+ export default meta;
28
+
29
+ type Story = StoryObj<typeof Menu>;
30
+
31
+ export const Default: Story = {
32
+ render: () => (
33
+ <Box p={50}>
34
+ <Menu
35
+ trigger={<Button>Open Menu</Button>}
36
+ items={[
37
+ { label: 'Edit', value: 'edit' },
38
+ { label: 'Duplicate', value: 'duplicate' },
39
+ { label: 'Delete', value: 'delete' },
40
+ ]}
41
+ onItemPress={(value) => console.log('Selected:', value)}
42
+ />
43
+ </Box>
44
+ ),
45
+ };
46
+
47
+ const PlacementDemo = () => (
48
+ <VStack space={spacing.lg}>
49
+ <Text weight="semiBold">Menu Placement</Text>
50
+ <HStack space={spacing.lg} wrap>
51
+ <Menu
52
+ trigger={<Button size="sm">Bottom Start</Button>}
53
+ placement="bottom-start"
54
+ items={[
55
+ { label: 'Item 1', value: '1' },
56
+ { label: 'Item 2', value: '2' },
57
+ ]}
58
+ onItemPress={(value) => console.log(value)}
59
+ />
60
+ <Menu
61
+ trigger={<Button size="sm">Bottom End</Button>}
62
+ placement="bottom-end"
63
+ items={[
64
+ { label: 'Item 1', value: '1' },
65
+ { label: 'Item 2', value: '2' },
66
+ ]}
67
+ onItemPress={(value) => console.log(value)}
68
+ />
69
+ </HStack>
70
+ </VStack>
71
+ );
72
+
73
+ export const Placement: Story = {
74
+ render: () => <PlacementDemo />,
75
+ };
76
+
77
+ const WithIcons = () => {
78
+ const EditIcon = (
79
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
80
+ <Path
81
+ d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
82
+ stroke={colors.text.secondary}
83
+ strokeWidth={2}
84
+ strokeLinecap="round"
85
+ strokeLinejoin="round"
86
+ />
87
+ </Svg>
88
+ );
89
+
90
+ const CopyIcon = (
91
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
92
+ <Path
93
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
94
+ stroke={colors.text.secondary}
95
+ strokeWidth={2}
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ />
99
+ </Svg>
100
+ );
101
+
102
+ const TrashIcon = (
103
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
104
+ <Path
105
+ d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
106
+ stroke={colors.text.secondary}
107
+ strokeWidth={2}
108
+ strokeLinecap="round"
109
+ strokeLinejoin="round"
110
+ />
111
+ </Svg>
112
+ );
113
+
114
+ return (
115
+ <VStack space={spacing.lg}>
116
+ <Text weight="semiBold">Menu with Icons</Text>
117
+ <Box p={50}>
118
+ <Menu
119
+ trigger={<Button>Actions</Button>}
120
+ items={[
121
+ { label: 'Edit', value: 'edit', icon: EditIcon },
122
+ { label: 'Duplicate', value: 'duplicate', icon: CopyIcon },
123
+ { label: 'Delete', value: 'delete', icon: TrashIcon },
124
+ ]}
125
+ onItemPress={(value) => console.log('Selected:', value)}
126
+ />
127
+ </Box>
128
+ </VStack>
129
+ );
130
+ };
131
+
132
+ export const Icons: Story = {
133
+ render: () => <WithIcons />,
134
+ };
135
+
136
+ const WithDivider = () => (
137
+ <VStack space={spacing.lg}>
138
+ <Text weight="semiBold">Menu with Dividers</Text>
139
+ <Box p={50}>
140
+ <Menu
141
+ trigger={<Button>File</Button>}
142
+ items={[
143
+ { label: 'New File', value: 'new' },
144
+ { label: 'Open', value: 'open' },
145
+ { label: 'Save', value: 'save' },
146
+ { label: '', value: 'divider1', isDivider: true },
147
+ { label: 'Export', value: 'export' },
148
+ { label: 'Print', value: 'print' },
149
+ { label: '', value: 'divider2', isDivider: true },
150
+ { label: 'Close', value: 'close' },
151
+ ]}
152
+ onItemPress={(value) => console.log('Selected:', value)}
153
+ />
154
+ </Box>
155
+ </VStack>
156
+ );
157
+
158
+ export const WithDividers: Story = {
159
+ render: () => <WithDivider />,
160
+ };
161
+
162
+ const DisabledItems = () => (
163
+ <VStack space={spacing.lg}>
164
+ <Text weight="semiBold">Menu with Disabled Items</Text>
165
+ <Box p={50}>
166
+ <Menu
167
+ trigger={<Button>Options</Button>}
168
+ items={[
169
+ { label: 'Available Option', value: 'available' },
170
+ { label: 'Disabled Option', value: 'disabled', isDisabled: true },
171
+ { label: 'Pro Feature (upgrade)', value: 'pro', isDisabled: true },
172
+ { label: 'Another Available', value: 'another' },
173
+ ]}
174
+ onItemPress={(value) => console.log('Selected:', value)}
175
+ />
176
+ </Box>
177
+ </VStack>
178
+ );
179
+
180
+ export const Disabled: Story = {
181
+ render: () => <DisabledItems />,
182
+ };
183
+
184
+ const ContextMenu = () => (
185
+ <VStack space={spacing.lg}>
186
+ <Text weight="semiBold">Context Menu Style</Text>
187
+ <Box p={50}>
188
+ <Menu
189
+ trigger={
190
+ <Box
191
+ p={spacing.md}
192
+ bg={colors.background.secondary}
193
+ rounded="md"
194
+ style={{ cursor: 'pointer' }}
195
+ >
196
+ <Text>Right-click or tap to open menu</Text>
197
+ </Box>
198
+ }
199
+ items={[
200
+ { label: 'Cut', value: 'cut' },
201
+ { label: 'Copy', value: 'copy' },
202
+ { label: 'Paste', value: 'paste' },
203
+ { label: '', value: 'divider', isDivider: true },
204
+ { label: 'Select All', value: 'select_all' },
205
+ ]}
206
+ onItemPress={(value) => console.log('Selected:', value)}
207
+ />
208
+ </Box>
209
+ </VStack>
210
+ );
211
+
212
+ export const Context: Story = {
213
+ render: () => <ContextMenu />,
214
+ };
215
+
216
+ const DropdownNavigation = () => (
217
+ <VStack space={spacing.lg}>
218
+ <Text weight="semiBold">Navigation Dropdown</Text>
219
+ <Box p={spacing.lg} bg={colors.white} rounded="lg" shadow="10">
220
+ <HStack space={spacing.lg}>
221
+ <Text>Home</Text>
222
+ <Menu
223
+ trigger={
224
+ <HStack space={spacing.xs} alignItems="center">
225
+ <Text>Products</Text>
226
+ <Text variant="small">▼</Text>
227
+ </HStack>
228
+ }
229
+ items={[
230
+ { label: 'All Products', value: 'all' },
231
+ { label: 'Electronics', value: 'electronics' },
232
+ { label: 'Clothing', value: 'clothing' },
233
+ { label: 'Home & Garden', value: 'home' },
234
+ ]}
235
+ onItemPress={(value) => console.log('Navigate to:', value)}
236
+ />
237
+ <Menu
238
+ trigger={
239
+ <HStack space={spacing.xs} alignItems="center">
240
+ <Text>Services</Text>
241
+ <Text variant="small">▼</Text>
242
+ </HStack>
243
+ }
244
+ items={[
245
+ { label: 'Consulting', value: 'consulting' },
246
+ { label: 'Development', value: 'development' },
247
+ { label: 'Support', value: 'support' },
248
+ ]}
249
+ onItemPress={(value) => console.log('Navigate to:', value)}
250
+ />
251
+ <Text>Contact</Text>
252
+ </HStack>
253
+ </Box>
254
+ </VStack>
255
+ );
256
+
257
+ export const Navigation: Story = {
258
+ render: () => <DropdownNavigation />,
259
+ };
260
+
261
+ const UserMenu = () => (
262
+ <VStack space={spacing.lg}>
263
+ <Text weight="semiBold">User Menu</Text>
264
+ <Box p={50}>
265
+ <Menu
266
+ trigger={
267
+ <HStack space={spacing.sm} alignItems="center" style={{ cursor: 'pointer' }}>
268
+ <Box
269
+ w={36}
270
+ h={36}
271
+ rounded="full"
272
+ bg={colors.brand.blue + '30'}
273
+ alignItems="center"
274
+ justifyContent="center"
275
+ >
276
+ <Text weight="semiBold">JD</Text>
277
+ </Box>
278
+ <Text>▼</Text>
279
+ </HStack>
280
+ }
281
+ placement="bottom-end"
282
+ items={[
283
+ { label: 'Profile', value: 'profile' },
284
+ { label: 'Settings', value: 'settings' },
285
+ { label: 'Notifications', value: 'notifications' },
286
+ { label: '', value: 'divider', isDivider: true },
287
+ { label: 'Help', value: 'help' },
288
+ { label: 'Sign Out', value: 'signout' },
289
+ ]}
290
+ onItemPress={(value) => console.log('Selected:', value)}
291
+ />
292
+ </Box>
293
+ </VStack>
294
+ );
295
+
296
+ export const User: Story = {
297
+ render: () => <UserMenu />,
298
+ };
299
+
300
+ const TableRowMenu = () => (
301
+ <VStack space={spacing.lg}>
302
+ <Text weight="semiBold">Table Row Actions</Text>
303
+ <Box p={spacing.lg} bg={colors.background.secondary} rounded="lg">
304
+ {['Project Alpha', 'Project Beta', 'Project Gamma'].map((project) => (
305
+ <HStack key={project} justifyContent="space-between" alignItems="center" py={spacing.md}>
306
+ <Text>{project}</Text>
307
+ <Menu
308
+ trigger={
309
+ <Box p={spacing.sm} rounded="md" bg={colors.border.default}>
310
+ <Text>⋮</Text>
311
+ </Box>
312
+ }
313
+ placement="bottom-end"
314
+ items={[
315
+ { label: 'View', value: 'view' },
316
+ { label: 'Edit', value: 'edit' },
317
+ { label: 'Share', value: 'share' },
318
+ { label: '', value: 'divider', isDivider: true },
319
+ { label: 'Archive', value: 'archive' },
320
+ { label: 'Delete', value: 'delete' },
321
+ ]}
322
+ onItemPress={(value) => console.log(`${value} ${project}`)}
323
+ />
324
+ </HStack>
325
+ ))}
326
+ </Box>
327
+ </VStack>
328
+ );
329
+
330
+ export const TableRow: Story = {
331
+ render: () => <TableRowMenu />,
332
+ };
333
+
334
+ const SortMenu = () => (
335
+ <VStack space={spacing.lg}>
336
+ <Text weight="semiBold">Sort Menu</Text>
337
+ <Box p={50}>
338
+ <Menu
339
+ trigger={
340
+ <Button variant="outline" size="sm">
341
+ Sort by ▼
342
+ </Button>
343
+ }
344
+ items={[
345
+ { label: 'Name (A-Z)', value: 'name_asc' },
346
+ { label: 'Name (Z-A)', value: 'name_desc' },
347
+ { label: '', value: 'divider', isDivider: true },
348
+ { label: 'Date (Newest)', value: 'date_desc' },
349
+ { label: 'Date (Oldest)', value: 'date_asc' },
350
+ { label: '', value: 'divider2', isDivider: true },
351
+ { label: 'Size (Largest)', value: 'size_desc' },
352
+ { label: 'Size (Smallest)', value: 'size_asc' },
353
+ ]}
354
+ onItemPress={(value) => console.log('Sort by:', value)}
355
+ />
356
+ </Box>
357
+ </VStack>
358
+ );
359
+
360
+ export const Sort: Story = {
361
+ render: () => <SortMenu />,
362
+ };
363
+
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Menu Component
3
+ * Dropdown menu for actions
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
+ } from 'react-native';
18
+ import { colors, spacing, borderRadius, typography, elevation } from '../../styles/tokens';
19
+
20
+ const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window');
21
+
22
+ export interface MenuItemType {
23
+ label: string;
24
+ value: string;
25
+ icon?: React.ReactNode;
26
+ isDisabled?: boolean;
27
+ isDivider?: boolean;
28
+ }
29
+
30
+ export interface MenuProps extends ViewProps {
31
+ /** Menu trigger element */
32
+ trigger: React.ReactElement;
33
+ /** Menu items */
34
+ items: MenuItemType[];
35
+ /** On item press handler */
36
+ onItemPress?: (value: string) => void;
37
+ /** Placement */
38
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
39
+ /** Close on item press */
40
+ closeOnSelect?: boolean;
41
+ }
42
+
43
+ export const Menu = forwardRef<View, MenuProps>(
44
+ (
45
+ {
46
+ style,
47
+ trigger,
48
+ items,
49
+ onItemPress,
50
+ placement = 'bottom-start',
51
+ closeOnSelect = true,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
56
+ const [isOpen, setIsOpen] = 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 handleTriggerPress = useCallback(() => {
63
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
64
+ setTriggerLayout({ x, y, width, height });
65
+ setIsOpen(true);
66
+ Animated.parallel([
67
+ Animated.timing(opacityAnim, {
68
+ toValue: 1,
69
+ duration: 150,
70
+ useNativeDriver: true,
71
+ }),
72
+ Animated.spring(scaleAnim, {
73
+ toValue: 1,
74
+ useNativeDriver: true,
75
+ tension: 100,
76
+ friction: 8,
77
+ }),
78
+ ]).start();
79
+ });
80
+ }, []);
81
+
82
+ const handleClose = useCallback(() => {
83
+ Animated.parallel([
84
+ Animated.timing(opacityAnim, {
85
+ toValue: 0,
86
+ duration: 100,
87
+ useNativeDriver: true,
88
+ }),
89
+ Animated.timing(scaleAnim, {
90
+ toValue: 0.95,
91
+ duration: 100,
92
+ useNativeDriver: true,
93
+ }),
94
+ ]).start(() => {
95
+ setIsOpen(false);
96
+ });
97
+ }, []);
98
+
99
+ const handleItemPress = (item: MenuItemType) => {
100
+ if (item.isDisabled || item.isDivider) return;
101
+ onItemPress?.(item.value);
102
+ if (closeOnSelect) {
103
+ handleClose();
104
+ }
105
+ };
106
+
107
+ const getMenuPosition = () => {
108
+ if (!triggerLayout) return {};
109
+
110
+ const menuWidth = 200;
111
+ const menuMaxHeight = 300;
112
+
113
+ let top = triggerLayout.y + triggerLayout.height + 4;
114
+ let left = triggerLayout.x;
115
+
116
+ if (placement.includes('end')) {
117
+ left = triggerLayout.x + triggerLayout.width - menuWidth;
118
+ }
119
+
120
+ if (placement.includes('top')) {
121
+ top = triggerLayout.y - menuMaxHeight - 4;
122
+ }
123
+
124
+ if (left + menuWidth > SCREEN_WIDTH) {
125
+ left = SCREEN_WIDTH - menuWidth - 16;
126
+ }
127
+ if (left < 16) {
128
+ left = 16;
129
+ }
130
+
131
+ return { top, left, minWidth: Math.min(triggerLayout.width, menuWidth) };
132
+ };
133
+
134
+ // Clone trigger and inject onPress handler
135
+ const triggerWithHandler = React.cloneElement(trigger, {
136
+ onPress: (e: any) => {
137
+ // Call original onPress if exists
138
+ (trigger.props as any)?.onPress?.(e);
139
+ // Then open menu
140
+ handleTriggerPress();
141
+ },
142
+ } as any);
143
+
144
+ return (
145
+ <View ref={ref} {...props}>
146
+ <View ref={triggerRef} collapsable={false}>
147
+ {triggerWithHandler}
148
+ </View>
149
+ <Modal
150
+ visible={isOpen}
151
+ transparent
152
+ animationType="none"
153
+ onRequestClose={handleClose}
154
+ >
155
+ <Pressable style={styles.overlay} onPress={handleClose}>
156
+ <Animated.View
157
+ style={[
158
+ styles.menu,
159
+ getMenuPosition(),
160
+ {
161
+ opacity: opacityAnim,
162
+ transform: [{ scale: scaleAnim }],
163
+ },
164
+ style,
165
+ ]}
166
+ >
167
+ {items.map((item, index) => {
168
+ if (item.isDivider) {
169
+ return <View key={`divider-${index}`} style={styles.divider} />;
170
+ }
171
+
172
+ return (
173
+ <Pressable
174
+ key={item.value}
175
+ onPress={() => handleItemPress(item)}
176
+ disabled={item.isDisabled}
177
+ style={[styles.menuItem, item.isDisabled && styles.menuItemDisabled]}
178
+ >
179
+ {item.icon && <View style={styles.menuItemIcon}>{item.icon}</View>}
180
+ <Text
181
+ style={[
182
+ styles.menuItemLabel,
183
+ item.isDisabled && styles.menuItemLabelDisabled,
184
+ ]}
185
+ >
186
+ {item.label}
187
+ </Text>
188
+ </Pressable>
189
+ );
190
+ })}
191
+ </Animated.View>
192
+ </Pressable>
193
+ </Modal>
194
+ </View>
195
+ );
196
+ }
197
+ );
198
+
199
+ Menu.displayName = 'Menu';
200
+
201
+ const styles = StyleSheet.create({
202
+ overlay: {
203
+ flex: 1,
204
+ },
205
+ menu: {
206
+ position: 'absolute',
207
+ backgroundColor: colors.background.default,
208
+ borderRadius: borderRadius.lg,
209
+ minWidth: 160,
210
+ maxWidth: 280,
211
+ paddingVertical: spacing.base,
212
+ ...elevation['30'],
213
+ },
214
+ menuItem: {
215
+ flexDirection: 'row',
216
+ alignItems: 'center',
217
+ paddingVertical: spacing['3x'],
218
+ paddingHorizontal: spacing['3x'],
219
+ },
220
+ menuItemDisabled: {
221
+ opacity: 0.5,
222
+ },
223
+ menuItemIcon: {
224
+ marginRight: spacing['3x'],
225
+ },
226
+ menuItemLabel: {
227
+ fontSize: typography.fontSize.body,
228
+ color: colors.text.default,
229
+ },
230
+ menuItemLabelDisabled: {
231
+ color: colors.text.disabled,
232
+ },
233
+ divider: {
234
+ height: 1,
235
+ backgroundColor: colors.border.disabled,
236
+ marginVertical: spacing.base,
237
+ },
238
+ });
@@ -0,0 +1,2 @@
1
+ export { Menu, type MenuProps, type MenuItemType } from './Menu';
2
+