clawdex-mobile 2.0.1 → 4.0.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 (81) hide show
  1. package/.github/workflows/ci.yml +4 -3
  2. package/.github/workflows/npm-release.yml +62 -2
  3. package/.github/workflows/pages.yml +41 -0
  4. package/AGENTS.md +263 -110
  5. package/README.md +15 -4
  6. package/apps/mobile/.env.example +2 -2
  7. package/apps/mobile/App.tsx +175 -14
  8. package/apps/mobile/app.json +27 -9
  9. package/apps/mobile/eas.json +14 -4
  10. package/apps/mobile/package.json +14 -13
  11. package/apps/mobile/src/api/__tests__/chatMapping.test.ts +219 -0
  12. package/apps/mobile/src/api/__tests__/client.test.ts +587 -6
  13. package/apps/mobile/src/api/__tests__/ws.test.ts +27 -0
  14. package/apps/mobile/src/api/account.ts +47 -0
  15. package/apps/mobile/src/api/chatMapping.ts +435 -18
  16. package/apps/mobile/src/api/client.ts +321 -36
  17. package/apps/mobile/src/api/rateLimits.ts +143 -0
  18. package/apps/mobile/src/api/types.ts +107 -0
  19. package/apps/mobile/src/api/ws.ts +10 -1
  20. package/apps/mobile/src/components/ChatHeader.tsx +12 -12
  21. package/apps/mobile/src/components/ChatInput.tsx +154 -88
  22. package/apps/mobile/src/components/ChatMessage.tsx +548 -93
  23. package/apps/mobile/src/components/ComposerUsageLimits.tsx +167 -0
  24. package/apps/mobile/src/components/SelectionSheet.tsx +466 -0
  25. package/apps/mobile/src/components/ToolBlock.tsx +17 -15
  26. package/apps/mobile/src/components/VoiceRecordingWaveform.tsx +181 -0
  27. package/apps/mobile/src/components/WorkspacePickerModal.tsx +812 -0
  28. package/apps/mobile/src/components/__tests__/chat-input-layout.test.ts +35 -0
  29. package/apps/mobile/src/components/__tests__/chatImageSource.test.ts +44 -0
  30. package/apps/mobile/src/components/__tests__/composerUsageLimits.test.ts +138 -0
  31. package/apps/mobile/src/components/__tests__/voiceWaveform.test.ts +31 -0
  32. package/apps/mobile/src/components/chat-input-layout.ts +59 -0
  33. package/apps/mobile/src/components/chatImageSource.ts +86 -0
  34. package/apps/mobile/src/components/usageLimitBadges.ts +109 -0
  35. package/apps/mobile/src/components/voiceWaveform.ts +46 -0
  36. package/apps/mobile/src/config.ts +9 -2
  37. package/apps/mobile/src/hooks/useVoiceRecorder.ts +8 -1
  38. package/apps/mobile/src/navigation/DrawerContent.tsx +607 -457
  39. package/apps/mobile/src/navigation/__tests__/chatThreadTree.test.ts +89 -0
  40. package/apps/mobile/src/navigation/__tests__/drawerChats.test.ts +65 -0
  41. package/apps/mobile/src/navigation/chatThreadTree.ts +191 -0
  42. package/apps/mobile/src/navigation/drawerChats.ts +9 -0
  43. package/apps/mobile/src/screens/GitScreen.tsx +2 -0
  44. package/apps/mobile/src/screens/MainScreen.tsx +4239 -1237
  45. package/apps/mobile/src/screens/OnboardingScreen.tsx +924 -310
  46. package/apps/mobile/src/screens/SettingsScreen.tsx +256 -226
  47. package/apps/mobile/src/screens/TerminalScreen.tsx +2 -5
  48. package/apps/mobile/src/screens/__tests__/agentThreadDisplay.test.ts +80 -0
  49. package/apps/mobile/src/screens/__tests__/agentThreads.test.ts +170 -0
  50. package/apps/mobile/src/screens/__tests__/planCardState.test.ts +88 -0
  51. package/apps/mobile/src/screens/__tests__/subAgentTranscript.test.ts +102 -0
  52. package/apps/mobile/src/screens/__tests__/transcriptMessages.test.ts +97 -0
  53. package/apps/mobile/src/screens/agentThreadDisplay.ts +261 -0
  54. package/apps/mobile/src/screens/agentThreads.ts +167 -0
  55. package/apps/mobile/src/screens/planCardState.ts +40 -0
  56. package/apps/mobile/src/screens/subAgentTranscript.ts +149 -0
  57. package/apps/mobile/src/screens/transcriptMessages.ts +102 -0
  58. package/apps/mobile/src/theme.ts +6 -12
  59. package/bin/clawdex.js +7 -6
  60. package/codex-rust-bridge +0 -0
  61. package/codex-rust-bridge.exe +0 -0
  62. package/docs/codex-app-server-cli-gap-tracker.md +14 -5
  63. package/docs/privacy-policy.md +54 -0
  64. package/docs/setup-and-operations.md +21 -15
  65. package/docs/terms-of-service.md +33 -0
  66. package/docs/troubleshooting.md +15 -19
  67. package/package.json +6 -5
  68. package/scripts/bridge-binary.js +194 -0
  69. package/scripts/setup-wizard.sh +17 -186
  70. package/scripts/start-bridge-secure.js +240 -0
  71. package/scripts/start-bridge-secure.sh +1 -40
  72. package/services/mac-bridge/package.json +6 -6
  73. package/services/rust-bridge/Cargo.lock +56 -47
  74. package/services/rust-bridge/Cargo.toml +1 -1
  75. package/services/rust-bridge/package.json +1 -1
  76. package/services/rust-bridge/src/main.rs +517 -9
  77. package/site/index.html +54 -0
  78. package/site/privacy/index.html +80 -0
  79. package/site/styles.css +135 -0
  80. package/site/support/index.html +51 -0
  81. package/site/terms/index.html +68 -0
