react-native-agentic-ai 0.0.2 → 0.3.0

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 (70) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +252 -14
  3. package/lib/module/components/AIAgent.js +185 -0
  4. package/lib/module/components/AIAgent.js.map +1 -0
  5. package/lib/module/components/AgentChatBar.js +268 -0
  6. package/lib/module/components/AgentChatBar.js.map +1 -0
  7. package/lib/module/components/AgentOverlay.js +53 -0
  8. package/lib/module/components/AgentOverlay.js.map +1 -0
  9. package/lib/module/core/AgentRuntime.js +640 -0
  10. package/lib/module/core/AgentRuntime.js.map +1 -0
  11. package/lib/module/core/FiberTreeWalker.js +362 -0
  12. package/lib/module/core/FiberTreeWalker.js.map +1 -0
  13. package/lib/module/core/MCPBridge.js +98 -0
  14. package/lib/module/core/MCPBridge.js.map +1 -0
  15. package/lib/module/core/ScreenDehydrator.js +46 -0
  16. package/lib/module/core/ScreenDehydrator.js.map +1 -0
  17. package/lib/module/core/systemPrompt.js +164 -0
  18. package/lib/module/core/systemPrompt.js.map +1 -0
  19. package/lib/module/core/types.js +2 -0
  20. package/lib/module/core/types.js.map +1 -0
  21. package/lib/module/hooks/useAction.js +32 -0
  22. package/lib/module/hooks/useAction.js.map +1 -0
  23. package/lib/module/index.js +17 -0
  24. package/lib/module/index.js.map +1 -0
  25. package/lib/module/package.json +1 -0
  26. package/lib/module/providers/GeminiProvider.js +294 -0
  27. package/lib/module/providers/GeminiProvider.js.map +1 -0
  28. package/lib/module/utils/logger.js +17 -0
  29. package/lib/module/utils/logger.js.map +1 -0
  30. package/lib/typescript/package.json +1 -0
  31. package/lib/typescript/src/components/AIAgent.d.ts +65 -0
  32. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -0
  33. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -0
  34. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -0
  35. package/lib/typescript/src/components/AgentOverlay.d.ts +10 -0
  36. package/lib/typescript/src/components/AgentOverlay.d.ts.map +1 -0
  37. package/lib/typescript/src/core/AgentRuntime.d.ts +53 -0
  38. package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -0
  39. package/lib/typescript/src/core/FiberTreeWalker.d.ts +31 -0
  40. package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -0
  41. package/lib/typescript/src/core/MCPBridge.d.ts +23 -0
  42. package/lib/typescript/src/core/MCPBridge.d.ts.map +1 -0
  43. package/lib/typescript/src/core/ScreenDehydrator.d.ts +20 -0
  44. package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +1 -0
  45. package/lib/typescript/src/core/systemPrompt.d.ts +9 -0
  46. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -0
  47. package/lib/typescript/src/core/types.d.ts +176 -0
  48. package/lib/typescript/src/core/types.d.ts.map +1 -0
  49. package/lib/typescript/src/hooks/useAction.d.ts +13 -0
  50. package/lib/typescript/src/hooks/useAction.d.ts.map +1 -0
  51. package/lib/typescript/src/index.d.ts +10 -0
  52. package/lib/typescript/src/index.d.ts.map +1 -0
  53. package/lib/typescript/src/providers/GeminiProvider.d.ts +43 -0
  54. package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -0
  55. package/lib/typescript/src/utils/logger.d.ts +7 -0
  56. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  57. package/package.json +135 -12
  58. package/src/components/AIAgent.tsx +262 -0
  59. package/src/components/AgentChatBar.tsx +258 -0
  60. package/src/components/AgentOverlay.tsx +48 -0
  61. package/src/core/AgentRuntime.ts +661 -0
  62. package/src/core/FiberTreeWalker.ts +404 -0
  63. package/src/core/MCPBridge.ts +110 -0
  64. package/src/core/ScreenDehydrator.ts +53 -0
  65. package/src/core/systemPrompt.ts +162 -0
  66. package/src/core/types.ts +233 -0
  67. package/src/hooks/useAction.ts +40 -0
  68. package/src/index.ts +22 -0
  69. package/src/providers/GeminiProvider.ts +283 -0
  70. package/src/utils/logger.ts +21 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * AIAgent — Root provider component for the AI agent.
