clawdex-mobile 3.0.0 → 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.
@@ -1,4 +1,5 @@
1
1
  import { Ionicons } from '@expo/vector-icons';
2
+ import { useEffect, useState } from 'react';
2
3
  import {
3
4
  ActivityIndicator,
4
5
  Modal,
@@ -12,10 +13,7 @@ import {
12
13
  } from 'react-native';
13
14
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
14
15
 
15
- import type {
16
- FileSystemEntry,
17
- WorkspaceSummary,
18
- } from '../api/types';
16
+ import type { FileSystemEntry, WorkspaceSummary } from '../api/types';
19
17
  import { colors, radius, spacing, typography } from '../theme';
20
18
 
21
19
  interface WorkspacePickerModalProps {
@@ -26,16 +24,20 @@ interface WorkspacePickerModalProps {
26
24
  currentPath?: string | null;
27
25
  parentPath?: string | null;
28
26
  entries: FileSystemEntry[];
29
- draftPath: string;
30
27
  loadingRecent?: boolean;
31
28
  loadingEntries?: boolean;
32
29
  error?: string | null;
33
- onDraftPathChange: (value: string) => void;
34
30
  onBrowsePath: (path: string | null) => void;
35
31
  onSelectPath: (path: string | null) => void;
36
32
  onClose: () => void;
37
33
  }
38
34
 
35
+ interface BreadcrumbItem {
36
+ key: string;
37
+ label: string;
38
+ path: string;
39
+ }
40
+
39
41
  export function WorkspacePickerModal({
40
42
  visible,
41
43
  selectedPath = null,
@@ -44,24 +46,39 @@ export function WorkspacePickerModal({
44
46
  currentPath = null,
45
47
  parentPath = null,
46
48
  entries,
47
- draftPath,
48
49
  loadingRecent = false,
49
50
  loadingEntries = false,
50
51
  error = null,
51
- onDraftPathChange,
52
52
  onBrowsePath,
53
53
  onSelectPath,
54
54
  onClose,
55
55
  }: WorkspacePickerModalProps) {
56
56
  const insets = useSafeAreaInsets();
57
57
  const { height: windowHeight } = useWindowDimensions();
58
+ const [searchQuery, setSearchQuery] = useState('');
58
59
  const topInset = Math.max(insets.top + spacing.lg, 72);
59
60
  const bottomInset = Math.max(insets.bottom + spacing.lg, 72);
60
61
  const cardHeight = Math.min(
61
- Math.max(520, Math.round(windowHeight * 0.76)),
62
+ Math.max(560, Math.round(windowHeight * 0.82)),
62
63
  windowHeight - topInset - bottomInset
63
64
  );
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
+
65
82
  return (
66
83
  <Modal
67
84
  visible={visible}
@@ -74,165 +91,240 @@ export function WorkspacePickerModal({
74
91
  <Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
75
92
  <View style={[styles.outer, { paddingTop: topInset, paddingBottom: bottomInset }]}>
76
93
  <View style={[styles.card, { height: cardHeight }]}>
77
- <View style={styles.handle} />
78
94
  <View style={styles.header}>
79
- <View style={styles.headerCopy}>
80
- <Text style={styles.eyebrow}>Workspace</Text>
81
- <Text style={styles.title}>Start directory</Text>
82
- <Text style={styles.subtitle}>
83
- Choose a known Codex workspace or browse any folder on the bridge host.
84
- </Text>
85
- </View>
95
+ <View style={styles.headerSpacer} />
96
+ <Text style={styles.title}>Choose Directory</Text>
86
97
  <Pressable
87
98
  onPress={onClose}
88
- style={({ pressed }) => [styles.closeIconButton, pressed && styles.pressed]}
99
+ style={({ pressed }) => [styles.closeButton, pressed && styles.pressed]}
89
100
  >
90
101
  <Ionicons name="close" size={18} color={colors.textSecondary} />
91
102
  </Pressable>
92
103
  </View>
93
104
 
94
105
  <View style={styles.body}>
95
- <View style={styles.quickPicksSection}>
96
- <Text style={styles.sectionTitle}>Quick picks</Text>
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
+
97
213
  <ScrollView
98
214
  horizontal
99
215
  showsHorizontalScrollIndicator={false}
100
- contentContainerStyle={styles.quickPicksContent}
216
+ contentContainerStyle={styles.breadcrumbScroll}
101
217
  >
102
- <QuickPickChip
103
- title="Bridge default"
104
- meta="Auto"
105
- selected={selectedPath === null}
106
- onPress={() => onSelectPath(null)}
107
- />
108
- {!loadingRecent
109
- ? recentWorkspaces.map((workspace) => (
110
- <QuickPickChip
111
- key={workspace.path}
112
- title={toPathBasename(workspace.path)}
113
- meta={workspace.chatCount === 1 ? '1 chat' : `${workspace.chatCount}`}
114
- selected={workspace.path === selectedPath}
115
- onPress={() => onSelectPath(workspace.path)}
116
- />
117
- ))
118
- : null}
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
+ )}
119
247
  </ScrollView>
120
- {loadingRecent ? (
121
- <Text style={styles.inlineStatusText}>Refreshing known workspaces…</Text>
122
- ) : null}
123
248
  </View>
124
249
 
125
- <View style={styles.browserPanel}>
126
- <View style={styles.browserHeader}>
127
- <View style={styles.browserHeaderCopy}>
128
- <Text style={styles.sectionTitle}>Browse folders</Text>
129
- <Text style={styles.pathValue} numberOfLines={2}>
130
- {currentPath ?? bridgeRoot ?? 'Loading…'}
131
- </Text>
132
- </View>
133
- <Pressable
134
- onPress={() => parentPath && onBrowsePath(parentPath)}
135
- disabled={!parentPath || loadingEntries}
136
- style={({ pressed }) => [
137
- styles.iconButton,
138
- (!parentPath || loadingEntries) && styles.buttonDisabled,
139
- pressed && parentPath && !loadingEntries && styles.pressed,
140
- ]}
141
- >
142
- <Ionicons name="arrow-up-outline" size={18} color={colors.textPrimary} />
143
- </Pressable>
144
- </View>
250
+ {error ? <Text style={styles.errorText}>{error}</Text> : null}
145
251
 
146
- <View style={styles.manualPathRow}>
147
- <TextInput
148
- value={draftPath}
149
- onChangeText={onDraftPathChange}
150
- keyboardAppearance="dark"
151
- placeholder={bridgeRoot ?? '/path/to/workspace'}
152
- placeholderTextColor={colors.textMuted}
153
- style={styles.manualPathInput}
154
- autoCapitalize="none"
155
- autoCorrect={false}
156
- returnKeyType="go"
157
- onSubmitEditing={() => onBrowsePath(draftPath)}
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
+ }
158
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}>
159
305
  <Pressable
160
- onPress={() => onBrowsePath(draftPath)}
161
- disabled={loadingEntries}
306
+ onPress={onClose}
162
307
  style={({ pressed }) => [
163
- styles.secondaryButton,
164
- loadingEntries && styles.buttonDisabled,
165
- pressed && !loadingEntries && styles.pressed,
308
+ styles.footerButton,
309
+ styles.footerButtonSecondary,
310
+ pressed && styles.pressed,
166
311
  ]}
167
312
  >
168
- <Text style={styles.secondaryButtonText}>Open</Text>
313
+ <Text style={styles.footerButtonSecondaryText}>Cancel</Text>
169
314
  </Pressable>
170
- </View>
171
-
172
- <View style={styles.browserActions}>
173
315
  <Pressable
174
316
  onPress={() => currentPath && onSelectPath(currentPath)}
175
317
  disabled={!currentPath || loadingEntries}
176
318
  style={({ pressed }) => [
177
- styles.primaryButton,
319
+ styles.footerButton,
320
+ styles.footerButtonPrimary,
178
321
  (!currentPath || loadingEntries) && styles.buttonDisabled,
179
- pressed && currentPath && !loadingEntries && styles.primaryButtonPressed,
322
+ pressed && currentPath && !loadingEntries && styles.footerButtonPrimaryPressed,
180
323
  ]}
181
324
  >
182
- <Ionicons name="checkmark-circle-outline" size={16} color={colors.bgMain} />
183
- <Text style={styles.primaryButtonText}>Use this folder</Text>
325
+ <Text style={styles.footerButtonPrimaryText}>Select Folder</Text>
184
326
  </Pressable>
185
327
  </View>
186
-
187
- {error ? <Text style={styles.errorText}>{error}</Text> : null}
188
-
189
- <View style={styles.entryListCard}>
190
- {loadingEntries ? (
191
- <LoadingRow label="Loading folders…" />
192
- ) : entries.length > 0 ? (
193
- <ScrollView
194
- style={styles.entryListScroll}
195
- contentContainerStyle={styles.entryListContent}
196
- showsVerticalScrollIndicator={false}
197
- keyboardShouldPersistTaps="handled"
198
- >
199
- {entries.map((entry) => (
200
- <Pressable
201
- key={entry.path}
202
- onPress={() => onBrowsePath(entry.path)}
203
- style={({ pressed }) => [
204
- styles.entryRow,
205
- entry.path === selectedPath && styles.entryRowSelected,
206
- pressed && styles.pressed,
207
- ]}
208
- >
209
- <View style={styles.entryIconWrap}>
210
- <Ionicons
211
- name={entry.isGitRepo ? 'git-branch-outline' : 'folder-outline'}
212
- size={18}
213
- color={colors.textPrimary}
214
- />
215
- </View>
216
- <View style={styles.entryCopy}>
217
- <Text style={styles.entryName} numberOfLines={1}>
218
- {entry.name}
219
- </Text>
220
- <Text style={styles.entryPath} numberOfLines={1}>
221
- {entry.path}
222
- </Text>
223
- </View>
224
- <Ionicons
225
- name="chevron-forward"
226
- size={16}
227
- color={colors.textMuted}
228
- />
229
- </Pressable>
230
- ))}
231
- </ScrollView>
232
- ) : (
233
- <EmptyRow label="No folders found here." />
234
- )}
235
- </View>
236
328
  </View>
237
329
  </View>
238
330
  </View>
@@ -242,53 +334,30 @@ export function WorkspacePickerModal({
242
334
  );
243
335
  }
244
336
 
245
- function QuickPickChip({
246
- title,
247
- meta,
248
- selected,
249
- onPress,
337
+ function LoadingRow({
338
+ label,
339
+ compact = false,
250
340
  }: {
251
- title: string;
252
- meta?: string;
253
- selected: boolean;
254
- onPress: () => void;
341
+ label: string;
342
+ compact?: boolean;
255
343
  }) {
256
344
  return (
257
- <Pressable
258
- onPress={onPress}
259
- style={({ pressed }) => [
260
- styles.quickPickChip,
261
- selected && styles.quickPickChipSelected,
262
- pressed && styles.pressed,
263
- ]}
264
- >
265
- <Text style={styles.quickPickTitle} numberOfLines={1}>
266
- {title}
267
- </Text>
268
- {meta ? (
269
- <Text style={styles.quickPickMeta} numberOfLines={1}>
270
- {meta}
271
- </Text>
272
- ) : null}
273
- {selected ? (
274
- <Ionicons name="checkmark-circle" size={16} color={colors.textPrimary} />
275
- ) : null}
276
- </Pressable>
277
- );
278
- }
279
-
280
- function LoadingRow({ label }: { label: string }) {
281
- return (
282
- <View style={styles.statusRow}>
345
+ <View style={[styles.statusRow, compact && styles.statusRowCompact]}>
283
346
  <ActivityIndicator color={colors.textPrimary} />
284
347
  <Text style={styles.statusText}>{label}</Text>
285
348
  </View>
286
349
  );
287
350
  }
288
351
 
289
- function EmptyRow({ label }: { label: string }) {
352
+ function EmptyRow({
353
+ label,
354
+ compact = false,
355
+ }: {
356
+ label: string;
357
+ compact?: boolean;
358
+ }) {
290
359
  return (
291
- <View style={styles.statusRow}>
360
+ <View style={[styles.statusRow, compact && styles.statusRowCompact]}>
292
361
  <Text style={styles.statusText}>{label}</Text>
293
362
  </View>
294
363
  );
@@ -302,10 +371,98 @@ function toPathBasename(path: string): string {
302
371
  return parts[parts.length - 1] ?? path;
303
372
  }
304
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
+
305
462
  const styles = StyleSheet.create({
306
463
  backdrop: {
307
464
  flex: 1,
308
- backgroundColor: 'rgba(0, 0, 0, 0.56)',
465
+ backgroundColor: 'rgba(0, 0, 0, 0.62)',
309
466
  },
310
467
  outer: {
311
468
  flex: 1,
@@ -313,195 +470,235 @@ const styles = StyleSheet.create({
313
470
  paddingHorizontal: spacing.lg,
314
471
  },
315
472
  card: {
316
- borderRadius: 26,
473
+ borderRadius: 28,
317
474
  borderCurve: 'continuous',
318
- backgroundColor: '#0F1218',
475
+ backgroundColor: '#07090C',
319
476
  borderWidth: 1,
320
- borderColor: colors.border,
477
+ borderColor: 'rgba(255, 255, 255, 0.09)',
321
478
  overflow: 'hidden',
322
479
  boxShadow: '0 24px 44px rgba(0, 0, 0, 0.34)',
323
480
  },
324
- handle: {
325
- alignSelf: 'center',
326
- width: 48,
327
- height: 5,
328
- borderRadius: radius.full,
329
- backgroundColor: 'rgba(255,255,255,0.16)',
330
- marginTop: spacing.sm,
331
- },
332
481
  header: {
482
+ minHeight: 68,
333
483
  flexDirection: 'row',
334
- alignItems: 'flex-start',
484
+ alignItems: 'center',
335
485
  justifyContent: 'space-between',
336
- gap: spacing.md,
337
- paddingHorizontal: spacing.xl,
338
- paddingTop: spacing.lg,
339
- paddingBottom: spacing.md,
340
- borderBottomWidth: 1,
341
- borderBottomColor: colors.borderLight,
486
+ paddingHorizontal: spacing.lg,
487
+ paddingTop: spacing.md,
342
488
  },
343
- headerCopy: {
344
- flex: 1,
345
- gap: spacing.xs,
346
- },
347
- eyebrow: {
348
- ...typography.caption,
349
- color: colors.textSecondary,
350
- textTransform: 'uppercase',
351
- letterSpacing: 1.2,
489
+ headerSpacer: {
490
+ width: 36,
352
491
  },
353
492
  title: {
354
- ...typography.largeTitle,
355
- fontSize: 22,
356
- },
357
- subtitle: {
358
- ...typography.body,
359
- color: colors.textMuted,
493
+ ...typography.headline,
494
+ fontSize: 18,
495
+ fontWeight: '700',
496
+ textAlign: 'center',
360
497
  },
361
- closeIconButton: {
362
- marginTop: 2,
363
- width: 34,
364
- height: 34,
498
+ closeButton: {
499
+ width: 36,
500
+ height: 36,
365
501
  borderRadius: radius.full,
366
- borderWidth: 1,
367
- borderColor: colors.borderLight,
368
502
  alignItems: 'center',
369
503
  justifyContent: 'center',
370
504
  backgroundColor: 'rgba(255,255,255,0.04)',
505
+ borderWidth: 1,
506
+ borderColor: colors.borderLight,
371
507
  },
372
508
  body: {
373
509
  flex: 1,
374
- paddingHorizontal: spacing.xl,
375
- paddingTop: spacing.lg,
376
- paddingBottom: spacing.xl,
377
- gap: spacing.lg,
510
+ paddingHorizontal: spacing.lg,
511
+ paddingBottom: spacing.lg,
512
+ gap: spacing.md,
378
513
  },
379
- quickPicksSection: {
380
- gap: spacing.sm,
514
+ connectionRow: {
515
+ flexDirection: 'row',
516
+ alignItems: 'center',
517
+ gap: spacing.md,
381
518
  },
382
- quickPicksContent: {
383
- gap: spacing.sm,
384
- paddingRight: spacing.lg,
519
+ connectionText: {
520
+ flex: 1,
521
+ ...typography.caption,
522
+ color: colors.textSecondary,
385
523
  },
386
- quickPickChip: {
387
- minWidth: 112,
388
- maxWidth: 172,
389
- minHeight: 54,
390
- borderRadius: radius.lg,
524
+ defaultButton: {
525
+ minHeight: 32,
526
+ paddingHorizontal: spacing.md,
527
+ borderRadius: radius.full,
391
528
  borderWidth: 1,
392
- borderColor: colors.borderLight,
529
+ borderColor: colors.border,
393
530
  backgroundColor: colors.bgItem,
394
- paddingHorizontal: spacing.md,
395
- paddingVertical: spacing.sm,
531
+ alignItems: 'center',
396
532
  justifyContent: 'center',
397
- gap: 2,
398
533
  },
399
- quickPickChipSelected: {
534
+ defaultButtonSelected: {
400
535
  borderColor: colors.borderHighlight,
401
- backgroundColor: 'rgba(255,255,255,0.10)',
536
+ backgroundColor: colors.bgInput,
402
537
  },
403
- quickPickTitle: {
404
- ...typography.body,
538
+ defaultButtonText: {
539
+ ...typography.caption,
540
+ color: colors.textSecondary,
405
541
  fontWeight: '600',
406
542
  },
407
- quickPickMeta: {
408
- ...typography.caption,
409
- color: colors.textMuted,
543
+ defaultButtonTextSelected: {
544
+ color: colors.textPrimary,
410
545
  },
411
- inlineStatusText: {
412
- ...typography.caption,
413
- color: colors.textMuted,
414
- },
415
- sectionTitle: {
416
- ...typography.headline,
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,
417
556
  },
418
- browserPanel: {
557
+ searchInput: {
419
558
  flex: 1,
420
- gap: spacing.md,
559
+ ...typography.body,
560
+ paddingVertical: 0,
421
561
  },
422
- browserHeader: {
562
+ breadcrumbRow: {
423
563
  flexDirection: 'row',
564
+ alignItems: 'center',
424
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',
425
574
  alignItems: 'center',
575
+ gap: 6,
576
+ backgroundColor: colors.bgItem,
426
577
  },
427
- browserHeaderCopy: {
428
- flex: 1,
578
+ upButtonText: {
579
+ ...typography.caption,
580
+ color: colors.textSecondary,
581
+ fontWeight: '600',
582
+ },
583
+ breadcrumbScroll: {
584
+ alignItems: 'center',
585
+ paddingRight: spacing.md,
429
586
  gap: spacing.xs,
430
587
  },
431
- iconButton: {
432
- width: 42,
433
- height: 42,
434
- borderRadius: radius.lg,
435
- borderWidth: 1,
436
- borderColor: colors.border,
437
- backgroundColor: colors.bgItem,
588
+ breadcrumbItem: {
589
+ flexDirection: 'row',
438
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,
439
601
  justifyContent: 'center',
440
602
  },
441
- pathValue: {
603
+ breadcrumbChipActive: {
604
+ backgroundColor: colors.bgInput,
605
+ borderWidth: 1,
606
+ borderColor: colors.borderHighlight,
607
+ },
608
+ breadcrumbText: {
442
609
  ...typography.mono,
443
- fontSize: 11,
610
+ fontSize: 12,
611
+ color: colors.textSecondary,
444
612
  },
445
- browserActions: {
446
- flexDirection: 'row',
447
- gap: spacing.sm,
613
+ breadcrumbTextActive: {
614
+ color: colors.textPrimary,
615
+ fontWeight: '700',
448
616
  },
449
- manualPathRow: {
450
- flexDirection: 'row',
451
- gap: spacing.sm,
452
- alignItems: 'center',
617
+ breadcrumbEmpty: {
618
+ ...typography.caption,
619
+ color: colors.textMuted,
453
620
  },
454
- manualPathInput: {
455
- flex: 1,
456
- minHeight: 46,
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,
457
634
  borderRadius: radius.lg,
458
635
  borderWidth: 1,
459
636
  borderColor: colors.borderLight,
460
- backgroundColor: colors.bgInput,
637
+ backgroundColor: colors.bgItem,
638
+ overflow: 'hidden',
639
+ },
640
+ recentRow: {
641
+ minHeight: 54,
461
642
  paddingHorizontal: spacing.md,
462
- ...typography.body,
643
+ paddingVertical: spacing.sm,
644
+ flexDirection: 'row',
645
+ alignItems: 'center',
646
+ gap: spacing.sm,
647
+ borderBottomWidth: StyleSheet.hairlineWidth,
648
+ borderBottomColor: colors.borderLight,
463
649
  },
464
- primaryButton: {
465
- minHeight: 46,
466
- borderRadius: radius.lg,
467
- backgroundColor: colors.textPrimary,
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,
468
660
  alignItems: 'center',
469
661
  justifyContent: 'center',
470
- flexDirection: 'row',
471
- gap: spacing.xs,
472
- paddingHorizontal: spacing.md,
473
- flex: 1,
662
+ backgroundColor: colors.bgInput,
663
+ borderWidth: 1,
664
+ borderColor: colors.borderLight,
474
665
  },
475
- primaryButtonPressed: {
476
- opacity: 0.82,
666
+ recentCopy: {
667
+ flex: 1,
668
+ gap: 2,
477
669
  },
478
- primaryButtonText: {
670
+ recentTitle: {
479
671
  ...typography.body,
480
- color: colors.bgMain,
672
+ fontSize: 13,
673
+ lineHeight: 18,
481
674
  fontWeight: '600',
482
675
  },
483
- secondaryButton: {
484
- minHeight: 46,
485
- borderRadius: radius.lg,
486
- borderWidth: 1,
487
- borderColor: colors.border,
488
- backgroundColor: colors.bgItem,
489
- alignItems: 'center',
490
- justifyContent: 'center',
491
- flexDirection: 'row',
492
- gap: spacing.xs,
493
- paddingHorizontal: spacing.md,
676
+ recentPath: {
677
+ ...typography.caption,
678
+ fontSize: 11,
679
+ lineHeight: 15,
680
+ color: colors.textMuted,
494
681
  },
495
- secondaryButtonText: {
496
- ...typography.body,
682
+ recentMeta: {
683
+ ...typography.caption,
684
+ fontSize: 11,
685
+ lineHeight: 15,
686
+ color: colors.textSecondary,
497
687
  fontWeight: '600',
498
688
  },
499
- buttonDisabled: {
500
- opacity: 0.42,
689
+ helperText: {
690
+ ...typography.caption,
691
+ fontSize: 11,
692
+ lineHeight: 15,
693
+ color: colors.textMuted,
694
+ },
695
+ errorText: {
696
+ ...typography.caption,
697
+ color: '#FF8A8A',
501
698
  },
502
- entryListCard: {
699
+ browserCard: {
503
700
  flex: 1,
504
- minHeight: 240,
701
+ minHeight: 228,
505
702
  borderRadius: radius.lg,
506
703
  borderWidth: 1,
507
704
  borderColor: colors.borderLight,
@@ -512,61 +709,104 @@ const styles = StyleSheet.create({
512
709
  flex: 1,
513
710
  },
514
711
  entryListContent: {
515
- paddingBottom: spacing.sm,
712
+ paddingVertical: spacing.xs,
516
713
  },
517
714
  entryRow: {
715
+ minHeight: 54,
716
+ paddingHorizontal: spacing.md,
518
717
  flexDirection: 'row',
519
718
  alignItems: 'center',
520
719
  gap: spacing.sm,
521
- paddingHorizontal: spacing.md,
522
- paddingVertical: spacing.md,
523
- backgroundColor: colors.bgItem,
524
720
  borderBottomWidth: StyleSheet.hairlineWidth,
525
721
  borderBottomColor: colors.borderLight,
526
722
  },
527
723
  entryRowSelected: {
528
- backgroundColor: 'rgba(255,255,255,0.10)',
724
+ backgroundColor: colors.bgInput,
725
+ },
726
+ entryRowLast: {
727
+ borderBottomWidth: 0,
529
728
  },
530
729
  entryIconWrap: {
531
- width: 34,
532
- height: 34,
730
+ width: 32,
731
+ height: 32,
533
732
  borderRadius: radius.md,
534
733
  alignItems: 'center',
535
734
  justifyContent: 'center',
536
- backgroundColor: 'rgba(255,255,255,0.06)',
735
+ backgroundColor: colors.bgInput,
537
736
  borderWidth: 1,
538
737
  borderColor: colors.borderLight,
539
738
  },
540
739
  entryCopy: {
541
740
  flex: 1,
542
- gap: 2,
543
741
  },
544
742
  entryName: {
545
743
  ...typography.body,
744
+ fontSize: 13,
745
+ lineHeight: 18,
546
746
  fontWeight: '600',
547
747
  },
548
- entryPath: {
549
- ...typography.caption,
748
+ footer: {
749
+ gap: spacing.sm,
750
+ },
751
+ footerPath: {
752
+ ...typography.mono,
753
+ fontSize: 10,
754
+ lineHeight: 14,
550
755
  color: colors.textMuted,
551
756
  },
552
- errorText: {
553
- ...typography.caption,
554
- color: '#FF8A8A',
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',
555
789
  },
556
790
  statusRow: {
557
791
  flex: 1,
558
- minHeight: 120,
792
+ minHeight: 132,
559
793
  alignItems: 'center',
560
794
  justifyContent: 'center',
561
795
  gap: spacing.sm,
562
796
  paddingHorizontal: spacing.lg,
563
797
  },
798
+ statusRowCompact: {
799
+ minHeight: 96,
800
+ },
564
801
  statusText: {
565
802
  ...typography.body,
566
803
  textAlign: 'center',
567
804
  color: colors.textMuted,
568
805
  },
806
+ buttonDisabled: {
807
+ opacity: 0.42,
808
+ },
569
809
  pressed: {
570
- opacity: 0.84,
810
+ opacity: 0.86,
571
811
  },
572
812
  });