@idealyst/components 1.0.41 → 1.0.44

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.
@@ -0,0 +1,287 @@
1
+ import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { getWebProps } from 'react-native-unistyles/web';
4
+ import { PopoverProps, PopoverPlacement } from './types';
5
+ import { popoverStyles } from './Popover.styles';
6
+
7
+ interface PopoverPosition {
8
+ top: number;
9
+ left: number;
10
+ placement: PopoverPlacement;
11
+ }
12
+
13
+ const calculatePosition = (
14
+ anchorRect: DOMRect,
15
+ popoverSize: { width: number; height: number },
16
+ placement: PopoverPlacement,
17
+ offset: number,
18
+ showArrow: boolean = false
19
+ ): PopoverPosition => {
20
+ const viewport = {
21
+ width: window.innerWidth,
22
+ height: window.innerHeight,
23
+ scrollX: window.scrollX,
24
+ scrollY: window.scrollY,
25
+ };
26
+
27
+ let position = { top: 0, left: 0 };
28
+ let finalPlacement = placement;
29
+
30
+ // Add extra offset for arrow
31
+ const arrowSize = 6;
32
+ const finalOffset = showArrow ? offset + arrowSize : offset;
33
+
34
+ // Calculate initial position based on placement
35
+ switch (placement) {
36
+ case 'top':
37
+ position = {
38
+ top: anchorRect.top + viewport.scrollY - popoverSize.height - finalOffset,
39
+ left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - popoverSize.width / 2,
40
+ };
41
+ break;
42
+ case 'top-start':
43
+ position = {
44
+ top: anchorRect.top + viewport.scrollY - popoverSize.height - finalOffset,
45
+ left: anchorRect.left + viewport.scrollX,
46
+ };
47
+ break;
48
+ case 'top-end':
49
+ position = {
50
+ top: anchorRect.top + viewport.scrollY - popoverSize.height - finalOffset,
51
+ left: anchorRect.right + viewport.scrollX - popoverSize.width,
52
+ };
53
+ break;
54
+ case 'bottom':
55
+ position = {
56
+ top: anchorRect.bottom + viewport.scrollY + finalOffset,
57
+ left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - popoverSize.width / 2,
58
+ };
59
+ break;
60
+ case 'bottom-start':
61
+ position = {
62
+ top: anchorRect.bottom + viewport.scrollY + finalOffset,
63
+ left: anchorRect.left + viewport.scrollX,
64
+ };
65
+ break;
66
+ case 'bottom-end':
67
+ position = {
68
+ top: anchorRect.bottom + viewport.scrollY + finalOffset,
69
+ left: anchorRect.right + viewport.scrollX - popoverSize.width,
70
+ };
71
+ break;
72
+ case 'left':
73
+ position = {
74
+ top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - popoverSize.height / 2,
75
+ left: anchorRect.left + viewport.scrollX - popoverSize.width - finalOffset,
76
+ };
77
+ break;
78
+ case 'left-start':
79
+ position = {
80
+ top: anchorRect.top + viewport.scrollY,
81
+ left: anchorRect.left + viewport.scrollX - popoverSize.width - finalOffset,
82
+ };
83
+ break;
84
+ case 'left-end':
85
+ position = {
86
+ top: anchorRect.bottom + viewport.scrollY - popoverSize.height,
87
+ left: anchorRect.left + viewport.scrollX - popoverSize.width - finalOffset,
88
+ };
89
+ break;
90
+ case 'right':
91
+ position = {
92
+ top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - popoverSize.height / 2,
93
+ left: anchorRect.right + viewport.scrollX + finalOffset,
94
+ };
95
+ break;
96
+ case 'right-start':
97
+ position = {
98
+ top: anchorRect.top + viewport.scrollY,
99
+ left: anchorRect.right + viewport.scrollX + finalOffset,
100
+ };
101
+ break;
102
+ case 'right-end':
103
+ position = {
104
+ top: anchorRect.bottom + viewport.scrollY - popoverSize.height,
105
+ left: anchorRect.right + viewport.scrollX + finalOffset,
106
+ };
107
+ break;
108
+ }
109
+
110
+ // Constrain to viewport
111
+ const padding = 8;
112
+ position.left = Math.max(padding, Math.min(position.left, viewport.width - popoverSize.width - padding));
113
+ position.top = Math.max(padding, Math.min(position.top, viewport.height + viewport.scrollY - popoverSize.height - padding));
114
+
115
+ return { ...position, placement: finalPlacement };
116
+ };
117
+
118
+ const Popover: React.FC<PopoverProps> = ({
119
+ open,
120
+ onOpenChange,
121
+ anchor,
122
+ children,
123
+ placement = 'bottom',
124
+ offset = 8,
125
+ closeOnClickOutside = true,
126
+ closeOnEscapeKey = true,
127
+ showArrow = false,
128
+ testID,
129
+ }) => {
130
+ const popoverRef = useRef<HTMLDivElement>(null);
131
+ const [isVisible, setIsVisible] = useState(false);
132
+ const [shouldRender, setShouldRender] = useState(false);
133
+ const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0, placement });
134
+
135
+ // Calculate position
136
+ const updatePosition = useCallback(() => {
137
+ if (!popoverRef.current) {
138
+ return;
139
+ }
140
+
141
+ let anchorElement: Element | null = null;
142
+
143
+ if (anchor && typeof anchor === 'object' && 'current' in anchor && anchor.current) {
144
+ anchorElement = anchor.current;
145
+ } else if (React.isValidElement(anchor)) {
146
+ console.warn('Popover: React element anchors need to be refs for positioning');
147
+ return;
148
+ }
149
+
150
+ if (!anchorElement) {
151
+ return;
152
+ }
153
+
154
+ const anchorRect = anchorElement.getBoundingClientRect();
155
+ const popoverRect = popoverRef.current.getBoundingClientRect();
156
+
157
+ const newPosition = calculatePosition(
158
+ anchorRect,
159
+ { width: popoverRect.width || 200, height: popoverRect.height || 100 },
160
+ placement,
161
+ offset,
162
+ showArrow
163
+ );
164
+
165
+ setPosition(newPosition);
166
+ }, [anchor, placement, offset, showArrow]);
167
+
168
+ // Handle mounting/unmounting with animation
169
+ useEffect(() => {
170
+ if (open && !shouldRender) {
171
+ setShouldRender(true);
172
+ // Set visible immediately to render the DOM element
173
+ setIsVisible(true);
174
+ } else if (!open && shouldRender) {
175
+ setIsVisible(false);
176
+ const timer = setTimeout(() => {
177
+ setShouldRender(false);
178
+ }, 150);
179
+ return () => clearTimeout(timer);
180
+ }
181
+ }, [open, shouldRender]);
182
+
183
+ // Position calculation after DOM is ready
184
+ useLayoutEffect(() => {
185
+ if (shouldRender && isVisible) {
186
+ // Use a microtask to ensure the ref is attached
187
+ Promise.resolve().then(() => {
188
+ if (popoverRef.current) {
189
+ updatePosition();
190
+ }
191
+ });
192
+ }
193
+ }, [shouldRender, isVisible, anchor, placement, offset, showArrow]);
194
+
195
+ // Update position on scroll/resize
196
+ useEffect(() => {
197
+ if (shouldRender && isVisible) {
198
+ const handleResize = () => updatePosition();
199
+ const handleScroll = () => updatePosition();
200
+
201
+ window.addEventListener('resize', handleResize);
202
+ window.addEventListener('scroll', handleScroll, true);
203
+
204
+ return () => {
205
+ window.removeEventListener('resize', handleResize);
206
+ window.removeEventListener('scroll', handleScroll, true);
207
+ };
208
+ }
209
+ }, [updatePosition, shouldRender, isVisible]);
210
+
211
+ // Handle escape key
212
+ useEffect(() => {
213
+ if (!open || !closeOnEscapeKey) return;
214
+
215
+ const handleEscape = (event: KeyboardEvent) => {
216
+ if (event.key === 'Escape') {
217
+ onOpenChange(false);
218
+ }
219
+ };
220
+
221
+ document.addEventListener('keydown', handleEscape);
222
+ return () => document.removeEventListener('keydown', handleEscape);
223
+ }, [open, closeOnEscapeKey, onOpenChange]);
224
+
225
+ // Handle click outside
226
+ useEffect(() => {
227
+ if (!open || !closeOnClickOutside) return;
228
+
229
+ const handleClickOutside = (event: MouseEvent) => {
230
+ if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
231
+ // Check if click was on anchor element
232
+ let anchorElement: Element | null = null;
233
+ if (anchor && typeof anchor === 'object' && 'current' in anchor && anchor.current) {
234
+ anchorElement = anchor.current;
235
+ }
236
+
237
+ if (anchorElement && anchorElement.contains(event.target as Node)) {
238
+ return; // Don't close if clicked on anchor
239
+ }
240
+
241
+ onOpenChange(false);
242
+ }
243
+ };
244
+
245
+ document.addEventListener('mousedown', handleClickOutside);
246
+ return () => document.removeEventListener('mousedown', handleClickOutside);
247
+ }, [open, closeOnClickOutside, onOpenChange, anchor]);
248
+
249
+ if (!shouldRender) return null;
250
+
251
+ // Use Unistyles with wrapper approach
252
+ popoverStyles.useVariants({});
253
+
254
+ const containerProps = getWebProps([
255
+ popoverStyles.container,
256
+ {
257
+ opacity: isVisible ? 1 : 0,
258
+ transform: isVisible ? 'scale(1)' : 'scale(0.95)',
259
+ }
260
+ ]);
261
+ const contentProps = getWebProps([popoverStyles.content]);
262
+
263
+ console.log(position)
264
+
265
+ const popoverContent = (
266
+ <div
267
+ ref={popoverRef}
268
+ style={{
269
+ position: 'fixed',
270
+ zIndex: 9999,
271
+ top: position.top,
272
+ left: position.left,
273
+ }}
274
+ data-testid={testID}
275
+ >
276
+ <div {...containerProps}>
277
+ <div {...contentProps}>
278
+ {children}
279
+ </div>
280
+ </div>
281
+ </div>
282
+ );
283
+
284
+ return createPortal(popoverContent, document.body);
285
+ };
286
+
287
+ export default Popover;
@@ -0,0 +1,2 @@
1
+ export { default } from './Popover.native';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { default } from './Popover.web';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { default } from './Popover.web';
2
+ export * from './types';
@@ -0,0 +1,65 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export type PopoverPlacement =
4
+ | 'top' | 'top-start' | 'top-end'
5
+ | 'bottom' | 'bottom-start' | 'bottom-end'
6
+ | 'left' | 'left-start' | 'left-end'
7
+ | 'right' | 'right-start' | 'right-end';
8
+
9
+ export interface PopoverProps {
10
+ /**
11
+ * Whether the popover is open/visible
12
+ */
13
+ open: boolean;
14
+
15
+ /**
16
+ * Called when the popover should be opened or closed
17
+ */
18
+ onOpenChange: (open: boolean) => void;
19
+
20
+ /**
21
+ * The anchor element to position the popover relative to
22
+ * Can be a React element or a ref to a DOM element
23
+ */
24
+ anchor: ReactNode | React.RefObject<Element>;
25
+
26
+ /**
27
+ * The content to display inside the popover
28
+ */
29
+ children: ReactNode;
30
+
31
+ /**
32
+ * Preferred placement of the popover relative to anchor
33
+ */
34
+ placement?: PopoverPlacement;
35
+
36
+ /**
37
+ * Distance from the anchor element in pixels
38
+ */
39
+ offset?: number;
40
+
41
+ /**
42
+ * Whether clicking outside should close the popover
43
+ */
44
+ closeOnClickOutside?: boolean;
45
+
46
+ /**
47
+ * Whether pressing escape key should close the popover (web only)
48
+ */
49
+ closeOnEscapeKey?: boolean;
50
+
51
+ /**
52
+ * Whether to show an arrow pointing to the anchor
53
+ */
54
+ showArrow?: boolean;
55
+
56
+ /**
57
+ * Additional styles (platform-specific)
58
+ */
59
+ style?: any;
60
+
61
+ /**
62
+ * Test ID for testing
63
+ */
64
+ testID?: string;
65
+ }
@@ -11,6 +11,8 @@ import { DividerExamples } from './DividerExamples';
11
11
  import { BadgeExamples } from './BadgeExamples';
