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
@@ -1,6 +1,9 @@
1
1
  import { Ionicons } from '@expo/vector-icons';
2
2
  import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { BlurView } from 'expo-blur';
3
4
  import { CameraView, type BarcodeScanningResult, useCameraPermissions } from 'expo-camera';
5
+ import * as Clipboard from 'expo-clipboard';
6
+ import { LinearGradient } from 'expo-linear-gradient';
4
7
  import {
5
8
  ActivityIndicator,
6
9
  KeyboardAvoidingView,
@@ -42,10 +45,59 @@ type ConnectionCheck =
42
45
  | { kind: 'error'; message: string };
43
46
  type OnboardingStep = 'intro' | 'connect';
44
47
  type PairingPayload = { bridgeToken: string; bridgeUrl?: string };
45
- type BridgeModePreset = 'local' | 'tailscale';
48
+ type IntroFeature = {
49
+ icon: keyof typeof Ionicons.glyphMap;
50
+ title: string;
51
+ description: string;
52
+ };
46
53
 
47
- const LOCAL_EXAMPLE_URL = 'http://192.168.1.20:8787';
48
- const TAILSCALE_EXAMPLE_URL = 'http://100.101.102.103:8787';
54
+ const BRIDGE_SETUP_COMMANDS = 'npm install -g clawdex-mobile@latest\nclawdex init';
55
+ const INTRO_FEATURES: IntroFeature[] = [
56
+ {
57
+ icon: 'pulse-outline',
58
+ title: 'Start and monitor runs',
59
+ description: 'Launch new Codex work from your phone and follow streaming updates as it runs.',
60
+ },
61
+ {
62
+ icon: 'chatbubble-ellipses-outline',
63
+ title: 'Resume threads',
64
+ description: 'Pick up existing chats and keep active work moving without going back to desktop.',
65
+ },
66
+ {
67
+ icon: 'shield-checkmark-outline',
68
+ title: 'Review approvals',
69
+ description: 'Handle clarification prompts and approve commands or file changes in-app.',
70
+ },
71
+ {
72
+ icon: 'folder-open-outline',
73
+ title: 'Browse workspaces',
74
+ description: 'Open recent folders or navigate server directories before starting work.',
75
+ },
76
+ {
77
+ icon: 'git-branch-outline',
78
+ title: 'Git actions',
79
+ description: 'Review diffs, commit, and push chat workspaces from the Git view.',
80
+ },
81
+ {
82
+ icon: 'mic-outline',
83
+ title: 'Voice, files, images',
84
+ description: 'Use push-to-talk and attach workspace files or phone images to prompts.',
85
+ },
86
+ ];
87
+ const SETUP_STAGES = [
88
+ {
89
+ title: 'Start bridge',
90
+ description: 'Run the CLI on your server and wait for the pairing QR.',
91
+ },
92
+ {
93
+ title: 'Pair bridge',
94
+ description: 'Scan QR or paste the bridge URL and token.',
95
+ },
96
+ {
97
+ title: 'Verify auth',
98
+ description: 'Confirm health and authenticated RPC before continuing.',
99
+ },
100
+ ] as const;
49
101
 
50
102
  export function OnboardingScreen({
51
103
  mode = 'initial',
@@ -83,7 +135,6 @@ export function OnboardingScreen({
83
135
  }, [initialBridgeToken]);
84
136
 
85
137
  const showIntroStep = mode === 'initial' && onboardingStep === 'intro';
86
-
87
138
  const normalizedBridgeUrl = useMemo(
88
139
  () => normalizeBridgeUrlInput(urlInput),
89
140
  [urlInput]
@@ -98,11 +149,25 @@ export function OnboardingScreen({
98
149
  : null;
99
150
  }, [allowInsecureRemoteBridge, normalizedBridgeUrl]);
100
151
 
101
- const modeTitle = mode === 'edit' ? 'Update Bridge URL' : 'Connect Your Bridge';
152
+ const modeTitle = mode === 'edit' ? 'Update Bridge URL' : 'Pair Your Bridge';
102
153
  const modeDescription =
103
154
  mode === 'edit'
104
- ? 'Switch to another host bridge without rebuilding the app.'
105
- : 'Set the host bridge URL once, then use Codex from LAN, VPN, or Tailscale.';
155
+ ? 'Switch this phone to another bridge without rebuilding the app.'
156
+ : 'Connect this phone to your private bridge and verify that it responds.';
157
+ const normalizedTokenPreview = tokenInput.trim();
158
+ const showOnboardingDock = mode === 'initial';
159
+ const currentSetupStage = useMemo(() => {
160
+ if (showIntroStep) {
161
+ return 1;
162
+ }
163
+ if (connectionCheck.kind === 'success') {
164
+ return 3;
165
+ }
166
+ if (normalizedBridgeUrl || normalizedTokenPreview) {
167
+ return 2;
168
+ }
169
+ return 1;
170
+ }, [connectionCheck.kind, normalizedBridgeUrl, normalizedTokenPreview, showIntroStep]);
106
171
 
107
172
  const validateInput = useCallback((): { bridgeUrl: string; bridgeToken: string } | null => {
108
173
  const normalized = normalizeBridgeUrlInput(urlInput);
@@ -209,16 +274,6 @@ export function OnboardingScreen({
209
274
  await runConnectionCheck(validated.bridgeUrl, normalizedToken);
210
275
  }, [normalizeTokenInput, runConnectionCheck, validateInput]);
211
276
 
212
- const applyPreset = useCallback((value: string) => {
213
- setUrlInput(value);
214
- setFormError(null);
215
- setConnectionCheck({ kind: 'idle' });
216
- }, []);
217
-
218
- const applyModePreset = useCallback((preset: BridgeModePreset) => {
219
- applyPreset(preset === 'local' ? LOCAL_EXAMPLE_URL : TAILSCALE_EXAMPLE_URL);
220
- }, [applyPreset]);
221
-
222
277
  const goToConnectStep = useCallback(() => {
223
278
  setOnboardingStep('connect');
224
279
  }, []);
@@ -281,6 +336,20 @@ export function OnboardingScreen({
281
336
 
282
337
  return (
283
338
  <View style={styles.container}>
339
+ <LinearGradient
340
+ colors={['#020304', '#05070C', '#0A0E16']}
341
+ style={StyleSheet.absoluteFill}
342
+ />
343
+ <View pointerEvents="none" style={styles.ambientCanvas}>
344
+ <LinearGradient
345
+ colors={['rgba(181, 189, 204, 0.20)', 'rgba(181, 189, 204, 0.04)', 'transparent']}
346
+ style={styles.ambientOrbPrimary}
347
+ />
348
+ <LinearGradient
349
+ colors={['rgba(255, 255, 255, 0.10)', 'rgba(255, 255, 255, 0.02)', 'transparent']}
350
+ style={styles.ambientOrbSecondary}
351
+ />
352
+ </View>
284
353
  <SafeAreaView style={styles.safeArea}>
285
354
  <KeyboardAvoidingView
286
355
  behavior={Platform.select({ ios: 'padding', default: undefined })}
@@ -288,64 +357,70 @@ export function OnboardingScreen({
288
357
  >
289
358
  {showIntroStep ? (
290
359
  <View style={styles.introRoot}>
291
- <View style={styles.introBrandRow}>
292
- <BrandMark size={24} />
293
- <Text style={styles.introBrandName}>Clawdex</Text>
360
+ <View style={styles.introHeader}>
361
+ <View style={styles.introBrandRow}>
362
+ <BrandMark size={24} />
363
+ <Text style={styles.introBrandName}>Clawdex</Text>
364
+ </View>
365
+ <AmbientBadge icon="lock-closed-outline" label="Private bridge" />
294
366
  </View>
295
- <View style={styles.introMain}>
296
- <View style={styles.heroCard}>
297
- <View style={styles.heroTopRow}>
298
- <View style={styles.heroIconWrap}>
299
- <Ionicons name="phone-portrait-outline" size={20} color={colors.textPrimary} />
300
- </View>
301
- </View>
302
- <Text style={styles.heroTitle}>Codex on mobile</Text>
303
- <Text style={styles.heroDescription}>
304
- Run your host-side Codex workflows from your phone across LAN, VPN, or Tailscale.
367
+
368
+ <ScrollView
369
+ style={styles.introScroll}
370
+ contentContainerStyle={styles.introScrollContent}
371
+ showsVerticalScrollIndicator={false}
372
+ >
373
+ <LinearGradient
374
+ colors={[
375
+ 'rgba(181, 189, 204, 0.22)',
376
+ 'rgba(34, 39, 48, 0.75)',
377
+ 'rgba(7, 9, 12, 0.96)',
378
+ ]}
379
+ start={{ x: 0, y: 0 }}
380
+ end={{ x: 1, y: 1 }}
381
+ style={styles.introHero}
382
+ >
383
+ <Text style={styles.introHeroEyebrow}>Mobile control surface</Text>
384
+ <Text style={styles.introHeroTitle}>Run Codex away from your desk.</Text>
385
+ <Text style={styles.introHeroDescription}>
386
+ Continue threads, review work, and approve changes from a paired private
387
+ bridge.
388
+ </Text>
389
+ </LinearGradient>
390
+
391
+ <View style={styles.introSectionHeader}>
392
+ <Text style={styles.introSectionTitle}>What you can do from mobile</Text>
393
+ <Text style={styles.introSectionSubtitle}>
394
+ The phone UI should feel like a control surface, not a read-only mirror.
305
395
  </Text>
306
396
  </View>
307
397
 
308
- <View style={[styles.formCard, styles.introFeaturesCard]}>
309
- <ScrollView
310
- style={styles.introFeaturesList}
311
- contentContainerStyle={styles.introFeaturesListContent}
312
- showsVerticalScrollIndicator
313
- >
314
- <Text style={styles.introSectionTitle}>What You Can Do</Text>
315
- <IntroFeatureRow
316
- icon="chatbubble-ellipses-outline"
317
- title="Continue threads"
318
- description="Follow active chats and start new runs from your phone."
319
- />
320
- <IntroFeatureRow
321
- icon="pulse-outline"
322
- title="Track run progress"
323
- description="See live status and streaming updates as Codex works."
324
- />
325
- <IntroFeatureRow
326
- icon="git-branch-outline"
327
- title="Handle git tasks"
328
- description="Review status, diffs, and commits for chat workspaces."
329
- />
330
- <IntroFeatureRow
331
- icon="mic-outline"
332
- title="Talk to Codex"
333
- description="Use voice input to speak your prompts directly from mobile."
334
- />
335
- <IntroFeatureRow
336
- icon="attach-outline"
337
- title="Share files and images"
338
- description="Attach workspace files and phone media to your prompts."
339
- />
340
- <IntroFeatureRow
341
- icon="shield-checkmark-outline"
342
- title="Approve actions"
343
- description="Review and approve command and file changes in-app."
398
+ <View style={styles.introFeatureGrid}>
399
+ {INTRO_FEATURES.map((feature) => (
400
+ <IntroFeatureCard
401
+ key={feature.title}
402
+ icon={feature.icon}
403
+ title={feature.title}
404
+ description={feature.description}
344
405
  />
345
- </ScrollView>
406
+ ))}
346
407
  </View>
347
- </View>
408
+
409
+ <BlurView intensity={40} tint="dark" style={styles.introContextCard}>
410
+ <Text style={styles.introContextTitle}>Best on trusted networks</Text>
411
+ <Text style={styles.introContextText}>
412
+ Built for your own machine and private network paths. Pair over LAN nearby, or
413
+ use Tailscale when the bridge lives on another device.
414
+ </Text>
415
+ <View style={styles.introContextPillRow}>
416
+ <AmbientBadge icon="git-network-outline" label="Private network only" />
417
+ <AmbientBadge icon="shield-checkmark-outline" label="Token auth required" />
418
+ </View>
419
+ </BlurView>
420
+ </ScrollView>
421
+
348
422
  <View style={styles.introFooter}>
423
+ {showOnboardingDock ? <OnboardingStepDock currentStage={currentSetupStage} /> : null}
349
424
  <Pressable
350
425
  onPress={goToConnectStep}
351
426
  style={({ pressed }) => [
@@ -353,18 +428,29 @@ export function OnboardingScreen({
353
428
  pressed && styles.introNextButtonPressed,
354
429
  ]}
355
430
  >
356
- <Text style={styles.introNextButtonText}>Next</Text>
431
+ <Text style={styles.introNextButtonText}>Pair your bridge</Text>
357
432
  <Ionicons name="arrow-forward" size={19} color={colors.black} />
358
433
  </Pressable>
359
434
  </View>
360
435
  </View>
361
436
  ) : (
362
- <ScrollView
363
- style={styles.scroll}
364
- contentContainerStyle={styles.scrollContent}
365
- keyboardShouldPersistTaps="handled"
366
- >
367
- <View style={styles.heroCard}>
437
+ <View style={styles.connectRoot}>
438
+ <ScrollView
439
+ style={styles.scroll}
440
+ contentContainerStyle={styles.scrollContent}
441
+ keyboardShouldPersistTaps="handled"
442
+ showsVerticalScrollIndicator={false}
443
+ >
444
+ <LinearGradient
445
+ colors={[
446
+ 'rgba(181, 189, 204, 0.18)',
447
+ 'rgba(22, 25, 31, 0.82)',
448
+ 'rgba(7, 9, 12, 0.98)',
449
+ ]}
450
+ start={{ x: 0, y: 0 }}
451
+ end={{ x: 1, y: 1 }}
452
+ style={styles.connectHero}
453
+ >
368
454
  <View style={styles.heroTopRow}>
369
455
  <View style={styles.heroIconWrap}>
370
456
  <Ionicons name="hardware-chip-outline" size={20} color={colors.textPrimary} />
@@ -379,97 +465,35 @@ export function OnboardingScreen({
379
465
  </Pressable>
380
466
  ) : null}
381
467
  </View>
468
+ <Text style={styles.connectEyebrow}>Bridge pairing</Text>
382
469
  <Text style={styles.heroTitle}>{modeTitle}</Text>
383
470
  <Text style={styles.heroDescription}>{modeDescription}</Text>
384
- </View>
471
+ </LinearGradient>
385
472
 
386
- <View style={styles.formCard}>
387
- <Text style={styles.label}>Bridge Mode</Text>
388
- <View style={styles.modeRow}>
389
- <Pressable
390
- onPress={() => applyModePreset('local')}
391
- style={({ pressed }) => [
392
- styles.modeButton,
393
- pressed && styles.modeButtonPressed,
394
- ]}
395
- >
396
- <Ionicons name="wifi-outline" size={16} color={colors.textPrimary} />
397
- <Text style={styles.modeButtonText}>Local (LAN)</Text>
398
- </Pressable>
399
- <Pressable
400
- onPress={() => applyModePreset('tailscale')}
401
- style={({ pressed }) => [
402
- styles.modeButton,
403
- pressed && styles.modeButtonPressed,
404
- ]}
405
- >
406
- <Ionicons name="shield-outline" size={16} color={colors.textPrimary} />
407
- <Text style={styles.modeButtonText}>Tailscale</Text>
408
- </Pressable>
473
+ <BlurView intensity={55} tint="dark" style={styles.formCard}>
474
+ <View style={styles.commandPanel}>
475
+ <View style={styles.formSectionHeader}>
476
+ <Text style={styles.formSectionEyebrow}>1. Start the bridge</Text>
477
+ <Text style={styles.formSectionTitle}>
478
+ Run these on your server first. This installs the CLI, starts the bridge,
479
+ and prints the pairing QR used here.
480
+ </Text>
481
+ </View>
482
+ <CommandSnippet
483
+ label="Run on your server"
484
+ command={BRIDGE_SETUP_COMMANDS}
485
+ hint="After it starts, scan the pairing QR in this screen."
486
+ />
409
487
  </View>
410
- <Text style={styles.helperText}>
411
- Pick the same mode used while starting the bridge, then adjust the IP if needed.
412
- </Text>
413
488
 
414
- <Text style={styles.label}>Bridge URL</Text>
415
- <TextInput
416
- value={urlInput}
417
- onChangeText={(value) => {
418
- setUrlInput(value);
419
- setFormError(null);
420
- setConnectionCheck({ kind: 'idle' });
421
- }}
422
- autoCapitalize="none"
423
- autoCorrect={false}
424
- keyboardType="url"
425
- placeholder="http://100.101.102.103:8787"
426
- placeholderTextColor={colors.textMuted}
427
- style={styles.input}
428
- returnKeyType="done"
429
- onSubmitEditing={() => {
430
- void handleSave();
431
- }}
432
- />
433
- <View style={styles.tokenHeaderRow}>
434
- <Text style={styles.label}>Bridge Token</Text>
435
- <Text style={styles.optionalLabel}>Required</Text>
436
- </View>
437
- <View style={styles.tokenInputWrap}>
438
- <TextInput
439
- value={tokenInput}
440
- onChangeText={(value) => {
441
- setTokenInput(value);
442
- setConnectionCheck({ kind: 'idle' });
443
- }}
444
- autoCapitalize="none"
445
- autoCorrect={false}
446
- keyboardType="default"
447
- placeholder="Paste bridge token"
448
- placeholderTextColor={colors.textMuted}
449
- style={[styles.input, styles.tokenInput]}
450
- secureTextEntry={tokenHidden}
451
- returnKeyType="done"
452
- onSubmitEditing={() => {
453
- void handleSave();
454
- }}
455
- />
456
- <Pressable
457
- onPress={() => setTokenHidden((prev) => !prev)}
458
- style={({ pressed }) => [
459
- styles.tokenRevealBtn,
460
- pressed && styles.tokenRevealBtnPressed,
461
- ]}
462
- >
463
- <Ionicons
464
- name={tokenHidden ? 'eye-outline' : 'eye-off-outline'}
465
- size={16}
466
- color={colors.textSecondary}
467
- />
468
- <Text style={styles.tokenRevealBtnText}>
469
- {tokenHidden ? 'Show' : 'Hide'}
470
- </Text>
471
- </Pressable>
489
+ <View style={styles.formSectionHeader}>
490
+ <Text style={styles.formSectionEyebrow}>2. Pair the bridge</Text>
491
+ <Text style={styles.formSectionTitle}>
492
+ Scan the bridge QR first. If that is not available, enter the URL and token
493
+ manually.
494
+ </Text>
472
495
  </View>
496
+
473
497
  <Pressable
474
498
  onPress={() => {
475
499
  void openScanner();
@@ -480,15 +504,96 @@ export function OnboardingScreen({
480
504
  ]}
481
505
  >
482
506
  <Ionicons name="qr-code-outline" size={16} color={colors.textPrimary} />
483
- <Text style={styles.scanButtonText}>Scan Bridge QR</Text>
507
+ <Text style={styles.scanButtonText}>Scan bridge QR</Text>
484
508
  </Pressable>
509
+ <Text style={styles.helperText}>
510
+ Recommended first. QR fills the bridge URL and token together.
511
+ </Text>
512
+
513
+ <View style={styles.fieldGroup}>
514
+ <Text style={styles.label}>Bridge URL</Text>
515
+ <View style={styles.inputRow}>
516
+ <View style={styles.inputIconWrap}>
517
+ <Ionicons name="globe-outline" size={16} color={colors.textSecondary} />
518
+ </View>
519
+ <TextInput
520
+ value={urlInput}
521
+ onChangeText={(value) => {
522
+ setUrlInput(value);
523
+ setFormError(null);
524
+ setConnectionCheck({ kind: 'idle' });
525
+ }}
526
+ keyboardAppearance="dark"
527
+ autoCapitalize="none"
528
+ autoCorrect={false}
529
+ keyboardType="url"
530
+ placeholder="http://100.101.102.103:8787"
531
+ placeholderTextColor={colors.textMuted}
532
+ style={styles.inputText}
533
+ returnKeyType="done"
534
+ onSubmitEditing={() => {
535
+ void handleSave();
536
+ }}
537
+ />
538
+ </View>
539
+ </View>
540
+
541
+ <View style={styles.fieldGroup}>
542
+ <View style={styles.tokenHeaderRow}>
543
+ <Text style={styles.label}>Bridge Token</Text>
544
+ <Text style={styles.optionalLabel}>Required</Text>
545
+ </View>
546
+ <View style={styles.tokenInputWrap}>
547
+ <View style={styles.inputRow}>
548
+ <View style={styles.inputIconWrap}>
549
+ <Ionicons name="key-outline" size={16} color={colors.textSecondary} />
550
+ </View>
551
+ <TextInput
552
+ value={tokenInput}
553
+ onChangeText={(value) => {
554
+ setTokenInput(value);
555
+ setConnectionCheck({ kind: 'idle' });
556
+ }}
557
+ keyboardAppearance="dark"
558
+ autoCapitalize="none"
559
+ autoCorrect={false}
560
+ keyboardType="default"
561
+ placeholder="Paste bridge token"
562
+ placeholderTextColor={colors.textMuted}
563
+ style={styles.inputText}
564
+ secureTextEntry={tokenHidden}
565
+ returnKeyType="done"
566
+ onSubmitEditing={() => {
567
+ void handleSave();
568
+ }}
569
+ />
570
+ </View>
571
+ <Pressable
572
+ onPress={() => setTokenHidden((prev) => !prev)}
573
+ style={({ pressed }) => [
574
+ styles.tokenRevealBtn,
575
+ pressed && styles.tokenRevealBtnPressed,
576
+ ]}
577
+ >
578
+ <Ionicons
579
+ name={tokenHidden ? 'eye-outline' : 'eye-off-outline'}
580
+ size={16}
581
+ color={colors.textSecondary}
582
+ />
583
+ <Text style={styles.tokenRevealBtnText}>
584
+ {tokenHidden ? 'Show' : 'Hide'}
585
+ </Text>
586
+ </Pressable>
587
+ </View>
588
+ </View>
589
+
485
590
  <Text style={styles.helperText}>
486
591
  URL supports `http`, `https`, `ws`, and `wss`. `/rpc` is added automatically.
487
592
  </Text>
488
593
 
489
594
  {normalizedBridgeUrl ? (
490
595
  <View style={styles.previewWrap}>
491
- <Text style={styles.previewLabel}>Normalized URL</Text>
596
+ <Text style={styles.previewLabel}>Normalized target</Text>
492
597
  <Text selectable style={styles.previewValue}>
493
598
  {normalizedBridgeUrl}
494
599
  </Text>
@@ -496,17 +601,38 @@ export function OnboardingScreen({
496
601
  ) : null}
497
602
 
498
603
  {insecureRemoteWarning ? (
499
- <Text style={styles.warningText}>{insecureRemoteWarning}</Text>
604
+ <StatusBanner
605
+ tone="warning"
606
+ icon="warning-outline"
607
+ message={insecureRemoteWarning}
608
+ />
500
609
  ) : null}
501
610
 
502
- {formError ? <Text style={styles.errorText}>{formError}</Text> : null}
611
+ {formError ? (
612
+ <StatusBanner tone="error" icon="close-circle-outline" message={formError} />
613
+ ) : null}
503
614
  {connectionCheck.kind === 'success' ? (
504
- <Text style={styles.successText}>{connectionCheck.message}</Text>
615
+ <StatusBanner
616
+ tone="success"
617
+ icon="checkmark-circle-outline"
618
+ message={connectionCheck.message}
619
+ />
505
620
  ) : null}
506
621
  {connectionCheck.kind === 'error' ? (
507
- <Text style={styles.errorText}>{connectionCheck.message}</Text>
622
+ <StatusBanner
623
+ tone="error"
624
+ icon="alert-circle-outline"
625
+ message={connectionCheck.message}
626
+ />
508
627
  ) : null}
509
628
 
629
+ <View style={styles.formSectionHeader}>
630
+ <Text style={styles.formSectionEyebrow}>3. Verify and continue</Text>
631
+ <Text style={styles.formSectionTitle}>
632
+ Confirm the bridge responds before saving it into the app.
633
+ </Text>
634
+ </View>
635
+
510
636
  <View style={styles.actionRow}>
511
637
  <Pressable
512
638
  onPress={() => {
@@ -547,15 +673,14 @@ export function OnboardingScreen({
547
673
  </Text>
548
674
  </Pressable>
549
675
  </View>
676
+ </BlurView>
677
+ </ScrollView>
678
+ {showOnboardingDock ? (
679
+ <View style={styles.connectFooter}>
680
+ <OnboardingStepDock currentStage={currentSetupStage} />
550
681
  </View>
551
-
552
- <View style={styles.hintCard}>
553
- <Text style={styles.hintTitle}>Quick Setup</Text>
554
- <Text style={styles.hintText}>1. Start the bridge in Local (LAN) or Tailscale mode.</Text>
555
- <Text style={styles.hintText}>2. Pick Local or Tailscale above, then confirm bridge URL.</Text>
556
- <Text style={styles.hintText}>3. Scan bridge QR, then test connection and continue.</Text>
557
- </View>
558
- </ScrollView>
682
+ ) : null}
683
+ </View>
559
684
  )}
560
685
  <Modal
561
686
  animationType="slide"
@@ -606,7 +731,7 @@ export function OnboardingScreen({
606
731
  );
607
732
  }
608
733
 
609
- function IntroFeatureRow({
734
+ function IntroFeatureCard({
610
735
  icon,
611
736
  title,
612
737
  description,
@@ -616,7 +741,7 @@ function IntroFeatureRow({
616
741
  description: string;
617
742
  }) {
618
743
  return (
619
- <View style={styles.introFeatureRow}>
744
+ <View style={styles.introFeatureCard}>
620
745
  <View style={styles.introFeatureIconWrap}>
621
746
  <Ionicons name={icon} size={16} color={colors.textPrimary} />
622
747
  </View>
@@ -628,6 +753,177 @@ function IntroFeatureRow({
628
753
  );
629
754
  }
630
755
 
756
+ function AmbientBadge({
757
+ icon,
758
+ label,
759
+ }: {
760
+ icon: keyof typeof Ionicons.glyphMap;
761
+ label: string;
762
+ }) {
763
+ return (
764
+ <View style={styles.ambientBadge}>
765
+ <Ionicons name={icon} size={13} color={colors.textSecondary} />
766
+ <Text style={styles.ambientBadgeText}>{label}</Text>
767
+ </View>
768
+ );
769
+ }
770
+
771
+ function OnboardingStepDock({ currentStage }: { currentStage: number }) {
772
+ return (
773
+ <BlurView intensity={45} tint="dark" style={styles.stepperDock}>
774
+ <View style={styles.stepperDockRow}>
775
+ {SETUP_STAGES.map((stage, index) => {
776
+ const stepNumber = index + 1;
777
+ const isActive = stepNumber === currentStage;
778
+ const isComplete = stepNumber < currentStage;
779
+ return (
780
+ <View
781
+ key={stage.title}
782
+ style={[
783
+ styles.stepperPill,
784
+ isActive && styles.stepperPillActive,
785
+ isComplete && styles.stepperPillComplete,
786
+ ]}
787
+ >
788
+ <View
789
+ style={[
790
+ styles.stepperPillIndex,
791
+ isActive && styles.stepperPillIndexActive,
792
+ isComplete && styles.stepperPillIndexComplete,
793
+ ]}
794
+ >
795
+ <Text
796
+ style={[
797
+ styles.stepperPillIndexText,
798
+ (isActive || isComplete) && styles.stepperPillIndexTextActive,
799
+ ]}
800
+ >
801
+ {isComplete ? '✓' : String(stepNumber)}
802
+ </Text>
803
+ </View>
804
+ <Text
805
+ numberOfLines={1}
806
+ style={[
807
+ styles.stepperPillTitle,
808
+ isActive && styles.stepperPillTitleActive,
809
+ isComplete && styles.stepperPillTitleComplete,
810
+ ]}
811
+ >
812
+ {stage.title}
813
+ </Text>
814
+ </View>
815
+ );
816
+ })}
817
+ </View>
818
+ </BlurView>
819
+ );
820
+ }
821
+
822
+ function CommandSnippet({
823
+ label,
824
+ command,
825
+ hint,
826
+ }: {
827
+ label: string;
828
+ command: string;
829
+ hint: string;
830
+ }) {
831
+ const [copied, setCopied] = useState(false);
832
+
833
+ const handleCopy = useCallback(async () => {
834
+ await Clipboard.setStringAsync(command);
835
+ setCopied(true);
836
+ setTimeout(() => {
837
+ setCopied(false);
838
+ }, 1400);
839
+ }, [command]);
840
+
841
+ return (
842
+ <View style={styles.commandCard}>
843
+ <View style={styles.commandCardHeader}>
844
+ <View style={styles.commandCardHeaderLeft}>
845
+ <Ionicons name="terminal-outline" size={14} color={colors.textSecondary} />
846
+ <Text style={styles.commandCardLabel}>{label}</Text>
847
+ </View>
848
+ <Pressable
849
+ onPress={() => {
850
+ void handleCopy();
851
+ }}
852
+ style={({ pressed }) => [
853
+ styles.commandCopyButton,
854
+ copied && styles.commandCopyButtonCopied,
855
+ pressed && styles.commandCopyButtonPressed,
856
+ ]}
857
+ >
858
+ <Ionicons
859
+ name={copied ? 'checkmark-outline' : 'copy-outline'}
860
+ size={14}
861
+ color={copied ? colors.black : colors.textPrimary}
862
+ />
863
+ <Text
864
+ style={[
865
+ styles.commandCopyButtonText,
866
+ copied && styles.commandCopyButtonTextCopied,
867
+ ]}
868
+ >
869
+ {copied ? 'Copied' : 'Copy'}
870
+ </Text>
871
+ </Pressable>
872
+ </View>
873
+ <View style={styles.commandCodeWrap}>
874
+ <Text selectable style={styles.commandCodeText}>
875
+ {command}
876
+ </Text>
877
+ </View>
878
+ <Text style={styles.commandCardHint}>{hint}</Text>
879
+ </View>
880
+ );
881
+ }
882
+
883
+ function StatusBanner({
884
+ tone,
885
+ icon,
886
+ message,
887
+ }: {
888
+ tone: 'warning' | 'error' | 'success';
889
+ icon: keyof typeof Ionicons.glyphMap;
890
+ message: string;
891
+ }) {
892
+ const iconColor =
893
+ tone === 'warning' ? '#F7D27E' : tone === 'success' ? colors.statusComplete : colors.error;
894
+
895
+ return (
896
+ <View
897
+ style={[
898
+ styles.statusBanner,
899
+ tone === 'warning'
900
+ ? styles.statusBannerWarning
901
+ : tone === 'success'
902
+ ? styles.statusBannerSuccess
903
+ : styles.statusBannerError,
904
+ ]}
905
+ >
906
+ <Ionicons
907
+ name={icon}
908
+ size={16}
909
+ color={iconColor}
910
+ />
911
+ <Text
912
+ style={[
913
+ styles.statusBannerText,
914
+ tone === 'warning'
915
+ ? styles.warningText
916
+ : tone === 'success'
917
+ ? styles.successText
918
+ : styles.errorText,
919
+ ]}
920
+ >
921
+ {message}
922
+ </Text>
923
+ </View>
924
+ );
925
+ }
926
+
631
927
  function parsePairingPayload(rawValue: string): PairingPayload | null {
632
928
  const raw = rawValue.trim();
633
929
  if (!raw) {
@@ -705,6 +1001,26 @@ const styles = StyleSheet.create({
705
1001
  keyboardAvoiding: {
706
1002
  flex: 1,
707
1003
  },
1004
+ ambientCanvas: {
1005
+ ...StyleSheet.absoluteFillObject,
1006
+ overflow: 'hidden',
1007
+ },
1008
+ ambientOrbPrimary: {
1009
+ position: 'absolute',
1010
+ top: -110,
1011
+ right: -70,
1012
+ width: 280,
1013
+ height: 280,
1014
+ borderRadius: 140,
1015
+ },
1016
+ ambientOrbSecondary: {
1017
+ position: 'absolute',
1018
+ bottom: 110,
1019
+ left: -90,
1020
+ width: 220,
1021
+ height: 220,
1022
+ borderRadius: 110,
1023
+ },
708
1024
  introRoot: {
709
1025
  flex: 1,
710
1026
  paddingHorizontal: spacing.lg,
@@ -712,11 +1028,16 @@ const styles = StyleSheet.create({
712
1028
  paddingBottom: spacing.xxl,
713
1029
  gap: spacing.md,
714
1030
  },
1031
+ introHeader: {
1032
+ flexDirection: 'row',
1033
+ alignItems: 'center',
1034
+ justifyContent: 'space-between',
1035
+ gap: spacing.md,
1036
+ },
715
1037
  introBrandRow: {
716
1038
  flexDirection: 'row',
717
1039
  alignItems: 'center',
718
1040
  gap: spacing.sm,
719
- paddingHorizontal: spacing.xs,
720
1041
  },
721
1042
  introBrandName: {
722
1043
  ...typography.headline,
@@ -724,23 +1045,223 @@ const styles = StyleSheet.create({
724
1045
  fontSize: 18,
725
1046
  letterSpacing: -0.2,
726
1047
  },
727
- introMain: {
1048
+ introScroll: {
728
1049
  flex: 1,
1050
+ },
1051
+ introScrollContent: {
1052
+ paddingBottom: spacing.lg,
729
1053
  gap: spacing.md,
730
1054
  },
731
- introFeaturesCard: {
1055
+ introHero: {
1056
+ borderRadius: 24,
1057
+ borderWidth: StyleSheet.hairlineWidth,
1058
+ borderColor: colors.borderHighlight,
1059
+ padding: spacing.lg,
1060
+ gap: spacing.sm,
1061
+ overflow: 'hidden',
1062
+ },
1063
+ introHeroEyebrow: {
1064
+ ...typography.caption,
1065
+ color: colors.textSecondary,
1066
+ textTransform: 'uppercase',
1067
+ letterSpacing: 0.8,
1068
+ },
1069
+ introHeroTitle: {
1070
+ ...typography.largeTitle,
1071
+ fontSize: 22,
1072
+ lineHeight: 26,
1073
+ letterSpacing: -0.4,
1074
+ },
1075
+ introHeroDescription: {
1076
+ ...typography.body,
1077
+ color: colors.textSecondary,
1078
+ fontSize: 14,
1079
+ lineHeight: 19,
1080
+ },
1081
+ ambientBadge: {
1082
+ flexDirection: 'row',
1083
+ alignItems: 'center',
1084
+ gap: 6,
1085
+ minHeight: 30,
1086
+ paddingHorizontal: spacing.sm,
1087
+ borderRadius: radius.full,
1088
+ backgroundColor: 'rgba(255,255,255,0.06)',
1089
+ borderWidth: StyleSheet.hairlineWidth,
1090
+ borderColor: colors.borderLight,
1091
+ },
1092
+ ambientBadgeText: {
1093
+ ...typography.caption,
1094
+ color: colors.textSecondary,
1095
+ fontWeight: '600',
1096
+ },
1097
+ stepperDock: {
1098
+ borderRadius: 16,
1099
+ borderWidth: StyleSheet.hairlineWidth,
1100
+ borderColor: colors.borderHighlight,
1101
+ backgroundColor: 'rgba(12, 14, 18, 0.76)',
1102
+ paddingHorizontal: spacing.sm,
1103
+ paddingVertical: spacing.xs,
1104
+ overflow: 'hidden',
1105
+ },
1106
+ stepperDockRow: {
1107
+ flexDirection: 'row',
1108
+ gap: spacing.xs,
1109
+ },
1110
+ stepperPill: {
732
1111
  flex: 1,
1112
+ minHeight: 36,
1113
+ borderRadius: radius.full,
1114
+ flexDirection: 'row',
1115
+ alignItems: 'center',
1116
+ justifyContent: 'center',
1117
+ paddingHorizontal: spacing.sm,
1118
+ paddingVertical: 6,
1119
+ gap: 6,
1120
+ backgroundColor: 'rgba(255,255,255,0.03)',
1121
+ borderWidth: StyleSheet.hairlineWidth,
1122
+ borderColor: colors.borderLight,
1123
+ },
1124
+ stepperPillActive: {
1125
+ backgroundColor: 'rgba(181, 189, 204, 0.10)',
1126
+ borderColor: colors.borderHighlight,
1127
+ },
1128
+ stepperPillComplete: {
1129
+ backgroundColor: 'rgba(198, 205, 217, 0.08)',
1130
+ borderColor: 'rgba(198, 205, 217, 0.22)',
1131
+ },
1132
+ stepperPillIndex: {
1133
+ width: 18,
1134
+ height: 18,
1135
+ borderRadius: 9,
1136
+ alignItems: 'center',
1137
+ justifyContent: 'center',
1138
+ backgroundColor: colors.bgInput,
1139
+ borderWidth: StyleSheet.hairlineWidth,
1140
+ borderColor: colors.border,
1141
+ },
1142
+ stepperPillIndexActive: {
1143
+ backgroundColor: colors.accent,
1144
+ borderColor: colors.accent,
1145
+ },
1146
+ stepperPillIndexComplete: {
1147
+ backgroundColor: colors.statusComplete,
1148
+ borderColor: colors.statusComplete,
1149
+ },
1150
+ stepperPillIndexText: {
1151
+ ...typography.caption,
1152
+ color: colors.textPrimary,
1153
+ fontWeight: '700',
1154
+ fontSize: 10,
1155
+ lineHeight: 12,
1156
+ },
1157
+ stepperPillIndexTextActive: {
1158
+ color: colors.black,
1159
+ },
1160
+ stepperPillTitle: {
1161
+ ...typography.caption,
1162
+ color: colors.textSecondary,
1163
+ fontWeight: '600',
1164
+ fontSize: 10,
1165
+ lineHeight: 12,
1166
+ },
1167
+ stepperPillTitleActive: {
1168
+ color: colors.textPrimary,
1169
+ },
1170
+ stepperPillTitleComplete: {
1171
+ color: colors.textPrimary,
1172
+ },
1173
+ introSectionHeader: {
1174
+ gap: spacing.xs,
1175
+ paddingHorizontal: spacing.xs,
1176
+ },
1177
+ introSectionTitle: {
1178
+ ...typography.caption,
1179
+ color: colors.textMuted,
1180
+ textTransform: 'uppercase',
1181
+ letterSpacing: 0.9,
1182
+ },
1183
+ introSectionSubtitle: {
1184
+ ...typography.body,
1185
+ color: colors.textSecondary,
1186
+ },
1187
+ introFeatureGrid: {
1188
+ gap: spacing.sm,
1189
+ },
1190
+ introFeatureCard: {
1191
+ flexDirection: 'row',
1192
+ gap: spacing.md,
1193
+ borderRadius: 18,
1194
+ borderWidth: StyleSheet.hairlineWidth,
1195
+ borderColor: colors.borderLight,
1196
+ backgroundColor: 'rgba(7, 9, 12, 0.72)',
1197
+ paddingHorizontal: spacing.md,
733
1198
  paddingVertical: spacing.md,
734
1199
  },
735
- introFeaturesList: {
1200
+ introFeatureIconWrap: {
1201
+ width: 36,
1202
+ height: 36,
1203
+ borderRadius: 14,
1204
+ alignItems: 'center',
1205
+ justifyContent: 'center',
1206
+ backgroundColor: 'rgba(255,255,255,0.07)',
1207
+ borderWidth: StyleSheet.hairlineWidth,
1208
+ borderColor: colors.borderLight,
1209
+ },
1210
+ introFeatureTextWrap: {
736
1211
  flex: 1,
1212
+ gap: 2,
737
1213
  },
738
- introFeaturesListContent: {
1214
+ introFeatureTitle: {
1215
+ ...typography.headline,
1216
+ fontSize: 14,
1217
+ },
1218
+ introFeatureDescription: {
1219
+ ...typography.caption,
1220
+ color: colors.textSecondary,
1221
+ lineHeight: 18,
1222
+ },
1223
+ introContextCard: {
1224
+ borderRadius: 20,
1225
+ borderWidth: StyleSheet.hairlineWidth,
1226
+ borderColor: colors.borderHighlight,
1227
+ backgroundColor: 'rgba(12, 14, 18, 0.76)',
1228
+ padding: spacing.lg,
1229
+ gap: spacing.sm,
1230
+ overflow: 'hidden',
1231
+ },
1232
+ introContextTitle: {
1233
+ ...typography.headline,
1234
+ },
1235
+ introContextText: {
1236
+ ...typography.body,
1237
+ color: colors.textSecondary,
1238
+ },
1239
+ introContextPillRow: {
1240
+ flexDirection: 'row',
1241
+ flexWrap: 'wrap',
739
1242
  gap: spacing.sm,
740
- paddingBottom: spacing.sm,
1243
+ marginTop: spacing.xs,
741
1244
  },
742
1245
  introFooter: {
743
- paddingTop: spacing.sm,
1246
+ gap: spacing.sm,
1247
+ },
1248
+ introNextButton: {
1249
+ borderRadius: 18,
1250
+ backgroundColor: colors.accent,
1251
+ minHeight: 58,
1252
+ flexDirection: 'row',
1253
+ alignItems: 'center',
1254
+ justifyContent: 'center',
1255
+ gap: spacing.sm,
1256
+ },
1257
+ introNextButtonPressed: {
1258
+ backgroundColor: colors.accentPressed,
1259
+ },
1260
+ introNextButtonText: {
1261
+ ...typography.headline,
1262
+ color: colors.black,
1263
+ fontSize: 17,
1264
+ fontWeight: '700',
744
1265
  },
745
1266
  scroll: {
746
1267
  flex: 1,
@@ -751,13 +1272,20 @@ const styles = StyleSheet.create({
751
1272
  paddingBottom: spacing.xxl,
752
1273
  gap: spacing.md,
753
1274
  },
754
- heroCard: {
755
- borderRadius: radius.lg,
1275
+ connectRoot: {
1276
+ flex: 1,
1277
+ },
1278
+ connectFooter: {
1279
+ paddingHorizontal: spacing.lg,
1280
+ paddingTop: spacing.sm,
1281
+ paddingBottom: spacing.md,
1282
+ },
1283
+ connectHero: {
1284
+ borderRadius: 22,
756
1285
  borderWidth: StyleSheet.hairlineWidth,
757
1286
  borderColor: colors.borderHighlight,
758
- backgroundColor: colors.black,
759
1287
  padding: spacing.lg,
760
- gap: spacing.sm,
1288
+ gap: spacing.xs,
761
1289
  overflow: 'hidden',
762
1290
  },
763
1291
  heroTopRow: {
@@ -766,9 +1294,9 @@ const styles = StyleSheet.create({
766
1294
  alignItems: 'center',
767
1295
  },
768
1296
  heroIconWrap: {
769
- width: 34,
770
- height: 34,
771
- borderRadius: 17,
1297
+ width: 32,
1298
+ height: 32,
1299
+ borderRadius: 16,
772
1300
  backgroundColor: 'rgba(255, 255, 255, 0.08)',
773
1301
  alignItems: 'center',
774
1302
  justifyContent: 'center',
@@ -786,91 +1314,185 @@ const styles = StyleSheet.create({
786
1314
  cancelBtnPressed: {
787
1315
  opacity: 0.75,
788
1316
  },
1317
+ connectEyebrow: {
1318
+ ...typography.caption,
1319
+ color: colors.textSecondary,
1320
+ textTransform: 'uppercase',
1321
+ letterSpacing: 0.8,
1322
+ },
789
1323
  heroTitle: {
790
1324
  ...typography.largeTitle,
791
- fontSize: 28,
792
- letterSpacing: -0.5,
1325
+ fontSize: 24,
1326
+ lineHeight: 28,
1327
+ letterSpacing: -0.45,
793
1328
  },
794
1329
  heroDescription: {
795
1330
  ...typography.body,
796
1331
  color: colors.textSecondary,
1332
+ fontSize: 14,
1333
+ lineHeight: 19,
797
1334
  },
798
- introSectionTitle: {
1335
+ formCard: {
1336
+ borderRadius: 20,
1337
+ borderWidth: StyleSheet.hairlineWidth,
1338
+ borderColor: colors.borderHighlight,
1339
+ backgroundColor: 'rgba(12, 14, 18, 0.76)',
1340
+ padding: spacing.lg,
1341
+ gap: spacing.md,
1342
+ overflow: 'hidden',
1343
+ },
1344
+ formSectionHeader: {
1345
+ gap: spacing.xs,
1346
+ },
1347
+ formSectionEyebrow: {
799
1348
  ...typography.caption,
800
- textTransform: 'uppercase',
801
- letterSpacing: 0.9,
802
1349
  color: colors.textMuted,
1350
+ textTransform: 'uppercase',
1351
+ letterSpacing: 0.85,
803
1352
  },
804
- introFeatureRow: {
1353
+ formSectionTitle: {
1354
+ ...typography.headline,
1355
+ fontSize: 15,
1356
+ lineHeight: 21,
1357
+ },
1358
+ modeCardGrid: {
805
1359
  flexDirection: 'row',
806
1360
  gap: spacing.sm,
807
- borderRadius: radius.md,
1361
+ },
1362
+ modePresetCard: {
1363
+ flex: 1,
1364
+ minHeight: 146,
1365
+ borderRadius: 18,
808
1366
  borderWidth: StyleSheet.hairlineWidth,
809
1367
  borderColor: colors.borderLight,
810
- backgroundColor: colors.black,
811
- paddingHorizontal: spacing.md,
812
- paddingVertical: spacing.md,
813
- minHeight: 62,
1368
+ backgroundColor: 'rgba(255,255,255,0.03)',
1369
+ padding: spacing.md,
1370
+ gap: spacing.sm,
1371
+ justifyContent: 'space-between',
814
1372
  },
815
- introFeatureIconWrap: {
816
- width: 30,
817
- height: 30,
818
- borderRadius: 15,
1373
+ modePresetCardSelected: {
1374
+ backgroundColor: 'rgba(181, 189, 204, 0.10)',
1375
+ borderColor: colors.borderHighlight,
1376
+ },
1377
+ modePresetCardPressed: {
1378
+ opacity: 0.84,
1379
+ },
1380
+ modePresetIconWrap: {
1381
+ width: 38,
1382
+ height: 38,
1383
+ borderRadius: 14,
819
1384
  alignItems: 'center',
820
1385
  justifyContent: 'center',
821
- backgroundColor: 'rgba(255,255,255,0.08)',
822
- marginTop: 2,
1386
+ backgroundColor: colors.bgInput,
1387
+ borderWidth: StyleSheet.hairlineWidth,
1388
+ borderColor: colors.borderLight,
823
1389
  },
824
- introFeatureTextWrap: {
825
- flex: 1,
826
- gap: 2,
1390
+ modePresetIconWrapSelected: {
1391
+ backgroundColor: 'rgba(181, 189, 204, 0.16)',
1392
+ borderColor: colors.borderHighlight,
827
1393
  },
828
- introFeatureTitle: {
1394
+ modePresetTitle: {
829
1395
  ...typography.headline,
830
- color: colors.textPrimary,
831
- fontSize: 14,
1396
+ fontSize: 15,
832
1397
  },
833
- introFeatureDescription: {
1398
+ modePresetDescription: {
834
1399
  ...typography.caption,
835
1400
  color: colors.textSecondary,
836
1401
  lineHeight: 18,
837
1402
  },
838
- introNextButton: {
839
- borderRadius: radius.md,
840
- backgroundColor: colors.accent,
841
- minHeight: 62,
1403
+ helperText: {
1404
+ ...typography.caption,
1405
+ color: colors.textMuted,
1406
+ lineHeight: 18,
1407
+ },
1408
+ commandPanel: {
1409
+ gap: spacing.sm,
1410
+ paddingTop: spacing.xs,
1411
+ },
1412
+ commandCard: {
1413
+ gap: spacing.xs,
1414
+ borderRadius: 16,
1415
+ borderWidth: StyleSheet.hairlineWidth,
1416
+ borderColor: colors.borderLight,
1417
+ backgroundColor: 'rgba(255,255,255,0.03)',
1418
+ padding: spacing.md,
1419
+ },
1420
+ commandCardHeader: {
842
1421
  flexDirection: 'row',
843
1422
  alignItems: 'center',
844
- justifyContent: 'center',
1423
+ justifyContent: 'space-between',
845
1424
  gap: spacing.sm,
846
- width: '100%',
847
1425
  },
848
- introNextButtonPressed: {
849
- backgroundColor: colors.accentPressed,
1426
+ commandCardHeaderLeft: {
1427
+ flexDirection: 'row',
1428
+ alignItems: 'center',
1429
+ gap: 6,
1430
+ flex: 1,
1431
+ minWidth: 0,
850
1432
  },
851
- introNextButtonText: {
852
- ...typography.headline,
1433
+ commandCardLabel: {
1434
+ ...typography.caption,
1435
+ color: colors.textSecondary,
1436
+ fontWeight: '600',
1437
+ letterSpacing: 0.2,
1438
+ },
1439
+ commandCopyButton: {
1440
+ minHeight: 30,
1441
+ paddingHorizontal: spacing.sm,
1442
+ borderRadius: radius.full,
1443
+ borderWidth: StyleSheet.hairlineWidth,
1444
+ borderColor: colors.border,
1445
+ backgroundColor: colors.bgInput,
1446
+ flexDirection: 'row',
1447
+ alignItems: 'center',
1448
+ justifyContent: 'center',
1449
+ gap: 4,
1450
+ flexShrink: 0,
1451
+ },
1452
+ commandCopyButtonCopied: {
1453
+ backgroundColor: colors.accent,
1454
+ borderColor: colors.accent,
1455
+ },
1456
+ commandCopyButtonPressed: {
1457
+ opacity: 0.84,
1458
+ },
1459
+ commandCopyButtonText: {
1460
+ ...typography.caption,
1461
+ color: colors.textPrimary,
1462
+ fontWeight: '600',
1463
+ },
1464
+ commandCopyButtonTextCopied: {
853
1465
  color: colors.black,
854
- fontSize: 18,
855
- fontWeight: '700',
856
1466
  },
857
- formCard: {
858
- borderRadius: radius.lg,
1467
+ commandCodeWrap: {
1468
+ borderRadius: 12,
859
1469
  borderWidth: StyleSheet.hairlineWidth,
860
- borderColor: colors.borderHighlight,
861
- backgroundColor: colors.black,
862
- padding: spacing.lg,
1470
+ borderColor: colors.border,
1471
+ backgroundColor: colors.bgInput,
1472
+ paddingHorizontal: spacing.sm,
1473
+ paddingVertical: spacing.sm,
1474
+ },
1475
+ commandCodeText: {
1476
+ ...typography.mono,
1477
+ color: colors.textPrimary,
1478
+ fontSize: 12,
1479
+ lineHeight: 18,
1480
+ },
1481
+ commandCardHint: {
1482
+ ...typography.caption,
1483
+ color: colors.textMuted,
1484
+ lineHeight: 16,
1485
+ },
1486
+ fieldGroup: {
863
1487
  gap: spacing.sm,
864
- overflow: 'hidden',
865
1488
  },
866
1489
  label: {
867
1490
  ...typography.caption,
868
1491
  textTransform: 'uppercase',
869
- letterSpacing: 0.9,
1492
+ letterSpacing: 0.85,
870
1493
  color: colors.textMuted,
871
1494
  },
872
1495
  tokenHeaderRow: {
873
- marginTop: spacing.xs,
874
1496
  flexDirection: 'row',
875
1497
  justifyContent: 'space-between',
876
1498
  alignItems: 'center',
@@ -881,32 +1503,46 @@ const styles = StyleSheet.create({
881
1503
  color: colors.textMuted,
882
1504
  fontSize: 11,
883
1505
  },
884
- tokenInputWrap: {
1506
+ inputRow: {
1507
+ flex: 1,
1508
+ minWidth: 0,
885
1509
  flexDirection: 'row',
886
1510
  alignItems: 'center',
887
- gap: spacing.xs,
888
- },
889
- input: {
890
- ...typography.body,
891
- color: colors.textPrimary,
892
1511
  borderWidth: 1,
893
1512
  borderColor: colors.border,
894
- borderRadius: radius.md,
895
- backgroundColor: colors.black,
1513
+ borderRadius: 16,
1514
+ backgroundColor: 'rgba(255,255,255,0.03)',
1515
+ minHeight: 54,
896
1516
  paddingHorizontal: spacing.md,
897
- paddingVertical: spacing.md,
1517
+ gap: spacing.sm,
898
1518
  },
899
- tokenInput: {
1519
+ inputIconWrap: {
1520
+ width: 24,
1521
+ alignItems: 'center',
1522
+ justifyContent: 'center',
1523
+ },
1524
+ inputText: {
900
1525
  flex: 1,
1526
+ minWidth: 0,
1527
+ ...typography.body,
1528
+ color: colors.textPrimary,
1529
+ paddingVertical: spacing.md,
1530
+ },
1531
+ tokenInputWrap: {
1532
+ flexDirection: 'row',
1533
+ alignItems: 'center',
1534
+ gap: spacing.xs,
1535
+ minWidth: 0,
901
1536
  },
902
1537
  tokenRevealBtn: {
903
1538
  minWidth: 74,
904
- borderRadius: radius.md,
1539
+ minHeight: 54,
1540
+ flexShrink: 0,
1541
+ borderRadius: 16,
905
1542
  borderWidth: 1,
906
1543
  borderColor: colors.border,
907
- backgroundColor: colors.bgMain,
1544
+ backgroundColor: colors.bgInput,
908
1545
  paddingHorizontal: spacing.sm,
909
- paddingVertical: spacing.sm,
910
1546
  flexDirection: 'row',
911
1547
  alignItems: 'center',
912
1548
  justifyContent: 'center',
@@ -920,39 +1556,12 @@ const styles = StyleSheet.create({
920
1556
  color: colors.textSecondary,
921
1557
  fontWeight: '600',
922
1558
  },
923
- modeRow: {
924
- flexDirection: 'row',
925
- gap: spacing.sm,
926
- marginBottom: spacing.xs,
927
- },
928
- modeButton: {
929
- flex: 1,
930
- minHeight: 44,
931
- borderRadius: radius.md,
932
- borderWidth: 1,
933
- borderColor: colors.border,
934
- backgroundColor: colors.bgMain,
935
- paddingHorizontal: spacing.md,
936
- flexDirection: 'row',
937
- alignItems: 'center',
938
- justifyContent: 'center',
939
- gap: spacing.xs,
940
- },
941
- modeButtonPressed: {
942
- opacity: 0.82,
943
- },
944
- modeButtonText: {
945
- ...typography.caption,
946
- color: colors.textPrimary,
947
- fontWeight: '700',
948
- },
949
1559
  scanButton: {
950
- marginTop: spacing.xs,
951
- borderRadius: radius.md,
1560
+ borderRadius: 16,
952
1561
  borderWidth: 1,
953
1562
  borderColor: colors.border,
954
- backgroundColor: colors.bgMain,
955
- minHeight: 44,
1563
+ backgroundColor: colors.bgInput,
1564
+ minHeight: 50,
956
1565
  paddingHorizontal: spacing.md,
957
1566
  flexDirection: 'row',
958
1567
  alignItems: 'center',
@@ -963,19 +1572,15 @@ const styles = StyleSheet.create({
963
1572
  opacity: 0.82,
964
1573
  },
965
1574
  scanButtonText: {
966
- ...typography.caption,
1575
+ ...typography.headline,
967
1576
  color: colors.textPrimary,
968
1577
  fontWeight: '700',
969
1578
  },
970
- helperText: {
971
- ...typography.caption,
972
- color: colors.textMuted,
973
- },
974
1579
  previewWrap: {
975
- borderRadius: radius.md,
1580
+ borderRadius: 16,
976
1581
  borderWidth: StyleSheet.hairlineWidth,
977
1582
  borderColor: colors.borderLight,
978
- backgroundColor: colors.black,
1583
+ backgroundColor: 'rgba(255,255,255,0.03)',
979
1584
  paddingHorizontal: spacing.md,
980
1585
  paddingVertical: spacing.sm,
981
1586
  gap: spacing.xs,
@@ -989,6 +1594,32 @@ const styles = StyleSheet.create({
989
1594
  color: colors.textPrimary,
990
1595
  fontSize: 13,
991
1596
  },
1597
+ statusBanner: {
1598
+ flexDirection: 'row',
1599
+ alignItems: 'flex-start',
1600
+ gap: spacing.sm,
1601
+ borderRadius: 16,
1602
+ borderWidth: StyleSheet.hairlineWidth,
1603
+ paddingHorizontal: spacing.md,
1604
+ paddingVertical: spacing.sm,
1605
+ },
1606
+ statusBannerWarning: {
1607
+ backgroundColor: 'rgba(247, 210, 126, 0.08)',
1608
+ borderColor: 'rgba(247, 210, 126, 0.22)',
1609
+ },
1610
+ statusBannerSuccess: {
1611
+ backgroundColor: 'rgba(198, 205, 217, 0.10)',
1612
+ borderColor: 'rgba(198, 205, 217, 0.22)',
1613
+ },
1614
+ statusBannerError: {
1615
+ backgroundColor: colors.errorBg,
1616
+ borderColor: 'rgba(239, 68, 68, 0.28)',
1617
+ },
1618
+ statusBannerText: {
1619
+ flex: 1,
1620
+ ...typography.caption,
1621
+ lineHeight: 18,
1622
+ },
992
1623
  warningText: {
993
1624
  ...typography.caption,
994
1625
  color: '#F7D27E',
@@ -1004,7 +1635,6 @@ const styles = StyleSheet.create({
1004
1635
  actionRow: {
1005
1636
  flexDirection: 'row',
1006
1637
  gap: spacing.sm,
1007
- marginTop: spacing.sm,
1008
1638
  },
1009
1639
  secondaryButton: {
1010
1640
  flex: 1,
@@ -1014,9 +1644,9 @@ const styles = StyleSheet.create({
1014
1644
  justifyContent: 'center',
1015
1645
  borderWidth: 1,
1016
1646
  borderColor: colors.border,
1017
- backgroundColor: colors.bgMain,
1018
- borderRadius: radius.md,
1019
- paddingVertical: spacing.md,
1647
+ backgroundColor: colors.bgInput,
1648
+ borderRadius: 16,
1649
+ minHeight: 54,
1020
1650
  },
1021
1651
  secondaryButtonPressed: {
1022
1652
  opacity: 0.8,
@@ -1036,8 +1666,8 @@ const styles = StyleSheet.create({
1036
1666
  alignItems: 'center',
1037
1667
  justifyContent: 'center',
1038
1668
  backgroundColor: colors.accent,
1039
- borderRadius: radius.md,
1040
- paddingVertical: spacing.md,
1669
+ borderRadius: 16,
1670
+ minHeight: 54,
1041
1671
  },
1042
1672
  primaryButtonPressed: {
1043
1673
  backgroundColor: colors.accentPressed,
@@ -1057,10 +1687,10 @@ const styles = StyleSheet.create({
1057
1687
  paddingHorizontal: spacing.lg,
1058
1688
  },
1059
1689
  scannerSheet: {
1060
- borderRadius: radius.lg,
1690
+ borderRadius: 22,
1061
1691
  borderWidth: StyleSheet.hairlineWidth,
1062
1692
  borderColor: colors.borderHighlight,
1063
- backgroundColor: colors.black,
1693
+ backgroundColor: '#07090C',
1064
1694
  padding: spacing.lg,
1065
1695
  gap: spacing.md,
1066
1696
  },
@@ -1089,11 +1719,11 @@ const styles = StyleSheet.create({
1089
1719
  scannerCameraFrame: {
1090
1720
  width: '100%',
1091
1721
  aspectRatio: 1,
1092
- borderRadius: radius.md,
1722
+ borderRadius: 18,
1093
1723
  overflow: 'hidden',
1094
1724
  borderWidth: StyleSheet.hairlineWidth,
1095
1725
  borderColor: colors.borderLight,
1096
- backgroundColor: colors.bgMain,
1726
+ backgroundColor: colors.bgItem,
1097
1727
  },
1098
1728
  scannerCamera: {
1099
1729
  flex: 1,
@@ -1113,20 +1743,4 @@ const styles = StyleSheet.create({
1113
1743
  ...typography.caption,
1114
1744
  color: colors.textMuted,
1115
1745
  },
1116
- hintCard: {
1117
- borderRadius: radius.md,
1118
- borderWidth: StyleSheet.hairlineWidth,
1119
- borderColor: colors.borderLight,
1120
- backgroundColor: colors.black,
1121
- padding: spacing.lg,
1122
- gap: spacing.sm,
1123
- },
1124
- hintTitle: {
1125
- ...typography.headline,
1126
- color: colors.textPrimary,
1127
- },
1128
- hintText: {
1129
- ...typography.caption,
1130
- color: colors.textSecondary,
1131
- },
1132
1746
  });