3
+ *
4
+ * Wraps the app and provides:
5
+ * - Fiber tree root ref for element auto-detection
6
+ * - Navigation ref for auto-navigation
7
+ * - Floating chat bar for user input
8
+ * - Agent runtime context for useAction hooks
9
+ */
10
+
11
+ import React, {
12
+ useCallback,
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from 'react';
18
+ import { View, StyleSheet } from 'react-native';
19
+ import { AgentRuntime } from '../core/AgentRuntime';
20
+ import { GeminiProvider } from '../providers/GeminiProvider';
21
+ import { AgentContext } from '../hooks/useAction';
22
+ import { AgentChatBar } from './AgentChatBar';
23
+ import { AgentOverlay } from './AgentOverlay';
24
+ import { logger } from '../utils/logger';
25
+ import { MCPBridge } from '../core/MCPBridge';
26
+ import type { AgentConfig, ExecutionResult, ToolDefinition, AgentStep } from '../core/types';
27
+
28
+ // ─── Context ───────────────────────────────────────────────────
29
+
30
+
31
+ // ─── Props ─────────────────────────────────────────────────────
32
+
33
+ interface AIAgentProps {
34
+ /** Gemini API key */
35
+ apiKey: string;
36
+ /** Gemini model name */
37
+ model?: string;
38
+ /** Navigation container ref (from useNavigationContainerRef) */
39
+ navRef?: any;
40
+ /** UI language */
41
+ language?: 'en' | 'ar';
42
+ /** Max agent steps per request */
43
+ maxSteps?: number;
44
+ /** Show/hide the chat bar */
45
+ showChatBar?: boolean;
46
+ /** Children — the actual app */
47
+ children: React.ReactNode;
48
+ /** Callback when agent completes */
49
+ onResult?: (result: ExecutionResult) => void;
50
+
51
+ // ── Security (mirrors page-agent.js) ──────────────────────
52
+
53
+ /** Refs of elements the AI must NOT interact with */
54
+ interactiveBlacklist?: React.RefObject<any>[];
55
+ /** If set, AI can ONLY interact with these elements */
56
+ interactiveWhitelist?: React.RefObject<any>[];
57
+ /** Called before each step */
58
+ onBeforeStep?: (stepCount: number) => Promise<void> | void;
59
+ /** Called after each step */
60
+ onAfterStep?: (history: AgentStep[]) => Promise<void> | void;
61
+ /** Called before task starts */
62
+ onBeforeTask?: () => Promise<void> | void;
63
+ /** Called after task completes */
64
+ onAfterTask?: (result: ExecutionResult) => Promise<void> | void;
65
+ /** Transform screen content before LLM sees it (for data masking) */
66
+ transformScreenContent?: (content: string) => Promise<string> | string;
67
+ /** Override or remove built-in tools (null = remove) */
68
+ customTools?: Record<string, ToolDefinition | null>;
69
+ /** Instructions to guide agent behavior */
70
+ instructions?: {
71
+ system?: string;
72
+ getScreenInstructions?: (screenName: string) => string | undefined | null;
73
+ };
74
+ /** Delay between steps in ms */
75
+ stepDelay?: number;
76
+ /** WebSocket URL to companion MCP server bridge (e.g., ws://localhost:3101) */
77
+ mcpServerUrl?: string;
78
+ /** Expo Router instance (from useRouter()) */
79
+ router?: {
80
+ push: (href: string) => void;
81
+ replace: (href: string) => void;
82
+ back: () => void;
83
+ };
84
+ /** Expo Router pathname (from usePathname()) */
85
+ pathname?: string;
86
+ }
87
+
88
+ // ─── Component ─────────────────────────────────────────────────
89
+
90
+ export function AIAgent({
91
+ apiKey,
92
+ model = 'gemini-2.5-flash',
93
+ navRef,
94
+ language = 'en',
95
+ maxSteps = 10,
96
+ showChatBar = true,
97
+ children,
98
+ onResult,
99
+ // Security props
100
+ interactiveBlacklist,
101
+ interactiveWhitelist,
102
+ onBeforeStep,
103
+ onAfterStep,
104
+ onBeforeTask,
105
+ onAfterTask,
106
+ transformScreenContent,
107
+ customTools,
108
+ instructions,
109
+ stepDelay,
110
+ mcpServerUrl,
111
+ router,
112
+ pathname,
113
+ }: AIAgentProps) {
114
+ const rootViewRef = useRef<any>(null);
115
+ const [isThinking, setIsThinking] = useState(false);
116
+ const [statusText, setStatusText] = useState('');
117
+ const [lastResult, setLastResult] = useState<ExecutionResult | null>(null);
118
+
119
+ // Ref-based resolver for ask_user — stays alive across renders
120
+ const askUserResolverRef = useRef<((answer: string) => void) | null>(null);
121
+
122
+ // ─── Create Runtime ──────────────────────────────────────────
123
+
124
+ const config: AgentConfig = useMemo(() => ({
125
+ apiKey,
126
+ model,
127
+ language,
128
+ maxSteps,
129
+ interactiveBlacklist,
130
+ interactiveWhitelist,
131
+ onBeforeStep,
132
+ onAfterStep,
133
+ onBeforeTask,
134
+ onAfterTask,
135
+ transformScreenContent,
136
+ customTools,
137
+ instructions,
138
+ stepDelay,
139
+ mcpServerUrl,
140
+ router,
141
+ pathname,
142
+ onStatusUpdate: setStatusText,
143
+ // Page-agent pattern: block the agent loop until user responds
144
+ onAskUser: (question: string) => {
145
+ return new Promise<string>((resolve) => {
146
+ askUserResolverRef.current = resolve;
147
+ // Show question in chat bar, allow user input
148
+ setLastResult({ success: true, message: `❓ ${question}`, steps: [] });
149
+ setIsThinking(false);
150
+ setStatusText('');
151
+ });
152
+ },
153
+ }), [
154
+ apiKey, model, language, maxSteps,
155
+ interactiveBlacklist, interactiveWhitelist,
156
+ onBeforeStep, onAfterStep, onBeforeTask, onAfterTask,
157
+ transformScreenContent, customTools, instructions, stepDelay,
158
+ mcpServerUrl, router, pathname,
159
+ ]);
160
+
161
+ const provider = useMemo(() => new GeminiProvider(apiKey, model), [apiKey, model]);
162
+
163
+ const runtime = useMemo(
164
+ () => new AgentRuntime(provider, config, rootViewRef.current, navRef),
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
166
+ [provider, config],
167
+ );
168
+
169
+ // Update refs when they change
170
+ useEffect(() => {
171
+ runtime.updateRefs(rootViewRef.current, navRef);
172
+ }, [runtime, navRef]);
173
+
174
+ // ─── MCP Bridge ──────────────────────────────────────────────
175
+
176
+ useEffect(() => {
177
+ if (!mcpServerUrl) return;
178
+
179
+ logger.info('AIAgent', `Setting up MCP bridge at ${mcpServerUrl}`);
180
+ const bridge = new MCPBridge(mcpServerUrl, runtime);
181
+
182
+ return () => {
183
+ bridge.destroy();
184
+ };
185
+ }, [mcpServerUrl, runtime]);
186
+
187
+ // ─── Execute ──────────────────────────────────────────────────
188
+
189
+ const handleSend = useCallback(async (message: string) => {
190
+ if (!message.trim()) return;
191
+
192
+ logger.info('AIAgent', `User message: "${message}"`);
193
+
194
+ // If there's a pending ask_user, resolve it instead of starting a new execution
195
+ if (askUserResolverRef.current) {
196
+ const resolver = askUserResolverRef.current;
197
+ askUserResolverRef.current = null;
198
+ setIsThinking(true);
199
+ setStatusText('Processing your answer...');
200
+ setLastResult(null);
201
+ resolver(message);
202
+ return;
203
+ }
204
+
205
+ // Normal execution — new task
206
+ setIsThinking(true);
207
+ setStatusText('Thinking...');
208
+ setLastResult(null);
209
+
210
+ try {
211
+ // Ensure we have the latest Fiber tree ref
212
+ runtime.updateRefs(rootViewRef.current, navRef);
213
+
214
+ const result = await runtime.execute(message);
215
+
216
+ setLastResult(result);
217
+ onResult?.(result);
218
+
219
+ logger.info('AIAgent', `Result: ${result.success ? '✅' : '❌'} ${result.message}`);
220
+ } catch (error: any) {
221
+ logger.error('AIAgent', 'Execution failed:', error);
222
+ setLastResult({
223
+ success: false,
224
+ message: `Error: ${error.message}`,
225
+ steps: [],
226
+ });
227
+ } finally {
228
+ setIsThinking(false);
229
+ setStatusText('');
230
+ }
231
+ }, [runtime, navRef, onResult]);
232
+
233
+ // ─── Render ──────────────────────────────────────────────────
234
+
235
+ return (
236
+ <AgentContext.Provider value={runtime}>
237
+ <View ref={rootViewRef} style={styles.root} collapsable={false}>
238
+ {children}
239
+ </View>
240
+
241
+ {/* Overlay (shown while thinking) */}
242
+ <AgentOverlay visible={isThinking} statusText={statusText} />
243
+
244
+ {/* Chat bar */}
245
+ {showChatBar && (
246
+ <AgentChatBar
247
+ onSend={handleSend}
248
+ isThinking={isThinking}
249
+ lastResult={lastResult}
250
+ language={language}
251
+ onDismiss={() => setLastResult(null)}
252
+ />
253
+ )}
254
+ </AgentContext.Provider>
255
+ );
256
+ }
257
+
258
+ const styles = StyleSheet.create({
259
+ root: {
260
+ flex: 1,
261
+ },
262
+ });
@@ -0,0 +1,258 @@
1
+ /**
2
+ * AgentChatBar — Floating, draggable, compressible chat widget.
3
+ * Does not block underlying UI natively.
4
+ */
5
+
6
+ import { useState, useRef } from 'react';
7
+ import {
8
+ View,
9
+ TextInput,
10
+ Pressable,
11
+ Text,
12
+ StyleSheet,
13
+ Animated,
14
+ PanResponder,
15
+ useWindowDimensions,
16
+ } from 'react-native';
17
+ import type { ExecutionResult } from '../core/types';
18
+
19
+ interface AgentChatBarProps {
20
+ onSend: (message: string) => void;
21
+ isThinking: boolean;
22
+ lastResult: ExecutionResult | null;
23
+ language: 'en' | 'ar';
24
+ onDismiss?: () => void;
25
+ }
26
+
27
+ export function AgentChatBar({ onSend, isThinking, lastResult, language, onDismiss }: AgentChatBarProps) {
28
+ const [text, setText] = useState('');
29
+ const [isExpanded, setIsExpanded] = useState(false);
30
+ const { height } = useWindowDimensions();
31
+ const isArabic = language === 'ar';
32
+
33
+ // Initial position: Bottom right for FAB, Bottom center for Expanded
34
+ // For simplicity, we just initialize to a safe generic spot on screen.
35
+ const pan = useRef(new Animated.ValueXY({ x: 10, y: height - 200 })).current;
36
+
37
+ // PanResponder for dragging the widget
38
+ const panResponder = useRef(
39
+ PanResponder.create({
40
+ onMoveShouldSetPanResponder: (_, gestureState) => {
41
+ // Only trigger drag if moving more than 5px (allows taps to register inside)
42
+ return Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5;
43
+ },
44
+ onPanResponderGrant: () => {
45
+ pan.setOffset({
46
+ x: (pan.x as any)._value,
47
+ y: (pan.y as any)._value,
48
+ });
49
+ pan.setValue({ x: 0, y: 0 });
50
+ },
51
+ onPanResponderMove: Animated.event(
52
+ [null, { dx: pan.x, dy: pan.y }],
53
+ { useNativeDriver: false }
54
+ ),
55
+ onPanResponderRelease: () => {
56
+ pan.flattenOffset();
57
+ },
58
+ })
59
+ ).current;
60
+
61
+ const handleSend = () => {
62
+ if (text.trim() && !isThinking) {
63
+ onSend(text.trim());
64
+ setText('');
65
+ }
66
+ };
67
+
68
+ // ─── Compressed State (FAB) ───
69
+ if (!isExpanded) {
70
+ return (
71
+ <Animated.View style={[styles.fabContainer, pan.getLayout()]} {...panResponder.panHandlers}>
72
+ <Pressable
73
+ style={styles.fab}
74
+ onPress={() => setIsExpanded(true)}
75
+ accessibilityLabel="Open AI Agent Chat"
76
+ >
77
+ <Text style={styles.fabIcon}>{isThinking ? '⏳' : '🤖'}</Text>
78
+ </Pressable>
79
+ </Animated.View>
80
+ );
81
+ }
82
+
83
+ // ─── Expanded State (Widget) ───
84
+ return (
85
+ <Animated.View style={[styles.expandedContainer, pan.getLayout()]}>
86
+ {/* Drag Handle Area */}
87
+ <View {...panResponder.panHandlers} style={styles.dragHandleArea} accessibilityLabel="Drag AI Agent">
88
+ <View style={styles.dragGrip} />
89
+ <Pressable onPress={() => setIsExpanded(false)} style={styles.minimizeBtn} accessibilityLabel="Minimize AI Agent">
90
+ <Text style={styles.minimizeText}>—</Text>
91
+ </Pressable>
92
+ </View>
93
+
94
+ {/* Result message */}
95
+ {lastResult && (
96
+ <View style={[styles.resultBubble, lastResult.success ? styles.resultSuccess : styles.resultError]}>
97
+ <Text style={styles.resultText}>{lastResult.message}</Text>
98
+ {onDismiss && (
99
+ <Pressable style={styles.dismissButton} onPress={onDismiss} hitSlop={12}>
100
+ <Text style={styles.dismissText}>✕</Text>
101
+ </Pressable>
102
+ )}
103
+ </View>
104
+ )}
105
+
106
+ {/* Input row */}
107
+ <View style={styles.inputRow}>
108
+ <TextInput
109
+ style={[styles.input, isArabic && styles.inputRTL]}
110
+ placeholder={isArabic ? 'اكتب طلبك...' : 'Ask AI...'}
111
+ placeholderTextColor="#999"
112
+ value={text}
113
+ onChangeText={setText}
114
+ onSubmitEditing={handleSend}
115
+ returnKeyType="send"
116
+ editable={!isThinking}
117
+ multiline={false}
118
+ />
119
+ <Pressable
120
+ style={[styles.sendButton, isThinking && styles.sendButtonDisabled]}
121
+ onPress={handleSend}
122
+ disabled={isThinking || !text.trim()}
123
+ accessibilityLabel="Send request to AI Agent"
124
+ >
125
+ <Text style={styles.sendButtonText}>
126
+ {isThinking ? '⏳' : '🚀'}
127
+ </Text>
128
+ </Pressable>
129
+ </View>
130
+ </Animated.View>
131
+ );
132
+ }
133
+
134
+ const styles = StyleSheet.create({
135
+ // FAB Styles
136
+ fabContainer: {
137
+ position: 'absolute',
138
+ zIndex: 9999,
139
+ },
140
+ fab: {
141
+ width: 60,
142
+ height: 60,
143
+ borderRadius: 30,
144
+ backgroundColor: '#1a1a2e',
145
+ justifyContent: 'center',
146
+ alignItems: 'center',
147
+ elevation: 5,
148
+ shadowColor: '#000',
149
+ shadowOffset: { width: 0, height: 4 },
150
+ shadowOpacity: 0.3,
151
+ shadowRadius: 6,
152
+ },
153
+ fabIcon: {
154
+ fontSize: 28,
155
+ },
156
+
157
+ // Expanded Styles
158
+ expandedContainer: {
159
+ position: 'absolute',
160
+ zIndex: 9999,
161
+ width: 340,
162
+ backgroundColor: 'rgba(26, 26, 46, 0.95)',
163
+ borderRadius: 24,
164
+ padding: 16,
165
+ paddingTop: 8,
166
+ elevation: 8,
167
+ shadowColor: '#000',
168
+ shadowOffset: { width: 0, height: 8 },
169
+ shadowOpacity: 0.4,
170
+ shadowRadius: 10,
171
+ },
172
+ dragHandleArea: {
173
+ width: '100%',
174
+ height: 30,
175
+ justifyContent: 'center',
176
+ alignItems: 'center',
177
+ marginBottom: 8,
178
+ },
179
+ dragGrip: {
180
+ width: 40,
181
+ height: 5,
182
+ backgroundColor: 'rgba(255, 255, 255, 0.3)',
183
+ borderRadius: 4,
184
+ },
185
+ minimizeBtn: {
186
+ position: 'absolute',
187
+ right: 0,
188
+ top: 0,
189
+ padding: 8,
190
+ },
191
+ minimizeText: {
192
+ color: '#fff',
193
+ fontSize: 18,
194
+ fontWeight: 'bold',
195
+ },
196
+
197
+ // Results & Input
198
+ resultBubble: {
199
+ borderRadius: 12,
200
+ padding: 12,
201
+ marginBottom: 12,
202
+ flexDirection: 'row',
203
+ alignItems: 'flex-start',
204
+ },
205
+ resultSuccess: {
206
+ backgroundColor: 'rgba(40, 167, 69, 0.2)',
207
+ },
208
+ resultError: {
209
+ backgroundColor: 'rgba(220, 53, 69, 0.2)',
210
+ },
211
+ resultText: {
212
+ color: '#fff',
213
+ fontSize: 14,
214
+ lineHeight: 20,
215
+ flex: 1,
216
+ },
217
+ dismissButton: {
218
+ marginLeft: 8,
219
+ padding: 2,
220
+ },
221
+ dismissText: {
222
+ color: 'rgba(255, 255, 255, 0.6)',
223
+ fontSize: 14,
224
+ fontWeight: 'bold',
225
+ },
226
+ inputRow: {
227
+ flexDirection: 'row',
228
+ alignItems: 'center',
229
+ gap: 8,
230
+ },
231
+ input: {
232
+ flex: 1,
233
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
234
+ borderRadius: 20,
235
+ paddingHorizontal: 16,
236
+ paddingVertical: 10,
237
+ color: '#fff',
238
+ fontSize: 16,
239
+ },
240
+ inputRTL: {
241
+ textAlign: 'right',
242
+ writingDirection: 'rtl',
243
+ },
244
+ sendButton: {
245
+ width: 40,
246
+ height: 40,
247
+ borderRadius: 20,
248
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
249
+ justifyContent: 'center',
250
+ alignItems: 'center',
251
+ },
252
+ sendButtonDisabled: {
253
+ opacity: 0.5,
254
+ },
255
+ sendButtonText: {
256
+ fontSize: 18,
257
+ },
258
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * AgentOverlay — Subtle thinking indicator shown while the AI agent is processing.
3
+ */
4
+
5
+ import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
6
+
7
+ interface AgentOverlayProps {
8
+ visible: boolean;
9
+ statusText: string;
10
+ }
11
+
12
+ export function AgentOverlay({ visible, statusText }: AgentOverlayProps) {
13
+ if (!visible) return null;
14
+
15
+ return (
16
+ <View style={styles.overlay} pointerEvents="none">
17
+ <View style={styles.pill}>
18
+ <ActivityIndicator size="small" color="#fff" />
19
+ <Text style={styles.text}>{statusText || 'Thinking...'}</Text>
20
+ </View>
21
+ </View>
22
+ );
23
+ }
24
+
25
+ const styles = StyleSheet.create({
26
+ overlay: {
27
+ position: 'absolute',
28
+ top: 60,
29
+ left: 0,
30
+ right: 0,
31
+ alignItems: 'center',
32
+ zIndex: 9999,
33
+ },
34
+ pill: {
35
+ flexDirection: 'row',
36
+ alignItems: 'center',
37
+ gap: 8,
38
+ backgroundColor: 'rgba(26, 26, 46, 0.9)',
39
+ paddingHorizontal: 16,
40
+ paddingVertical: 10,
41
+ borderRadius: 20,
42
+ },
43
+ text: {
44
+ color: '#fff',
45
+ fontSize: 14,
46
+ fontWeight: '500',
47
+ },
48
+ });