@lmb-it/kitsconcerto-native 0.1.287 → 0.1.289

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,5 +1,5 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import React, { useState, useRef, useCallback, useMemo, useReducer, useEffect } from 'react';
2
+ import React, { forwardRef, useState, useRef, useCallback, useMemo, useReducer, useEffect, useImperativeHandle } from 'react';
3
3
  import { PanResponder, View, Text, Image, ActivityIndicator, Pressable } from 'react-native';
4
4
  import Slider from '@react-native-community/slider';
5
5
  import { Icon } from '../../primitives/Icon/index.js';
@@ -43,181 +43,216 @@ const touchMidpoint = (event, origin) => {
43
43
  y: ((a.pageY ?? 0) + (b.pageY ?? 0)) / 2 - origin.y
44
44
  };
45
45
  };
46
- function ImageCropper(rawProps) {
47
- const { mergedProps: props, elementStyles } = useComponentDefaults("ImageCropper", rawProps);
48
- const el = useResolvedElementStyles(elementStyles);
49
- const {
50
- source,
51
- aspect = DEFAULT_ASPECT,
52
- maskShape = "rectangle",
53
- output,
54
- onCrop,
55
- onCancel,
56
- onError,
57
- onInteractionChange,
58
- title,
59
- hidePreview,
60
- hideGrid,
61
- maxZoom = DEFAULT_MAX_ZOOM,
62
- labels,
63
- testID
64
- } = props;
65
- const text = { ...DEFAULT_LABELS, ...labels ?? {} };
66
- const ratio = aspectOf(aspect);
67
- const isCircle = maskShape === "circle";
68
- const { image, loading, error } = useImageSource(source, onError);
69
- const [viewport, setViewport] = useState({ width: 0, height: 0 });
70
- const originRef = useRef({ x: 0, y: 0 });
71
- const stageRef = useRef(null);
72
- const onStageLayout = useCallback((e) => {
73
- const { width, height } = e.nativeEvent.layout;
74
- setViewport({ width: Math.round(width), height: Math.round(height) });
75
- stageRef.current?.measureInWindow((x, y) => {
76
- originRef.current = { x, y };
77
- });
78
- }, []);
79
- const config = useMemo(
80
- () => ({
81
- source: image ? { width: image.width, height: image.height } : { width: 1, height: 1 },
82
- viewport: viewport.width > 0 ? viewport : { width: 1, height: 1 },
83
- ratio,
84
- maxZoom,
85
- // ⚠️ ALWAYS FALSE ON NATIVE — see the note at the top of the file.
86
- resizable: false,
87
- inset: STAGE_INSET_NATIVE,
88
- squareFactor: SQUARE_CROP_FACTOR
89
- }),
90
- [image, viewport, ratio, maxZoom]
91
- );
92
- const [state, dispatch] = useReducer(cropperReducer, config, initialCropperState);
93
- useEffect(() => {
94
- dispatch({ type: "configure", config });
95
- }, [config]);
96
- const ready = !!image && viewport.width > 1 && viewport.height > 1;
97
- const pinchRef = useRef(null);
98
- const lastPanRef = useRef({ x: 0, y: 0 });
99
- const scaleRef = useRef(state.transform.scale);
100
- scaleRef.current = state.transform.scale;
101
- const resetPan = useCallback(() => {
102
- lastPanRef.current = { x: 0, y: 0 };
103
- }, []);
104
- const beginGesture = useCallback(() => {
105
- pinchRef.current = null;
106
- lastPanRef.current = { x: 0, y: 0 };
107
- }, []);
108
- const applyPan = useCallback((dx, dy) => {
109
- dispatch({
110
- type: "pan",
111
- dx: dx - lastPanRef.current.x,
112
- dy: dy - lastPanRef.current.y
113
- });
114
- lastPanRef.current = { x: dx, y: dy };
115
- }, []);
116
- const applyPinch = useCallback((reading, anchor) => {
117
- if (!isUsablePinchReading(reading)) return;
118
- const pinch = pinchRef.current;
119
- if (!pinch) {
120
- pinchRef.current = { reading, scale: scaleRef.current };
121
- return;
122
- }
123
- dispatch({ type: "zoomAt", anchor, scale: pinchScale(pinch, reading) });
124
- }, []);
125
- const interactingRef = useRef(false);
126
- const setInteracting = useCallback(
127
- (active) => {
128
- if (interactingRef.current === active) return;
129
- interactingRef.current = active;
130
- onInteractionChange?.(active);
131
- },
132
- [onInteractionChange]
133
- );
134
- const detector = useMemo(() => {
135
- if (!gestureHandler) return null;
136
- const { Gesture } = gestureHandler;
137
- const pan = Gesture.Pan().runOnJS(true).onBegin(() => {
138
- setInteracting(true);
139
- resetPan();
140
- }).onUpdate((e) => {
141
- if (e.numberOfPointers > 1 || pinchRef.current) {
142
- lastPanRef.current = { x: e.translationX, y: e.translationY };
46
+ const ImageCropper = forwardRef(
47
+ function ImageCropper2(rawProps, ref) {
48
+ const { mergedProps: props, elementStyles } = useComponentDefaults("ImageCropper", rawProps);
49
+ const el = useResolvedElementStyles(elementStyles);
50
+ const {
51
+ source,
52
+ aspect = DEFAULT_ASPECT,
53
+ maskShape = "rectangle",
54
+ output,
55
+ onCrop,
56
+ onCancel,
57
+ onError,
58
+ onInteractionChange,
59
+ title,
60
+ hidePreview,
61
+ hideFooter,
62
+ onFrameChange,
63
+ hideGrid,
64
+ maxZoom = DEFAULT_MAX_ZOOM,
65
+ labels,
66
+ testID
67
+ } = props;
68
+ const text = { ...DEFAULT_LABELS, ...labels ?? {} };
69
+ const ratio = aspectOf(aspect);
70
+ const isCircle = maskShape === "circle";
71
+ const { image, loading, error } = useImageSource(source, onError);
72
+ const [viewport, setViewport] = useState({ width: 0, height: 0 });
73
+ const originRef = useRef({ x: 0, y: 0 });
74
+ const stageRef = useRef(null);
75
+ const onStageLayout = useCallback((e) => {
76
+ const { width, height } = e.nativeEvent.layout;
77
+ setViewport({ width: Math.round(width), height: Math.round(height) });
78
+ stageRef.current?.measureInWindow((x, y) => {
79
+ originRef.current = { x, y };
80
+ });
81
+ }, []);
82
+ const config = useMemo(
83
+ () => ({
84
+ source: image ? { width: image.width, height: image.height } : { width: 1, height: 1 },
85
+ viewport: viewport.width > 0 ? viewport : { width: 1, height: 1 },
86
+ ratio,
87
+ maxZoom,
88
+ // ⚠️ ALWAYS FALSE ON NATIVE — see the note at the top of the file.
89
+ resizable: false,
90
+ inset: STAGE_INSET_NATIVE,
91
+ squareFactor: SQUARE_CROP_FACTOR
92
+ }),
93
+ [image, viewport, ratio, maxZoom]
94
+ );
95
+ const [state, dispatch] = useReducer(cropperReducer, config, initialCropperState);
96
+ useEffect(() => {
97
+ dispatch({ type: "configure", config });
98
+ }, [config]);
99
+ const ready = !!image && viewport.width > 1 && viewport.height > 1;
100
+ const pinchRef = useRef(null);
101
+ const lastPanRef = useRef({ x: 0, y: 0 });
102
+ const scaleRef = useRef(state.transform.scale);
103
+ scaleRef.current = state.transform.scale;
104
+ const resetPan = useCallback(() => {
105
+ lastPanRef.current = { x: 0, y: 0 };
106
+ }, []);
107
+ const beginGesture = useCallback(() => {
108
+ pinchRef.current = null;
109
+ lastPanRef.current = { x: 0, y: 0 };
110
+ }, []);
111
+ const applyPan = useCallback((dx, dy) => {
112
+ dispatch({
113
+ type: "pan",
114
+ dx: dx - lastPanRef.current.x,
115
+ dy: dy - lastPanRef.current.y
116
+ });
117
+ lastPanRef.current = { x: dx, y: dy };
118
+ }, []);
119
+ const applyPinch = useCallback((reading, anchor) => {
120
+ if (!isUsablePinchReading(reading)) return;
121
+ const pinch = pinchRef.current;
122
+ if (!pinch) {
123
+ pinchRef.current = { reading, scale: scaleRef.current };
143
124
  return;
144
125
  }
145
- applyPan(e.translationX, e.translationY);
146
- }).onFinalize(() => {
147
- setInteracting(false);
148
- resetPan();
149
- });
150
- const pinch = Gesture.Pinch().runOnJS(true).onBegin(() => {
151
- setInteracting(true);
152
- pinchRef.current = { reading: 1, scale: scaleRef.current };
153
- }).onUpdate((e) => {
154
- applyPinch(e.scale, { x: e.focalX, y: e.focalY });
155
- }).onFinalize(() => {
156
- setInteracting(false);
157
- pinchRef.current = null;
158
- });
159
- return Gesture.Simultaneous(pan, pinch);
160
- }, [applyPan, applyPinch, resetPan, setInteracting]);
161
- const scrollBlocker = useMemo(
162
- () => PanResponder.create({
163
- onStartShouldSetPanResponder: () => true,
164
- onMoveShouldSetPanResponder: () => true,
165
- onPanResponderTerminationRequest: () => false,
166
- onShouldBlockNativeResponder: () => true,
167
- onPanResponderGrant: () => setInteracting(true),
168
- onPanResponderRelease: () => setInteracting(false),
169
- onPanResponderTerminate: () => setInteracting(false)
170
- }),
171
- [setInteracting]
172
- );
173
- const responder = useMemo(
174
- () => PanResponder.create({
175
- onStartShouldSetPanResponder: () => true,
176
- onMoveShouldSetPanResponder: () => true,
177
- // ⚠️ Refuses a JS-level handover. It does NOT save a vertical
178
- // drag inside a ScrollView — that cancellation is native and
179
- // never asks — but it does hold the gesture against JS
180
- // responders nearer the touch.
181
- onPanResponderTerminationRequest: () => false,
182
- onPanResponderGrant: () => {
183
- setInteracting(true);
184
- beginGesture();
126
+ dispatch({ type: "zoomAt", anchor, scale: pinchScale(pinch, reading) });
127
+ }, []);
128
+ const interactingRef = useRef(false);
129
+ const setInteracting = useCallback(
130
+ (active) => {
131
+ if (interactingRef.current === active) return;
132
+ interactingRef.current = active;
133
+ onInteractionChange?.(active);
185
134
  },
186
- onPanResponderMove: (event, gesture) => {
187
- const touches = event.nativeEvent.touches.length;
188
- if (touches >= 2) {
189
- applyPinch(
190
- touchDistance(event),
191
- touchMidpoint(event, originRef.current)
192
- );
193
- return;
194
- }
195
- if (pinchRef.current) {
196
- pinchRef.current = null;
197
- lastPanRef.current = { x: gesture.dx, y: gesture.dy };
135
+ [onInteractionChange]
136
+ );
137
+ const detector = useMemo(() => {
138
+ if (!gestureHandler) return null;
139
+ const { Gesture } = gestureHandler;
140
+ const pan = Gesture.Pan().runOnJS(true).onBegin(() => {
141
+ setInteracting(true);
142
+ resetPan();
143
+ }).onUpdate((e) => {
144
+ if (e.numberOfPointers > 1 || pinchRef.current) {
145
+ lastPanRef.current = { x: e.translationX, y: e.translationY };
198
146
  return;
199
147
  }
200
- applyPan(gesture.dx, gesture.dy);
201
- },
202
- onPanResponderRelease: () => {
148
+ applyPan(e.translationX, e.translationY);
149
+ }).onFinalize(() => {
203
150
  setInteracting(false);
204
- beginGesture();
205
- },
206
- onPanResponderTerminate: () => {
151
+ resetPan();
152
+ });
153
+ const pinch = Gesture.Pinch().runOnJS(true).onBegin(() => {
154
+ setInteracting(true);
155
+ pinchRef.current = { reading: 1, scale: scaleRef.current };
156
+ }).onUpdate((e) => {
157
+ applyPinch(e.scale, { x: e.focalX, y: e.focalY });
158
+ }).onFinalize(() => {
207
159
  setInteracting(false);
208
- beginGesture();
160
+ pinchRef.current = null;
161
+ });
162
+ return Gesture.Simultaneous(pan, pinch);
163
+ }, [applyPan, applyPinch, resetPan, setInteracting]);
164
+ const scrollBlocker = useMemo(
165
+ () => PanResponder.create({
166
+ onStartShouldSetPanResponder: () => true,
167
+ onMoveShouldSetPanResponder: () => true,
168
+ onPanResponderTerminationRequest: () => false,
169
+ onShouldBlockNativeResponder: () => true,
170
+ onPanResponderGrant: () => setInteracting(true),
171
+ onPanResponderRelease: () => setInteracting(false),
172
+ onPanResponderTerminate: () => setInteracting(false)
173
+ }),
174
+ [setInteracting]
175
+ );
176
+ const responder = useMemo(
177
+ () => PanResponder.create({
178
+ onStartShouldSetPanResponder: () => true,
179
+ onMoveShouldSetPanResponder: () => true,
180
+ // ⚠️ Refuses a JS-level handover. It does NOT save a vertical
181
+ // drag inside a ScrollView — that cancellation is native and
182
+ // never asks — but it does hold the gesture against JS
183
+ // responders nearer the touch.
184
+ onPanResponderTerminationRequest: () => false,
185
+ onPanResponderGrant: () => {
186
+ setInteracting(true);
187
+ beginGesture();
188
+ },
189
+ onPanResponderMove: (event, gesture) => {
190
+ const touches = event.nativeEvent.touches.length;
191
+ if (touches >= 2) {
192
+ applyPinch(
193
+ touchDistance(event),
194
+ touchMidpoint(event, originRef.current)
195
+ );
196
+ return;
197
+ }
198
+ if (pinchRef.current) {
199
+ pinchRef.current = null;
200
+ lastPanRef.current = { x: gesture.dx, y: gesture.dy };
201
+ return;
202
+ }
203
+ applyPan(gesture.dx, gesture.dy);
204
+ },
205
+ onPanResponderRelease: () => {
206
+ setInteracting(false);
207
+ beginGesture();
208
+ },
209
+ onPanResponderTerminate: () => {
210
+ setInteracting(false);
211
+ beginGesture();
212
+ }
213
+ }),
214
+ [applyPan, applyPinch, beginGesture, setInteracting]
215
+ );
216
+ const [busy, setBusy] = useState(false);
217
+ const canSave = ready && !busy && hasCropEncoder();
218
+ const save = useCallback(async () => {
219
+ if (!ready || !image || busy) return;
220
+ setBusy(true);
221
+ try {
222
+ const rect = cropToSourceRect(state.crop, state.transform, config.source, state.rotation);
223
+ const format = output?.format ?? DEFAULT_FORMAT;
224
+ const result = await encodeCropNative(
225
+ image.uri,
226
+ rect,
227
+ config.source,
228
+ state.rotation,
229
+ {
230
+ width: output?.maxWidth,
231
+ height: output?.maxHeight,
232
+ bytes: output?.maxBytes ?? DEFAULT_MAX_BYTES
233
+ },
234
+ format
235
+ );
236
+ if (!result.withinBudget) {
237
+ onError?.({
238
+ reason: "too-large",
239
+ message: `The smallest version this could produce is ${Math.round(result.size / 1024)} KB.`
240
+ });
241
+ }
242
+ const { withinBudget: _ignored, ...payload } = result;
243
+ onCrop?.(payload);
244
+ } catch (cause) {
245
+ onError?.({
246
+ reason: "encode-failed",
247
+ message: cause instanceof Error ? cause.message : "The image could not be saved."
248
+ });
249
+ } finally {
250
+ setBusy(false);
209
251
  }
210
- }),
211
- [applyPan, applyPinch, beginGesture, setInteracting]
212
- );
213
- const [busy, setBusy] = useState(false);
214
- const canSave = ready && !busy && hasCropEncoder();
215
- const save = useCallback(async () => {
216
- if (!ready || !image || busy) return;
217
- setBusy(true);
218
- try {
252
+ }, [ready, image, busy, state, config.source, output, onCrop, onError]);
253
+ const estimate = useCallback(async () => {
254
+ if (!ready || !image || !hasCropEncoder()) return null;
219
255
  const rect = cropToSourceRect(state.crop, state.transform, config.source, state.rotation);
220
- const format = output?.format ?? DEFAULT_FORMAT;
221
256
  const result = await encodeCropNative(
222
257
  image.uri,
223
258
  rect,
@@ -228,411 +263,410 @@ function ImageCropper(rawProps) {
228
263
  height: output?.maxHeight,
229
264
  bytes: output?.maxBytes ?? DEFAULT_MAX_BYTES
230
265
  },
231
- format
266
+ output?.format ?? DEFAULT_FORMAT
232
267
  );
233
- if (!result.withinBudget) {
234
- onError?.({
235
- reason: "too-large",
236
- message: `The smallest version this could produce is ${Math.round(result.size / 1024)} KB.`
237
- });
238
- }
239
- const { withinBudget: _ignored, ...payload } = result;
240
- onCrop?.(payload);
241
- } catch (cause) {
242
- onError?.({
243
- reason: "encode-failed",
244
- message: cause instanceof Error ? cause.message : "The image could not be saved."
268
+ return result.size;
269
+ }, [ready, image, state, config.source, output]);
270
+ useImperativeHandle(ref, () => ({ save: () => void save(), estimate }), [save, estimate]);
271
+ const frameCallbackRef = useRef(onFrameChange);
272
+ frameCallbackRef.current = onFrameChange;
273
+ useEffect(() => {
274
+ if (!ready || !image) return;
275
+ frameCallbackRef.current?.({
276
+ crop: state.crop,
277
+ transform: state.transform,
278
+ rotation: state.rotation,
279
+ source: { width: image.width, height: image.height },
280
+ oriented: orientedSize({ width: image.width, height: image.height }, state.rotation)
245
281
  });
246
- } finally {
247
- setBusy(false);
248
- }
249
- }, [ready, image, busy, state, config.source, output, onCrop, onError]);
250
- const { crop, transform, rotation } = state;
251
- const oriented = image ? orientedSize({ width: image.width, height: image.height }, rotation) : null;
252
- const band = scaleRange(state);
253
- const zoomLabel = Number.isFinite(band.min) && band.min > 0 ? `${Math.round(transform.scale / band.min * 100)}%` : "100%";
254
- const windowRadius = isCircle ? Math.max(crop.width, crop.height) / 2 : Number(el.mask?.borderRadius ?? 0);
255
- const SCRIM = Math.ceil(Math.max(viewport.width, viewport.height)) || 1;
256
- const aspectLabel = `${aspect[0]}:${aspect[1]}`;
257
- const subtitle = ready ? "Drag to move \xB7 pinch to zoom" : isCircle ? `Circle \xB7 ${aspectLabel}` : `Banner \xB7 ${aspectLabel}`;
258
- const budgetKb = Math.round((output?.maxBytes ?? DEFAULT_MAX_BYTES) / 1024);
259
- const previewWidth = isCircle ? 60 : 160;
260
- const previewHeight = isCircle ? 60 : Math.round(160 / ratio);
261
- const stageTool = (icon, label, onPress) => /* @__PURE__ */ jsxs(
262
- Pressable,
263
- {
264
- onPress,
265
- disabled: !ready,
266
- accessibilityRole: "button",
267
- accessibilityLabel: label,
268
- accessibilityState: { disabled: !ready },
269
- style: { width: 58, alignItems: "center", gap: 4 },
270
- children: [
271
- /* @__PURE__ */ jsx(View, { style: {
272
- width: 44,
273
- height: 44,
274
- borderRadius: 22,
275
- backgroundColor: "rgba(255,255,255,0.09)",
276
- alignItems: "center",
277
- justifyContent: "center"
278
- }, children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 19, color: "#FFFFFF" }) }),
279
- /* @__PURE__ */ jsx(Text, { style: { fontSize: 11, fontWeight: "600", color: "rgba(255,255,255,0.75)" }, children: label })
280
- ]
281
- }
282
- );
283
- return /* @__PURE__ */ jsxs(
284
- View,
285
- {
286
- testID,
287
- style: [
288
- /**
289
- * ⚠️ NO `flex: 1` HERE, THOUGH THE MOCKUP IS FULL-SCREEN. A
290
- * child with `flex: 1` inside a parent that was never given a
291
- * height measures 0 on Yoga, and this component does not own
292
- * the screen — a consumer mounts it in a modal, a sheet or a
293
- * form. The footer's `marginTop: 'auto'` gets the mockup's
294
- * pinned-to-the-bottom footer when the parent DOES bound the
295
- * height, and does nothing when it does not.
296
- */
297
- { width: "100%" },
298
- el.root,
299
- /**
300
- * ⛔ THE PHONE CROPPER IS A DARK SHEET, AND THAT OVERRIDES THE
301
- * `root` SLOT ON NATIVE ONLY. The mockup draws web as a light
302
- * modal card and the phone as a full-screen dark surface, and
303
- * one `root` token cannot be both. It has to be the dark one
304
- * here because the controls below it are load-bearing white:
305
- * on a light sheet the Rotate and Reset labels vanish.
306
- *
307
- * ⚠️ SO `slots.root.backgroundColor` DOES NOT REACH THIS ON
308
- * NATIVE. That is the deliberate cost; the stage's own colour
309
- * has carried the same argument since the component shipped.
310
- */
311
- { backgroundColor: "#0B1517" }
312
- ],
313
- children: [
314
- title != null && /* @__PURE__ */ jsxs(View, { style: [
315
- { alignItems: "center" },
316
- el.header,
317
- {
318
- paddingLeft: 18,
319
- paddingRight: 18,
320
- paddingTop: 12,
321
- paddingBottom: 14,
322
- borderBottomWidth: 0,
323
- gap: 0
324
- }
325
- ], children: [
326
- /* @__PURE__ */ jsx(Text, { style: [el.title, { fontSize: 16, fontWeight: "800", color: "#FFFFFF" }], children: title }),
327
- /* @__PURE__ */ jsx(Text, { style: { marginTop: 3, fontSize: 12, color: "rgba(255,255,255,0.6)" }, children: subtitle })
328
- ] }),
329
- /* @__PURE__ */ jsxs(View, { style: [el.content, { paddingLeft: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 0 }], children: [
330
- /* @__PURE__ */ jsx(StageFrame, { gesture: detector, children: /* @__PURE__ */ jsxs(
331
- View,
282
+ }, [ready, image, state]);
283
+ const { crop, transform, rotation } = state;
284
+ const oriented = image ? orientedSize({ width: image.width, height: image.height }, rotation) : null;
285
+ const band = scaleRange(state);
286
+ const zoomLabel = Number.isFinite(band.min) && band.min > 0 ? `${Math.round(transform.scale / band.min * 100)}%` : "100%";
287
+ const windowRadius = isCircle ? Math.max(crop.width, crop.height) / 2 : Number(el.mask?.borderRadius ?? 0);
288
+ const SCRIM = Math.ceil(Math.max(viewport.width, viewport.height)) || 1;
289
+ const aspectLabel = `${aspect[0]}:${aspect[1]}`;
290
+ const subtitle = ready ? "Drag to move \xB7 pinch to zoom" : isCircle ? `Circle \xB7 ${aspectLabel}` : `Banner \xB7 ${aspectLabel}`;
291
+ const budgetKb = Math.round((output?.maxBytes ?? DEFAULT_MAX_BYTES) / 1024);
292
+ const previewWidth = isCircle ? 60 : 160;
293
+ const previewHeight = isCircle ? 60 : Math.round(160 / ratio);
294
+ const stageTool = (icon, label, onPress) => /* @__PURE__ */ jsxs(
295
+ Pressable,
296
+ {
297
+ onPress,
298
+ disabled: !ready,
299
+ accessibilityRole: "button",
300
+ accessibilityLabel: label,
301
+ accessibilityState: { disabled: !ready },
302
+ style: { width: 58, alignItems: "center", gap: 4 },
303
+ children: [
304
+ /* @__PURE__ */ jsx(View, { style: {
305
+ width: 44,
306
+ height: 44,
307
+ borderRadius: 22,
308
+ backgroundColor: "rgba(255,255,255,0.09)",
309
+ alignItems: "center",
310
+ justifyContent: "center"
311
+ }, children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 19, color: "#FFFFFF" }) }),
312
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 11, fontWeight: "600", color: "rgba(255,255,255,0.75)" }, children: label })
313
+ ]
314
+ }
315
+ );
316
+ return /* @__PURE__ */ jsxs(
317
+ View,
318
+ {
319
+ testID,
320
+ style: [
321
+ /**
322
+ * ⚠️ NO `flex: 1` HERE, THOUGH THE MOCKUP IS FULL-SCREEN. A
323
+ * child with `flex: 1` inside a parent that was never given a
324
+ * height measures 0 on Yoga, and this component does not own
325
+ * the screen — a consumer mounts it in a modal, a sheet or a
326
+ * form. The footer's `marginTop: 'auto'` gets the mockup's
327
+ * pinned-to-the-bottom footer when the parent DOES bound the
328
+ * height, and does nothing when it does not.
329
+ */
330
+ { width: "100%" },
331
+ el.root,
332
+ /**
333
+ * ⛔ THE PHONE CROPPER IS A DARK SHEET, AND THAT OVERRIDES THE
334
+ * `root` SLOT ON NATIVE ONLY. The mockup draws web as a light
335
+ * modal card and the phone as a full-screen dark surface, and
336
+ * one `root` token cannot be both. It has to be the dark one
337
+ * here because the controls below it are load-bearing white:
338
+ * on a light sheet the Rotate and Reset labels vanish.
339
+ *
340
+ * ⚠️ SO `slots.root.backgroundColor` DOES NOT REACH THIS ON
341
+ * NATIVE. That is the deliberate cost; the stage's own colour
342
+ * has carried the same argument since the component shipped.
343
+ */
344
+ { backgroundColor: "#0B1517" }
345
+ ],
346
+ children: [
347
+ title != null && /* @__PURE__ */ jsxs(View, { style: [
348
+ { alignItems: "center" },
349
+ el.header,
332
350
  {
333
- ref: stageRef,
334
- onLayout: onStageLayout,
335
- ...detector ? scrollBlocker.panHandlers : responder.panHandlers,
336
- style: [
337
- { width: "100%", aspectRatio: STAGE_ASPECT_NATIVE, overflow: "hidden" },
338
- el.container
339
- ],
340
- children: [
341
- ready && image && oriented && /* @__PURE__ */ jsx(
342
- Image,
343
- {
344
- source: { uri: image.uri },
345
- resizeMode: "stretch",
346
- style: {
347
- position: "absolute",
348
- left: transform.x,
349
- top: transform.y,
350
- width: oriented.width * transform.scale,
351
- height: oriented.height * transform.scale,
352
- transform: [{ rotate: `${rotation}deg` }]
353
- }
354
- }
355
- ),
356
- (!source || error) && /* @__PURE__ */ jsxs(View, { style: [
357
- {
358
- position: "absolute",
359
- left: 18,
360
- top: 18,
361
- right: 18,
362
- bottom: 18,
363
- alignItems: "center",
364
- justifyContent: "center"
365
- },
366
- el.dropZone,
367
- { borderRadius: 18 }
368
- ], children: [
369
- /* @__PURE__ */ jsx(View, { style: {
370
- width: 60,
371
- height: 60,
372
- borderRadius: 30,
373
- backgroundColor: "rgba(32,170,176,0.16)",
374
- alignItems: "center",
375
- justifyContent: "center"
376
- }, children: /* @__PURE__ */ jsx(Icon, { name: "image", size: 26, color: CROP_ACCENT_SOFT }) }),
377
- /* @__PURE__ */ jsx(Text, { style: { fontSize: 16, fontWeight: "800", color: "#FFFFFF", textAlign: "center" }, children: text.dropZone }),
378
- /* @__PURE__ */ jsx(Text, { style: { fontSize: 12.5, color: "rgba(255,255,255,0.6)", textAlign: "center" }, children: "JPG, PNG or WebP" }),
379
- error && /* @__PURE__ */ jsx(Text, { style: {
380
- fontSize: 12.5,
381
- fontWeight: "600",
382
- color: CROP_DANGER,
383
- textAlign: "center"
384
- }, children: error.message })
385
- ] }),
386
- ready && /* @__PURE__ */ jsxs(Fragment, { children: [
387
- /* @__PURE__ */ jsx(
388
- View,
351
+ paddingLeft: 18,
352
+ paddingRight: 18,
353
+ paddingTop: 12,
354
+ paddingBottom: 14,
355
+ borderBottomWidth: 0,
356
+ gap: 0
357
+ }
358
+ ], children: [
359
+ /* @__PURE__ */ jsx(Text, { style: [el.title, { fontSize: 16, fontWeight: "800", color: "#FFFFFF" }], children: title }),
360
+ /* @__PURE__ */ jsx(Text, { style: { marginTop: 3, fontSize: 12, color: "rgba(255,255,255,0.6)" }, children: subtitle })
361
+ ] }),
362
+ /* @__PURE__ */ jsxs(View, { style: [el.content, { paddingLeft: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 0 }], children: [
363
+ /* @__PURE__ */ jsx(StageFrame, { gesture: detector, children: /* @__PURE__ */ jsxs(
364
+ View,
365
+ {
366
+ ref: stageRef,
367
+ onLayout: onStageLayout,
368
+ ...detector ? scrollBlocker.panHandlers : responder.panHandlers,
369
+ style: [
370
+ { width: "100%", aspectRatio: STAGE_ASPECT_NATIVE, overflow: "hidden" },
371
+ el.container
372
+ ],
373
+ children: [
374
+ ready && image && oriented && /* @__PURE__ */ jsx(
375
+ Image,
389
376
  {
390
- pointerEvents: "none",
391
- style: [
392
- {
393
- position: "absolute",
394
- left: crop.x - SCRIM,
395
- top: crop.y - SCRIM,
396
- width: crop.width + SCRIM * 2,
397
- height: crop.height + SCRIM * 2,
398
- borderWidth: SCRIM,
399
- borderColor: el.shade?.backgroundColor ?? "rgba(0,0,0,0.6)",
400
- borderRadius: windowRadius + SCRIM
401
- }
402
- ]
377
+ source: { uri: image.uri },
378
+ resizeMode: "stretch",
379
+ style: {
380
+ position: "absolute",
381
+ left: transform.x,
382
+ top: transform.y,
383
+ width: oriented.width * transform.scale,
384
+ height: oriented.height * transform.scale,
385
+ transform: [{ rotate: `${rotation}deg` }]
386
+ }
403
387
  }
404
388
  ),
405
- /* @__PURE__ */ jsx(
406
- View,
389
+ (!source || error) && /* @__PURE__ */ jsxs(View, { style: [
407
390
  {
408
- pointerEvents: "none",
409
- style: [
410
- {
411
- position: "absolute",
412
- left: crop.x,
413
- top: crop.y,
414
- width: crop.width,
415
- height: crop.height,
416
- borderStyle: "solid"
417
- },
418
- el.cropWindow,
419
- { borderRadius: windowRadius }
420
- ],
421
- children: !hideGrid && [1, 2].map((n) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
422
- /* @__PURE__ */ jsx(View, { style: [
391
+ position: "absolute",
392
+ left: 18,
393
+ top: 18,
394
+ right: 18,
395
+ bottom: 18,
396
+ alignItems: "center",
397
+ justifyContent: "center"
398
+ },
399
+ el.dropZone,
400
+ { borderRadius: 18 }
401
+ ], children: [
402
+ /* @__PURE__ */ jsx(View, { style: {
403
+ width: 60,
404
+ height: 60,
405
+ borderRadius: 30,
406
+ backgroundColor: "rgba(32,170,176,0.16)",
407
+ alignItems: "center",
408
+ justifyContent: "center"
409
+ }, children: /* @__PURE__ */ jsx(Icon, { name: "image", size: 26, color: CROP_ACCENT_SOFT }) }),
410
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 16, fontWeight: "800", color: "#FFFFFF", textAlign: "center" }, children: text.dropZone }),
411
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 12.5, color: "rgba(255,255,255,0.6)", textAlign: "center" }, children: "JPG, PNG or WebP" }),
412
+ error && /* @__PURE__ */ jsx(Text, { style: {
413
+ fontSize: 12.5,
414
+ fontWeight: "600",
415
+ color: CROP_DANGER,
416
+ textAlign: "center"
417
+ }, children: error.message })
418
+ ] }),
419
+ ready && /* @__PURE__ */ jsxs(Fragment, { children: [
420
+ /* @__PURE__ */ jsx(
421
+ View,
422
+ {
423
+ pointerEvents: "none",
424
+ style: [
423
425
  {
424
426
  position: "absolute",
425
- top: 0,
426
- bottom: 0,
427
- width: 1,
428
- left: crop.width * n / 3
429
- },
430
- el.gridLine
431
- ] }),
432
- /* @__PURE__ */ jsx(View, { style: [
427
+ left: crop.x - SCRIM,
428
+ top: crop.y - SCRIM,
429
+ width: crop.width + SCRIM * 2,
430
+ height: crop.height + SCRIM * 2,
431
+ borderWidth: SCRIM,
432
+ borderColor: el.shade?.backgroundColor ?? "rgba(0,0,0,0.6)",
433
+ borderRadius: windowRadius + SCRIM
434
+ }
435
+ ]
436
+ }
437
+ ),
438
+ /* @__PURE__ */ jsx(
439
+ View,
440
+ {
441
+ pointerEvents: "none",
442
+ style: [
433
443
  {
434
444
  position: "absolute",
435
- left: 0,
436
- right: 0,
437
- height: 1,
438
- top: crop.height * n / 3
445
+ left: crop.x,
446
+ top: crop.y,
447
+ width: crop.width,
448
+ height: crop.height,
449
+ borderStyle: "solid"
439
450
  },
440
- el.gridLine
441
- ] })
442
- ] }, n))
443
- }
444
- ),
445
- /* @__PURE__ */ jsx(
451
+ el.cropWindow,
452
+ { borderRadius: windowRadius }
453
+ ],
454
+ children: !hideGrid && [1, 2].map((n) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
455
+ /* @__PURE__ */ jsx(View, { style: [
456
+ {
457
+ position: "absolute",
458
+ top: 0,
459
+ bottom: 0,
460
+ width: 1,
461
+ left: crop.width * n / 3
462
+ },
463
+ el.gridLine
464
+ ] }),
465
+ /* @__PURE__ */ jsx(View, { style: [
466
+ {
467
+ position: "absolute",
468
+ left: 0,
469
+ right: 0,
470
+ height: 1,
471
+ top: crop.height * n / 3
472
+ },
473
+ el.gridLine
474
+ ] })
475
+ ] }, n))
476
+ }
477
+ ),
478
+ /* @__PURE__ */ jsx(
479
+ View,
480
+ {
481
+ pointerEvents: "none",
482
+ style: { position: "absolute", top: 12, left: 0, right: 0, alignItems: "center" },
483
+ children: /* @__PURE__ */ jsx(
484
+ View,
485
+ {
486
+ style: [
487
+ { alignItems: "center", justifyContent: "center" },
488
+ el.zoomChip
489
+ ],
490
+ children: /* @__PURE__ */ jsx(Text, { style: {
491
+ color: el.zoomChip?.color ?? "#FFFFFF",
492
+ fontSize: 11.5,
493
+ fontWeight: "700"
494
+ }, children: zoomLabel })
495
+ }
496
+ )
497
+ }
498
+ )
499
+ ] }),
500
+ (loading || busy) && /* @__PURE__ */ jsxs(
446
501
  View,
447
502
  {
448
503
  pointerEvents: "none",
449
- style: { position: "absolute", top: 12, left: 0, right: 0, alignItems: "center" },
450
- children: /* @__PURE__ */ jsx(
451
- View,
452
- {
453
- style: [
454
- { alignItems: "center", justifyContent: "center" },
455
- el.zoomChip
456
- ],
457
- children: /* @__PURE__ */ jsx(Text, { style: {
458
- color: el.zoomChip?.color ?? "#FFFFFF",
459
- fontSize: 11.5,
460
- fontWeight: "700"
461
- }, children: zoomLabel })
462
- }
463
- )
504
+ style: {
505
+ position: "absolute",
506
+ left: 0,
507
+ top: 0,
508
+ right: 0,
509
+ bottom: 0,
510
+ backgroundColor: "rgba(6,14,16,0.74)",
511
+ alignItems: "center",
512
+ justifyContent: "center",
513
+ gap: 14
514
+ },
515
+ children: [
516
+ /* @__PURE__ */ jsx(
517
+ ActivityIndicator,
518
+ {
519
+ size: "large",
520
+ color: el.progressFill?.backgroundColor ?? CROP_ACCENT
521
+ }
522
+ ),
523
+ busy && /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, fontWeight: "700", color: "#FFFFFF" }, children: text.processing })
524
+ ]
464
525
  }
465
526
  )
466
- ] }),
467
- (loading || busy) && /* @__PURE__ */ jsxs(
468
- View,
469
- {
470
- pointerEvents: "none",
471
- style: {
472
- position: "absolute",
473
- left: 0,
474
- top: 0,
475
- right: 0,
476
- bottom: 0,
477
- backgroundColor: "rgba(6,14,16,0.74)",
478
- alignItems: "center",
479
- justifyContent: "center",
480
- gap: 14
481
- },
482
- children: [
483
- /* @__PURE__ */ jsx(
484
- ActivityIndicator,
485
- {
486
- size: "large",
487
- color: el.progressFill?.backgroundColor ?? CROP_ACCENT
488
- }
489
- ),
490
- busy && /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, fontWeight: "700", color: "#FFFFFF" }, children: text.processing })
491
- ]
492
- }
493
- )
494
- ]
495
- }
496
- ) }),
497
- /* @__PURE__ */ jsxs(
498
- View,
499
- {
500
- pointerEvents: ready ? "auto" : "none",
501
- style: {
502
- paddingTop: 16,
503
- paddingLeft: 18,
504
- paddingRight: 18,
505
- gap: 16,
506
- opacity: ready ? 1 : 0.4
507
- },
508
- children: [
509
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [
510
- !hidePreview && ready && image && oriented && /* The preview is the same image, clipped and offset so the
511
- crop fills the frame — no pixel work, and therefore
512
- nothing that has to wait for an encoder. */
513
- /* @__PURE__ */ jsx(
514
- View,
515
- {
516
- style: [
517
- { width: previewWidth, height: previewHeight, overflow: "hidden" },
518
- el.previewFrame,
519
- isCircle ? { borderRadius: previewWidth / 2 } : null,
520
- /* ⚠️ A 2px RING, NOT THE CARD'S 4. The slot
521
- carries the desktop preview's ring; at 60pt
522
- a 4pt ring eats the thumbnail. */
523
- { borderWidth: 2 },
524
- /* The mockup's float, in the native spelling —
525
- `boxShadow` is the web file's half of this. */
526
- {
527
- shadowColor: "#000000",
528
- shadowOpacity: 0.14,
529
- shadowRadius: 14,
530
- shadowOffset: { width: 0, height: 4 },
531
- elevation: 4
532
- }
533
- ],
534
- children: /* @__PURE__ */ jsx(
535
- Image,
536
- {
537
- source: { uri: image.uri },
538
- resizeMode: "stretch",
539
- style: {
540
- position: "absolute",
541
- width: oriented.width * transform.scale * (previewWidth / crop.width),
542
- height: oriented.height * transform.scale * (previewWidth / crop.width),
543
- left: (transform.x - crop.x) * (previewWidth / crop.width),
544
- top: (transform.y - crop.y) * (previewWidth / crop.width),
545
- transform: [{ rotate: `${rotation}deg` }]
527
+ ]
528
+ }
529
+ ) }),
530
+ /* @__PURE__ */ jsxs(
531
+ View,
532
+ {
533
+ pointerEvents: ready ? "auto" : "none",
534
+ style: {
535
+ paddingTop: 16,
536
+ paddingLeft: 18,
537
+ paddingRight: 18,
538
+ gap: 16,
539
+ opacity: ready ? 1 : 0.4
540
+ },
541
+ children: [
542
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [
543
+ !hidePreview && ready && image && oriented && /* The preview is the same image, clipped and offset so the
544
+ crop fills the frame — no pixel work, and therefore
545
+ nothing that has to wait for an encoder. */
546
+ /* @__PURE__ */ jsx(
547
+ View,
548
+ {
549
+ style: [
550
+ { width: previewWidth, height: previewHeight, overflow: "hidden" },
551
+ el.previewFrame,
552
+ isCircle ? { borderRadius: previewWidth / 2 } : null,
553
+ /* ⚠️ A 2px RING, NOT THE CARD'S 4. The slot
554
+ carries the desktop preview's ring; at 60pt
555
+ a 4pt ring eats the thumbnail. */
556
+ { borderWidth: 2 },
557
+ /* The mockup's float, in the native spelling —
558
+ `boxShadow` is the web file's half of this. */
559
+ {
560
+ shadowColor: "#000000",
561
+ shadowOpacity: 0.14,
562
+ shadowRadius: 14,
563
+ shadowOffset: { width: 0, height: 4 },
564
+ elevation: 4
546
565
  }
547
- }
548
- )
549
- }
550
- ),
551
- /* @__PURE__ */ jsxs(View, { style: { flex: 1, alignItems: "flex-end" }, children: [
552
- /* @__PURE__ */ jsx(Text, { style: { fontSize: 17, fontWeight: "800", color: "#FFFFFF" }, children: zoomLabel }),
553
- /* @__PURE__ */ jsx(Text, { style: { marginTop: 2, fontSize: 11.5, color: "rgba(255,255,255,0.6)" }, children: `Under ${budgetKb} KB` })
566
+ ],
567
+ children: /* @__PURE__ */ jsx(
568
+ Image,
569
+ {
570
+ source: { uri: image.uri },
571
+ resizeMode: "stretch",
572
+ style: {
573
+ position: "absolute",
574
+ width: oriented.width * transform.scale * (previewWidth / crop.width),
575
+ height: oriented.height * transform.scale * (previewWidth / crop.width),
576
+ left: (transform.x - crop.x) * (previewWidth / crop.width),
577
+ top: (transform.y - crop.y) * (previewWidth / crop.width),
578
+ transform: [{ rotate: `${rotation}deg` }]
579
+ }
580
+ }
581
+ )
582
+ }
583
+ ),
584
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, alignItems: "flex-end" }, children: [
585
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 17, fontWeight: "800", color: "#FFFFFF" }, children: zoomLabel }),
586
+ /* @__PURE__ */ jsx(Text, { style: { marginTop: 2, fontSize: 11.5, color: "rgba(255,255,255,0.6)" }, children: `Under ${budgetKb} KB` })
587
+ ] })
588
+ ] }),
589
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 10 }, children: [
590
+ stageTool("rotate-cw", text.rotate, () => dispatch({ type: "rotate" })),
591
+ /* @__PURE__ */ jsx(
592
+ Slider,
593
+ {
594
+ style: { flex: 1 },
595
+ minimumValue: 0,
596
+ maximumValue: 1,
597
+ value: zoomFraction(state),
598
+ disabled: !ready,
599
+ minimumTrackTintColor: el.thumb?.backgroundColor,
600
+ maximumTrackTintColor: el.track?.backgroundColor,
601
+ thumbTintColor: el.thumb?.backgroundColor,
602
+ onValueChange: (value) => dispatch({ type: "setZoom", value })
603
+ }
604
+ ),
605
+ stageTool("rotate-ccw", text.reset, () => dispatch({ type: "reset" }))
554
606
  ] })
