@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,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
|
+
});
|