react-native-image-stitcher 0.12.0 → 0.13.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.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.13.0] — 2026-05-29
20
+
21
+ ### Added — Layer-2 components absorbed into `<Camera>` (opt-out)
22
+
23
+ The flagship `<Camera>` now ships built-in defaults for every UX
24
+ chrome piece previously exposed only as a Layer-2 component. Hosts
25
+ adopting `<Camera>` directly get a complete capture surface — flash
26
+ button, pan-speed pill, drift-marker guide, header chrome,
27
+ capture-history strip, and post-stitch preview — without having to
28
+ import and wire each piece by hand.
29
+
30
+ All built-ins use the opt-out pattern: enabled by default, disabled
31
+ by setting the corresponding boolean to `false` or by omitting the
32
+ corresponding payload prop. Hosts that want their own chrome can
33
+ opt out per piece and layer custom UI on top of `<Camera>` (the
34
+ Layer-2 components remain exported and are unchanged).
35
+
36
+ #### Flash control
37
+
38
+ - `flash?: 'on' | 'off'` — controlled torch state. Omit to let
39
+ `<Camera>` own it internally.
40
+ - `onFlashChange?` — fires on tap (controlled and uncontrolled both).
41
+ - `showFlashButton?: boolean` (default `true`) — built-in flash button
42
+ in the bottom-left slot. AR mode auto-disables (ARKit / ARCore own
43
+ the device's torch; surfaces "Flash unavailable in AR mode" a11y
44
+ label and greyed styling).
45
+
46
+ #### Pan guidance
47
+
48
+ - `panGuide?: boolean` (default `true`) — built-in
49
+ `IncrementalPanGuide` ("keep the arrow on the line" drift marker).
50
+ - `panoramaGuidance?: boolean` (default `true`) — built-in
51
+ `PanoramaGuidance` pan-speed pill.
52
+ - Both are gyroscope-driven and only subscribe to the sensor while
53
+ recording — no idle cost.
54
+
55
+ #### Header
56
+
57
+ - `headerTitle?: string` — when set, renders a built-in
58
+ `CaptureHeader` at the top of the screen. The existing settings
59
+ gear is absorbed into the header's right side (no duplicate gear).
60
+ - `onHeaderBack?`, `headerBackLabel?`, `headerGuidance?`,
61
+ `headerColors?` — pass-through to `CaptureHeader`.
62
+
63
+ #### Capture history + preview
64
+
65
+ - `thumbnails?: CaptureThumbnailItem[]` — when supplied (even `[]`),
66
+ renders the built-in `CaptureThumbnailStrip` above the bottom
67
+ controls. Hidden during recording so it doesn't overlap the
68
+ panorama band overlay.
69
+ - `thumbnailsMin?`, `thumbnailsMax?` — count-line hints.
70
+ - `onThumbnailPress?` — replaces the strip's built-in
71
+ tap-to-preview modal with a host handler.
72
+ - `capturePreview?` — when set, renders a built-in `CapturePreview`
73
+ modal showing the supplied image. Use for post-stitch
74
+ confirmation; the host clears the prop on dismiss via
75
+ `onCapturePreviewClose`.
76
+ - `capturePreviewActions?` — pass-through action buttons for the
77
+ preview modal.
78
+
79
+ ### Migration
80
+
81
+ - Hosts that were importing Layer-2 components (`CaptureHeader`,
82
+ `CaptureControlsBar`, `IncrementalPanGuide`, `PanoramaGuidance`,
83
+ `CaptureThumbnailStrip`, `CapturePreview`) directly can now drop
84
+ those imports and use the corresponding `<Camera>` props.
85
+ - The Layer-2 components remain exported and unchanged in v0.13 for
86
+ backward compatibility. Deprecation of those exports is targeted
87
+ for v0.14.
88
+ - No behaviour change for hosts that already use `<Camera>` and
89
+ don't supply any of the new props — every new built-in defaults
90
+ to the previous (omitted) UX, except the flash button which
91
+ appears in the now-occupied bottom-left slot. Hosts that previously
92
+ rendered chrome in that slot above `<Camera>` can pass
93
+ `showFlashButton={false}`.
94
+
19
95
  ## [0.12.0] — 2026-05-28
20
96
 
21
97
  ### Added — Orientation-aware `<Camera>` (R2-lite)
@@ -41,6 +41,9 @@
41
41
  import React from 'react';
42
42
  import { type StyleProp, type ViewStyle } from 'react-native';
43
43
  import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
44
+ import { type CaptureHeaderProps } from './CaptureHeader';
45
+ import { type CapturePreviewAction } from './CapturePreview';
46
+ import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
44
47
  export type CaptureSource = 'ar' | 'non-ar';
45
48
  export type CameraLens = '1x' | '0.5x';
46
49
  export type StitchMode = 'auto' | 'panorama' | 'scans';