555
- ] }),
556
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 10 }, children: [
557
- stageTool("rotate-cw", text.rotate, () => dispatch({ type: "rotate" })),
558
- /* @__PURE__ */ jsx(
559
- Slider,
560
- {
561
- style: { flex: 1 },
562
- minimumValue: 0,
563
- maximumValue: 1,
564
- value: zoomFraction(state),
565
- disabled: !ready,
566
- minimumTrackTintColor: el.thumb?.backgroundColor,
567
- maximumTrackTintColor: el.track?.backgroundColor,
568
- thumbTintColor: el.thumb?.backgroundColor,
569
- onValueChange: (value) => dispatch({ type: "setZoom", value })
570
- }
571
- ),
572
- stageTool("rotate-ccw", text.reset, () => dispatch({ type: "reset" }))
573
- ] })
574
- ]
575
- }
576
- )
577
- ] }),
578
- /* @__PURE__ */ jsxs(View, { style: [
579
- { flexDirection: "row", alignItems: "center", marginTop: "auto" },
580
- el.footer,
581
- {
582
- paddingLeft: 18,
583
- paddingRight: 18,
584
- paddingTop: 12,
585
- paddingBottom: 28,
586
- borderTopWidth: 1,
587
- borderTopColor: "rgba(255,255,255,0.08)",
588
- gap: 12
589
- }
590
- ], children: [
591
- /* @__PURE__ */ jsx(
592
- Pressable,
607
+ ]
608
+ }
609
+ )
610
+ ] }),
611
+ !hideFooter && /* @__PURE__ */ jsxs(View, { style: [
612
+ { flexDirection: "row", alignItems: "center", marginTop: "auto" },
613
+ el.footer,
593
614
  {
594
- onPress: () => onCancel?.(),
595
- accessibilityRole: "button",
596
- accessibilityLabel: text.cancel,
597
- style: {
598
- flex: 1,
599
- height: 52,
600
- borderRadius: 26,
601
- backgroundColor: "rgba(255,255,255,0.1)",
602
- alignItems: "center",
603
- justifyContent: "center"
604
- },
605
- children: /* @__PURE__ */ jsx(Text, { style: { fontSize: 15, fontWeight: "700", color: "#FFFFFF" }, children: text.cancel })
615
+ paddingLeft: 18,
616
+ paddingRight: 18,
617
+ paddingTop: 12,
618
+ paddingBottom: 28,
619
+ borderTopWidth: 1,
620
+ borderTopColor: "rgba(255,255,255,0.08)",
621
+ gap: 12
606
622
  }
607
- ),
608
- /* @__PURE__ */ jsx(
609
- Pressable,
610
- {
611
- onPress: () => void save(),
612
- disabled: !canSave,
613
- accessibilityRole: "button",
614
- accessibilityLabel: text.save,
615
- accessibilityState: { disabled: !canSave },
616
- style: {
617
- flex: 1.4,
618
- height: 52,
619
- borderRadius: 26,
620
- backgroundColor: canSave ? CROP_ACCENT : "rgba(32,170,176,0.35)",
621
- alignItems: "center",
622
- justifyContent: "center"
623
- },
624
- children: /* @__PURE__ */ jsx(Text, { style: {
625
- fontSize: 15,
626
- fontWeight: "800",
627
- color: canSave ? "#FFFFFF" : "rgba(255,255,255,0.55)"
628
- }, children: busy ? text.processing : text.save })
629
- }
630
- )
631
- ] })
632
- ]
633
- }
634
- );
635
- }
623
+ ], children: [
624
+ /* @__PURE__ */ jsx(
625
+ Pressable,
626
+ {
627
+ onPress: () => onCancel?.(),
628
+ accessibilityRole: "button",
629
+ accessibilityLabel: text.cancel,
630
+ style: {
631
+ flex: 1,
632
+ height: 52,
633
+ borderRadius: 26,
634
+ backgroundColor: "rgba(255,255,255,0.1)",
635
+ alignItems: "center",
636
+ justifyContent: "center"
637
+ },
638
+ children: /* @__PURE__ */ jsx(Text, { style: { fontSize: 15, fontWeight: "700", color: "#FFFFFF" }, children: text.cancel })
639
+ }
640
+ ),
641
+ /* @__PURE__ */ jsx(
642
+ Pressable,
643
+ {
644
+ onPress: () => void save(),
645
+ disabled: !canSave,
646
+ accessibilityRole: "button",
647
+ accessibilityLabel: text.save,
648
+ accessibilityState: { disabled: !canSave },
649
+ style: {
650
+ flex: 1.4,
651
+ height: 52,
652
+ borderRadius: 26,
653
+ backgroundColor: canSave ? CROP_ACCENT : "rgba(32,170,176,0.35)",
654
+ alignItems: "center",
655
+ justifyContent: "center"
656
+ },
657
+ children: /* @__PURE__ */ jsx(Text, { style: {
658
+ fontSize: 15,
659
+ fontWeight: "800",
660
+ color: canSave ? "#FFFFFF" : "rgba(255,255,255,0.55)"
661
+ }, children: busy ? text.processing : text.save })
662
+ }
663
+ )
664
+ ] })
665
+ ]
666
+ }
667
+ );
668
+ }
669
+ );
636
670
 
637
671
  export { ImageCropper };
638
672
  //# sourceMappingURL=index.js.map