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,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,99 +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
- keyboardAppearance="dark"
423
- autoCapitalize="none"
424
- autoCorrect={false}
425
- keyboardType="url"
426
- placeholder="http://100.101.102.103:8787"
427
- placeholderTextColor={colors.textMuted}
428
- style={styles.input}
429
- returnKeyType="done"
430
- onSubmitEditing={() => {
431
- void handleSave();
432
- }}
433
- />
434
- <View style={styles.tokenHeaderRow}>
435
- <Text style={styles.label}>Bridge Token</Text>
436
- <Text style={styles.optionalLabel}>Required</Text>
437
- </View>
438
- <View style={styles.tokenInputWrap}>
439
- <TextInput
440
- value={tokenInput}
441
- onChangeText={(value) => {
442
- setTokenInput(value);
443
- setConnectionCheck({ kind: 'idle' });
444
- }}
445
- keyboardAppearance="dark"
446
- autoCapitalize="none"
447
- autoCorrect={false}
448
- keyboardType="default"
449
- placeholder="Paste bridge token"
450
- placeholderTextColor={colors.textMuted}
451
- style={[styles.input, styles.tokenInput]}
452
- secureTextEntry={tokenHidden}
453
- returnKeyType="done"
454
- onSubmitEditing={() => {
455
- void handleSave();
456
- }}
457
- />
458
- <Pressable
459
- onPress={() => setTokenHidden((prev) => !prev)}
460
- style={({ pressed }) => [
461
- styles.tokenRevealBtn,
462
- pressed && styles.tokenRevealBtnPressed,
463
- ]}
464
- >
465
- <Ionicons
466
- name={tokenHidden ? 'eye-outline' : 'eye-off-outline'}
467
- size={16}
468
- color={colors.textSecondary}
469
- />
470
- <Text style={styles.tokenRevealBtnText}>
471
- {tokenHidden ? 'Show' : 'Hide'}
472
- </Text>
473
- </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>
474
495
  </View>
496
+
475
497
  <Pressable
