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