@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.
- package/.cursor/rules +313 -0
- package/.rnstorybook/index.ts +11 -0
- package/.rnstorybook/main.ts +8 -0
- package/.rnstorybook/preview.tsx +14 -0
- package/.rnstorybook/storybook.requires.ts +49 -0
- package/.storybook/main.ts +16 -0
- package/.storybook/preview.ts +32 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/App.tsx +422 -0
- package/README.md +229 -0
- package/app.json +33 -0
- package/assets/adaptive-icon.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/splash-icon.png +0 -0
- package/babel.config.js +16 -0
- package/docs/components/CodeBlock.tsx +80 -0
- package/docs/components/PropTable.tsx +93 -0
- package/docs/components/Sidebar.tsx +199 -0
- package/docs/components/index.ts +8 -0
- package/docs/data/colorTokens.ts +70 -0
- package/docs/data/componentData.tsx +1685 -0
- package/docs/data/index.ts +7 -0
- package/docs/index.ts +19 -0
- package/docs/navigation.ts +94 -0
- package/docs/pages/ColorsPage.tsx +226 -0
- package/docs/pages/ComponentPage.tsx +235 -0
- package/docs/pages/InstallationPage.tsx +232 -0
- package/docs/pages/IntroductionPage.tsx +163 -0
- package/docs/pages/ThemingPage.tsx +251 -0
- package/docs/pages/index.ts +10 -0
- package/docs/theme.ts +64 -0
- package/docs/types.ts +54 -0
- package/index.ts +8 -0
- package/llms.txt +1893 -0
- package/mcp-config.example.json +10 -0
- package/mcp-server/README.md +192 -0
- package/mcp-server/package-lock.json +1707 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/src/index.ts +1136 -0
- package/mcp-server/src/registry/components.ts +1446 -0
- package/mcp-server/src/registry/index.ts +3 -0
- package/mcp-server/src/registry/tokens.ts +256 -0
- package/mcp-server/tsconfig.json +19 -0
- package/package.json +92 -0
- package/src/components/Accordion/Accordion.stories.tsx +226 -0
- package/src/components/Accordion/Accordion.tsx +255 -0
- package/src/components/Accordion/index.ts +12 -0
- package/src/components/ActionSheet/ActionSheet.stories.tsx +393 -0
- package/src/components/ActionSheet/ActionSheet.tsx +258 -0
- package/src/components/ActionSheet/index.ts +2 -0
- package/src/components/Alert/Alert.stories.tsx +165 -0
- package/src/components/Alert/Alert.tsx +164 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/AlertDialog/AlertDialog.stories.tsx +330 -0
- package/src/components/AlertDialog/AlertDialog.tsx +234 -0
- package/src/components/AlertDialog/index.ts +2 -0
- package/src/components/Avatar/Avatar.stories.tsx +154 -0
- package/src/components/Avatar/Avatar.tsx +219 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.stories.tsx +146 -0
- package/src/components/Badge/Badge.tsx +125 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Box/Box.stories.tsx +192 -0
- package/src/components/Box/Box.tsx +184 -0
- package/src/components/Box/index.ts +2 -0
- package/src/components/Button/Button.stories.tsx +157 -0
- package/src/components/Button/Button.tsx +180 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Card/Card.stories.tsx +145 -0
- package/src/components/Card/Card.tsx +169 -0
- package/src/components/Card/index.ts +11 -0
- package/src/components/Center/Center.stories.tsx +215 -0
- package/src/components/Center/Center.tsx +29 -0
- package/src/components/Center/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +94 -0
- package/src/components/Checkbox/Checkbox.tsx +242 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +623 -0
- package/src/components/DatePicker/DatePicker.tsx +1228 -0
- package/src/components/DatePicker/index.ts +8 -0
- package/src/components/Divider/Divider.stories.tsx +224 -0
- package/src/components/Divider/Divider.tsx +73 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/Drawer/Drawer.stories.tsx +414 -0
- package/src/components/Drawer/Drawer.tsx +342 -0
- package/src/components/Drawer/index.ts +11 -0
- package/src/components/Fab/Fab.stories.tsx +360 -0
- package/src/components/Fab/Fab.tsx +185 -0
- package/src/components/Fab/index.ts +2 -0
- package/src/components/FormControl/FormControl.stories.tsx +276 -0
- package/src/components/FormControl/FormControl.tsx +185 -0
- package/src/components/FormControl/index.ts +12 -0
- package/src/components/Grid/Grid.stories.tsx +244 -0
- package/src/components/Grid/Grid.tsx +93 -0
- package/src/components/Grid/index.ts +2 -0
- package/src/components/HStack/HStack.stories.tsx +230 -0
- package/src/components/HStack/HStack.tsx +80 -0
- package/src/components/HStack/index.ts +2 -0
- package/src/components/Heading/Heading.stories.tsx +111 -0
- package/src/components/Heading/Heading.tsx +85 -0
- package/src/components/Heading/index.ts +2 -0
- package/src/components/Icon/Icon.stories.tsx +320 -0
- package/src/components/Icon/Icon.tsx +117 -0
- package/src/components/Icon/index.ts +2 -0
- package/src/components/Image/Image.stories.tsx +357 -0
- package/src/components/Image/Image.tsx +168 -0
- package/src/components/Image/index.ts +2 -0
- package/src/components/Input/Input.stories.tsx +164 -0
- package/src/components/Input/Input.tsx +274 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Link/Link.stories.tsx +187 -0
- package/src/components/Link/Link.tsx +104 -0
- package/src/components/Link/index.ts +2 -0
- package/src/components/Menu/Menu.stories.tsx +363 -0
- package/src/components/Menu/Menu.tsx +238 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/Modal/Modal.stories.tsx +156 -0
- package/src/components/Modal/Modal.tsx +280 -0
- package/src/components/Modal/index.ts +11 -0
- package/src/components/Popover/Popover.stories.tsx +330 -0
- package/src/components/Popover/Popover.tsx +315 -0
- package/src/components/Popover/index.ts +11 -0
- package/src/components/Portal/Portal.stories.tsx +376 -0
- package/src/components/Portal/Portal.tsx +100 -0
- package/src/components/Portal/index.ts +2 -0
- package/src/components/Pressable/Pressable.stories.tsx +338 -0
- package/src/components/Pressable/Pressable.tsx +71 -0
- package/src/components/Pressable/index.ts +2 -0
- package/src/components/Progress/Progress.stories.tsx +131 -0
- package/src/components/Progress/Progress.tsx +219 -0
- package/src/components/Progress/index.ts +2 -0
- package/src/components/Radio/Radio.stories.tsx +101 -0
- package/src/components/Radio/Radio.tsx +234 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Select/Select.stories.tsx +908 -0
- package/src/components/Select/Select.tsx +659 -0
- package/src/components/Select/index.ts +8 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +154 -0
- package/src/components/Skeleton/Skeleton.tsx +192 -0
- package/src/components/Skeleton/index.ts +8 -0
- package/src/components/Slider/Slider.stories.tsx +363 -0
- package/src/components/Slider/Slider.tsx +209 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Spinner/Spinner.stories.tsx +108 -0
- package/src/components/Spinner/Spinner.tsx +121 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Switch/Switch.stories.tsx +116 -0
- package/src/components/Switch/Switch.tsx +172 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/Table.stories.tsx +417 -0
- package/src/components/Table/Table.tsx +233 -0
- package/src/components/Table/index.ts +2 -0
- package/src/components/Text/Text.stories.tsx +93 -0
- package/src/components/Text/Text.tsx +119 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.stories.tsx +280 -0
- package/src/components/Textarea/Textarea.tsx +212 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Toast/Toast.stories.tsx +446 -0
- package/src/components/Toast/Toast.tsx +221 -0
- package/src/components/Toast/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +354 -0
- package/src/components/Tooltip/Tooltip.tsx +261 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/VStack/VStack.stories.tsx +183 -0
- package/src/components/VStack/VStack.tsx +76 -0
- package/src/components/VStack/index.ts +2 -0
- package/src/components/index.ts +62 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useControllableState.ts +41 -0
- package/src/hooks/useDisclosure.ts +51 -0
- package/src/index.ts +22 -0
- package/src/stories/Button.stories.tsx +53 -0
- package/src/stories/Button.tsx +101 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/Header.stories.tsx +33 -0
- package/src/stories/Header.tsx +75 -0
- package/src/stories/Page.stories.tsx +25 -0
- package/src/stories/Page.tsx +154 -0
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +1 -0
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +1 -0
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +1 -0
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +1 -0
- package/src/stories/assets/youtube.svg +1 -0
- package/src/styles/index.ts +7 -0
- package/src/styles/tokens.ts +318 -0
- package/src/styles/unistyles.ts +254 -0
- package/src/utils/createContext.tsx +25 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/mergeRefs.ts +21 -0
- package/tsconfig.json +26 -0
- package/urbint-cl-1.0.0.tgz +0 -0
- package/vitest.config.ts +37 -0
- 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
|
+
});
|