476
498
  onPress={() => {
477
499
  void openScanner();
@@ -482,15 +504,96 @@ export function OnboardingScreen({
482
504
  ]}
483
505
  >
484
506
  <Ionicons name="qr-code-outline" size={16} color={colors.textPrimary} />
485
- <Text style={styles.scanButtonText}>Scan Bridge QR</Text>
507
+ <Text style={styles.scanButtonText}>Scan bridge QR</Text>
486
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
+
487
590
  <Text style={styles.helperText}>
488
591
  URL supports `http`, `https`, `ws`, and `wss`. `/rpc` is added automatically.
489
592
  </Text>
490
593
 
491
594
  {normalizedBridgeUrl ? (
492
595
  <View style={styles.previewWrap}>
493
- <Text style={styles.previewLabel}>Normalized URL</Text>
596
+ <Text style={styles.previewLabel}>Normalized target</Text>
494
597
  <Text selectable style={styles.previewValue}>
495
598
  {normalizedBridgeUrl}
496
599
  </Text>
@@ -498,17 +601,38 @@ export function OnboardingScreen({
498
601
  ) : null}
499
602
 
500
603
  {insecureRemoteWarning ? (
501
- <Text style={styles.warningText}>{insecureRemoteWarning}</Text>
604
+ <StatusBanner
605
+ tone="warning"
606
+ icon="warning-outline"
607
+ message={insecureRemoteWarning}
608
+ />
502
609
  ) : null}
503
610
 
504
- {formError ? <Text style={styles.errorText}>{formError}</Text> : null}
611
+ {formError ? (
612
+ <StatusBanner tone="error" icon="close-circle-outline" message={formError} />
613
+ ) : null}
505
614
  {connectionCheck.kind === 'success' ? (
506
- <Text style={styles.successText}>{connectionCheck.message}</Text>
615
+ <StatusBanner
616
+ tone="success"
617
+ icon="checkmark-circle-outline"
618
+ message={connectionCheck.message}
619
+ />
507
620
  ) : null}
508
621
  {connectionCheck.kind === 'error' ? (
509
- <Text style={styles.errorText}>{connectionCheck.message}</Text>
622
+ <StatusBanner
623
+ tone="error"
624
+ icon="alert-circle-outline"
625
+ message={connectionCheck.message}
626
+ />
510
627
  ) : null}
511
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
+
512
636
  <View style={styles.actionRow}>
513
637
  <Pressable
514
638
  onPress={() => {
@@ -549,15 +673,14 @@ export function OnboardingScreen({
549
673
  </Text>
550
674
  </Pressable>
551
675
  </View>
676
+ </BlurView>
677
+ </ScrollView>
678
+ {showOnboardingDock ? (
679
+ <View style={styles.connectFooter}>
680
+ <OnboardingStepDock currentStage={currentSetupStage} />
552
681
  </View>
553
-
554
- <View style={styles.hintCard}>
555
- <Text style={styles.hintTitle}>Quick Setup</Text>
556
- <Text style={styles.hintText}>1. Start the bridge in Local (LAN) or Tailscale mode.</Text>
557
- <Text style={styles.hintText}>2. Pick Local or Tailscale above, then confirm bridge URL.</Text>
558
- <Text style={styles.hintText}>3. Scan bridge QR, then test connection and continue.</Text>
559
- </View>
560
- </ScrollView>
682
+ ) : null}
683
+ </View>
561
684
  )}
562
685
  <Modal
563
686
  animationType="slide"
@@ -608,7 +731,7 @@ export function OnboardingScreen({
608
731
  );
609
732
  }
610
733
 
611
- function IntroFeatureRow({
734
+ function IntroFeatureCard({
612
735
  icon,
613
736
  title,
614
737
  description,
@@ -618,7 +741,7 @@ function IntroFeatureRow({
618
741
  description: string;
619
742
  }) {
620
743
  return (
621
- <View style={styles.introFeatureRow}>
744
+ <View style={styles.introFeatureCard}>
622
745
  <View style={styles.introFeatureIconWrap}>
623
746
  <Ionicons name={icon} size={16} color={colors.textPrimary} />
624
747
  </View>
@@ -630,6 +753,177 @@ function IntroFeatureRow({
630
753
  );
631
754
  }
632
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
+
633
927
  function parsePairingPayload(rawValue: string): PairingPayload | null {
634
928
  const raw = rawValue.trim();
635
929
  if (!raw) {
@@ -707,6 +1001,26 @@ const styles = StyleSheet.create({
707
1001
  keyboardAvoiding: {
708
1002
  flex: 1,
709
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
+ },
710
1024
  introRoot: {
711
1025
  flex: 1,
712
1026
  paddingHorizontal: spacing.lg,
@@ -714,11 +1028,16 @@ const styles = StyleSheet.create({
714
1028
  paddingBottom: spacing.xxl,
715
1029
  gap: spacing.md,
716
1030
  },
1031
+ introHeader: {
1032
+ flexDirection: 'row',
1033
+ alignItems: 'center',
1034
+ justifyContent: 'space-between',
1035
+ gap: spacing.md,
1036
+ },
717
1037
  introBrandRow: {
718
1038
  flexDirection: 'row',
719
1039
  alignItems: 'center',
720
1040
  gap: spacing.sm,
721
- paddingHorizontal: spacing.xs,
722
1041
  },
723
1042
  introBrandName: {
724
1043
  ...typography.headline,
@@ -726,23 +1045,223 @@ const styles = StyleSheet.create({
726
1045
  fontSize: 18,
727
1046
  letterSpacing: -0.2,
728
1047
  },
729
- introMain: {
1048
+ introScroll: {
730
1049
  flex: 1,
1050
+ },
1051
+ introScrollContent: {
1052
+ paddingBottom: spacing.lg,
731
1053
  gap: spacing.md,
732
1054
  },
733
- 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: {
734
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,
735
1198
  paddingVertical: spacing.md,
736
1199
  },
737
- 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: {
738
1211
  flex: 1,
1212
+ gap: 2,
739
1213
  },
740
- 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',
741
1242
  gap: spacing.sm,
742
- paddingBottom: spacing.sm,
1243
+ marginTop: spacing.xs,
743
1244
  },
744
1245
  introFooter: {
745
- 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',
746
1265
  },
747
1266
  scroll: {
748
1267
  flex: 1,
@@ -753,13 +1272,20 @@ const styles = StyleSheet.create({
753
1272
  paddingBottom: spacing.xxl,
754
1273
  gap: spacing.md,
755
1274
  },
756
- heroCard: {
757
- 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,
758
1285
  borderWidth: StyleSheet.hairlineWidth,
759
1286
  borderColor: colors.borderHighlight,
760
- backgroundColor: colors.black,
761
1287
  padding: spacing.lg,
762
- gap: spacing.sm,
1288
+ gap: spacing.xs,
763
1289
  overflow: 'hidden',
764
1290
  },
765
1291
  heroTopRow: {
@@ -768,9 +1294,9 @@ const styles = StyleSheet.create({
768
1294
  alignItems: 'center',
769
1295
  },
770
1296
  heroIconWrap: {
771
- width: 34,
772
- height: 34,
773
- borderRadius: 17,
1297
+ width: 32,
1298
+ height: 32,
1299
+ borderRadius: 16,
774
1300
  backgroundColor: 'rgba(255, 255, 255, 0.08)',
775
1301
  alignItems: 'center',
776
1302
  justifyContent: 'center',
@@ -788,91 +1314,185 @@ const styles = StyleSheet.create({
788
1314
  cancelBtnPressed: {
789
1315
  opacity: 0.75,
790
1316
  },
1317
+ connectEyebrow: {
1318
+ ...typography.caption,
1319
+ color: colors.textSecondary,
1320
+ textTransform: 'uppercase',
1321
+ letterSpacing: 0.8,
1322
+ },
791
1323
  heroTitle: {
792
1324
  ...typography.largeTitle,
793
- fontSize: 28,
794
- letterSpacing: -0.5,
1325
+ fontSize: 24,
1326
+ lineHeight: 28,
1327
+ letterSpacing: -0.45,
795
1328
  },
796
1329
  heroDescription: {
797
1330
  ...typography.body,
798
1331
  color: colors.textSecondary,
1332
+ fontSize: 14,
1333
+ lineHeight: 19,
799
1334
  },
800
- 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: {
801
1348
  ...typography.caption,
802
- textTransform: 'uppercase',
803
- letterSpacing: 0.9,
804
1349
  color: colors.textMuted,
1350
+ textTransform: 'uppercase',
1351
+ letterSpacing: 0.85,
805
1352
  },
806
- introFeatureRow: {
1353
+ formSectionTitle: {
1354
+ ...typography.headline,
1355
+ fontSize: 15,
1356
+ lineHeight: 21,
1357
+ },
1358
+ modeCardGrid: {
807
1359
  flexDirection: 'row',
808
1360
  gap: spacing.sm,
809
- borderRadius: radius.md,
1361
+ },
1362
+ modePresetCard: {
1363
+ flex: 1,
1364
+ minHeight: 146,
1365
+ borderRadius: 18,
810
1366
  borderWidth: StyleSheet.hairlineWidth,
811
1367
  borderColor: colors.borderLight,
812
- backgroundColor: colors.black,
813
- paddingHorizontal: spacing.md,
814
- paddingVertical: spacing.md,
815
- minHeight: 62,
1368
+ backgroundColor: 'rgba(255,255,255,0.03)',
1369
+ padding: spacing.md,
1370
+ gap: spacing.sm,
1371
+ justifyContent: 'space-between',
816
1372
  },
817
- introFeatureIconWrap: {
818
- width: 30,
819
- height: 30,
820
- 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,
821
1384
  alignItems: 'center',
822
1385
  justifyContent: 'center',
823
- backgroundColor: 'rgba(255,255,255,0.08)',
824
- marginTop: 2,
1386
+ backgroundColor: colors.bgInput,
1387
+ borderWidth: StyleSheet.hairlineWidth,
1388
+ borderColor: colors.borderLight,
825
1389
  },
826
- introFeatureTextWrap: {
827
- flex: 1,
828
- gap: 2,
1390
+ modePresetIconWrapSelected: {
1391
+ backgroundColor: 'rgba(181, 189, 204, 0.16)',
1392
+ borderColor: colors.borderHighlight,
829
1393
  },
830
- introFeatureTitle: {
1394
+ modePresetTitle: {
831
1395
  ...typography.headline,
832
- color: colors.textPrimary,
833
- fontSize: 14,
1396
+ fontSize: 15,
834
1397
  },
835
- introFeatureDescription: {
1398
+ modePresetDescription: {
836
1399
  ...typography.caption,
837
1400
  color: colors.textSecondary,
838
1401
  lineHeight: 18,
839
1402
  },
840
- introNextButton: {
841
- borderRadius: radius.md,
842
- backgroundColor: colors.accent,
843
- 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: {
844
1421
  flexDirection: 'row',
845
1422
  alignItems: 'center',
846
- justifyContent: 'center',
1423
+ justifyContent: 'space-between',
847
1424
  gap: spacing.sm,
848
- width: '100%',
849
1425
  },
850
- introNextButtonPressed: {
851
- backgroundColor: colors.accentPressed,
1426
+ commandCardHeaderLeft: {
1427
+ flexDirection: 'row',
1428
+ alignItems: 'center',
1429
+ gap: 6,
1430
+ flex: 1,
1431
+ minWidth: 0,
852
1432
  },
853
- introNextButtonText: {
854
- ...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: {
855
1465
  color: colors.black,
856
- fontSize: 18,
857
- fontWeight: '700',
858
1466
  },
859
- formCard: {
860
- borderRadius: radius.lg,
1467
+ commandCodeWrap: {
1468
+ borderRadius: 12,
861
1469
  borderWidth: StyleSheet.hairlineWidth,
862
- borderColor: colors.borderHighlight,
863
- backgroundColor: colors.black,
864
- 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: {
865
1487
  gap: spacing.sm,
866
- overflow: 'hidden',
867
1488
  },
868
1489
  label: {
869
1490
  ...typography.caption,
870
1491
  textTransform: 'uppercase',
871
- letterSpacing: 0.9,
1492
+ letterSpacing: 0.85,
872
1493
  color: colors.textMuted,
873
1494
  },
874
1495
  tokenHeaderRow: {
875
- marginTop: spacing.xs,
876
1496
  flexDirection: 'row',
877
1497
  justifyContent: 'space-between',
878
1498
  alignItems: 'center',
@@ -883,32 +1503,46 @@ const styles = StyleSheet.create({
883
1503
  color: colors.textMuted,
884
1504
  fontSize: 11,
885
1505
  },
886
- tokenInputWrap: {
1506
+ inputRow: {
1507
+ flex: 1,
1508
+ minWidth: 0,
887
1509
  flexDirection: 'row',
888
1510
  alignItems: 'center',
889
- gap: spacing.xs,
890
- },
891
- input: {
892
- ...typography.body,
893
- color: colors.textPrimary,
894
1511
  borderWidth: 1,
895
1512
  borderColor: colors.border,
896
- borderRadius: radius.md,
897
- backgroundColor: colors.black,
1513
+ borderRadius: 16,
1514
+ backgroundColor: 'rgba(255,255,255,0.03)',
1515
+ minHeight: 54,
898
1516
  paddingHorizontal: spacing.md,
899
- paddingVertical: spacing.md,
1517
+ gap: spacing.sm,
900
1518
  },
901
- tokenInput: {
1519
+ inputIconWrap: {
1520
+ width: 24,
1521
+ alignItems: 'center',
1522
+ justifyContent: 'center',
1523
+ },
1524
+ inputText: {
902
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,
903
1536
  },
904
1537
  tokenRevealBtn: {
905
1538
  minWidth: 74,
906
- borderRadius: radius.md,
1539
+ minHeight: 54,
1540
+ flexShrink: 0,
1541
+ borderRadius: 16,
907
1542
  borderWidth: 1,
908
1543
  borderColor: colors.border,
909
- backgroundColor: colors.bgMain,
1544
+ backgroundColor: colors.bgInput,
910
1545
  paddingHorizontal: spacing.sm,
911
- paddingVertical: spacing.sm,
912
1546
  flexDirection: 'row',
913
1547
  alignItems: 'center',
914
1548
  justifyContent: 'center',
@@ -922,39 +1556,12 @@ const styles = StyleSheet.create({
922
1556
  color: colors.textSecondary,
923
1557
  fontWeight: '600',
924
1558
  },
925
- modeRow: {
926
- flexDirection: 'row',
927
- gap: spacing.sm,
928
- marginBottom: spacing.xs,
929
- },
930
- modeButton: {
931
- flex: 1,
932
- minHeight: 44,
933
- borderRadius: radius.md,
934
- borderWidth: 1,
935
- borderColor: colors.border,
936
- backgroundColor: colors.bgMain,
937
- paddingHorizontal: spacing.md,
938
- flexDirection: 'row',
939
- alignItems: 'center',
940
- justifyContent: 'center',
941
- gap: spacing.xs,
942
- },
943
- modeButtonPressed: {
944
- opacity: 0.82,
945
- },
946
- modeButtonText: {
947
- ...typography.caption,
948
- color: colors.textPrimary,
949
- fontWeight: '700',
950
- },
951
1559
  scanButton: {
952
- marginTop: spacing.xs,
953
- borderRadius: radius.md,
1560
+ borderRadius: 16,
954
1561
  borderWidth: 1,
955
1562
  borderColor: colors.border,
956
- backgroundColor: colors.bgMain,
957
- minHeight: 44,
1563
+ backgroundColor: colors.bgInput,
1564
+ minHeight: 50,
958
1565
  paddingHorizontal: spacing.md,
959
1566
  flexDirection: 'row',
960
1567
  alignItems: 'center',
@@ -965,19 +1572,15 @@ const styles = StyleSheet.create({
965
1572
  opacity: 0.82,
966
1573
  },
967
1574
  scanButtonText: {
968
- ...typography.caption,
1575
+ ...typography.headline,
969
1576
  color: colors.textPrimary,
970
1577
  fontWeight: '700',
971
1578
  },
972
- helperText: {
973
- ...typography.caption,
974
- color: colors.textMuted,
975
- },
976
1579
  previewWrap: {
977
- borderRadius: radius.md,
1580
+ borderRadius: 16,
978
1581
  borderWidth: StyleSheet.hairlineWidth,
979
1582
  borderColor: colors.borderLight,
980
- backgroundColor: colors.black,
1583
+ backgroundColor: 'rgba(255,255,255,0.03)',
981
1584
  paddingHorizontal: spacing.md,
982
1585
  paddingVertical: spacing.sm,
983
1586
  gap: spacing.xs,
@@ -991,6 +1594,32 @@ const styles = StyleSheet.create({
991
1594
  color: colors.textPrimary,
992
1595
  fontSize: 13,
993
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
+ },
994
1623
  warningText: {
995
1624
  ...typography.caption,
996
1625
  color: '#F7D27E',
@@ -1006,7 +1635,6 @@ const styles = StyleSheet.create({
1006
1635
  actionRow: {
1007
1636
  flexDirection: 'row',
1008
1637
  gap: spacing.sm,
1009
- marginTop: spacing.sm,
1010
1638
  },
1011
1639
  secondaryButton: {
1012
1640
  flex: 1,
@@ -1016,9 +1644,9 @@ const styles = StyleSheet.create({
1016
1644
  justifyContent: 'center',
1017
1645
  borderWidth: 1,
1018
1646
  borderColor: colors.border,
1019
- backgroundColor: colors.bgMain,
1020
- borderRadius: radius.md,
1021
- paddingVertical: spacing.md,
1647
+ backgroundColor: colors.bgInput,
1648
+ borderRadius: 16,
1649
+ minHeight: 54,
1022
1650
  },
1023
1651
  secondaryButtonPressed: {
1024
1652
  opacity: 0.8,
@@ -1038,8 +1666,8 @@ const styles = StyleSheet.create({
1038
1666
  alignItems: 'center',
1039
1667
  justifyContent: 'center',
1040
1668
  backgroundColor: colors.accent,
1041
- borderRadius: radius.md,
1042
- paddingVertical: spacing.md,
1669
+ borderRadius: 16,
1670
+ minHeight: 54,
1043
1671
  },
1044
1672
  primaryButtonPressed: {
1045
1673
  backgroundColor: colors.accentPressed,
@@ -1059,10 +1687,10 @@ const styles = StyleSheet.create({
1059
1687
  paddingHorizontal: spacing.lg,
1060
1688
  },
1061
1689
  scannerSheet: {
1062
- borderRadius: radius.lg,
1690
+ borderRadius: 22,
1063
1691
  borderWidth: StyleSheet.hairlineWidth,
1064
1692
  borderColor: colors.borderHighlight,
1065
- backgroundColor: colors.black,
1693
+ backgroundColor: '#07090C',
1066
1694
  padding: spacing.lg,
1067
1695
  gap: spacing.md,
1068
1696
  },
@@ -1091,11 +1719,11 @@ const styles = StyleSheet.create({
1091
1719
  scannerCameraFrame: {
1092
1720
  width: '100%',
1093
1721
  aspectRatio: 1,
1094
- borderRadius: radius.md,
1722
+ borderRadius: 18,
1095
1723
  overflow: 'hidden',
1096
1724
  borderWidth: StyleSheet.hairlineWidth,
1097
1725
  borderColor: colors.borderLight,
1098
- backgroundColor: colors.bgMain,
1726
+ backgroundColor: colors.bgItem,
1099
1727
  },
1100
1728
  scannerCamera: {
1101
1729
  flex: 1,
@@ -1115,20 +1743,4 @@ const styles = StyleSheet.create({
1115
1743
  ...typography.caption,
1116
1744
  color: colors.textMuted,
1117
1745
  },
1118
- hintCard: {
1119
- borderRadius: radius.md,
1120
- borderWidth: StyleSheet.hairlineWidth,
1121
- borderColor: colors.borderLight,
1122
- backgroundColor: colors.black,
1123
- padding: spacing.lg,
1124
- gap: spacing.sm,
1125
- },
1126
- hintTitle: {
1127
- ...typography.headline,
1128
- color: colors.textPrimary,
1129
- },
1130
- hintText: {
1131
- ...typography.caption,
1132
- color: colors.textSecondary,
1133
- },
1134
1746
  });