@@ -211,6 +214,174 @@ export interface CameraProps {
211
214
  * for an abandoned capture.
212
215
  */
213
216
  onCaptureAbandoned?: (reason: 'orientation-drift') => void;
217
+ /**
218
+ * v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
219
+ *
220
+ * - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
221
+ * state internally. Tapping the built-in flash button toggles
222
+ * it on/off. `onFlashChange` (if supplied) fires for telemetry.
223
+ * - **Controlled** (supply `flash`): the parent owns the state.
224
+ * The built-in button still renders and fires `onFlashChange`
225
+ * on press, but it's a no-op unless the parent updates `flash`
226
+ * in response.
227
+ *
228
+ * Both shapes coexist with the v0.13 "flash button is on by default"
229
+ * built-in (see the bottom-left bar slot in the JSX). Hosts that
230
+ * want their own flash chrome can opt out via `showFlashButton={false}`
231
+ * and drive the underlying torch by controlling `flash` directly.
232
+ *
233
+ * ## AR-mode behaviour
234
+ *
235
+ * In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
236
+ * ARKit / ARCore own the `AVCaptureDevice` and don't expose the
237
+ * torch through vision-camera's pipeline. The built-in flash
238
+ * button renders as visibly disabled (a11y label "Flash unavailable
239
+ * in AR mode") and `flash` is forced to `'off'` regardless of
240
+ * controlled/uncontrolled state. Hosts that need flash should
241
+ * toggle to non-AR before enabling.
242
+ */
243
+ flash?: 'on' | 'off';
244
+ /**
245
+ * v0.13.0 — fires when the user taps the built-in flash button.
246
+ * In uncontrolled mode, the internal state has already flipped
247
+ * (single render delay). In controlled mode, the parent must
248
+ * update the `flash` prop in response or the visual toggle is
249
+ * a no-op. Useful in either mode for telemetry.
250
+ */
251
+ onFlashChange?: (next: 'on' | 'off') => void;
252
+ /**
253
+ * v0.13.0 — show the built-in flash button in the bottom-left
254
+ * slot. Defaults to `true`. Hosts that render their own flash
255
+ * chrome (and drive the underlying torch via the controlled
256
+ * `flash` prop) can opt out by setting this to `false`.
257
+ */
258
+ showFlashButton?: boolean;
259
+ /**
260
+ * v0.13.0 — show the built-in IncrementalPanGuide ("keep the
261
+ * arrow on the line" drift marker) while recording. Defaults
262
+ * to `true`. The guide is gyroscope-driven and only active
263
+ * during the recording phase (no idle sensor cost). Hosts that
264
+ * want their own pan-guide chrome can opt out via `false`.
265
+ */
266
+ panGuide?: boolean;
267
+ /**
268
+ * v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
269
+ * ("Pan slowly" / "Slow down" / "Too fast") while recording.
270
+ * Defaults to `true`. Gyroscope-driven, only active during
271
+ * recording. Hosts that want their own speed chrome can opt
272
+ * out via `false`.
273
+ */
274
+ panoramaGuidance?: boolean;
275
+ /**
276
+ * v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
277
+ * renders a top-of-screen header showing this title (centred)
278
+ * with an optional back affordance + guidance subtitle + the
279
+ * existing settings gear absorbed into the header's right side.
280
+ *
281
+ * When `headerTitle` is undefined the header is not rendered
282
+ * (matches pre-v0.13 behaviour: top of preview is bare except
283
+ * for the standalone settings gear gated on `showSettingsButton`).
284
+ *
285
+ * Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
286
+ * and `headerColors` to customise the rest of the header. Hosts
287
+ * that need richer header chrome can omit `headerTitle` and
288
+ * compose their own `<CaptureHeader>` above `<Camera>`.
289
+ */
290
+ headerTitle?: string;
291
+ /**
292
+ * v0.13.0 — header back-button callback. When supplied (and
293
+ * `headerTitle` is set), the header renders a back affordance
294
+ * on the left. Omitted ⇒ no back button (the title stays
295
+ * centred).
296
+ */
297
+ onHeaderBack?: () => void;
298
+ /**
299
+ * v0.13.0 — header back-button label. Defaults to "‹ Back".
300
+ * No effect unless `headerTitle` and `onHeaderBack` are both set.
301
+ */
302
+ headerBackLabel?: string;
303
+ /**
304
+ * v0.13.0 — optional second-line subtitle shown below the
305
+ * header title. E.g. "Photograph the promotional cola end cap."
306
+ * Renders nothing when undefined. No effect unless `headerTitle`
307
+ * is set.
308
+ */
309
+ headerGuidance?: string;
310
+ /**
311
+ * v0.13.0 — colour overrides for the built-in header. Defaults
312
+ * are white-on-black to stay legible over the camera preview.
313
+ * No effect unless `headerTitle` is set.
314
+ */
315
+ headerColors?: CaptureHeaderProps['colors'];
316
+ /**
317
+ * v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
318
+ * built-in `CaptureThumbnailStrip` above the bottom controls
319
+ * showing the host's capture history. Each item is a plain
320
+ * `{ id, uri, width?, height? }` object; the strip handles
321
+ * aspect-ratio rendering, tap-to-preview, and the count line.
322
+ *
323
+ * Omit (`undefined`) to skip the strip entirely. Hosts using
324
+ * the strip independently (e.g. on a non-camera screen) can keep
325
+ * importing `CaptureThumbnailStrip` directly from the library —
326
+ * the prop here is the convenience wiring for in-`<Camera>` use.
327
+ *
328
+ * Captures emitted by `<Camera>`'s `onCapture` are NOT added to
329
+ * this array automatically — the host owns the canonical list
330
+ * (typically persisted to its own DB) and updates the prop in
331
+ * response. This matches the SDK's "Camera owns runtime state,
332
+ * host persists" pattern.
333
+ */
334
+ thumbnails?: CaptureThumbnailItem[];
335
+ /**
336
+ * v0.13.0 — minimum-photos hint for the count line. Renders
337
+ * "n / minPhotos min" with the success colour when reached,
338
+ * warning colour otherwise.
339
+ */
340
+ thumbnailsMin?: number;
341
+ /**
342
+ * v0.13.0 — maximum-photos hint for the count line. Renders
343
+ * "· maxPhotos max" suffix. No enforcement — the host decides
344
+ * what to do at the cap.
345
+ */
346
+ thumbnailsMax?: number;
347
+ /**
348
+ * v0.13.0 — tap handler for thumbnails. When set, replaces the
349
+ * strip's built-in tap-to-preview modal; the host shows its own
350
+ * preview UI (e.g. with delete / recapture buttons gated on
351
+ * sync state). Omit to use the built-in preview.
352
+ */
353
+ onThumbnailPress?: (item: CaptureThumbnailItem) => void;
354
+ /**
355
+ * v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
356
+ * modal as `visible`. Use this for post-stitch confirmation:
357
+ * after `onCapture` emits, the host stores the result and sets
358
+ * `capturePreview` to the new image, with `capturePreviewActions`
359
+ * = `[Discard, Save]` (or similar). Setting `undefined` hides
360
+ * the modal.
361
+ *
362
+ * Hosts using the modal for thumbnail tap-to-preview can leave
363
+ * this undefined and let the built-in strip's preview handle
364
+ * that case.
365
+ */
366
+ capturePreview?: {
367
+ imageUri: string;
368
+ imageWidth?: number;
369
+ imageHeight?: number;
370
+ title?: string;
371
+ };
372
+ /**
373
+ * v0.13.0 — action buttons rendered along the bottom of the
374
+ * `CapturePreview` modal. Empty array (or undefined) renders
375
+ * no buttons, only the close affordance.
376
+ */
377
+ capturePreviewActions?: CapturePreviewAction[];
378
+ /**
379
+ * v0.13.0 — fires when the user dismisses the `capturePreview`
380
+ * modal (tap close, backdrop tap, hardware back on Android).
381
+ * The host is expected to clear the `capturePreview` prop in
382
+ * response.
383
+ */
384
+ onCapturePreviewClose?: () => void;
214
385
  /**
215
386
  * Optional host-supplied vision-camera frame processor.
216
387
  *
@@ -83,13 +83,18 @@ const useARSession_1 = require("../ar/useARSession");
83
83
  const ARCameraView_1 = require("./ARCameraView");
84
84
  const CameraShutter_1 = require("./CameraShutter");
85
85
  const CameraView_1 = require("./CameraView");
86
+ const CaptureHeader_1 = require("./CaptureHeader");
87
+ const CapturePreview_1 = require("./CapturePreview");
88
+ const CaptureThumbnailStrip_1 = require("./CaptureThumbnailStrip");
86
89
  const CaptureStatusOverlay_1 = require("./CaptureStatusOverlay");
87
90
  const CaptureDebugOverlay_1 = require("./CaptureDebugOverlay");
88
91
  const CaptureMemoryPill_1 = require("./CaptureMemoryPill");
89
92
  const CaptureKeyframePill_1 = require("./CaptureKeyframePill");
90
93
  const CaptureOrientationPill_1 = require("./CaptureOrientationPill");
91
94
  const CaptureStitchStatsToast_1 = require("./CaptureStitchStatsToast");
95
+ const IncrementalPanGuide_1 = require("./IncrementalPanGuide");
92
96
  const PanoramaBandOverlay_1 = require("./PanoramaBandOverlay");
97
+ const PanoramaGuidance_1 = require("./PanoramaGuidance");
93
98
  const PanoramaSettingsBridge_1 = require("./PanoramaSettingsBridge");
94
99
  const PanoramaSettingsModal_1 = require("./PanoramaSettingsModal");
95
100
  const buildPanoramaInitialSettings_1 = require("./buildPanoramaInitialSettings");
@@ -272,7 +277,7 @@ function extractPanoramaOverrides(props) {
272
277
  * The public `<Camera>` component.
273
278
  */
274
279
  function Camera(props) {
275
- const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
280
+ const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, panGuide = true, panoramaGuidance = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
276
281
  const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
277
282
  // v0.12.0 — JS-layout orientation independent of device-physical.
278
283
  // `useWindowDimensions().width > height` tells us if the OS
@@ -285,6 +290,12 @@ function Camera(props) {
285
290
  // ── State ───────────────────────────────────────────────────────
286
291
  const [arPreference, setArPreference] = (0, react_1.useState)(defaultCaptureSource === 'ar');
287
292
  const [lens, setLens] = (0, react_1.useState)(defaultLens);
293
+ // v0.13.0 — flash state. Controlled by `controlledFlash` when the
294
+ // host supplies the `flash` prop; otherwise owned internally and
295
+ // toggled by the built-in flash button. `effectiveFlash` below
296
+ // also forces 'off' in AR mode (ARKit / ARCore own the device's
297
+ // torch and don't surface it through vision-camera's pipeline).
298
+ const [internalFlash, setInternalFlash] = (0, react_1.useState)('off');
288
299
  const [settings, setSettings] = (0, react_1.useState)(() => (0, buildPanoramaInitialSettings_1.buildPanoramaInitialSettings)(extractPanoramaOverrides(props), (0, lowMemDevice_1.isLowMemDevice)()));
289
300
  const [settingsModalVisible, setSettingsModalVisible] = (0, react_1.useState)(false);
290
301
  const [statusPhase, setStatusPhase] = (0, react_1.useState)('idle');
@@ -867,6 +878,22 @@ function Camera(props) {
867
878
  const handleARToggle = (0, react_1.useCallback)(() => {
868
879
  setArPreference((prev) => !prev);
869
880
  }, []);
881
+ // ── v0.13.0 — Flash control ─────────────────────────────────────
882
+ //
883
+ // `flashRequested` is what the host / built-in button asks for.
884
+ // `effectiveFlash` is what we actually drive into vision-camera —
885
+ // AR mode forces 'off' because ARKit / ARCore own AVCaptureDevice
886
+ // and the torch isn't exposed. This way the button's visual state
887
+ // (a11y, styling) tracks `flashRequested` while the underlying
888
+ // camera always sees the correct value.
889
+ const flashRequested = controlledFlash ?? internalFlash;
890
+ const effectiveFlash = isAR ? 'off' : flashRequested;
891
+ const toggleFlash = (0, react_1.useCallback)(() => {
892
+ const next = flashRequested === 'on' ? 'off' : 'on';
893
+ if (controlledFlash == null)
894
+ setInternalFlash(next);
895
+ onFlashChange?.(next);
896
+ }, [flashRequested, controlledFlash, onFlashChange]);
870
897
  // ── JSX ─────────────────────────────────────────────────────────
871
898
  return (react_1.default.createElement(react_native_1.View, { style: [styles.container, style] },
872
899
  inFlightTransition ? (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] },
@@ -878,7 +905,7 @@ function Camera(props) {
878
905
  // the very first buffered preview frame. Android takeSnapshot
879
906
  // works either way. Pattern matches AuditCaptureScreen.tsx
880
907
  // which has run on `video` (true) for months without issue.
881
- video: true, flash: "off", style: react_native_1.StyleSheet.absoluteFill,
908
+ video: true, flash: effectiveFlash, style: react_native_1.StyleSheet.absoluteFill,
882
909
  // F8 (FrameProcessor port) — host-supplied worklet runs on
883
910
  // the camera producer thread for every frame. Only wired
884
911
  // in non-AR mode; AR mode uses ARCameraView which doesn't
@@ -899,24 +926,37 @@ function Camera(props) {
899
926
  onError?.(new CameraError('VISION_CAMERA_RUNTIME', `${codeStr}: ${msg}`, err));
900
927
  } })),
901
928
  react_1.default.createElement(CaptureStatusOverlay_1.CaptureStatusOverlay, { phase: statusPhase, topInset: insets.top, recordingStartedAt: recordingStartedAt ?? undefined }),
929
+ panGuide && (react_1.default.createElement(IncrementalPanGuide_1.IncrementalPanGuide, { active: statusPhase === 'recording' })),
930
+ panoramaGuidance && (react_1.default.createElement(PanoramaGuidance_1.PanoramaGuidance, { active: statusPhase === 'recording' })),
902
931
  settings.debug && (react_1.default.createElement(react_1.default.Fragment, null,
903
932
  react_1.default.createElement(CaptureOrientationPill_1.CaptureOrientationPill, { orientation: deviceOrientation, topInset: insets.top }),
904
933
  react_1.default.createElement(CaptureKeyframePill_1.CaptureKeyframePill, { state: incrementalState, topInset: insets.top }),
905
934
  react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { topInset: insets.top }),
906
935
  react_1.default.createElement(CaptureDebugOverlay_1.CaptureDebugOverlay, { incrementalState: incrementalState, imuTranslationMetres: isNonAR ? imuGate.getTranslationMetres() : null, captureSource: effectiveCaptureSource, frameSelectionMode: settings.frameSelection.mode, stitchMode: settings.stitcher.stitchMode }))),
907
936
  react_1.default.createElement(CaptureStitchStatsToast_1.CaptureStitchStatsToast, { message: stitchToast.message, topInset: insets.top }),
908
- showSettingsButton && (react_1.default.createElement(SettingsButton, { topInset: insets.top, onPress: () => setSettingsModalVisible(true) })),
937
+ headerTitle != null ? (react_1.default.createElement(react_native_1.View, { style: styles.headerWrap, pointerEvents: "box-none" },
938
+ react_1.default.createElement(CaptureHeader_1.CaptureHeader, { title: headerTitle, onBack: onHeaderBack, backLabel: headerBackLabel, guidance: headerGuidance, colors: headerColors, topInset: insets.top, onSettingsPress: showSettingsButton
939
+ ? () => setSettingsModalVisible(true)
940
+ : undefined }))) : (showSettingsButton && (react_1.default.createElement(SettingsButton, { topInset: insets.top, onPress: () => setSettingsModalVisible(true) }))),
941
+ thumbnails != null && statusPhase !== 'recording' && (react_1.default.createElement(react_native_1.View, { style: styles.thumbnailStripWrap, pointerEvents: "box-none" },
942
+ react_1.default.createElement(CaptureThumbnailStrip_1.CaptureThumbnailStrip, { items: thumbnails, minPhotos: thumbnailsMin, maxPhotos: thumbnailsMax, onItemPress: onThumbnailPress }))),
909
943
  react_1.default.createElement(react_native_1.View, { pointerEvents: "box-none", style: bottomAreaStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation), insets.bottom + 12, insets.top + 12) },
910
944
  statusPhase === 'recording' && (react_1.default.createElement(PanoramaBandOverlay_1.PanoramaBandOverlay, { state: incrementalState, frameUris: batchKeyframeThumbnails, captureOrientation: deviceOrientation, vertical: isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) })),
