react-native-image-stitcher 0.1.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 (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. package/src/types.ts +78 -0
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * CaptureHeader — top-of-screen header for any capture surface.
4
+ *
5
+ * ┌──────────────────────────────────────────────────────────┐
6
+ * │ ‹ Back Cola Promo End Cap │
7
+ * │ Photograph the promotional cola end cap. │
8
+ * └──────────────────────────────────────────────────────────┘
9
+ *
10
+ * Two stacked rows — title row (back arrow + centred title) and an
11
+ * optional guidance line below. Lives in the SDK so every capture
12
+ * surface gets identical chrome without re-implementing safe-area
13
+ * handling, accessibility labels, and contrast on a black preview.
14
+ *
15
+ * Theming:
16
+ * The host has its own theme system; rather than coupling the SDK
17
+ * to it, the component exposes a small set of color props
18
+ * (defaulted to white-on-black for visibility against the camera
19
+ * preview). Hosts that want to override pass `colors`.
20
+ */
21
+
22
+ import React from 'react';
23
+ import {
24
+ Pressable,
25
+ StyleSheet,
26
+ Text,
27
+ View,
28
+ type StyleProp,
29
+ type ViewStyle,
30
+ } from 'react-native';
31
+
32
+
33
+ export interface CaptureHeaderProps {
34
+ /** The audit / surface title. Centred horizontally. */
35
+ title: string;
36
+ /**
37
+ * Called when the back affordance is pressed. If omitted, the
38
+ * back button is hidden entirely (use this for surfaces invoked
39
+ * as a top-level tab where back doesn't make sense).
40
+ */
41
+ onBack?: () => void;
42
+ /** Custom label for the back button. Defaults to "‹ Back". */
43
+ backLabel?: string;
44
+ /**
45
+ * Called when the gear / settings affordance is pressed. If
46
+ * omitted, no settings icon is rendered. Wire this to the
47
+ * host's PanoramaSettingsModal `visible` state.
48
+ */
49
+ onSettingsPress?: () => void;
50
+ /**
51
+ * Optional second-line guidance text shown below the title row.
52
+ * Renders nothing if absent.
53
+ */
54
+ guidance?: string;
55
+ /**
56
+ * Top inset in pixels. Pass `useSafeAreaInsets().top` from
57
+ * react-native-safe-area-context if your app uses it; otherwise a
58
+ * sensible default is applied.
59
+ */
60
+ topInset?: number;
61
+ /** Override the default text/background colors. */
62
+ colors?: {
63
+ background?: string;
64
+ title?: string;
65
+ accent?: string;
66
+ guidanceBackground?: string;
67
+ guidanceText?: string;
68
+ };
69
+ /** Additional style applied to the outer container. */
70
+ style?: StyleProp<ViewStyle>;
71
+ }
72
+
73
+
74
+ export function CaptureHeader({
75
+ title,
76
+ onBack,
77
+ backLabel = '‹ Back',
78
+ onSettingsPress,
79
+ guidance,
80
+ topInset = 0,
81
+ colors,
82
+ style,
83
+ }: CaptureHeaderProps): React.JSX.Element {
84
+ const bg = colors?.background ?? '#000000';
85
+ const titleColor = colors?.title ?? '#ffffff';
86
+ const accent = colors?.accent ?? '#FF9F0A';
87
+ const guidanceBg = colors?.guidanceBackground ?? 'rgba(255,255,255,0.08)';
88
+ const guidanceColor = colors?.guidanceText ?? '#ffffff';
89
+
90
+ return (
91
+ <View style={[{ backgroundColor: bg }, style]}>
92
+ <View style={[styles.titleRow, { paddingTop: topInset + 8 }]}>
93
+ {onBack ? (
94
+ <Pressable
95
+ onPress={onBack}
96
+ hitSlop={12}
97
+ accessibilityRole="button"
98
+ accessibilityLabel="Go back"
99
+ style={styles.backButton}
100
+ >
101
+ <Text style={[styles.backText, { color: accent }]}>
102
+ {backLabel}
103
+ </Text>
104
+ </Pressable>
105
+ ) : (
106
+ // Empty spacer keeps the title centred even when back is hidden.
107
+ <View style={styles.backButton} />
108
+ )}
109
+ <Text
110
+ style={[styles.title, { color: titleColor }]}
111
+ numberOfLines={1}
112
+ accessibilityRole="header"
113
+ >
114
+ {title}
115
+ </Text>
116
+ {/* Settings gear (right side). Falls back to a spacer when
117
+ * the host doesn't wire a handler — keeps the title centred. */}
118
+ {onSettingsPress ? (
119
+ <Pressable
120
+ onPress={onSettingsPress}
121
+ hitSlop={12}
122
+ accessibilityRole="button"
123
+ accessibilityLabel="Open panorama settings"
124
+ style={styles.backButton}
125
+ >
126
+ <Text style={[styles.gearIcon, { color: accent }]}>⚙</Text>
127
+ </Pressable>
128
+ ) : (
129
+ <View style={styles.backButton} />
130
+ )}
131
+ </View>
132
+
133
+ {guidance ? (
134
+ <View
135
+ style={[styles.guidance, { backgroundColor: guidanceBg }]}
136
+ accessibilityRole="text"
137
+ >
138
+ <Text
139
+ style={[styles.guidanceText, { color: guidanceColor }]}
140
+ numberOfLines={2}
141
+ >
142
+ {guidance}
143
+ </Text>
144
+ </View>
145
+ ) : null}
146
+ </View>
147
+ );
148
+ }
149
+
150
+
151
+ const styles = StyleSheet.create({
152
+ titleRow: {
153
+ flexDirection: 'row',
154
+ alignItems: 'center',
155
+ justifyContent: 'space-between',
156
+ paddingHorizontal: 16,
157
+ paddingBottom: 8,
158
+ },
159
+ backButton: {
160
+ minWidth: 64,
161
+ paddingVertical: 4,
162
+ },
163
+ backText: {
164
+ fontSize: 16,
165
+ fontWeight: '500',
166
+ },
167
+ title: {
168
+ flex: 1,
169
+ textAlign: 'center',
170
+ fontSize: 16,
171
+ fontWeight: '600',
172
+ },
173
+ guidance: {
174
+ paddingHorizontal: 16,
175
+ paddingVertical: 8,
176
+ },
177
+ guidanceText: {
178
+ fontSize: 13,
179
+ },
180
+ gearIcon: {
181
+ fontSize: 22,
182
+ textAlign: 'right',
183
+ },
184
+ });
@@ -0,0 +1,318 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * CapturePreview — shared full-screen image preview used for BOTH:
4
+ * 1. Tap-to-preview from <CaptureThumbnailStrip> (existing thumbnails)
5
+ * 2. Post-stitch confirmation (newly produced panorama)
6
+ *
7
+ * ┌──────────────────────────────────────────────────────────┐
8
+ * │ [✕ close] │
9
+ * │ ┌──────────────────────────────┐ │
10
+ * │ │ image (resizeMode= │ │
11
+ * │ │ contain) │ │
12
+ * │ └──────────────────────────────┘ │
13
+ * │ │
14
+ * │ [action 1] [action 2] [action 3] │
15
+ * └──────────────────────────────────────────────────────────┘
16
+ *
17
+ * Actions are passed in by the host so a single component can
18
+ * render the right buttons for each context:
19
+ * - Thumbnail tap, unsubmitted capture → [Delete] [Recapture]
20
+ * - Thumbnail tap, already-synced capture → [] (just close)
21
+ * - Post-stitch → [Discard] [Retry] [Save]
22
+ *
23
+ * The component is presentational — it does not know about audit
24
+ * state, sync state, or any host-domain concept beyond the URI +
25
+ * dimensions to display and the action callbacks to fire.
26
+ */
27
+
28
+ import React from 'react';
29
+ import {
30
+ Image,
31
+ Modal,
32
+ Pressable,
33
+ StyleSheet,
34
+ Text,
35
+ View,
36
+ } from 'react-native';
37
+
38
+
39
+ export type CapturePreviewActionVariant =
40
+ | 'primary'
41
+ | 'neutral'
42
+ | 'ghost'
43
+ | 'destructive';
44
+
45
+
46
+ export interface CapturePreviewAction {
47
+ /** Button label (visible text). */
48
+ label: string;
49
+ /** Optional leading glyph — usually a unicode arrow / check / cross. */
50
+ icon?: string;
51
+ /** Visual variant. Defaults to "neutral". */
52
+ variant?: CapturePreviewActionVariant;
53
+ /** Disabled state — useful while an async action is in flight. */
54
+ disabled?: boolean;
55
+ /**
56
+ * Called when the user presses the button. Caller decides
57
+ * whether the preview should close after — call `onClose` from
58
+ * inside if that's the desired behaviour.
59
+ */
60
+ onPress: () => void;
61
+ }
62
+
63
+
64
+ export interface CapturePreviewProps {
65
+ /** Whether the modal is shown. Drive from host state. */
66
+ visible: boolean;
67
+ /** file:// or http(s) URI to display. */
68
+ imageUri: string;
69
+ /** Image width in px (for aspect-ratio rendering). */
70
+ imageWidth?: number;
71
+ /** Image height in px. */
72
+ imageHeight?: number;
73
+ /**
74
+ * Action buttons rendered along the bottom. Empty array (or
75
+ * undefined) renders no buttons — only the close affordance is
76
+ * available. Up to 3 actions display cleanly across most
77
+ * widths; more wraps on a typical phone.
78
+ */
79
+ actions?: CapturePreviewAction[];
80
+ /**
81
+ * Called when the user dismisses the preview without choosing an
82
+ * action — tap on the close button, tap on the backdrop outside
83
+ * the image, or hardware back on Android.
84
+ */
85
+ onClose: () => void;
86
+ /** Optional title shown at the top of the modal. */
87
+ title?: string;
88
+ }
89
+
90
+
91
+ export function CapturePreview({
92
+ visible,
93
+ imageUri,
94
+ imageWidth,
95
+ imageHeight,
96
+ actions,
97
+ onClose,
98
+ title,
99
+ }: CapturePreviewProps): React.JSX.Element {
100
+ const aspectRatio =
101
+ imageWidth && imageHeight && imageWidth > 0 && imageHeight > 0
102
+ ? imageWidth / imageHeight
103
+ : 16 / 9;
104
+ const hasActions = actions && actions.length > 0;
105
+
106
+ return (
107
+ <Modal
108
+ visible={visible}
109
+ animationType="fade"
110
+ transparent
111
+ statusBarTranslucent
112
+ onRequestClose={onClose}
113
+ >
114
+ <View style={styles.backdrop}>
115
+ {/* Top bar — title centred, close X right-aligned. */}
116
+ <View style={styles.topBar}>
117
+ <View style={styles.topBarSpacer} />
118
+ {title ? (
119
+ <Text style={styles.title} accessibilityRole="header">
120
+ {title}
121
+ </Text>
122
+ ) : (
123
+ <View style={styles.topBarSpacer} />
124
+ )}
125
+ <Pressable
126
+ onPress={onClose}
127
+ hitSlop={20}
128
+ style={styles.closeButton}
129
+ accessibilityRole="button"
130
+ accessibilityLabel="Close preview"
131
+ >
132
+ <Text style={styles.closeText}>×</Text>
133
+ </Pressable>
134
+ </View>
135
+
136
+ {/* Tapping outside the image (on the dim backdrop) also
137
+ * closes — matches the quick-dismiss pattern users learn
138
+ * from iOS share sheets. */}
139
+ <Pressable
140
+ style={styles.imageWrapper}
141
+ onPress={onClose}
142
+ accessibilityRole="button"
143
+ accessibilityLabel="Close preview"
144
+ >
145
+ <Image
146
+ source={{ uri: imageUri }}
147
+ style={[styles.image, { aspectRatio }]}
148
+ resizeMode="contain"
149
+ accessibilityIgnoresInvertColors
150
+ />
151
+ </Pressable>
152
+
153
+ {hasActions ? (
154
+ <View style={styles.buttonRow}>
155
+ {actions!.map((action, idx) => (
156
+ <Pressable
157
+ key={`${action.label}-${idx}`}
158
+ onPress={action.onPress}
159
+ disabled={action.disabled}
160
+ style={[
161
+ styles.button,
162
+ buttonStyleFor(action.variant ?? 'neutral'),
163
+ action.disabled ? styles.buttonDisabled : null,
164
+ ]}
165
+ accessibilityRole="button"
166
+ accessibilityLabel={action.label}
167
+ accessibilityState={{ disabled: action.disabled }}
168
+ >
169
+ <Text
170
+ style={[
171
+ styles.buttonText,
172
+ buttonTextStyleFor(action.variant ?? 'neutral'),
173
+ ]}
174
+ numberOfLines={1}
175
+ >
176
+ {action.icon ? `${action.icon} ` : ''}{action.label}
177
+ </Text>
178
+ </Pressable>
179
+ ))}
180
+ </View>
181
+ ) : null}
182
+ </View>
183
+ </Modal>
184
+ );
185
+ }
186
+
187
+
188
+ function buttonStyleFor(variant: CapturePreviewActionVariant) {
189
+ switch (variant) {
190
+ case 'primary':
191
+ return styles.buttonPrimary;
192
+ case 'destructive':
193
+ return styles.buttonDestructive;
194
+ case 'ghost':
195
+ return styles.buttonGhost;
196
+ case 'neutral':
197
+ default:
198
+ return styles.buttonNeutral;
199
+ }
200
+ }
201
+
202
+
203
+ function buttonTextStyleFor(variant: CapturePreviewActionVariant) {
204
+ switch (variant) {
205
+ case 'primary':
206
+ return styles.buttonTextPrimary;
207
+ case 'destructive':
208
+ return styles.buttonTextDestructive;
209
+ case 'ghost':
210
+ return styles.buttonTextGhost;
211
+ case 'neutral':
212
+ default:
213
+ return styles.buttonTextNeutral;
214
+ }
215
+ }
216
+
217
+
218
+ const styles = StyleSheet.create({
219
+ backdrop: {
220
+ flex: 1,
221
+ backgroundColor: 'rgba(0,0,0,0.96)',
222
+ paddingTop: 56,
223
+ paddingBottom: 32,
224
+ paddingHorizontal: 16,
225
+ },
226
+ topBar: {
227
+ flexDirection: 'row',
228
+ alignItems: 'center',
229
+ marginBottom: 12,
230
+ },
231
+ topBarSpacer: {
232
+ width: 44,
233
+ },
234
+ title: {
235
+ flex: 1,
236
+ color: '#ffffff',
237
+ fontSize: 16,
238
+ fontWeight: '600',
239
+ textAlign: 'center',
240
+ },
241
+ closeButton: {
242
+ width: 44,
243
+ height: 44,
244
+ borderRadius: 22,
245
+ backgroundColor: 'rgba(255,255,255,0.12)',
246
+ alignItems: 'center',
247
+ justifyContent: 'center',
248
+ },
249
+ closeText: {
250
+ color: '#ffffff',
251
+ fontSize: 28,
252
+ fontWeight: '300',
253
+ lineHeight: 32,
254
+ marginTop: -2,
255
+ },
256
+ imageWrapper: {
257
+ flex: 1,
258
+ alignItems: 'center',
259
+ justifyContent: 'center',
260
+ },
261
+ image: {
262
+ width: '100%',
263
+ maxHeight: '100%',
264
+ backgroundColor: '#111',
265
+ borderRadius: 4,
266
+ },
267
+ buttonRow: {
268
+ flexDirection: 'row',
269
+ justifyContent: 'space-between',
270
+ alignItems: 'center',
271
+ marginTop: 16,
272
+ gap: 12,
273
+ },
274
+ button: {
275
+ flex: 1,
276
+ paddingVertical: 14,
277
+ borderRadius: 10,
278
+ alignItems: 'center',
279
+ justifyContent: 'center',
280
+ },
281
+ buttonDisabled: {
282
+ opacity: 0.45,
283
+ },
284
+ buttonPrimary: {
285
+ backgroundColor: '#34C759',
286
+ },
287
+ buttonNeutral: {
288
+ backgroundColor: 'rgba(255,255,255,0.12)',
289
+ },
290
+ buttonGhost: {
291
+ backgroundColor: 'transparent',
292
+ borderWidth: 1,
293
+ borderColor: 'rgba(255,255,255,0.3)',
294
+ },
295
+ buttonDestructive: {
296
+ backgroundColor: '#FF3B30',
297
+ },
298
+ buttonText: {
299
+ fontSize: 14,
300
+ fontWeight: '600',
301
+ },
302
+ buttonTextPrimary: {
303
+ color: '#ffffff',
304
+ fontWeight: '700',
305
+ },
306
+ buttonTextNeutral: {
307
+ color: '#ffffff',
308
+ },
309
+ buttonTextGhost: {
310
+ color: '#ffffff',
311
+ opacity: 0.9,
312
+ fontWeight: '500',
313
+ },
314
+ buttonTextDestructive: {
315
+ color: '#ffffff',
316
+ fontWeight: '700',
317
+ },
318
+ });