@@ -0,0 +1,812 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ ActivityIndicator,
5
+ Modal,
6
+ Pressable,
7
+ ScrollView,
8
+ StyleSheet,
9
+ Text,
10
+ TextInput,
11
+ useWindowDimensions,
12
+ View,
13
+ } from 'react-native';
14
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
+
16
+ import type { FileSystemEntry, WorkspaceSummary } from '../api/types';
17
+ import { colors, radius, spacing, typography } from '../theme';
18
+
19
+ interface WorkspacePickerModalProps {
20
+ visible: boolean;
21
+ selectedPath?: string | null;
22
+ bridgeRoot?: string | null;
23
+ recentWorkspaces: WorkspaceSummary[];
24
+ currentPath?: string | null;
25
+ parentPath?: string | null;
26
+ entries: FileSystemEntry[];
27
+ loadingRecent?: boolean;
28
+ loadingEntries?: boolean;
29
+ error?: string | null;
30
+ onBrowsePath: (path: string | null) => void;
31
+ onSelectPath: (path: string | null) => void;
32
+ onClose: () => void;
33
+ }
34
+
35
+ interface BreadcrumbItem {
36
+ key: string;
37
+ label: string;
38
+ path: string;
39
+ }
40
+
41
+ export function WorkspacePickerModal({
42
+ visible,
43
+ selectedPath = null,
44
+ bridgeRoot = null,
45
+ recentWorkspaces,
46
+ currentPath = null,
47
+ parentPath = null,
48
+ entries,
49
+ loadingRecent = false,
50
+ loadingEntries = false,
51
+ error = null,
52
+ onBrowsePath,
53
+ onSelectPath,
54
+ onClose,
55
+ }: WorkspacePickerModalProps) {
56
+ const insets = useSafeAreaInsets();
57
+ const { height: windowHeight } = useWindowDimensions();
58
+ const [searchQuery, setSearchQuery] = useState('');
59
+ const topInset = Math.max(insets.top + spacing.lg, 72);
60
+ const bottomInset = Math.max(insets.bottom + spacing.lg, 72);
61
+ const cardHeight = Math.min(
62
+ Math.max(560, Math.round(windowHeight * 0.82)),
63
+ windowHeight - topInset - bottomInset
64
+ );
65
+
66
+ useEffect(() => {
67
+ if (!visible) {
68
+ setSearchQuery('');
69
+ }
70
+ }, [visible]);
71
+
72
+ const normalizedSearch = searchQuery.trim().toLowerCase();
73
+ const filteredRecentWorkspaces = recentWorkspaces.filter((workspace) =>
74
+ matchesSearch([workspace.path, toPathBasename(workspace.path)], normalizedSearch)
75
+ );
76
+ const filteredEntries = entries.filter((entry) =>
77
+ matchesSearch([entry.name, entry.path], normalizedSearch)
78
+ );
79
+ const breadcrumbs = buildPathBreadcrumbs(currentPath ?? bridgeRoot);
80
+ const footerPath = currentPath ?? bridgeRoot ?? 'Bridge default workspace';
81
+
82
+ return (
83
+ <Modal
84
+ visible={visible}
85
+ transparent
86
+ animationType="fade"
87
+ presentationStyle="overFullScreen"
88
+ onRequestClose={onClose}
89
+ >
90
+ <View style={styles.backdrop}>
91
+ <Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
92
+ <View style={[styles.outer, { paddingTop: topInset, paddingBottom: bottomInset }]}>
93
+ <View style={[styles.card, { height: cardHeight }]}>
94
+ <View style={styles.header}>
95
+ <View style={styles.headerSpacer} />
96
+ <Text style={styles.title}>Choose Directory</Text>
97
+ <Pressable
98
+ onPress={onClose}
99
+ style={({ pressed }) => [styles.closeButton, pressed && styles.pressed]}
100
+ >
101
+ <Ionicons name="close" size={18} color={colors.textSecondary} />
102
+ </Pressable>
103
+ </View>
104
+
105
+ <View style={styles.body}>
106
+ <View style={styles.connectionRow}>
107
+ <Text style={styles.connectionText} numberOfLines={1}>
108
+ {bridgeRoot ? `Bridge root: ${bridgeRoot}` : 'Browse folders on the bridge host'}
109
+ </Text>
110
+ <Pressable
111
+ onPress={() => onSelectPath(null)}
112
+ style={({ pressed }) => [
113
+ styles.defaultButton,
114
+ selectedPath === null && styles.defaultButtonSelected,
115
+ pressed && styles.pressed,
116
+ ]}
117
+ >
118
+ <Text
119
+ style={[
120
+ styles.defaultButtonText,
121
+ selectedPath === null && styles.defaultButtonTextSelected,
122
+ ]}
123
+ >
124
+ {selectedPath === null ? 'Default' : 'Use default'}
125
+ </Text>
126
+ </Pressable>
127
+ </View>
128
+
129
+ <View style={styles.searchField}>
130
+ <Ionicons name="search" size={16} color={colors.textMuted} />
131
+ <TextInput
132
+ value={searchQuery}
133
+ onChangeText={setSearchQuery}
134
+ keyboardAppearance="dark"
135
+ placeholder="Search folders"
136
+ placeholderTextColor={colors.textMuted}
137
+ style={styles.searchInput}
138
+ autoCapitalize="none"
139
+ autoCorrect={false}
140
+ returnKeyType="search"
141
+ />
142
+ </View>
143
+
144
+ <View style={styles.sectionHeader}>
145
+ <Text style={styles.sectionTitle}>Recent Directories</Text>
146
+ </View>
147
+
148
+ <View style={styles.recentCard}>
149
+ {loadingRecent ? (
150
+ <LoadingRow label="Refreshing recent directories..." compact />
151
+ ) : filteredRecentWorkspaces.length > 0 ? (
152
+ <ScrollView
153
+ showsVerticalScrollIndicator={false}
154
+ keyboardShouldPersistTaps="handled"
155
+ >
156
+ {filteredRecentWorkspaces.map((workspace, index) => (
157
+ <Pressable
158
+ key={workspace.path}
159
+ onPress={() => onBrowsePath(workspace.path)}
160
+ style={({ pressed }) => [
161
+ styles.recentRow,
162
+ workspace.path === currentPath && styles.recentRowSelected,
163
+ index === filteredRecentWorkspaces.length - 1 &&
164
+ styles.recentRowLast,
165
+ pressed && styles.pressed,
166
+ ]}
167
+ >
168
+ <View style={styles.recentIconWrap}>
169
+ <Ionicons name="time-outline" size={16} color={colors.textSecondary} />
170
+ </View>
171
+ <View style={styles.recentCopy}>
172
+ <Text style={styles.recentTitle} numberOfLines={1}>
173
+ {toPathBasename(workspace.path)}
174
+ </Text>
175
+ <Text style={styles.recentPath} numberOfLines={1}>
176
+ {workspace.path}
177
+ </Text>
178
+ </View>
179
+ <Text style={styles.recentMeta}>
180
+ {formatWorkspaceMeta(workspace)}
181
+ </Text>
182
+ </Pressable>
183
+ ))}
184
+ </ScrollView>
185
+ ) : (
186
+ <EmptyRow
187
+ label={
188
+ normalizedSearch
189
+ ? 'No recent directories match this search.'
190
+ : 'No recent directories yet.'
191
+ }
192
+ compact
193
+ />
194
+ )}
195
+ </View>
196
+
197
+ <Text style={styles.helperText}>Open a recent folder or browse below.</Text>
198
+
199
+ <View style={styles.breadcrumbRow}>
200
+ <Pressable
201
+ onPress={() => parentPath && onBrowsePath(parentPath)}
202
+ disabled={!parentPath || loadingEntries}
203
+ style={({ pressed }) => [
204
+ styles.upButton,
205
+ (!parentPath || loadingEntries) && styles.buttonDisabled,
206
+ pressed && parentPath && !loadingEntries && styles.pressed,
207
+ ]}
208
+ >
209
+ <Ionicons name="return-up-back" size={14} color={colors.textSecondary} />
210
+ <Text style={styles.upButtonText}>Up one level</Text>
211
+ </Pressable>
212
+
213
+ <ScrollView
214
+ horizontal
215
+ showsHorizontalScrollIndicator={false}
216
+ contentContainerStyle={styles.breadcrumbScroll}
217
+ >
218
+ {breadcrumbs.length > 0
219
+ ? breadcrumbs.map((item, index) => {
220
+ const isLast = index === breadcrumbs.length - 1;
221
+ return (
222
+ <View key={item.key} style={styles.breadcrumbItem}>
223
+ {index > 0 ? <Text style={styles.breadcrumbSlash}>/</Text> : null}
224
+ <Pressable
225
+ onPress={() => onBrowsePath(item.path)}
226
+ style={({ pressed }) => [
227
+ styles.breadcrumbChip,
228
+ isLast && styles.breadcrumbChipActive,
229
+ pressed && styles.pressed,
230
+ ]}
231
+ >
232
+ <Text
233
+ style={[
234
+ styles.breadcrumbText,
235
+ isLast && styles.breadcrumbTextActive,
236
+ ]}
237
+ >
238
+ {item.label}
239
+ </Text>
240
+ </Pressable>
241
+ </View>
242
+ );
243
+ })
244
+ : (
245
+ <Text style={styles.breadcrumbEmpty}>Loading path...</Text>
246
+ )}
247
+ </ScrollView>
248
+ </View>
249
+
250
+ {error ? <Text style={styles.errorText}>{error}</Text> : null}
251
+
252
+ <View style={styles.browserCard}>
253
+ {loadingEntries ? (
254
+ <LoadingRow label="Loading folders..." />
255
+ ) : filteredEntries.length > 0 ? (
256
+ <ScrollView
257
+ style={styles.entryListScroll}
258
+ contentContainerStyle={styles.entryListContent}
259
+ showsVerticalScrollIndicator={false}
260
+ keyboardShouldPersistTaps="handled"
261
+ >
262
+ {filteredEntries.map((entry, index) => (
263
+ <Pressable
264
+ key={entry.path}
265
+ onPress={() => onBrowsePath(entry.path)}
266
+ style={({ pressed }) => [
267
+ styles.entryRow,
268
+ entry.path === selectedPath && styles.entryRowSelected,
269
+ index === filteredEntries.length - 1 && styles.entryRowLast,
270
+ pressed && styles.pressed,
271
+ ]}
272
+ >
273
+ <View style={styles.entryIconWrap}>
274
+ <Ionicons
275
+ name={entry.isGitRepo ? 'git-branch-outline' : 'folder-outline'}
276
+ size={18}
277
+ color={colors.textSecondary}
278
+ />
279
+ </View>
280
+ <View style={styles.entryCopy}>
281
+ <Text style={styles.entryName} numberOfLines={1}>
282
+ {entry.name}
283
+ </Text>
284
+ </View>
285
+ <Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
286
+ </Pressable>
287
+ ))}
288
+ </ScrollView>
289
+ ) : (
290
+ <EmptyRow
291
+ label={
292
+ normalizedSearch
293
+ ? 'No folders match this search.'
294
+ : 'No folders found here.'
295
+ }
296
+ />
297
+ )}
298
+ </View>
299
+
300
+ <View style={styles.footer}>
301
+ <Text style={styles.footerPath} numberOfLines={1}>
302
+ {footerPath}
303
+ </Text>
304
+ <View style={styles.footerActions}>
305
+ <Pressable
306
+ onPress={onClose}
307
+ style={({ pressed }) => [
308
+ styles.footerButton,
309
+ styles.footerButtonSecondary,
310
+ pressed && styles.pressed,
311
+ ]}
312
+ >
313
+ <Text style={styles.footerButtonSecondaryText}>Cancel</Text>
314
+ </Pressable>
315
+ <Pressable
316
+ onPress={() => currentPath && onSelectPath(currentPath)}
317
+ disabled={!currentPath || loadingEntries}
318
+ style={({ pressed }) => [
319
+ styles.footerButton,
320
+ styles.footerButtonPrimary,
321
+ (!currentPath || loadingEntries) && styles.buttonDisabled,
322
+ pressed && currentPath && !loadingEntries && styles.footerButtonPrimaryPressed,
323
+ ]}
324
+ >
325
+ <Text style={styles.footerButtonPrimaryText}>Select Folder</Text>
326
+ </Pressable>
327
+ </View>
328
+ </View>
329
+ </View>
330
+ </View>
331
+ </View>
332
+ </View>
333
+ </Modal>
334
+ );
335
+ }
336
+
337
+ function LoadingRow({
338
+ label,
339
+ compact = false,
340
+ }: {
341
+ label: string;
342
+ compact?: boolean;
343
+ }) {
344
+ return (
345
+ <View style={[styles.statusRow, compact && styles.statusRowCompact]}>
346
+ <ActivityIndicator color={colors.textPrimary} />
347
+ <Text style={styles.statusText}>{label}</Text>
348
+ </View>
349
+ );
350
+ }
351
+
352
+ function EmptyRow({
353
+ label,
354
+ compact = false,
355
+ }: {
356
+ label: string;
357
+ compact?: boolean;
358
+ }) {
359
+ return (
360
+ <View style={[styles.statusRow, compact && styles.statusRowCompact]}>
361
+ <Text style={styles.statusText}>{label}</Text>
362
+ </View>
363
+ );
364
+ }
365
+
366
+ function toPathBasename(path: string): string {
367
+ const parts = path.split(/[\\/]/).filter(Boolean);
368
+ if (parts.length === 0) {
369
+ return path;
370
+ }
371
+ return parts[parts.length - 1] ?? path;
372
+ }
373
+
374
+ function matchesSearch(values: string[], query: string): boolean {
375
+ if (!query) {
376
+ return true;
377
+ }
378
+
379
+ return values.some((value) => value.toLowerCase().includes(query));
380
+ }
381
+
382
+ function buildPathBreadcrumbs(path: string | null): BreadcrumbItem[] {
383
+ if (!path) {
384
+ return [];
385
+ }
386
+
387
+ const normalized = path.replace(/\\/g, '/');
388
+ const driveMatch = normalized.match(/^[A-Za-z]:/);
389
+ const isAbsolute = normalized.startsWith('/');
390
+ let remainder = normalized;
391
+ const items: BreadcrumbItem[] = [];
392
+
393
+ if (driveMatch) {
394
+ const root = driveMatch[0];
395
+ items.push({ key: root, label: root, path: root });
396
+ remainder = normalized.slice(root.length).replace(/^\/+/, '');
397
+ } else if (isAbsolute) {
398
+ items.push({ key: '/', label: '/', path: '/' });
399
+ remainder = normalized.slice(1);
400
+ }
401
+
402
+ const parts = remainder.split('/').filter(Boolean);
403
+ let accumulated = driveMatch ? driveMatch[0] : isAbsolute ? '' : '';
404
+
405
+ for (const part of parts) {
406
+ accumulated = driveMatch
407
+ ? `${accumulated}/${part}`
408
+ : isAbsolute
409
+ ? `${accumulated}/${part}`
410
+ : accumulated
411
+ ? `${accumulated}/${part}`
412
+ : part;
413
+ items.push({
414
+ key: accumulated,
415
+ label: part,
416
+ path: accumulated,
417
+ });
418
+ }
419
+
420
+ return items;
421
+ }
422
+
423
+ function formatWorkspaceMeta(workspace: WorkspaceSummary): string {
424
+ const relative = formatRelativeTime(workspace.updatedAt);
425
+ if (relative) {
426
+ return relative;
427
+ }
428
+
429
+ if (workspace.chatCount === 1) {
430
+ return '1 chat';
431
+ }
432
+
433
+ return `${String(workspace.chatCount)} chats`;
434
+ }
435
+
436
+ function formatRelativeTime(iso?: string): string | null {
437
+ if (!iso) {
438
+ return null;
439
+ }
440
+
441
+ const timestamp = Date.parse(iso);
442
+ if (!Number.isFinite(timestamp)) {
443
+ return null;
444
+ }
445
+
446
+ const diffMs = Math.max(0, Date.now() - timestamp);
447
+ const seconds = Math.floor(diffMs / 1000);
448
+ const minutes = Math.floor(diffMs / 60000);
449
+ const hours = Math.floor(diffMs / 3600000);
450
+ const days = Math.floor(diffMs / 86400000);
451
+ const weeks = Math.floor(days / 7);
452
+
453
+ if (seconds < 10) return 'now';
454
+ if (seconds < 60) return `${String(seconds)} sec ago`;
455
+ if (minutes < 60) return `${String(minutes)} min ago`;
456
+ if (hours < 24) return `${String(hours)} hr ago`;
457
+ if (days < 7) return `${String(days)} ${days === 1 ? 'day' : 'days'} ago`;
458
+ if (weeks < 5) return `${String(weeks)} wk ago`;
459
+ return `${String(Math.floor(days / 30))} mo ago`;
460
+ }
461
+
462
+ const styles = StyleSheet.create({
463
+ backdrop: {
464
+ flex: 1,
465
+ backgroundColor: 'rgba(0, 0, 0, 0.62)',
466
+ },
467
+ outer: {
468
+ flex: 1,
469
+ justifyContent: 'center',
470
+ paddingHorizontal: spacing.lg,
471
+ },
472
+ card: {
473
+ borderRadius: 28,
474
+ borderCurve: 'continuous',
475
+ backgroundColor: '#07090C',
476
+ borderWidth: 1,
477
+ borderColor: 'rgba(255, 255, 255, 0.09)',
478
+ overflow: 'hidden',
479
+ boxShadow: '0 24px 44px rgba(0, 0, 0, 0.34)',
480
+ },
481
+ header: {
482
+ minHeight: 68,
483
+ flexDirection: 'row',
484
+ alignItems: 'center',
485
+ justifyContent: 'space-between',
486
+ paddingHorizontal: spacing.lg,
487
+ paddingTop: spacing.md,
488
+ },
489
+ headerSpacer: {
490
+ width: 36,
491
+ },
492
+ title: {
493
+ ...typography.headline,
494
+ fontSize: 18,
495
+ fontWeight: '700',
496
+ textAlign: 'center',
497
+ },
498
+ closeButton: {
499
+ width: 36,
500
+ height: 36,
501
+ borderRadius: radius.full,
502
+ alignItems: 'center',
503
+ justifyContent: 'center',
504
+ backgroundColor: 'rgba(255,255,255,0.04)',
505
+ borderWidth: 1,
506
+ borderColor: colors.borderLight,
507
+ },
508
+ body: {
509
+ flex: 1,
510
+ paddingHorizontal: spacing.lg,
511
+ paddingBottom: spacing.lg,
512
+ gap: spacing.md,
513
+ },
514
+ connectionRow: {
515
+ flexDirection: 'row',
516
+ alignItems: 'center',
517
+ gap: spacing.md,
518
+ },
519
+ connectionText: {
520
+ flex: 1,
521
+ ...typography.caption,
522
+ color: colors.textSecondary,
523
+ },
524
+ defaultButton: {
525
+ minHeight: 32,
526
+ paddingHorizontal: spacing.md,
527
+ borderRadius: radius.full,
528
+ borderWidth: 1,
529
+ borderColor: colors.border,
530
+ backgroundColor: colors.bgItem,
531
+ alignItems: 'center',
532
+ justifyContent: 'center',
533
+ },
534
+ defaultButtonSelected: {
535
+ borderColor: colors.borderHighlight,
536
+ backgroundColor: colors.bgInput,
537
+ },
538
+ defaultButtonText: {
539
+ ...typography.caption,
540
+ color: colors.textSecondary,
541
+ fontWeight: '600',
542
+ },
543
+ defaultButtonTextSelected: {
544
+ color: colors.textPrimary,
545
+ },
546
+ searchField: {
547
+ minHeight: 44,
548
+ borderRadius: radius.lg,
549
+ borderWidth: 1,
550
+ borderColor: colors.borderLight,
551
+ backgroundColor: colors.bgInput,
552
+ paddingHorizontal: spacing.md,
553
+ flexDirection: 'row',
554
+ alignItems: 'center',
555
+ gap: spacing.sm,
556
+ },
557
+ searchInput: {
558
+ flex: 1,
559
+ ...typography.body,
560
+ paddingVertical: 0,
561
+ },
562
+ breadcrumbRow: {
563
+ flexDirection: 'row',
564
+ alignItems: 'center',
565
+ gap: spacing.sm,
566
+ },
567
+ upButton: {
568
+ minHeight: 32,
569
+ paddingHorizontal: spacing.sm,
570
+ borderRadius: radius.full,
571
+ borderWidth: 1,
572
+ borderColor: colors.borderLight,
573
+ flexDirection: 'row',
574
+ alignItems: 'center',
575
+ gap: 6,
576
+ backgroundColor: colors.bgItem,
577
+ },
578
+ upButtonText: {
579
+ ...typography.caption,
580
+ color: colors.textSecondary,
581
+ fontWeight: '600',
582
+ },
583
+ breadcrumbScroll: {
584
+ alignItems: 'center',
585
+ paddingRight: spacing.md,
586
+ gap: spacing.xs,
587
+ },
588
+ breadcrumbItem: {
589
+ flexDirection: 'row',
590
+ alignItems: 'center',
591
+ gap: spacing.xs,
592
+ },
593
+ breadcrumbSlash: {
594
+ ...typography.mono,
595
+ color: colors.textMuted,
596
+ },
597
+ breadcrumbChip: {
598
+ minHeight: 30,
599
+ paddingHorizontal: spacing.sm,
600
+ borderRadius: radius.md,
601
+ justifyContent: 'center',
602
+ },
603
+ breadcrumbChipActive: {
604
+ backgroundColor: colors.bgInput,
605
+ borderWidth: 1,
606
+ borderColor: colors.borderHighlight,
607
+ },
608
+ breadcrumbText: {
609
+ ...typography.mono,
610
+ fontSize: 12,
611
+ color: colors.textSecondary,
612
+ },
613
+ breadcrumbTextActive: {
614
+ color: colors.textPrimary,
615
+ fontWeight: '700',
616
+ },
617
+ breadcrumbEmpty: {
618
+ ...typography.caption,
619
+ color: colors.textMuted,
620
+ },
621
+ sectionHeader: {
622
+ paddingTop: spacing.xs,
623
+ },
624
+ sectionTitle: {
625
+ ...typography.caption,
626
+ fontSize: 11,
627
+ color: colors.textSecondary,
628
+ fontWeight: '600',
629
+ textTransform: 'uppercase',
630
+ letterSpacing: 0.8,
631
+ },
632
+ recentCard: {
633
+ maxHeight: 184,
634
+ borderRadius: radius.lg,
635
+ borderWidth: 1,
636
+ borderColor: colors.borderLight,
637
+ backgroundColor: colors.bgItem,
638
+ overflow: 'hidden',
639
+ },
640
+ recentRow: {
641
+ minHeight: 54,
642
+ paddingHorizontal: spacing.md,
643
+ paddingVertical: spacing.sm,
644
+ flexDirection: 'row',
645
+ alignItems: 'center',
646
+ gap: spacing.sm,
647
+ borderBottomWidth: StyleSheet.hairlineWidth,
648
+ borderBottomColor: colors.borderLight,
649
+ },
650
+ recentRowSelected: {
651
+ backgroundColor: colors.bgInput,
652
+ },
653
+ recentRowLast: {
654
+ borderBottomWidth: 0,
655
+ },
656
+ recentIconWrap: {
657
+ width: 30,
658
+ height: 30,
659
+ borderRadius: radius.full,
660
+ alignItems: 'center',
661
+ justifyContent: 'center',
662
+ backgroundColor: colors.bgInput,
663
+ borderWidth: 1,
664
+ borderColor: colors.borderLight,
665
+ },
666
+ recentCopy: {
667
+ flex: 1,
668
+ gap: 2,
669
+ },
670
+ recentTitle: {
671
+ ...typography.body,
672
+ fontSize: 13,
673
+ lineHeight: 18,
674
+ fontWeight: '600',
675
+ },
676
+ recentPath: {
677
+ ...typography.caption,
678
+ fontSize: 11,
679
+ lineHeight: 15,
680
+ color: colors.textMuted,
681
+ },
682
+ recentMeta: {
683
+ ...typography.caption,
684
+ fontSize: 11,
685
+ lineHeight: 15,
686
+ color: colors.textSecondary,
687
+ fontWeight: '600',
688
+ },
689
+ helperText: {
690
+ ...typography.caption,
691
+ fontSize: 11,
692
+ lineHeight: 15,
693
+ color: colors.textMuted,
694
+ },
695
+ errorText: {
696
+ ...typography.caption,
697
+ color: '#FF8A8A',
698
+ },
699
+ browserCard: {
700
+ flex: 1,
701
+ minHeight: 228,
702
+ borderRadius: radius.lg,
703
+ borderWidth: 1,
704
+ borderColor: colors.borderLight,
705
+ backgroundColor: colors.bgItem,
706
+ overflow: 'hidden',
707
+ },
708
+ entryListScroll: {
709
+ flex: 1,
710
+ },
711
+ entryListContent: {
712
+ paddingVertical: spacing.xs,
713
+ },
714
+ entryRow: {
715
+ minHeight: 54,
716
+ paddingHorizontal: spacing.md,
717
+ flexDirection: 'row',
718
+ alignItems: 'center',
719
+ gap: spacing.sm,
720
+ borderBottomWidth: StyleSheet.hairlineWidth,
721
+ borderBottomColor: colors.borderLight,
722
+ },
723
+ entryRowSelected: {
724
+ backgroundColor: colors.bgInput,
725
+ },
726
+ entryRowLast: {
727
+ borderBottomWidth: 0,
728
+ },
729
+ entryIconWrap: {
730
+ width: 32,
731
+ height: 32,
732
+ borderRadius: radius.md,
733
+ alignItems: 'center',
734
+ justifyContent: 'center',
735
+ backgroundColor: colors.bgInput,
736
+ borderWidth: 1,
737
+ borderColor: colors.borderLight,
738
+ },
739
+ entryCopy: {
740
+ flex: 1,
741
+ },
742
+ entryName: {
743
+ ...typography.body,
744
+ fontSize: 13,
745
+ lineHeight: 18,
746
+ fontWeight: '600',
747
+ },
748
+ footer: {
749
+ gap: spacing.sm,
750
+ },
751
+ footerPath: {
752
+ ...typography.mono,
753
+ fontSize: 10,
754
+ lineHeight: 14,
755
+ color: colors.textMuted,
756
+ },
757
+ footerActions: {
758
+ flexDirection: 'row',
759
+ gap: spacing.sm,
760
+ },
761
+ footerButton: {
762
+ minHeight: 48,
763
+ borderRadius: radius.lg,
764
+ alignItems: 'center',
765
+ justifyContent: 'center',
766
+ paddingHorizontal: spacing.lg,
767
+ flex: 1,
768
+ },
769
+ footerButtonSecondary: {
770
+ backgroundColor: colors.bgMain,
771
+ borderWidth: 1,
772
+ borderColor: colors.border,
773
+ },
774
+ footerButtonPrimary: {
775
+ backgroundColor: colors.accent,
776
+ },
777
+ footerButtonPrimaryPressed: {
778
+ backgroundColor: colors.accentPressed,
779
+ },
780
+ footerButtonSecondaryText: {
781
+ ...typography.body,
782
+ color: colors.textSecondary,
783
+ fontWeight: '600',
784
+ },
785
+ footerButtonPrimaryText: {
786
+ ...typography.body,
787
+ color: colors.black,
788
+ fontWeight: '700',
789
+ },
790
+ statusRow: {
791
+ flex: 1,
792
+ minHeight: 132,
793
+ alignItems: 'center',
794
+ justifyContent: 'center',
795
+ gap: spacing.sm,
796
+ paddingHorizontal: spacing.lg,
797
+ },
798
+ statusRowCompact: {
799
+ minHeight: 96,
800
+ },
801
+ statusText: {
802
+ ...typography.body,
803
+ textAlign: 'center',
804
+ color: colors.textMuted,
805
+ },
806
+ buttonDisabled: {
807
+ opacity: 0.42,
808
+ },
809
+ pressed: {
810
+ opacity: 0.86,
811
+ },
812
+ });