911
945
  react_1.default.createElement(react_native_1.View, { style: bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) },
912
- react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft }),
946
+ react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft }, showFlashButton && (react_1.default.createElement(react_native_1.Pressable, { onPress: isAR ? undefined : toggleFlash, accessibilityRole: "button", accessibilityLabel: isAR ? 'Flash unavailable in AR mode' : `Flash ${flashRequested === 'on' ? 'on' : 'off'}`, accessibilityState: { selected: flashRequested === 'on', disabled: isAR }, disabled: isAR, hitSlop: 8, style: [
947
+ styles.flashButton,
948
+ flashRequested === 'on' && !isAR && styles.flashButtonActive,
949
+ isAR && styles.flashButtonDisabled,
950
+ ] },
951
+ react_1.default.createElement(react_native_1.Text, { style: styles.flashIcon }, "\u26A1")))),
913
952
  react_1.default.createElement(react_native_1.View, { style: styles.bottomBarCenter },
914
953
  react_1.default.createElement(LensChip, { lens: lens, onChange: handleLensChange, has0_5x: has0_5x }),
915
954
  react_1.default.createElement(react_native_1.View, { style: styles.shutterWrap },
916
955
  react_1.default.createElement(CameraShutter_1.CameraShutter, { onTap: handleTap, onHoldStart: enablePanoramaMode ? handleHoldStart : noop, onHoldComplete: enablePanoramaMode ? handleHoldEnd : noop, isProcessing: statusPhase === 'stitching', disabled: statusPhase === 'stitching' }))),
