@lmb-it/kitsconcerto-native 0.1.285 → 0.1.287

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/apps/mobile/src/ui/accordion/index.js +2 -2
  2. package/components/ImageCropper/compression.cjs +63 -0
  3. package/components/ImageCropper/compression.cjs.map +1 -0
  4. package/components/ImageCropper/compression.js +58 -0
  5. package/components/ImageCropper/compression.js.map +1 -0
  6. package/components/ImageCropper/constants.cjs +47 -0
  7. package/components/ImageCropper/constants.cjs.map +1 -0
  8. package/components/ImageCropper/constants.js +29 -0
  9. package/components/ImageCropper/constants.js.map +1 -0
  10. package/components/ImageCropper/cropperState.cjs +129 -0
  11. package/components/ImageCropper/cropperState.cjs.map +1 -0
  12. package/components/ImageCropper/cropperState.js +124 -0
  13. package/components/ImageCropper/cropperState.js.map +1 -0
  14. package/components/ImageCropper/encode.cjs +45 -0
  15. package/components/ImageCropper/encode.cjs.map +1 -0
  16. package/components/ImageCropper/encode.js +42 -0
  17. package/components/ImageCropper/encode.js.map +1 -0
  18. package/components/ImageCropper/encoderRegistry.cjs +13 -0
  19. package/components/ImageCropper/encoderRegistry.cjs.map +1 -0
  20. package/components/ImageCropper/encoderRegistry.js +9 -0
  21. package/components/ImageCropper/encoderRegistry.js.map +1 -0
  22. package/components/ImageCropper/encoders/rnImageEncoder.cjs +54 -0
  23. package/components/ImageCropper/encoders/rnImageEncoder.cjs.map +1 -0
  24. package/components/ImageCropper/encoders/rnImageEncoder.js +51 -0
  25. package/components/ImageCropper/encoders/rnImageEncoder.js.map +1 -0
  26. package/components/ImageCropper/geometry.cjs +208 -0
  27. package/components/ImageCropper/geometry.cjs.map +1 -0
  28. package/components/ImageCropper/geometry.js +186 -0
  29. package/components/ImageCropper/geometry.js.map +1 -0
  30. package/components/ImageCropper/index.cjs +640 -0
  31. package/components/ImageCropper/index.cjs.map +1 -0
  32. package/components/ImageCropper/index.js +638 -0
  33. package/components/ImageCropper/index.js.map +1 -0
  34. package/components/ImageCropper/pinchZoom.cjs +8 -0
  35. package/components/ImageCropper/pinchZoom.cjs.map +1 -0
  36. package/components/ImageCropper/pinchZoom.js +5 -0
  37. package/components/ImageCropper/pinchZoom.js.map +1 -0
  38. package/components/ImageCropper/useImageSource.cjs +101 -0
  39. package/components/ImageCropper/useImageSource.cjs.map +1 -0
  40. package/components/ImageCropper/useImageSource.js +97 -0
  41. package/components/ImageCropper/useImageSource.js.map +1 -0
  42. package/hooks/useComponentDefaults.cjs +12 -0
  43. package/hooks/useComponentDefaults.cjs.map +1 -1
  44. package/hooks/useComponentDefaults.js +12 -0
  45. package/hooks/useComponentDefaults.js.map +1 -1
  46. package/index.cjs +120 -115
  47. package/index.cjs.map +1 -1
  48. package/index.d.ts +394 -3
  49. package/index.js +2 -0
  50. package/index.js.map +1 -1
  51. package/package.json +13 -1
  52. package/packages/types/src/Theme/defaultTheme.cjs +189 -0
  53. package/packages/types/src/Theme/defaultTheme.cjs.map +1 -1
  54. package/packages/types/src/Theme/defaultTheme.js +189 -0
  55. package/packages/types/src/Theme/defaultTheme.js.map +1 -1
