@metacells/mcellui-mcp-server 0.1.0 → 0.1.2

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 (49) hide show
  1. package/dist/index.js +70 -7
  2. package/package.json +7 -5
  3. package/registry/registry.json +717 -0
  4. package/registry/ui/accordion.tsx +416 -0
  5. package/registry/ui/action-sheet.tsx +396 -0
  6. package/registry/ui/alert-dialog.tsx +355 -0
  7. package/registry/ui/avatar-stack.tsx +278 -0
  8. package/registry/ui/avatar.tsx +116 -0
  9. package/registry/ui/badge.tsx +125 -0
  10. package/registry/ui/button.tsx +240 -0
  11. package/registry/ui/card.tsx +675 -0
  12. package/registry/ui/carousel.tsx +431 -0
  13. package/registry/ui/checkbox.tsx +252 -0
  14. package/registry/ui/chip.tsx +271 -0
  15. package/registry/ui/column.tsx +133 -0
  16. package/registry/ui/datetime-picker.tsx +578 -0
  17. package/registry/ui/dialog.tsx +292 -0
  18. package/registry/ui/fab.tsx +225 -0
  19. package/registry/ui/form.tsx +323 -0
  20. package/registry/ui/horizontal-list.tsx +200 -0
  21. package/registry/ui/icon-button.tsx +244 -0
  22. package/registry/ui/image-gallery.tsx +455 -0
  23. package/registry/ui/image.tsx +283 -0
  24. package/registry/ui/input.tsx +242 -0
  25. package/registry/ui/label.tsx +99 -0
  26. package/registry/ui/list.tsx +519 -0
  27. package/registry/ui/progress.tsx +168 -0
  28. package/registry/ui/pull-to-refresh.tsx +231 -0
  29. package/registry/ui/radio-group.tsx +294 -0
  30. package/registry/ui/rating.tsx +311 -0
  31. package/registry/ui/row.tsx +136 -0
  32. package/registry/ui/screen.tsx +153 -0
  33. package/registry/ui/search-input.tsx +281 -0
  34. package/registry/ui/section-header.tsx +258 -0
  35. package/registry/ui/segmented-control.tsx +229 -0
  36. package/registry/ui/select.tsx +311 -0
  37. package/registry/ui/separator.tsx +74 -0
  38. package/registry/ui/sheet.tsx +362 -0
  39. package/registry/ui/skeleton.tsx +156 -0
  40. package/registry/ui/slider.tsx +307 -0
  41. package/registry/ui/spinner.tsx +100 -0
  42. package/registry/ui/stepper.tsx +314 -0
  43. package/registry/ui/stories.tsx +463 -0
  44. package/registry/ui/swipeable-row.tsx +362 -0
  45. package/registry/ui/switch.tsx +246 -0
  46. package/registry/ui/tabs.tsx +348 -0
  47. package/registry/ui/textarea.tsx +265 -0
  48. package/registry/ui/toast.tsx +316 -0
  49. package/registry/ui/tooltip.tsx +369 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Toast