917
956
  react_1.default.createElement(react_native_1.View, { style: styles.bottomBarRight }, lens === '1x' && isARSupportedOnDevice && (react_1.default.createElement(ARToggle, { arEnabled: arPreference, onToggle: handleARToggle }))))),
918
957
  react_1.default.createElement(PanoramaSettingsModal_1.PanoramaSettingsModal, { visible: settingsModalVisible, settings: settings, onChange: setSettings, onClose: () => setSettingsModalVisible(false) }),
919
- react_1.default.createElement(OrientationDriftModal_1.OrientationDriftModal, { visible: drift.drifted && !driftModalDismissed, captureOrientation: drift.captureOrientation, currentOrientation: drift.currentOrientation, onAcknowledge: () => setDriftModalDismissed(true) })));
958
+ react_1.default.createElement(OrientationDriftModal_1.OrientationDriftModal, { visible: drift.drifted && !driftModalDismissed, captureOrientation: drift.captureOrientation, currentOrientation: drift.currentOrientation, onAcknowledge: () => setDriftModalDismissed(true) }),
959
+ react_1.default.createElement(CapturePreview_1.CapturePreview, { visible: capturePreview != null, imageUri: capturePreview?.imageUri ?? '', imageWidth: capturePreview?.imageWidth, imageHeight: capturePreview?.imageHeight, title: capturePreview?.title, actions: capturePreviewActions, onClose: onCapturePreviewClose ?? noop })));
920
960
  }