@@ -0,0 +1,640 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+ var reactNative = require('react-native');
6
+ var Slider = require('@react-native-community/slider');
7
+ var index_native = require('../../primitives/Icon/index.cjs');
8
+ require('../../contexts/DialogContext.cjs');
9
+ require('../../contexts/PermissionsContext.cjs');
10
+ require('../../contexts/BreadcrumbContext.cjs');
11
+ var useComponentDefaults = require('../../hooks/useComponentDefaults.cjs');
12
+ var useResolvedElementStyles = require('../../hooks/useResolvedElementStyles.cjs');
13
+ var cropperState = require('./cropperState.cjs');
14
+ var geometry = require('./geometry.cjs');
15
+ var pinchZoom = require('./pinchZoom.cjs');
16
+ var constants = require('./constants.cjs');
17
+ var useImageSource_native = require('./useImageSource.cjs');
18
+ var encode_native = require('./encode.cjs');
19
+ var encoderRegistry = require('./encoderRegistry.cjs');
20
+
21
+ const gestureHandler = (() => {
22
+ try {
23
+ return require("react-native-gesture-handler");
24
+ } catch {
25
+ return null;
26
+ }
27
+ })();
28
+ const StageFrame = ({ gesture, children }) => {
29
+ if (!gestureHandler || !gesture) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
30
+ const { GestureDetector, GestureHandlerRootView } = gestureHandler;
31
+ return /* @__PURE__ */ jsxRuntime.jsx(GestureHandlerRootView, { style: { width: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx(GestureDetector, { gesture, children }) });
32
+ };
33
+ const touchDistance = (event) => {
34
+ const touches = event.nativeEvent.touches;
35
+ if (touches.length < 2) return 0;
36
+ const [a, b] = touches;
37
+ return Math.hypot((b.pageX ?? 0) - (a.pageX ?? 0), (b.pageY ?? 0) - (a.pageY ?? 0));
38
+ };
39
+ const touchMidpoint = (event, origin) => {
40
+ const touches = event.nativeEvent.touches;
41
+ if (touches.length < 2) return { x: 0, y: 0 };
42
+ const [a, b] = touches;
43
+ return {
44
+ x: ((a.pageX ?? 0) + (b.pageX ?? 0)) / 2 - origin.x,
45
+ y: ((a.pageY ?? 0) + (b.pageY ?? 0)) / 2 - origin.y
46
+ };
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 };
145
+ return;
146
+ }
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();
187
+ },
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 };
200
+ return;
201
+ }
202
+ applyPan(gesture.dx, gesture.dy);
203
+ },
204
+ onPanResponderRelease: () => {
205
+ setInteracting(false);
206
+ beginGesture();
207
+ },
208
+ onPanResponderTerminate: () => {
209
+ setInteracting(false);
210
+ beginGesture();
211
+ }
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 {
221
+ const rect = geometry.cropToSourceRect(state.crop, state.transform, config.source, state.rotation);
222
+ const format = output?.format ?? constants.DEFAULT_FORMAT;
223
+ const result = await encode_native.encodeCropNative(
224
+ image.uri,
225
+ rect,
226
+ config.source,
227
+ state.rotation,
228
+ {
229
+ width: output?.maxWidth,
230
+ height: output?.maxHeight,
231
+ bytes: output?.maxBytes ?? constants.DEFAULT_MAX_BYTES
232
+ },
233
+ format
234
+ );
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."
247
+ });
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,
334
+ {
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,
391
+ {
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
+ ]
405
+ }
406
+ ),
407
+ /* @__PURE__ */ jsxRuntime.jsx(
408
+ reactNative.View,
409
+ {
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: [
425
+ {
426
+ 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: [
435
+ {
436
+ position: "absolute",
437
+ left: 0,
438
+ right: 0,
439
+ height: 1,
440
+ top: crop.height * n / 3
441
+ },
442
+ el.gridLine
443
+ ] })
444
+ ] }, n))
445
+ }
446
+ ),
447
+ /* @__PURE__ */ jsxRuntime.jsx(
448
+ reactNative.View,
449
+ {
450
+ 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
+ )
466
+ }
467
+ )
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` }]
548
+ }
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` })
556
+ ] })
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,
595
+ {
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 })
608
+ }
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
+ }
638
+
639
+ exports.ImageCropper = ImageCropper;
640
+ //# sourceMappingURL=index.cjs.map