12
12
  import { AvatarExamples } from './AvatarExamples';
13
13
  import { ScreenExamples } from './ScreenExamples';
14
+ import { DialogExamples } from './DialogExamples';
15
+ import { PopoverExamples } from './PopoverExamples';
14
16
  import { ThemeExtensionExamples } from './ThemeExtensionExamples';
15
17
 
16
18
  export const AllExamples = () => {
@@ -60,6 +62,12 @@ export const AllExamples = () => {
60
62
 
61
63
  <ScreenExamples />
62
64
  <Divider spacing="medium" />
65
+
66
+ <DialogExamples />
67
+ <Divider spacing="medium" />
68
+
69
+ <PopoverExamples />
70
+ <Divider spacing="medium" />
63
71
 
64
72
  <Divider spacing="large" intent="success">
65
73
  <Text size="small" weight="semibold" color="green">THEME SYSTEM</Text>
@@ -0,0 +1,157 @@
1
+ import { useState } from 'react';
2
+ import { Screen, View, Button, Text, Dialog } from '../index';
3
+
4
+ export const DialogExamples = () => {
5
+ const [basicOpen, setBasicOpen] = useState(false);
6
+ const [alertOpen, setAlertOpen] = useState(false);
7
+ const [confirmationOpen, setConfirmationOpen] = useState(false);
8
+ const [sizesOpen, setSizesOpen] = useState<string | null>(null);
9
+
10
+ return (
11
+ <Screen background="primary" padding="lg">
12
+ <View spacing="none">
13
+ <Text size="large" weight="bold" align="center">
14
+ Dialog Examples
15
+ </Text>
16
+
17
+ {/* Basic Dialog */}
18
+ <View spacing="md">
19
+ <Text size="medium" weight="semibold">Basic Dialog</Text>
20
+ <Button onPress={() => setBasicOpen(true)}>
21
+ Open Basic Dialog
22
+ </Button>
23
+ <Dialog
24
+ open={basicOpen}
25
+ onOpenChange={setBasicOpen}
26
+ title="Basic Dialog"
27
+ >
28
+ <Text>This is a basic dialog with a title and some content.</Text>
29
+ <View spacing="md" style={{ marginTop: 16 }}>
30
+ <Button
31
+ variant="contained"
32
+ intent="primary"
33
+ onPress={() => setBasicOpen(false)}
34
+ >
35
+ Close Dialog
36
+ </Button>
37
+ </View>
38
+ </Dialog>
39
+ </View>
40
+
41
+ {/* Dialog Variants */}
42
+ <View spacing="md">
43
+ <Text size="medium" weight="semibold">Dialog Variants</Text>
44
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
45
+ <Button onPress={() => setAlertOpen(true)}>
46
+ Alert Dialog
47
+ </Button>
48
+ <Button onPress={() => setConfirmationOpen(true)}>
49
+ Confirmation Dialog
50
+ </Button>
51
+ </View>
52
+
53
+ {/* Alert Dialog */}
54
+ <Dialog
55
+ open={alertOpen}
56
+ onOpenChange={setAlertOpen}
57
+ title="Important Alert"
58
+ variant="alert"
59
+ >
60
+ <Text>This is an alert dialog. It has a top border to indicate importance.</Text>
61
+ <View spacing="md" style={{ marginTop: 16 }}>
62
+ <Button
63
+ variant="contained"
64
+ intent="primary"
65
+ onPress={() => setAlertOpen(false)}
66
+ >
67
+ Acknowledge
68
+ </Button>
69
+ </View>
70
+ </Dialog>
71
+
72
+ {/* Confirmation Dialog */}
73
+ <Dialog
74
+ open={confirmationOpen}
75
+ onOpenChange={setConfirmationOpen}
76
+ title="Confirm Action"
77
+ variant="confirmation"
78
+ closeOnBackdropClick={false}
79
+ >
80
+ <Text>Are you sure you want to delete this item? This action cannot be undone.</Text>
81
+ <View style={{ flexDirection: 'row', gap: 12, marginTop: 16 }}>
82
+ <Button
83
+ variant="outlined"
84
+ intent="neutral"
85
+ onPress={() => setConfirmationOpen(false)}
86
+ >
87
+ Cancel
88
+ </Button>
89
+ <Button
90
+ variant="contained"
91
+ intent="error"
92
+ onPress={() => setConfirmationOpen(false)}
93
+ >
94
+ Delete
95
+ </Button>
96
+ </View>
97
+ </Dialog>
98
+ </View>
99
+
100
+ {/* Dialog Sizes */}
101
+ <View spacing="md">
102
+ <Text size="medium" weight="semibold">Dialog Sizes</Text>
103
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
104
+ {['small', 'medium', 'large'].map((size) => (
105
+ <Button
106
+ key={size}
107
+ onPress={() => setSizesOpen(size)}
108
+ >
109
+ {size.charAt(0).toUpperCase() + size.slice(1)} Dialog
110
+ </Button>
111
+ ))}
112
+ </View>
113
+
114
+ {sizesOpen && (
115
+ <Dialog
116
+ open={!!sizesOpen}
117
+ onOpenChange={() => setSizesOpen(null)}
118
+ title={`${sizesOpen.charAt(0).toUpperCase() + sizesOpen.slice(1)} Dialog`}
119
+ size={sizesOpen as 'small' | 'medium' | 'large'}
120
+ >
121
+ <Text>
122
+ This is a {sizesOpen} dialog. The width and maximum width are adjusted based on the size prop.
123
+ </Text>
124
+ <View spacing="md" style={{ marginTop: 16 }}>
125
+ <Button
126
+ variant="contained"
127
+ intent="primary"
128
+ onPress={() => setSizesOpen(null)}
129
+ >
130
+ Close
131
+ </Button>
132
+ </View>
133
+ </Dialog>
134
+ )}
135
+ </View>
136
+
137
+
138
+ {/* Dialog Options */}
139
+ <View spacing="md">
140
+ <Text size="medium" weight="semibold">Dialog Options</Text>
141
+ <Text size="small" color="secondary">
142
+ • Close on backdrop click: Enabled by default, disabled for confirmation dialog above
143
+ </Text>
144
+ <Text size="small" color="secondary">
145
+ • Close on escape key: Enabled by default (web only)
146
+ </Text>
147
+ <Text size="small" color="secondary">
148
+ • Hardware back button: Handled automatically (native only)
149
+ </Text>
150
+ <Text size="small" color="secondary">
151
+ • Focus management: Automatic focus trapping and restoration (web only)
152
+ </Text>
153
+ </View>
154
+ </View>
155
+ </Screen>
156
+ );
157
+ };
@@ -0,0 +1,155 @@
1
+ import { useState, useRef } from 'react';
2
+ import { Screen, View, Button, Text, Popover } from '../index';
3
+
4
+ export const PopoverExamples = () => {
5
+ const [basicOpen, setBasicOpen] = useState(false);
6
+ const [placementOpen, setPlacementOpen] = useState<string | null>(null);
7
+ const [arrowOpen, setArrowOpen] = useState(false);
8
+
9
+ const basicButtonRef = useRef<HTMLDivElement>(null);
10
+ const placementButtonRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
11
+ const arrowButtonRef = useRef<HTMLDivElement>(null);
12
+
13
+ const placements = [
14
+ 'top', 'top-start', 'top-end',
15
+ 'bottom', 'bottom-start', 'bottom-end',
16
+ 'left', 'left-start', 'left-end',
17
+ 'right', 'right-start', 'right-end',
18
+ ];
19
+
20
+ return (
21
+ <Screen background="primary" padding="lg">
22
+ <View spacing="none">
23
+ <Text size="large" weight="bold" align="center">
24
+ Popover Examples
25
+ </Text>
26
+
27
+ {/* Basic Popover */}
28
+ <View spacing="md">
29
+ <Text size="medium" weight="semibold">Basic Popover</Text>
30
+ <div ref={basicButtonRef} style={{ display: 'inline-block' }}>
31
+ <Button onPress={() => setBasicOpen(true)}>
32
+ Open Basic Popover
33
+ </Button>
34
+ </div>
35
+ <Popover
36
+ open={basicOpen}
37
+ onOpenChange={setBasicOpen}
38
+ anchor={basicButtonRef}
39
+ placement="bottom"
40
+ >
41
+ <View spacing="sm">
42
+ <Text weight="bold">Basic Popover</Text>
43
+ <Text size="small">This is a basic popover with some content.</Text>
44
+ <Button size="small" onPress={() => setBasicOpen(false)}>
45
+ Close
46
+ </Button>
47
+ </View>
48
+ </Popover>
49
+ </View>
50
+
51
+ {/* Placement Examples */}
52
+ <View spacing="md">
53
+ <Text size="medium" weight="semibold">Placement Options</Text>
54
+ <View style={{
55
+ display: 'grid',
56
+ gridTemplateColumns: 'repeat(3, 1fr)',
57
+ gap: 8,
58
+ maxWidth: 400
59
+ }}>
60
+ {placements.map((placement) => (
61
+ <div
62
+ key={placement}
63
+ ref={(ref) => placementButtonRefs.current[placement] = ref}
64
+ style={{ display: 'inline-block' }}
65
+ >
66
+ <Button
67
+ size="small"
68
+ variant="outlined"
69
+ onPress={() => setPlacementOpen(placement)}
70
+ >
71
+ {placement}
72
+ </Button>
73
+ </div>
74
+ ))}
75
+ </View>
76
+
77
+ {placementOpen && (
78
+ <Popover
79
+ open={!!placementOpen}
80
+ onOpenChange={() => setPlacementOpen(null)}
81
+ anchor={{ current: placementButtonRefs.current[placementOpen] }}
82
+ placement={placementOpen as any}
83
+ >
84
+ <View spacing="sm">
85
+ <Text weight="bold">{placementOpen} placement</Text>
86
+ <Text size="small">
87
+ Positioned {placementOpen} relative to the button
88
+ </Text>
89
+ <Button size="small" onPress={() => setPlacementOpen(null)}>
90
+ Close
91
+ </Button>
92
+ </View>
93
+ </Popover>
94
+ )}
95
+ </View>
96
+
97
+ {/* Arrow Example */}
98
+ <View spacing="md">
99
+ <Text size="medium" weight="semibold">With Arrow</Text>
100
+ <div ref={arrowButtonRef} style={{ display: 'inline-block' }}>
101
+ <Button
102
+ variant="contained"
103
+ intent="success"
104
+ onPress={() => setArrowOpen(true)}
105
+ >
106
+ Popover with Arrow
107
+ </Button>
108
+ </div>
109
+ <Popover
110
+ open={arrowOpen}
111
+ onOpenChange={setArrowOpen}
112
+ anchor={arrowButtonRef}
113
+ placement="top"
114
+ showArrow={true}
115
+ >
116
+ <View spacing="sm">
117
+ <Text weight="bold">Arrow Popover</Text>
118
+ <Text size="small">
119
+ This popover includes an arrow pointing to the anchor element.
120
+ </Text>
121
+ <Button size="small" onPress={() => setArrowOpen(false)}>
122
+ Close
123
+ </Button>
124
+ </View>
125
+ </Popover>
126
+ </View>
127
+
128
+ {/* Features Description */}
129
+ <View spacing="md">
130
+ <Text size="medium" weight="semibold">Features</Text>
131
+ <View spacing="sm">
132
+ <Text size="small" color="secondary">
133
+ • Automatically positions within viewport bounds
134
+ </Text>
135
+ <Text size="small" color="secondary">
136
+ • 12 placement options (top, bottom, left, right with start/end variants)
137
+ </Text>
138
+ <Text size="small" color="secondary">
139
+ • Optional arrow pointing to anchor element
140
+ </Text>
141
+ <Text size="small" color="secondary">
142
+ • Click outside or escape key to close
143
+ </Text>
144
+ <Text size="small" color="secondary">
145
+ • Smooth animations and transitions
146
+ </Text>
147
+ <Text size="small" color="secondary">
148
+ • Follows anchor element on scroll/resize (web)
149
+ </Text>
150
+ </View>
151
+ </View>
152
+ </View>
153
+ </Screen>
154
+ );
155
+ };