921
961
  function noop() {
922
962
  /* no-op handler used when panorama mode is disabled */
@@ -1045,6 +1085,8 @@ const styles = react_native_1.StyleSheet.create({
1045
1085
  },
1046
1086
  bottomBarLeft: {
1047
1087
  flex: 1,
1088
+ alignItems: 'flex-start',
1089
+ justifyContent: 'flex-end',
1048
1090
  },
1049
1091
  bottomBarCenter: {
1050
1092
  flex: 1,
@@ -1058,5 +1100,35 @@ const styles = react_native_1.StyleSheet.create({
1058
1100
  shutterWrap: {
1059
1101
  marginTop: 12,
1060
1102
  },
1103
+ headerWrap: {
1104
+ position: 'absolute',
1105
+ top: 0,
1106
+ left: 0,
1107
+ right: 0,
1108
+ },
1109
+ thumbnailStripWrap: {
1110
+ position: 'absolute',
1111
+ left: 0,
1112
+ right: 0,
1113
+ bottom: 160,
1114
+ },
1115
+ flashButton: {
1116
+ width: 44,
1117
+ height: 44,
1118
+ borderRadius: 22,
1119
+ alignItems: 'center',
1120
+ justifyContent: 'center',
1121
+ backgroundColor: 'rgba(0,0,0,0.45)',
1122
+ },
1123
+ flashButtonActive: {
1124
+ backgroundColor: '#ffd34d',
1125
+ },
1126
+ flashButtonDisabled: {
1127
+ opacity: 0.35,
1128
+ },
1129
+ flashIcon: {
1130
+ fontSize: 20,
1131
+ color: '#ffffff',
1132
+ },
1061
1133
  });
1062
1134
  //# sourceMappingURL=Camera.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-image-stitcher",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -67,13 +67,21 @@ import { useARSession } from '../ar/useARSession';
67
67
  import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
68
68
  import { CameraShutter } from './CameraShutter';
69
69
  import { CameraView } from './CameraView';
70
+ import { CaptureHeader, type CaptureHeaderProps } from './CaptureHeader';
71
+ import { CapturePreview, type CapturePreviewAction } from './CapturePreview';
72
+ import {
73
+ CaptureThumbnailStrip,
74
+ type CaptureThumbnailItem,
75
+ } from './CaptureThumbnailStrip';
70
76
  import { CaptureStatusOverlay, type CaptureStatusPhase } from './CaptureStatusOverlay';
71
77
  import { CaptureDebugOverlay } from './CaptureDebugOverlay';
72
78
  import { CaptureMemoryPill } from './CaptureMemoryPill';
73
79
  import { CaptureKeyframePill } from './CaptureKeyframePill';
74
80
  import { CaptureOrientationPill } from './CaptureOrientationPill';
75
81
  import { CaptureStitchStatsToast, useStitchStatsToast } from './CaptureStitchStatsToast';
82
+ import { IncrementalPanGuide } from './IncrementalPanGuide';
76
83
  import { PanoramaBandOverlay } from './PanoramaBandOverlay';
84
+ import { PanoramaGuidance } from './PanoramaGuidance';
77
85
  import { type PanoramaSettings } from './PanoramaSettings';
78
86
  import { panoramaSettingsToNativeConfig } from './PanoramaSettingsBridge';
79
87
  import { PanoramaSettingsModal } from './PanoramaSettingsModal';
@@ -317,6 +325,191 @@ export interface CameraProps {
317
325
  */
318
326
  onCaptureAbandoned?: (reason: 'orientation-drift') => void;
319
327
 
328
+ /**
329
+ * v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
330
+ *
331
+ * - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
332
+ * state internally. Tapping the built-in flash button toggles
333
+ * it on/off. `onFlashChange` (if supplied) fires for telemetry.
334
+ * - **Controlled** (supply `flash`): the parent owns the state.
335
+ * The built-in button still renders and fires `onFlashChange`
336
+ * on press, but it's a no-op unless the parent updates `flash`
337
+ * in response.
338
+ *
339
+ * Both shapes coexist with the v0.13 "flash button is on by default"
340
+ * built-in (see the bottom-left bar slot in the JSX). Hosts that
341
+ * want their own flash chrome can opt out via `showFlashButton={false}`
342
+ * and drive the underlying torch by controlling `flash` directly.
343
+ *
344
+ * ## AR-mode behaviour
345
+ *
346
+ * In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
347
+ * ARKit / ARCore own the `AVCaptureDevice` and don't expose the
348
+ * torch through vision-camera's pipeline. The built-in flash
349
+ * button renders as visibly disabled (a11y label "Flash unavailable
350
+ * in AR mode") and `flash` is forced to `'off'` regardless of
351
+ * controlled/uncontrolled state. Hosts that need flash should
352
+ * toggle to non-AR before enabling.
353
+ */
354
+ flash?: 'on' | 'off';
355
+
356
+ /**
357
+ * v0.13.0 — fires when the user taps the built-in flash button.
358
+ * In uncontrolled mode, the internal state has already flipped
359
+ * (single render delay). In controlled mode, the parent must
360
+ * update the `flash` prop in response or the visual toggle is
361
+ * a no-op. Useful in either mode for telemetry.
362
+ */
363
+ onFlashChange?: (next: 'on' | 'off') => void;
364
+
365
+ /**
366
+ * v0.13.0 — show the built-in flash button in the bottom-left
367
+ * slot. Defaults to `true`. Hosts that render their own flash
368
+ * chrome (and drive the underlying torch via the controlled
369
+ * `flash` prop) can opt out by setting this to `false`.
370
+ */
371
+ showFlashButton?: boolean;
372
+
373
+ /**
374
+ * v0.13.0 — show the built-in IncrementalPanGuide ("keep the
375
+ * arrow on the line" drift marker) while recording. Defaults
376
+ * to `true`. The guide is gyroscope-driven and only active
377
+ * during the recording phase (no idle sensor cost). Hosts that
378
+ * want their own pan-guide chrome can opt out via `false`.
379
+ */
380
+ panGuide?: boolean;
381
+
382
+ /**
383
+ * v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
384
+ * ("Pan slowly" / "Slow down" / "Too fast") while recording.
385
+ * Defaults to `true`. Gyroscope-driven, only active during
386
+ * recording. Hosts that want their own speed chrome can opt
387
+ * out via `false`.
388
+ */
389
+ panoramaGuidance?: boolean;
390
+
391
+ /**
392
+ * v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
393
+ * renders a top-of-screen header showing this title (centred)
394
+ * with an optional back affordance + guidance subtitle + the
395
+ * existing settings gear absorbed into the header's right side.
396
+ *
397
+ * When `headerTitle` is undefined the header is not rendered
398
+ * (matches pre-v0.13 behaviour: top of preview is bare except
399
+ * for the standalone settings gear gated on `showSettingsButton`).
400
+ *
401
+ * Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
402
+ * and `headerColors` to customise the rest of the header. Hosts
403
+ * that need richer header chrome can omit `headerTitle` and
404
+ * compose their own `<CaptureHeader>` above `<Camera>`.
405
+ */
406
+ headerTitle?: string;
407
+
408
+ /**
409
+ * v0.13.0 — header back-button callback. When supplied (and
410
+ * `headerTitle` is set), the header renders a back affordance
411
+ * on the left. Omitted ⇒ no back button (the title stays
412
+ * centred).
413
+ */
414
+ onHeaderBack?: () => void;
415
+
416
+ /**
417
+ * v0.13.0 — header back-button label. Defaults to "‹ Back".
418
+ * No effect unless `headerTitle` and `onHeaderBack` are both set.
419
+ */
420
+ headerBackLabel?: string;
421
+
422
+ /**
423
+ * v0.13.0 — optional second-line subtitle shown below the
424
+ * header title. E.g. "Photograph the promotional cola end cap."
425
+ * Renders nothing when undefined. No effect unless `headerTitle`
426
+ * is set.
427
+ */
428
+ headerGuidance?: string;
429
+
430
+ /**
431
+ * v0.13.0 — colour overrides for the built-in header. Defaults
432
+ * are white-on-black to stay legible over the camera preview.
433
+ * No effect unless `headerTitle` is set.
434
+ */
435
+ headerColors?: CaptureHeaderProps['colors'];
436
+
437
+ /**
438
+ * v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
439
+ * built-in `CaptureThumbnailStrip` above the bottom controls
440
+ * showing the host's capture history. Each item is a plain
441
+ * `{ id, uri, width?, height? }` object; the strip handles
442
+ * aspect-ratio rendering, tap-to-preview, and the count line.
443
+ *
444
+ * Omit (`undefined`) to skip the strip entirely. Hosts using
445
+ * the strip independently (e.g. on a non-camera screen) can keep
446
+ * importing `CaptureThumbnailStrip` directly from the library —
447
+ * the prop here is the convenience wiring for in-`<Camera>` use.
448
+ *
449
+ * Captures emitted by `<Camera>`'s `onCapture` are NOT added to
450
+ * this array automatically — the host owns the canonical list
451
+ * (typically persisted to its own DB) and updates the prop in
452
+ * response. This matches the SDK's "Camera owns runtime state,
453
+ * host persists" pattern.
454
+ */
455
+ thumbnails?: CaptureThumbnailItem[];
456
+
457
+ /**
458
+ * v0.13.0 — minimum-photos hint for the count line. Renders
459
+ * "n / minPhotos min" with the success colour when reached,
460
+ * warning colour otherwise.
461
+ */
462
+ thumbnailsMin?: number;
463
+
464
+ /**
465
+ * v0.13.0 — maximum-photos hint for the count line. Renders
466
+ * "· maxPhotos max" suffix. No enforcement — the host decides
467
+ * what to do at the cap.
468
+ */
469
+ thumbnailsMax?: number;
470
+
471
+ /**
472
+ * v0.13.0 — tap handler for thumbnails. When set, replaces the
473
+ * strip's built-in tap-to-preview modal; the host shows its own
474
+ * preview UI (e.g. with delete / recapture buttons gated on
475
+ * sync state). Omit to use the built-in preview.
476
+ */
477
+ onThumbnailPress?: (item: CaptureThumbnailItem) => void;
478
+
479
+ /**
480
+ * v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
481
+ * modal as `visible`. Use this for post-stitch confirmation:
482
+ * after `onCapture` emits, the host stores the result and sets
483
+ * `capturePreview` to the new image, with `capturePreviewActions`
484
+ * = `[Discard, Save]` (or similar). Setting `undefined` hides
485
+ * the modal.
486
+ *
487
+ * Hosts using the modal for thumbnail tap-to-preview can leave
488
+ * this undefined and let the built-in strip's preview handle
489
+ * that case.
490
+ */
491
+ capturePreview?: {
492
+ imageUri: string;
493
+ imageWidth?: number;
494
+ imageHeight?: number;
495
+ title?: string;
496
+ };
497
+
498
+ /**
499
+ * v0.13.0 — action buttons rendered along the bottom of the
500
+ * `CapturePreview` modal. Empty array (or undefined) renders
501
+ * no buttons, only the close affordance.
502
+ */
503
+ capturePreviewActions?: CapturePreviewAction[];
504
+
505
+ /**
506
+ * v0.13.0 — fires when the user dismisses the `capturePreview`
507
+ * modal (tap close, backdrop tap, hardware back on Android).
508
+ * The host is expected to clear the `capturePreview` prop in
509
+ * response.
510
+ */
511
+ onCapturePreviewClose?: () => void;
512
+
320
513
  /**
321
514
  * Optional host-supplied vision-camera frame processor.
322
515
  *
@@ -681,6 +874,23 @@ export function Camera(props: CameraProps): React.JSX.Element {
681
874
  onFramesDropped,
682
875
  onError,
683
876
  onCaptureAbandoned,
877
+ flash: controlledFlash,
878
+ onFlashChange,
879
+ showFlashButton = true,
880
+ panGuide = true,
881
+ panoramaGuidance = true,
882
+ headerTitle,
883
+ onHeaderBack,
884
+ headerBackLabel,
885
+ headerGuidance,
886
+ headerColors,
887
+ thumbnails,
888
+ thumbnailsMin,
889
+ thumbnailsMax,
890
+ onThumbnailPress,
891
+ capturePreview,
892
+ capturePreviewActions,
893
+ onCapturePreviewClose,
684
894
  frameProcessor: hostFrameProcessor,
685
895
  engine = 'batch-keyframe',
686
896
  } = props;
@@ -700,6 +910,12 @@ export function Camera(props: CameraProps): React.JSX.Element {
700
910
  defaultCaptureSource === 'ar',
701
911
  );
702
912
  const [lens, setLens] = useState<CameraLens>(defaultLens);
913
+ // v0.13.0 — flash state. Controlled by `controlledFlash` when the
914
+ // host supplies the `flash` prop; otherwise owned internally and
915
+ // toggled by the built-in flash button. `effectiveFlash` below
916
+ // also forces 'off' in AR mode (ARKit / ARCore own the device's
917
+ // torch and don't surface it through vision-camera's pipeline).
918
+ const [internalFlash, setInternalFlash] = useState<'on' | 'off'>('off');
703
919
  const [settings, setSettings] = useState<PanoramaSettings>(() =>
704
920
  buildPanoramaInitialSettings(
705
921
  extractPanoramaOverrides(props),
@@ -1349,6 +1565,22 @@ export function Camera(props: CameraProps): React.JSX.Element {
1349
1565
  setArPreference((prev) => !prev);
1350
1566
  }, []);
1351
1567
 
1568
+ // ── v0.13.0 — Flash control ─────────────────────────────────────
1569
+ //
1570
+ // `flashRequested` is what the host / built-in button asks for.
1571
+ // `effectiveFlash` is what we actually drive into vision-camera —
1572
+ // AR mode forces 'off' because ARKit / ARCore own AVCaptureDevice
1573
+ // and the torch isn't exposed. This way the button's visual state
1574
+ // (a11y, styling) tracks `flashRequested` while the underlying
1575
+ // camera always sees the correct value.
1576
+ const flashRequested: 'on' | 'off' = controlledFlash ?? internalFlash;
1577
+ const effectiveFlash: 'on' | 'off' = isAR ? 'off' : flashRequested;
1578
+ const toggleFlash = useCallback(() => {
1579
+ const next: 'on' | 'off' = flashRequested === 'on' ? 'off' : 'on';
1580
+ if (controlledFlash == null) setInternalFlash(next);
1581
+ onFlashChange?.(next);
1582
+ }, [flashRequested, controlledFlash, onFlashChange]);
1583
+
1352
1584
  // ── JSX ─────────────────────────────────────────────────────────
1353
1585
 
1354
1586
  return (
@@ -1380,7 +1612,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
1380
1612
  // works either way. Pattern matches AuditCaptureScreen.tsx
1381
1613
  // which has run on `video` (true) for months without issue.
1382
1614
  video
1383
- flash="off"
1615
+ flash={effectiveFlash}
1384
1616
  style={StyleSheet.absoluteFill}
1385
1617
  // F8 (FrameProcessor port) — host-supplied worklet runs on
1386
1618
  // the camera producer thread for every frame. Only wired
@@ -1416,6 +1648,20 @@ export function Camera(props: CameraProps): React.JSX.Element {
1416
1648
  recordingStartedAt={recordingStartedAt ?? undefined}
1417
1649
  />
1418
1650
 
1651
+ {/* v0.13.0 — built-in pan guidance overlays. Both sit on top
1652
+ of the camera preview but under the controls. Each is
1653
+ gyroscope-driven and only subscribes while `active` is
1654
+ true — flipping `active` false on capture-end tears the
1655
+ subscription down so the sensor isn't running idle. Hosts
1656
+ can opt out per overlay via the `panGuide` / `panoramaGuidance`
1657
+ boolean props (both default true). */}
1658
+ {panGuide && (
1659
+ <IncrementalPanGuide active={statusPhase === 'recording'} />
1660
+ )}
1661
+ {panoramaGuidance && (
1662
+ <PanoramaGuidance active={statusPhase === 'recording'} />
1663
+ )}
1664
+
1419
1665
  {/*
1420
1666
  2026-05-22 (audit F9 + F3) — debug UI suite, all gated on
1421
1667
  settings.debug. Mounts in <Camera> automatically; Layer-2
@@ -1459,12 +1705,52 @@ export function Camera(props: CameraProps): React.JSX.Element {
1459
1705
  topInset={insets.top}
1460
1706
  />
1461
1707
 
1462
- {/* Settings gear (top-right), gated on showSettingsButton. */}
1463
- {showSettingsButton && (
1464
- <SettingsButton
1465
- topInset={insets.top}
1466
- onPress={() => setSettingsModalVisible(true)}
1467
- />
1708
+ {/* v0.13.0 built-in CaptureHeader, gated on `headerTitle`.
1709
+ When the header is mounted, it absorbs the settings gear
1710
+ on its right side (avoids stacking with the standalone
1711
+ gear). Hosts that DON'T set `headerTitle` get the legacy
1712
+ standalone gear, still gated on `showSettingsButton`. */}
1713
+ {headerTitle != null ? (
1714
+ <View style={styles.headerWrap} pointerEvents="box-none">
1715
+ <CaptureHeader
1716
+ title={headerTitle}
1717
+ onBack={onHeaderBack}
1718
+ backLabel={headerBackLabel}
1719
+ guidance={headerGuidance}
1720
+ colors={headerColors}
1721
+ topInset={insets.top}
1722
+ onSettingsPress={
1723
+ showSettingsButton
1724
+ ? () => setSettingsModalVisible(true)
1725
+ : undefined
1726
+ }
1727
+ />
1728
+ </View>
1729
+ ) : (
1730
+ showSettingsButton && (
1731
+ <SettingsButton
1732
+ topInset={insets.top}
1733
+ onPress={() => setSettingsModalVisible(true)}
1734
+ />
1735
+ )
1736
+ )}
1737
+
1738
+ {/* v0.13.0 — built-in capture-history thumbnail strip. Renders
1739
+ when the host supplies a `thumbnails` array (even empty),
1740
+ hidden during recording so it doesn't overlap the band
1741
+ overlay. Sits above the bottom controls in JS-bottom
1742
+ coordinates; landscape/non-locked layouts get the strip in
1743
+ the same place (no orientation-aware repositioning for now —
1744
+ the strip is intrinsically horizontal). */}
1745
+ {thumbnails != null && statusPhase !== 'recording' && (
1746
+ <View style={styles.thumbnailStripWrap} pointerEvents="box-none">
1747
+ <CaptureThumbnailStrip
1748
+ items={thumbnails}
1749
+ minPhotos={thumbnailsMin}
1750
+ maxPhotos={thumbnailsMax}
1751
+ onItemPress={onThumbnailPress}
1752
+ />
1753
+ </View>
1468
1754
  )}
1469
1755
 
1470
1756
  {/*
@@ -1505,7 +1791,25 @@ export function Camera(props: CameraProps): React.JSX.Element {
1505
1791
  vertical column when on left/right (slots stack along
1506
1792
  the narrow strip). Touch targets stay axis-aligned. */}
1507
1793
  <View style={bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation))}>
1508
- <View style={styles.bottomBarLeft} />
1794
+ <View style={styles.bottomBarLeft}>
1795
+ {showFlashButton && (
1796
+ <Pressable
1797
+ onPress={isAR ? undefined : toggleFlash}
1798
+ accessibilityRole="button"
1799
+ accessibilityLabel={isAR ? 'Flash unavailable in AR mode' : `Flash ${flashRequested === 'on' ? 'on' : 'off'}`}
1800
+ accessibilityState={{ selected: flashRequested === 'on', disabled: isAR }}
1801
+ disabled={isAR}
1802
+ hitSlop={8}
1803
+ style={[
1804
+ styles.flashButton,
1805
+ flashRequested === 'on' && !isAR && styles.flashButtonActive,
1806
+ isAR && styles.flashButtonDisabled,
1807
+ ]}
1808
+ >
1809
+ <Text style={styles.flashIcon}>⚡</Text>
1810
+ </Pressable>
1811
+ )}
1812
+ </View>
1509
1813
  <View style={styles.bottomBarCenter}>
1510
1814
  <LensChip
1511
1815
  lens={lens}
@@ -1550,6 +1854,21 @@ export function Camera(props: CameraProps): React.JSX.Element {
1550
1854
  currentOrientation={drift.currentOrientation}
1551
1855
  onAcknowledge={() => setDriftModalDismissed(true)}
1552
1856
  />
1857
+
1858
+ {/* v0.13.0 — built-in post-stitch / tap-to-preview modal.
1859
+ Visible when the host supplies `capturePreview`. When
1860
+ undefined the modal stays hidden (visible=false) so it
1861
+ doesn't intercept touches. Host is expected to clear
1862
+ `capturePreview` via `onCapturePreviewClose` on dismiss. */}
1863
+ <CapturePreview
1864
+ visible={capturePreview != null}
1865
+ imageUri={capturePreview?.imageUri ?? ''}
1866
+ imageWidth={capturePreview?.imageWidth}
1867
+ imageHeight={capturePreview?.imageHeight}
1868
+ title={capturePreview?.title}
1869
+ actions={capturePreviewActions}
1870
+ onClose={onCapturePreviewClose ?? noop}
1871
+ />
1553
1872
  </View>
1554
1873
  );
1555
1874
  }
@@ -1731,6 +2050,8 @@ const styles = StyleSheet.create({
1731
2050
  },
1732
2051
  bottomBarLeft: {
1733
2052
  flex: 1,
2053
+ alignItems: 'flex-start',
2054
+ justifyContent: 'flex-end',
1734
2055
  },
1735
2056
  bottomBarCenter: {
1736
2057
  flex: 1,
@@ -1744,4 +2065,34 @@ const styles = StyleSheet.create({
1744
2065
  shutterWrap: {
1745
2066
  marginTop: 12,
1746
2067
  },
2068
+ headerWrap: {
2069
+ position: 'absolute',
2070
+ top: 0,
2071
+ left: 0,
2072
+ right: 0,
2073
+ },
2074
+ thumbnailStripWrap: {
2075
+ position: 'absolute',
2076
+ left: 0,
2077
+ right: 0,
2078
+ bottom: 160,
2079
+ },
2080
+ flashButton: {
2081
+ width: 44,
2082
+ height: 44,
2083
+ borderRadius: 22,
2084
+ alignItems: 'center',
2085
+ justifyContent: 'center',
2086
+ backgroundColor: 'rgba(0,0,0,0.45)',
2087
+ },
2088
+ flashButtonActive: {
2089
+ backgroundColor: '#ffd34d',
2090
+ },
2091
+ flashButtonDisabled: {
2092
+ opacity: 0.35,
2093
+ },
2094
+ flashIcon: {
2095
+ fontSize: 20,
2096
+ color: '#ffffff',
2097
+ },
1747
2098
  });