3
+ *
4
+ * A notification system for displaying brief messages.
5
+ * Auto-dismisses after duration. Supports variants and actions.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Using ToastProvider at app root
10
+ * <ToastProvider>
11
+ * <App />
12
+ * </ToastProvider>
13
+ *
14
+ * // Using toast anywhere in your app
15
+ * import { useToast } from '@/components/ui/toast';
16
+ *
17
+ * function MyComponent() {
18
+ * const { toast } = useToast();
19
+ *
20
+ * return (
21
+ * <Button onPress={() => toast({ title: 'Saved!', variant: 'success' })}>
22
+ * Save
23
+ * </Button>
24
+ * );
25
+ * }
26
+ * ```
27
+ */
28
+
29
+ import React, { createContext, useContext, useCallback, useRef, useEffect } from 'react';
30
+ import {
31
+ View,
32
+ Text,
33
+ StyleSheet,
34
+ ViewStyle,
35
+ TextStyle,
36
+ Pressable,
37
+ Dimensions,
38
+ } from 'react-native';
39
+ import Animated, {
40
+ useSharedValue,
41
+ useAnimatedStyle,
42
+ withSpring,
43
+ withTiming,
44
+ runOnJS,
45
+ SlideInUp,
46
+ SlideOutUp,
47
+ FadeIn,
48
+ FadeOut,
49
+ Layout,
50
+ } from 'react-native-reanimated';
51
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
52
+ import { useTheme, TOAST_CONSTANTS } from '@nativeui/core';
53
+ import { haptic } from '@nativeui/core';
54
+
55
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
56
+
57
+ export type ToastVariant = 'default' | 'success' | 'error' | 'warning';
58
+
59
+ export interface ToastData {
60
+ id: string;
61
+ title: string;
62
+ description?: string;
63
+ variant?: ToastVariant;
64
+ duration?: number;
65
+ action?: {
66
+ label: string;
67
+ onPress: () => void;
68
+ };
69
+ }
70
+
71
+ export interface ToastOptions {
72
+ title: string;
73
+ description?: string;
74
+ variant?: ToastVariant;
75
+ duration?: number;
76
+ action?: {
77
+ label: string;
78
+ onPress: () => void;
79
+ };
80
+ }
81
+
82
+ interface ToastContextValue {
83
+ toast: (options: ToastOptions) => string;
84
+ dismiss: (id: string) => void;
85
+ dismissAll: () => void;
86
+ }
87
+
88
+ const ToastContext = createContext<ToastContextValue | null>(null);
89
+
90
+ export function useToast() {
91
+ const context = useContext(ToastContext);
92
+ if (!context) {
93
+ throw new Error('useToast must be used within a ToastProvider');
94
+ }
95
+ return context;
96
+ }
97
+
98
+ export interface ToastProviderProps {
99
+ children: React.ReactNode;
100
+ /** Maximum number of toasts visible at once */
101
+ maxToasts?: number;
102
+ /** Default duration in ms */
103
+ defaultDuration?: number;
104
+ }
105
+
106
+ export function ToastProvider({
107
+ children,
108
+ maxToasts = TOAST_CONSTANTS.maxToasts,
109
+ defaultDuration = TOAST_CONSTANTS.defaultDuration,
110
+ }: ToastProviderProps) {
111
+ const [toasts, setToasts] = React.useState<ToastData[]>([]);
112
+ const insets = useSafeAreaInsets();
113
+
114
+ const toast = useCallback(
115
+ (options: ToastOptions): string => {
116
+ const id = Math.random().toString(36).slice(2, 9);
117
+ const newToast: ToastData = {
118
+ id,
119
+ ...options,
120
+ duration: options.duration ?? defaultDuration,
121
+ };
122
+
123
+ // Haptic feedback based on variant
124
+ if (options.variant === 'success') {
125
+ haptic('success');
126
+ } else if (options.variant === 'error') {
127
+ haptic('error');
128
+ } else if (options.variant === 'warning') {
129
+ haptic('warning');
130
+ } else {
131
+ haptic('light');
132
+ }
133
+
134
+ setToasts((prev) => {
135
+ const next = [newToast, ...prev];
136
+ return next.slice(0, maxToasts);
137
+ });
138
+
139
+ return id;
140
+ },
141
+ [defaultDuration, maxToasts]
142
+ );
143
+
144
+ const dismiss = useCallback((id: string) => {
145
+ setToasts((prev) => prev.filter((t) => t.id !== id));
146
+ }, []);
147
+
148
+ const dismissAll = useCallback(() => {
149
+ setToasts([]);
150
+ }, []);
151
+
152
+ return (
153
+ <ToastContext.Provider value={{ toast, dismiss, dismissAll }}>
154
+ {children}
155
+ <View
156
+ style={[
157
+ styles.container,
158
+ { top: insets.top + TOAST_CONSTANTS.containerTopOffset },
159
+ ]}
160
+ pointerEvents="box-none"
161
+ >
162
+ {toasts.map((t) => (
163
+ <ToastItem
164
+ key={t.id}
165
+ data={t}
166
+ onDismiss={() => dismiss(t.id)}
167
+ />
168
+ ))}
169
+ </View>
170
+ </ToastContext.Provider>
171
+ );
172
+ }
173
+
174
+ interface ToastItemProps {
175
+ data: ToastData;
176
+ onDismiss: () => void;
177
+ }
178
+
179
+ function ToastItem({ data, onDismiss }: ToastItemProps) {
180
+ const { colors, radius, platformShadow, spacing } = useTheme();
181
+ const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
182
+
183
+ useEffect(() => {
184
+ if (data.duration && data.duration > 0) {
185
+ timerRef.current = setTimeout(onDismiss, data.duration);
186
+ }
187
+ return () => {
188
+ if (timerRef.current) clearTimeout(timerRef.current);
189
+ };
190
+ }, [data.duration, onDismiss]);
191
+
192
+ const getVariantStyles = () => {
193
+ switch (data.variant) {
194
+ case 'success':
195
+ return {
196
+ backgroundColor: colors.success ?? TOAST_CONSTANTS.fallbackColors.success,
197
+ textColor: TOAST_CONSTANTS.fallbackColors.successForeground,
198
+ };
199
+ case 'error':
200
+ return {
201
+ backgroundColor: colors.destructive,
202
+ textColor: colors.destructiveForeground,
203
+ };
204
+ case 'warning':
205
+ return {
206
+ backgroundColor: colors.warning ?? TOAST_CONSTANTS.fallbackColors.warning,
207
+ textColor: TOAST_CONSTANTS.fallbackColors.warningForeground,
208
+ };
209
+ default:
210
+ return {
211
+ backgroundColor: colors.card,
212
+ textColor: colors.foreground,
213
+ };
214
+ }
215
+ };
216
+
217
+ const variantStyles = getVariantStyles();
218
+
219
+ return (
220
+ <Animated.View
221
+ entering={SlideInUp.springify().damping(20).stiffness(200)}
222
+ exiting={FadeOut.duration(TOAST_CONSTANTS.fadeOutDuration)}
223
+ layout={Layout.springify().damping(20)}
224
+ style={[
225
+ styles.toast,
226
+ {
227
+ backgroundColor: variantStyles.backgroundColor,
228
+ borderRadius: radius.lg,
229
+ padding: spacing[4],
230
+ marginHorizontal: spacing[4],
231
+ marginBottom: spacing[2],
232
+ },
233
+ platformShadow('lg'),
234
+ ]}
235
+ >
236
+ <Pressable
237
+ style={styles.toastContent}
238
+ onPress={onDismiss}
239
+ >
240
+ <View style={styles.textContainer}>
241
+ <Text style={[styles.title, { color: variantStyles.textColor }]}>
242
+ {data.title}
243
+ </Text>
244
+ {data.description && (
245
+ <Text
246
+ style={[
247
+ styles.description,
248
+ {
249
+ color: variantStyles.textColor,
250
+ opacity: 0.9,
251
+ marginTop: spacing[1],
252
+ },
253
+ ]}
254
+ >
255
+ {data.description}
256
+ </Text>
257
+ )}
258
+ </View>
259
+ {data.action && (
260
+ <Pressable
261
+ onPress={() => {
262
+ data.action?.onPress();
263
+ onDismiss();
264
+ }}
265
+ style={[
266
+ styles.actionButton,
267
+ {
268
+ marginLeft: spacing[3],
269
+ paddingHorizontal: spacing[3],
270
+ paddingVertical: spacing[1.5],
271
+ backgroundColor: 'rgba(255,255,255,0.2)',
272
+ borderRadius: radius.md,
273
+ },
274
+ ]}
275
+ >
276
+ <Text style={[styles.actionText, { color: variantStyles.textColor }]}>
277
+ {data.action.label}
278
+ </Text>
279
+ </Pressable>
280
+ )}
281
+ </Pressable>
282
+ </Animated.View>
283
+ );
284
+ }
285
+
286
+ const styles = StyleSheet.create({
287
+ container: {
288
+ position: 'absolute',
289
+ left: 0,
290
+ right: 0,
291
+ zIndex: 9999,
292
+ },
293
+ toast: {
294
+ width: SCREEN_WIDTH - TOAST_CONSTANTS.widthMargin,
295
+ alignSelf: 'center',
296
+ },
297
+ toastContent: {
298
+ flexDirection: 'row',
299
+ alignItems: 'center',
300
+ },
301
+ textContainer: {
302
+ flex: 1,
303
+ },
304
+ title: {
305
+ fontSize: 14,
306
+ fontWeight: '600',
307
+ },
308
+ description: {
309
+ fontSize: 13,
310
+ },
311
+ actionButton: {},
312
+ actionText: {
313
+ fontSize: 13,
314
+ fontWeight: '600',
315
+ },
316
+ });
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Tooltip
3
+ *
4
+ * Small popup that appears on long-press to show additional information.
5
+ * Automatically positions itself above or below the trigger element.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Basic tooltip
10
+ * <Tooltip content="This is helpful information">
11
+ * <Button>Long press me</Button>
12
+ * </Tooltip>
13
+ *
14
+ * // With custom delay
15
+ * <Tooltip content="Appears after 500ms" delayMs={500}>
16
+ * <IconButton icon={<InfoIcon />} />
17
+ * </Tooltip>
18
+ *
19
+ * // Positioned below
20
+ * <Tooltip content="I appear below" position="bottom">
21
+ * <Text>Long press me</Text>
22
+ * </Tooltip>
23
+ * ```
24
+ */
25
+
26
+ import React, { useState, useRef, useCallback, Children, isValidElement, cloneElement } from 'react';
27
+ import {
28
+ View,
29
+ Text,
30
+ Pressable,
31
+ StyleSheet,
32
+ Modal,
33
+ Dimensions,
34
+ LayoutRectangle,
35
+ } from 'react-native';
36
+ import Animated, {
37
+ useAnimatedStyle,
38
+ useSharedValue,
39
+ withTiming,
40
+ withSpring,
41
+ } from 'react-native-reanimated';
42
+ import { useTheme, haptic } from '@nativeui/core';
43
+
44
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // Types
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ export type TooltipPosition = 'top' | 'bottom';
51
+
52
+ export interface TooltipProps {
53
+ /** Tooltip content text */
54
+ content: string;
55
+ /** Trigger element */
56
+ children: React.ReactNode;
57
+ /** Preferred position (auto-adjusts if not enough space) */
58
+ position?: TooltipPosition;
59
+ /** Delay before showing tooltip (ms) */
60
+ delayMs?: number;
61
+ /** Controlled open state */
62
+ open?: boolean;
63
+ /** Callback when open state changes */
64
+ onOpenChange?: (open: boolean) => void;
65
+ /** Whether tooltip is disabled */
66
+ disabled?: boolean;
67
+ /** Maximum width of tooltip */
68
+ maxWidth?: number;
69
+ }
70
+
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // Constants
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+
75
+ const TOOLTIP_PADDING = 12;
76
+ const TOOLTIP_MARGIN = 8;
77
+ const ARROW_SIZE = 8;
78
+
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ // Tooltip Component
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+
83
+ export function Tooltip({
84
+ content,
85
+ children,
86
+ position = 'top',
87
+ delayMs = 500,
88
+ open: controlledOpen,
89
+ onOpenChange,
90
+ disabled = false,
91
+ maxWidth = 250,
92
+ }: TooltipProps) {
93
+ const { colors, radius, fontSize } = useTheme();
94
+
95
+ const isControlled = controlledOpen !== undefined;
96
+ const [internalOpen, setInternalOpen] = useState(false);
97
+ const isOpen = isControlled ? controlledOpen : internalOpen;
98
+
99
+ const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
100
+ const [tooltipSize, setTooltipSize] = useState({ width: 0, height: 0 });
101
+ const triggerRef = useRef<View>(null);
102
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
103
+
104
+ const opacity = useSharedValue(0);
105
+ const scale = useSharedValue(0.9);
106
+
107
+ const setOpen = useCallback(
108
+ (value: boolean) => {
109
+ if (isControlled) {
110
+ onOpenChange?.(value);
111
+ } else {
112
+ setInternalOpen(value);
113
+ }
114
+
115
+ if (value) {
116
+ opacity.value = withTiming(1, { duration: 150 });
117
+ scale.value = withSpring(1, { damping: 20, stiffness: 300 });
118
+ } else {
119
+ opacity.value = withTiming(0, { duration: 100 });
120
+ scale.value = withTiming(0.9, { duration: 100 });
121
+ }
122
+ },
123
+ [isControlled, onOpenChange, opacity, scale]
124
+ );
125
+
126
+ const showTooltip = useCallback(() => {
127
+ if (triggerRef.current) {
128
+ triggerRef.current.measureInWindow((x, y, width, height) => {
129
+ setTriggerLayout({ x, y, width, height });
130
+ haptic('light');
131
+ setOpen(true);
132
+ });
133
+ }
134
+ }, [setOpen]);
135
+
136
+ // Timer-based approach for showing tooltip after delay
137
+ // Works consistently for both pressable and non-pressable components
138
+ const startDelayTimer = useCallback((customDelay?: number) => {
139
+ if (disabled) return;
140
+ if (timerRef.current) {
141
+ clearTimeout(timerRef.current);
142
+ }
143
+ const delay = customDelay ?? delayMs;
144
+ timerRef.current = setTimeout(() => {
145
+ showTooltip();
146
+ }, delay);
147
+ }, [disabled, delayMs, showTooltip]);
148
+
149
+ const cancelDelayTimer = useCallback(() => {
150
+ if (timerRef.current) {
151
+ clearTimeout(timerRef.current);
152
+ timerRef.current = null;
153
+ }
154
+ }, []);
155
+
156
+ const handleClose = useCallback(() => {
157
+ setOpen(false);
158
+ }, [setOpen]);
159
+
160
+ // Calculate tooltip position
161
+ const calculatePosition = useCallback((): {
162
+ top: number;
163
+ left: number;
164
+ actualPosition: TooltipPosition;
165
+ } => {
166
+ if (!triggerLayout) {
167
+ return { top: 0, left: 0, actualPosition: position };
168
+ }
169
+
170
+ const { x: triggerX, y: triggerY, width: triggerWidth, height: triggerHeight } = triggerLayout;
171
+
172
+ // Center horizontally
173
+ let left = triggerX + triggerWidth / 2 - tooltipSize.width / 2;
174
+
175
+ // Clamp to screen bounds
176
+ left = Math.max(TOOLTIP_MARGIN, Math.min(left, SCREEN_WIDTH - tooltipSize.width - TOOLTIP_MARGIN));
177
+
178
+ // Calculate vertical position
179
+ let actualPosition = position;
180
+ let top: number;
181
+
182
+ if (position === 'top') {
183
+ top = triggerY - tooltipSize.height - ARROW_SIZE - 4;
184
+ // If not enough space above, show below
185
+ if (top < TOOLTIP_MARGIN) {
186
+ actualPosition = 'bottom';
187
+ top = triggerY + triggerHeight + ARROW_SIZE + 4;
188
+ }
189
+ } else {
190
+ top = triggerY + triggerHeight + ARROW_SIZE + 4;
191
+ // If not enough space below, show above
192
+ if (top + tooltipSize.height > SCREEN_HEIGHT - TOOLTIP_MARGIN) {
193
+ actualPosition = 'top';
194
+ top = triggerY - tooltipSize.height - ARROW_SIZE - 4;
195
+ }
196
+ }
197
+
198
+ return { top, left, actualPosition };
199
+ }, [triggerLayout, tooltipSize, position]);
200
+
201
+ const { top, left, actualPosition } = calculatePosition();
202
+
203
+ // Calculate arrow position
204
+ const arrowLeft = triggerLayout
205
+ ? triggerLayout.x + triggerLayout.width / 2 - left - ARROW_SIZE
206
+ : 0;
207
+
208
+ const animatedStyle = useAnimatedStyle(() => ({
209
+ opacity: opacity.value,
210
+ transform: [{ scale: scale.value }],
211
+ }));
212
+
213
+ const handleTooltipLayout = useCallback(
214
+ (event: { nativeEvent: { layout: { width: number; height: number } } }) => {
215
+ const { width, height } = event.nativeEvent.layout;
216
+ if (width !== tooltipSize.width || height !== tooltipSize.height) {
217
+ setTooltipSize({ width, height });
218
+ }
219
+ },
220
+ [tooltipSize]
221
+ );
222
+
223
+ // Render the trigger element
224
+ const renderTrigger = () => {
225
+ const child = Children.only(children);
226
+
227
+ if (isValidElement(child)) {
228
+ const childProps = child.props as Record<string, unknown>;
229
+
230
+ // Check if child is a pressable component (has onPress or similar)
231
+ const componentName = typeof child.type === 'function'
232
+ ? ((child.type as any).displayName || child.type.name || '')
233
+ : '';
234
+ const isPressable =
235
+ 'onPress' in childProps ||
236
+ 'onLongPress' in childProps ||
237
+ 'onPressIn' in childProps ||
238
+ child.type === Pressable ||
239
+ ['Button', 'IconButton', 'Pressable', 'TouchableOpacity', 'TouchableHighlight'].includes(componentName);
240
+
241
+ // IconButtons should show tooltip immediately (no delay) for better UX
242
+ const isIconButton = componentName === 'IconButton';
243
+ const effectiveDelay = isIconButton ? 0 : delayMs;
244
+
245
+ if (isPressable) {
246
+ // Clone and inject onPressIn/onPressOut for pressable components
247
+ // Use timer-based approach for consistent delay behavior
248
+ const existingOnPressIn = childProps.onPressIn as ((e: any) => void) | undefined;
249
+ const existingOnPressOut = childProps.onPressOut as ((e: any) => void) | undefined;
250
+
251
+ return cloneElement(child as React.ReactElement<Record<string, unknown>>, {
252
+ ref: triggerRef,
253
+ onPressIn: (e: any) => {
254
+ startDelayTimer(effectiveDelay);
255
+ existingOnPressIn?.(e);
256
+ },
257
+ onPressOut: (e: any) => {
258
+ cancelDelayTimer();
259
+ existingOnPressOut?.(e);
260
+ },
261
+ });
262
+ }
263
+ }
264
+
265
+ // For non-pressable elements, wrap in a View with touch handlers
266
+ return (
267
+ <View
268
+ ref={triggerRef}
269
+ collapsable={false}
270
+ onTouchStart={() => startDelayTimer()}
271
+ onTouchEnd={cancelDelayTimer}
272
+ onTouchCancel={cancelDelayTimer}
273
+ >
274
+ {children}
275
+ </View>
276
+ );
277
+ };
278
+
279
+ return (
280
+ <>
281
+ {renderTrigger()}
282
+
283
+ <Modal visible={isOpen} transparent animationType="none" statusBarTranslucent>
284
+ <Pressable style={styles.overlay} onPress={handleClose}>
285
+ <Animated.View
286
+ style={[
287
+ styles.tooltip,
288
+ {
289
+ backgroundColor: colors.foreground,
290
+ borderRadius: radius.md,
291
+ maxWidth,
292
+ top,
293
+ left,
294
+ },
295
+ animatedStyle,
296
+ ]}
297
+ onLayout={handleTooltipLayout}
298
+ >
299
+ {/* Arrow */}
300
+ <View
301
+ style={[
302
+ styles.arrow,
303
+ actualPosition === 'top' ? styles.arrowBottom : styles.arrowTop,
304
+ {
305
+ borderTopColor: actualPosition === 'top' ? colors.foreground : 'transparent',
306
+ borderBottomColor: actualPosition === 'bottom' ? colors.foreground : 'transparent',
307
+ left: Math.max(ARROW_SIZE, Math.min(arrowLeft, maxWidth - ARROW_SIZE * 3)),
308
+ },
309
+ ]}
310
+ />
311
+
312
+ {/* Content */}
313
+ <Text
314
+ style={[
315
+ styles.content,
316
+ {
317
+ color: colors.background,
318
+ fontSize: fontSize.sm,
319
+ paddingHorizontal: TOOLTIP_PADDING,
320
+ paddingVertical: TOOLTIP_PADDING - 4,
321
+ },
322
+ ]}
323
+ >
324
+ {content}
325
+ </Text>
326
+ </Animated.View>
327
+ </Pressable>
328
+ </Modal>
329
+ </>
330
+ );
331
+ }
332
+
333
+ // ─────────────────────────────────────────────────────────────────────────────
334
+ // Styles
335
+ // ─────────────────────────────────────────────────────────────────────────────
336
+
337
+ const styles = StyleSheet.create({
338
+ overlay: {
339
+ flex: 1,
340
+ },
341
+ tooltip: {
342
+ position: 'absolute',
343
+ shadowColor: '#000',
344
+ shadowOffset: { width: 0, height: 2 },
345
+ shadowOpacity: 0.2,
346
+ shadowRadius: 8,
347
+ elevation: 8,
348
+ },
349
+ arrow: {
350
+ position: 'absolute',
351
+ width: 0,
352
+ height: 0,
353
+ borderLeftWidth: ARROW_SIZE,
354
+ borderRightWidth: ARROW_SIZE,
355
+ borderLeftColor: 'transparent',
356
+ borderRightColor: 'transparent',
357
+ },
358
+ arrowTop: {
359
+ top: -ARROW_SIZE,
360
+ borderBottomWidth: ARROW_SIZE,
361
+ },
362
+ arrowBottom: {
363
+ bottom: -ARROW_SIZE,
364
+ borderTopWidth: ARROW_SIZE,
365
+ },
366
+ content: {
367
+ textAlign: 'center',
368
+ },
369
+ });