@10play/expo-air 0.12.1 → 0.12.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.
- package/cli/dist/server/promptServer.d.ts.map +1 -1
- package/cli/dist/server/promptServer.js +2 -0
- package/cli/dist/server/promptServer.js.map +1 -1
- package/ios/widget.jsbundle +11 -5
- package/package.json +1 -1
- package/widget/BubbleContent.tsx +50 -755
- package/widget/components/FormattedText.tsx +320 -0
- package/widget/components/Header.tsx +225 -0
- package/widget/components/MessageItems.tsx +308 -0
- package/widget/components/ResponseArea.tsx +3 -754
- package/widget/components/TabBar.tsx +161 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, Text, StyleSheet, Platform, Linking } from "react-native";
|
|
3
|
+
import { SPACING, COLORS, TYPOGRAPHY } from "../constants/design";
|
|
4
|
+
|
|
5
|
+
// Renders formatted text with markdown-style lists, code, and emphasis
|
|
6
|
+
export function FormattedText({ content, isStreaming }: { content: string; isStreaming?: boolean }) {
|
|
7
|
+
const lines = content.split('\n');
|
|
8
|
+
const elements: React.ReactNode[] = [];
|
|
9
|
+
let i = 0;
|
|
10
|
+
|
|
11
|
+
while (i < lines.length) {
|
|
12
|
+
const line = lines[i];
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
|
|
15
|
+
// Code block start (check first to avoid matching content inside code blocks)
|
|
16
|
+
if (trimmed.startsWith('```')) {
|
|
17
|
+
const codeLines: string[] = [];
|
|
18
|
+
const lang = trimmed.slice(3).trim();
|
|
19
|
+
i++;
|
|
20
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
21
|
+
codeLines.push(lines[i]);
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
elements.push(
|
|
25
|
+
<View key={`code-${i}`} style={styles.codeBlock}>
|
|
26
|
+
{lang && <Text style={styles.codeLang}>{lang}</Text>}
|
|
27
|
+
<Text style={styles.codeText} selectable>{codeLines.join('\n')}</Text>
|
|
28
|
+
</View>
|
|
29
|
+
);
|
|
30
|
+
i++; // skip closing ```
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Heading (# ## ### etc.)
|
|
35
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
36
|
+
if (headingMatch) {
|
|
37
|
+
const level = headingMatch[1].length;
|
|
38
|
+
const headingStyle = level <= 2 ? styles.heading1 : level <= 4 ? styles.heading2 : styles.heading3;
|
|
39
|
+
elements.push(
|
|
40
|
+
<Text key={i} style={headingStyle} selectable>{formatInlineText(headingMatch[2])}</Text>
|
|
41
|
+
);
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Blockquote (> text) - collect consecutive > lines
|
|
47
|
+
if (trimmed.startsWith('> ') || trimmed === '>') {
|
|
48
|
+
const quoteLines: string[] = [];
|
|
49
|
+
while (i < lines.length && (lines[i].trim().startsWith('> ') || lines[i].trim() === '>')) {
|
|
50
|
+
const qContent = lines[i].trim().startsWith('> ') ? lines[i].trim().slice(2) : '';
|
|
51
|
+
quoteLines.push(qContent);
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
elements.push(
|
|
55
|
+
<View key={`quote-${i}`} style={styles.blockquote}>
|
|
56
|
+
<Text style={styles.blockquoteText} selectable>{formatInlineText(quoteLines.join('\n'))}</Text>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Task list item (- [ ] or - [x])
|
|
63
|
+
const taskMatch = trimmed.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
|
|
64
|
+
if (taskMatch) {
|
|
65
|
+
const checked = taskMatch[1] !== ' ';
|
|
66
|
+
elements.push(
|
|
67
|
+
<View key={i} style={styles.listItem}>
|
|
68
|
+
<Text style={styles.listBullet}>{checked ? '☑' : '☐'}</Text>
|
|
69
|
+
<Text style={styles.listText} selectable>{formatInlineText(taskMatch[2])}</Text>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Numbered list item (1. 2. 3.) - with nesting via indentation
|
|
77
|
+
const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
78
|
+
if (numberedMatch) {
|
|
79
|
+
const indent = Math.floor(numberedMatch[1].length / 2);
|
|
80
|
+
const [, , num, text] = numberedMatch;
|
|
81
|
+
elements.push(
|
|
82
|
+
<View key={i} style={[styles.listItem, indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG }]}>
|
|
83
|
+
<Text style={styles.listNumber}>{num}.</Text>
|
|
84
|
+
<Text style={styles.listText} selectable>{formatInlineText(text)}</Text>
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bullet list item (- or *) - with nesting via indentation
|
|
92
|
+
const bulletMatch = line.match(/^(\s*)[-*]\s+(.+)$/);
|
|
93
|
+
if (bulletMatch) {
|
|
94
|
+
const indent = Math.floor(bulletMatch[1].length / 2);
|
|
95
|
+
const bulletChar = indent === 0 ? '•' : indent === 1 ? '◦' : '▪';
|
|
96
|
+
elements.push(
|
|
97
|
+
<View key={i} style={[styles.listItem, indent > 0 && { paddingLeft: SPACING.SM + indent * SPACING.LG }]}>
|
|
98
|
+
<Text style={styles.listBullet}>{bulletChar}</Text>
|
|
99
|
+
<Text style={styles.listText} selectable>{formatInlineText(bulletMatch[2])}</Text>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Horizontal rule (---, ***, ___)
|
|
107
|
+
if (/^[-*_]{3,}$/.test(trimmed)) {
|
|
108
|
+
elements.push(<View key={i} style={styles.horizontalRule} />);
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Empty line = paragraph break
|
|
114
|
+
if (!trimmed) {
|
|
115
|
+
elements.push(<View key={i} style={styles.paragraphBreak} />);
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Regular text
|
|
121
|
+
elements.push(
|
|
122
|
+
<Text key={i} style={styles.responseText} selectable>{formatInlineText(line)}</Text>
|
|
123
|
+
);
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<View style={styles.formattedContainer}>
|
|
129
|
+
{elements}
|
|
130
|
+
{isStreaming && <View style={styles.cursor} />}
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Format inline elements: code spans first, then links, then emphasis
|
|
136
|
+
function formatInlineText(text: string): React.ReactNode {
|
|
137
|
+
// Split on inline code first to avoid processing markdown inside code spans
|
|
138
|
+
const codeParts = text.split(/(`[^`]+`)/g);
|
|
139
|
+
if (codeParts.length === 1) {
|
|
140
|
+
return formatLinks(text);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return codeParts.map((part, i) => {
|
|
144
|
+
if (part.startsWith('`') && part.endsWith('`')) {
|
|
145
|
+
return <Text key={i} style={styles.inlineCode}>{part.slice(1, -1)}</Text>;
|
|
146
|
+
}
|
|
147
|
+
return <React.Fragment key={i}>{formatLinks(part)}</React.Fragment>;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Format markdown links [text](url)
|
|
152
|
+
function formatLinks(text: string): React.ReactNode {
|
|
153
|
+
const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
|
|
154
|
+
if (parts.length === 1) return formatEmphasis(text);
|
|
155
|
+
|
|
156
|
+
return parts.map((part, i) => {
|
|
157
|
+
const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
158
|
+
if (linkMatch) {
|
|
159
|
+
return (
|
|
160
|
+
<Text
|
|
161
|
+
key={i}
|
|
162
|
+
style={styles.linkText}
|
|
163
|
+
onPress={() => Linking.openURL(linkMatch[2])}
|
|
164
|
+
>
|
|
165
|
+
{linkMatch[1]}
|
|
166
|
+
</Text>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return <React.Fragment key={i}>{formatEmphasis(part)}</React.Fragment>;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Format bold, italic, and strikethrough emphasis
|
|
174
|
+
function formatEmphasis(text: string): React.ReactNode {
|
|
175
|
+
// Match **bold**, ~~strikethrough~~, *italic*, and plain text segments
|
|
176
|
+
const parts = text.split(/(\*\*[^*]+\*\*|~~[^~]+~~|\*[^*]+\*)/g);
|
|
177
|
+
if (parts.length === 1) return text;
|
|
178
|
+
|
|
179
|
+
return parts.map((part, i) => {
|
|
180
|
+
if (part.startsWith('**') && part.endsWith('**')) {
|
|
181
|
+
return <Text key={i} style={styles.boldText}>{part.slice(2, -2)}</Text>;
|
|
182
|
+
}
|
|
183
|
+
if (part.startsWith('~~') && part.endsWith('~~')) {
|
|
184
|
+
return <Text key={i} style={styles.strikethroughText}>{part.slice(2, -2)}</Text>;
|
|
185
|
+
}
|
|
186
|
+
if (part.startsWith('*') && part.endsWith('*')) {
|
|
187
|
+
return <Text key={i} style={styles.italicText}>{part.slice(1, -1)}</Text>;
|
|
188
|
+
}
|
|
189
|
+
return part;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
formattedContainer: {
|
|
195
|
+
flexDirection: "column",
|
|
196
|
+
},
|
|
197
|
+
responseText: {
|
|
198
|
+
color: "rgba(255,255,255,0.95)",
|
|
199
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
200
|
+
lineHeight: 22,
|
|
201
|
+
},
|
|
202
|
+
cursor: {
|
|
203
|
+
width: 2,
|
|
204
|
+
height: 18,
|
|
205
|
+
backgroundColor: COLORS.TEXT_PRIMARY,
|
|
206
|
+
marginLeft: 2,
|
|
207
|
+
opacity: 0.7,
|
|
208
|
+
},
|
|
209
|
+
listItem: {
|
|
210
|
+
flexDirection: "row",
|
|
211
|
+
marginVertical: SPACING.XS / 2,
|
|
212
|
+
paddingLeft: SPACING.SM,
|
|
213
|
+
},
|
|
214
|
+
listNumber: {
|
|
215
|
+
color: COLORS.TEXT_MUTED,
|
|
216
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
217
|
+
lineHeight: 22,
|
|
218
|
+
width: 24,
|
|
219
|
+
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
220
|
+
},
|
|
221
|
+
listBullet: {
|
|
222
|
+
color: COLORS.TEXT_MUTED,
|
|
223
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
224
|
+
lineHeight: 22,
|
|
225
|
+
width: 18,
|
|
226
|
+
},
|
|
227
|
+
listText: {
|
|
228
|
+
flex: 1,
|
|
229
|
+
color: "rgba(255,255,255,0.95)",
|
|
230
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
231
|
+
lineHeight: 22,
|
|
232
|
+
},
|
|
233
|
+
codeBlock: {
|
|
234
|
+
backgroundColor: "rgba(255,255,255,0.06)",
|
|
235
|
+
borderRadius: SPACING.SM,
|
|
236
|
+
padding: SPACING.MD,
|
|
237
|
+
marginVertical: SPACING.SM,
|
|
238
|
+
},
|
|
239
|
+
codeLang: {
|
|
240
|
+
color: COLORS.TEXT_MUTED,
|
|
241
|
+
fontSize: TYPOGRAPHY.SIZE_XS,
|
|
242
|
+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
243
|
+
marginBottom: SPACING.XS,
|
|
244
|
+
textTransform: "uppercase",
|
|
245
|
+
},
|
|
246
|
+
codeText: {
|
|
247
|
+
color: "rgba(255,255,255,0.85)",
|
|
248
|
+
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
249
|
+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
250
|
+
lineHeight: 18,
|
|
251
|
+
},
|
|
252
|
+
inlineCode: {
|
|
253
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
254
|
+
color: "rgba(255,255,255,0.9)",
|
|
255
|
+
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
|
256
|
+
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
257
|
+
paddingHorizontal: 4,
|
|
258
|
+
borderRadius: 3,
|
|
259
|
+
},
|
|
260
|
+
heading1: {
|
|
261
|
+
color: COLORS.TEXT_PRIMARY,
|
|
262
|
+
fontSize: TYPOGRAPHY.SIZE_XL,
|
|
263
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
264
|
+
lineHeight: 24,
|
|
265
|
+
marginTop: SPACING.MD,
|
|
266
|
+
marginBottom: SPACING.XS,
|
|
267
|
+
},
|
|
268
|
+
heading2: {
|
|
269
|
+
color: "rgba(255,255,255,0.9)",
|
|
270
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
271
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
272
|
+
lineHeight: 22,
|
|
273
|
+
marginTop: SPACING.SM,
|
|
274
|
+
marginBottom: SPACING.XS,
|
|
275
|
+
},
|
|
276
|
+
heading3: {
|
|
277
|
+
color: "rgba(255,255,255,0.85)",
|
|
278
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
279
|
+
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
280
|
+
lineHeight: 22,
|
|
281
|
+
marginTop: SPACING.SM,
|
|
282
|
+
marginBottom: SPACING.XS,
|
|
283
|
+
},
|
|
284
|
+
boldText: {
|
|
285
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
286
|
+
color: COLORS.TEXT_PRIMARY,
|
|
287
|
+
},
|
|
288
|
+
italicText: {
|
|
289
|
+
fontStyle: "italic",
|
|
290
|
+
color: "rgba(255,255,255,0.85)",
|
|
291
|
+
},
|
|
292
|
+
strikethroughText: {
|
|
293
|
+
textDecorationLine: "line-through",
|
|
294
|
+
color: COLORS.TEXT_MUTED,
|
|
295
|
+
},
|
|
296
|
+
linkText: {
|
|
297
|
+
color: COLORS.STATUS_INFO,
|
|
298
|
+
textDecorationLine: "underline",
|
|
299
|
+
},
|
|
300
|
+
blockquote: {
|
|
301
|
+
borderLeftWidth: 3,
|
|
302
|
+
borderLeftColor: "rgba(255,255,255,0.15)",
|
|
303
|
+
paddingLeft: SPACING.MD,
|
|
304
|
+
marginVertical: SPACING.XS,
|
|
305
|
+
},
|
|
306
|
+
blockquoteText: {
|
|
307
|
+
color: COLORS.TEXT_SECONDARY,
|
|
308
|
+
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
309
|
+
lineHeight: 22,
|
|
310
|
+
fontStyle: "italic",
|
|
311
|
+
},
|
|
312
|
+
horizontalRule: {
|
|
313
|
+
height: 1,
|
|
314
|
+
backgroundColor: COLORS.BORDER,
|
|
315
|
+
marginVertical: SPACING.MD,
|
|
316
|
+
},
|
|
317
|
+
paragraphBreak: {
|
|
318
|
+
height: SPACING.SM,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from "react";
|
|
2
|
+
import { View, Text, StyleSheet, NativeModules, TouchableOpacity, Animated, Easing, type TextProps, type ViewProps } from "react-native";
|
|
3
|
+
import type { ConnectionStatus } from "../services/websocket";
|
|
4
|
+
import { SPACING, LAYOUT, COLORS, TYPOGRAPHY, SIZES } from "../constants/design";
|
|
5
|
+
|
|
6
|
+
// Typed animated components for React 19 compatibility
|
|
7
|
+
const AnimatedView = Animated.View as React.ComponentClass<Animated.AnimatedProps<ViewProps>>;
|
|
8
|
+
|
|
9
|
+
// WidgetBridge is a simple native module available in the widget runtime
|
|
10
|
+
// ExpoAir is the main app's module (fallback)
|
|
11
|
+
const { WidgetBridge, ExpoAir } = NativeModules;
|
|
12
|
+
|
|
13
|
+
function handleCollapse() {
|
|
14
|
+
try {
|
|
15
|
+
// Try WidgetBridge first (widget runtime), then ExpoAir (main app)
|
|
16
|
+
if (WidgetBridge?.collapse) {
|
|
17
|
+
WidgetBridge.collapse();
|
|
18
|
+
} else if (ExpoAir?.collapse) {
|
|
19
|
+
ExpoAir.collapse();
|
|
20
|
+
} else {
|
|
21
|
+
console.warn("[expo-air] No collapse method available");
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.warn("[expo-air] Failed to collapse:", e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface HeaderProps {
|
|
29
|
+
status: ConnectionStatus;
|
|
30
|
+
branchName: string;
|
|
31
|
+
onBranchPress: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Header({ status, branchName, onBranchPress }: HeaderProps) {
|
|
35
|
+
const statusColors = {
|
|
36
|
+
disconnected: COLORS.STATUS_ERROR,
|
|
37
|
+
connecting: COLORS.STATUS_INFO,
|
|
38
|
+
connected: COLORS.STATUS_SUCCESS,
|
|
39
|
+
processing: COLORS.STATUS_INFO,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.header}>
|
|
44
|
+
<TouchableOpacity onPress={handleCollapse} style={styles.closeButton}>
|
|
45
|
+
<Text style={styles.closeButtonText}>✕</Text>
|
|
46
|
+
</TouchableOpacity>
|
|
47
|
+
|
|
48
|
+
<TouchableOpacity style={styles.branchButton} onPress={onBranchPress} disabled={!branchName}>
|
|
49
|
+
{branchName ? (
|
|
50
|
+
<>
|
|
51
|
+
<Text style={styles.branchName} numberOfLines={1}>
|
|
52
|
+
{branchName}
|
|
53
|
+
</Text>
|
|
54
|
+
<Text style={styles.branchChevron}>▾</Text>
|
|
55
|
+
</>
|
|
56
|
+
) : (
|
|
57
|
+
<View style={styles.branchLoadingBar} />
|
|
58
|
+
)}
|
|
59
|
+
</TouchableOpacity>
|
|
60
|
+
|
|
61
|
+
<View style={[styles.statusDot, { backgroundColor: statusColors[status] }]} />
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function PulsingIndicator({ status }: { status: ConnectionStatus }) {
|
|
67
|
+
const colors = {
|
|
68
|
+
disconnected: COLORS.STATUS_ERROR,
|
|
69
|
+
connecting: COLORS.STATUS_INFO,
|
|
70
|
+
connected: COLORS.STATUS_SUCCESS,
|
|
71
|
+
processing: COLORS.STATUS_INFO,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const isAnimating = status === "processing" || status === "connecting";
|
|
75
|
+
|
|
76
|
+
// Animated values for the pulsing ring
|
|
77
|
+
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
78
|
+
const opacityAnim = useRef(new Animated.Value(0.4)).current;
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (isAnimating) {
|
|
82
|
+
// Create a soft pulsing animation
|
|
83
|
+
const pulseAnimation = Animated.loop(
|
|
84
|
+
Animated.sequence([
|
|
85
|
+
Animated.parallel([
|
|
86
|
+
Animated.timing(scaleAnim, {
|
|
87
|
+
toValue: 1.3,
|
|
88
|
+
duration: 1200,
|
|
89
|
+
easing: Easing.inOut(Easing.ease),
|
|
90
|
+
useNativeDriver: true,
|
|
91
|
+
}),
|
|
92
|
+
Animated.timing(opacityAnim, {
|
|
93
|
+
toValue: 0,
|
|
94
|
+
duration: 1200,
|
|
95
|
+
easing: Easing.inOut(Easing.ease),
|
|
96
|
+
useNativeDriver: true,
|
|
97
|
+
}),
|
|
98
|
+
]),
|
|
99
|
+
Animated.parallel([
|
|
100
|
+
Animated.timing(scaleAnim, {
|
|
101
|
+
toValue: 1,
|
|
102
|
+
duration: 0,
|
|
103
|
+
useNativeDriver: true,
|
|
104
|
+
}),
|
|
105
|
+
Animated.timing(opacityAnim, {
|
|
106
|
+
toValue: 0.4,
|
|
107
|
+
duration: 0,
|
|
108
|
+
useNativeDriver: true,
|
|
109
|
+
}),
|
|
110
|
+
]),
|
|
111
|
+
])
|
|
112
|
+
);
|
|
113
|
+
pulseAnimation.start();
|
|
114
|
+
return () => pulseAnimation.stop();
|
|
115
|
+
} else {
|
|
116
|
+
// Reset when not animating
|
|
117
|
+
scaleAnim.setValue(1);
|
|
118
|
+
opacityAnim.setValue(0.4);
|
|
119
|
+
}
|
|
120
|
+
}, [isAnimating, scaleAnim, opacityAnim]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={styles.indicatorContainer}>
|
|
124
|
+
<View
|
|
125
|
+
style={[
|
|
126
|
+
styles.indicator,
|
|
127
|
+
{ backgroundColor: colors[status] },
|
|
128
|
+
]}
|
|
129
|
+
/>
|
|
130
|
+
{isAnimating && (
|
|
131
|
+
<AnimatedView
|
|
132
|
+
style={[
|
|
133
|
+
styles.indicatorRing,
|
|
134
|
+
{
|
|
135
|
+
borderColor: colors[status],
|
|
136
|
+
transform: [{ scale: scaleAnim }],
|
|
137
|
+
opacity: opacityAnim,
|
|
138
|
+
},
|
|
139
|
+
]}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const styles = StyleSheet.create({
|
|
147
|
+
header: {
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
alignItems: "center",
|
|
150
|
+
paddingHorizontal: LAYOUT.CONTENT_PADDING_H,
|
|
151
|
+
paddingVertical: SPACING.MD + 2, // 14px for comfortable header height
|
|
152
|
+
borderBottomWidth: 1,
|
|
153
|
+
borderBottomColor: COLORS.BORDER,
|
|
154
|
+
},
|
|
155
|
+
closeButton: {
|
|
156
|
+
width: SIZES.CLOSE_BUTTON,
|
|
157
|
+
height: SIZES.CLOSE_BUTTON,
|
|
158
|
+
borderRadius: SIZES.CLOSE_BUTTON / 2,
|
|
159
|
+
// Make invisible - native close button handles the tap
|
|
160
|
+
backgroundColor: "transparent",
|
|
161
|
+
alignItems: "center",
|
|
162
|
+
justifyContent: "center",
|
|
163
|
+
marginRight: SPACING.MD,
|
|
164
|
+
},
|
|
165
|
+
closeButtonText: {
|
|
166
|
+
// Hide the text - native button shows the X
|
|
167
|
+
color: "transparent",
|
|
168
|
+
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
169
|
+
fontWeight: TYPOGRAPHY.WEIGHT_SEMIBOLD,
|
|
170
|
+
},
|
|
171
|
+
branchButton: {
|
|
172
|
+
flex: 1,
|
|
173
|
+
flexDirection: "row",
|
|
174
|
+
alignItems: "center",
|
|
175
|
+
},
|
|
176
|
+
branchName: {
|
|
177
|
+
flexShrink: 1,
|
|
178
|
+
color: COLORS.TEXT_SECONDARY,
|
|
179
|
+
fontSize: TYPOGRAPHY.SIZE_MD,
|
|
180
|
+
fontWeight: TYPOGRAPHY.WEIGHT_MEDIUM,
|
|
181
|
+
},
|
|
182
|
+
branchChevron: {
|
|
183
|
+
color: COLORS.TEXT_MUTED,
|
|
184
|
+
fontSize: TYPOGRAPHY.SIZE_SM,
|
|
185
|
+
marginLeft: SPACING.XS,
|
|
186
|
+
},
|
|
187
|
+
branchLoadingBar: {
|
|
188
|
+
width: 80,
|
|
189
|
+
height: 12,
|
|
190
|
+
borderRadius: 6,
|
|
191
|
+
backgroundColor: "rgba(255,255,255,0.08)",
|
|
192
|
+
},
|
|
193
|
+
statusDot: {
|
|
194
|
+
width: SIZES.STATUS_DOT,
|
|
195
|
+
height: SIZES.STATUS_DOT,
|
|
196
|
+
borderRadius: SIZES.STATUS_DOT / 2,
|
|
197
|
+
marginLeft: SPACING.MD, // Match the closeButton marginRight for visual balance
|
|
198
|
+
},
|
|
199
|
+
collapsedPill: {
|
|
200
|
+
width: 100,
|
|
201
|
+
height: 32,
|
|
202
|
+
backgroundColor: "transparent",
|
|
203
|
+
alignItems: "center",
|
|
204
|
+
justifyContent: "center",
|
|
205
|
+
},
|
|
206
|
+
indicatorContainer: {
|
|
207
|
+
width: 20,
|
|
208
|
+
height: 20,
|
|
209
|
+
alignItems: "center",
|
|
210
|
+
justifyContent: "center",
|
|
211
|
+
},
|
|
212
|
+
indicator: {
|
|
213
|
+
width: SIZES.STATUS_DOT,
|
|
214
|
+
height: SIZES.STATUS_DOT,
|
|
215
|
+
borderRadius: SIZES.STATUS_DOT / 2,
|
|
216
|
+
},
|
|
217
|
+
indicatorRing: {
|
|
218
|
+
position: "absolute",
|
|
219
|
+
width: 16,
|
|
220
|
+
height: 16,
|
|
221
|
+
borderRadius: 8,
|
|
222
|
+
borderWidth: 1.5,
|
|
223
|
+
opacity: 0.4,
|
|
224
|
+
},
|
|
225
|
+
});
|