@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,638 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import React, { useState, useRef, useCallback, useMemo, useReducer, useEffect } from 'react';
3
+ import { PanResponder, View, Text, Image, ActivityIndicator, Pressable } from 'react-native';
4
+ import Slider from '@react-native-community/slider';
5
+ import { Icon } from '../../primitives/Icon/index.js';
6
+ import '../../contexts/DialogContext.js';
7
+ import '../../contexts/PermissionsContext.js';
8
+ import '../../contexts/BreadcrumbContext.js';
9
+ import useComponentDefaults from '../../hooks/useComponentDefaults.js';
10
+ import useResolvedElementStyles from '../../hooks/useResolvedElementStyles.js';
11
+ import { cropperReducer, initialCropperState, scaleRange, zoomFraction } from './cropperState.js';
12
+ import { aspectOf, cropToSourceRect, orientedSize } from './geometry.js';
13
+ import { isUsablePinchReading, pinchScale } from './pinchZoom.js';
14
+ import { DEFAULT_ASPECT, DEFAULT_MAX_ZOOM, SQUARE_CROP_FACTOR, STAGE_INSET_NATIVE, DEFAULT_FORMAT, DEFAULT_MAX_BYTES, STAGE_ASPECT_NATIVE, CROP_ACCENT_SOFT, DEFAULT_LABELS, CROP_DANGER, CROP_ACCENT } from './constants.js';
15
+ import { useImageSource } from './useImageSource.js';
16
+ import { encodeCropNative } from './encode.js';
17
+ import { hasCropEncoder } from './encoderRegistry.js';
18
+
19
+ const gestureHandler = (() => {
20
+ try {
21
+ return require("react-native-gesture-handler");
22
+ } catch {
23
+ return null;
24
+ }
25
+ })();
26
+ const StageFrame = ({ gesture, children }) => {
27
+ if (!gestureHandler || !gesture) return /* @__PURE__ */ jsx(Fragment, { children });
28
+ const { GestureDetector, GestureHandlerRootView } = gestureHandler;
29
+ return /* @__PURE__ */ jsx(GestureHandlerRootView, { style: { width: "100%" }, children: /* @__PURE__ */ jsx(GestureDetector, { gesture, children }) });
30
+ };
31
+ const touchDistance = (event) => {
32
+ const touches = event.nativeEvent.touches;
33
+ if (touches.length < 2) return 0;
34
+ const [a, b] = touches;
35
+ return Math.hypot((b.pageX ?? 0) - (a.pageX ?? 0), (b.pageY ?? 0) - (a.pageY ?? 0));
36
+ };
37
+ const touchMidpoint = (event, origin) => {
38
+ const touches = event.nativeEvent.touches;
39
+ if (touches.length < 2) return { x: 0, y: 0 };
40
+ const [a, b] = touches;
41
+ return {
42
+ x: ((a.pageX ?? 0) + (b.pageX ?? 0)) / 2 - origin.x,
43
+ y: ((a.pageY ?? 0) + (b.pageY ?? 0)) / 2 - origin.y
44
+ };
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 };
143
+ return;
144
+ }
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();
185
+ },
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 };
198
+ return;
199
+ }
200
+ applyPan(gesture.dx, gesture.dy);
201
+ },
202
+ onPanResponderRelease: () => {
203
+ setInteracting(false);
204
+ beginGesture();
205
+ },
206
+ onPanResponderTerminate: () => {
207
+ setInteracting(false);
208
+ beginGesture();
209
+ }
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 {
219
+ const rect = cropToSourceRect(state.crop, state.transform, config.source, state.rotation);
220
+ const format = output?.format ?? DEFAULT_FORMAT;
221
+ const result = await encodeCropNative(
222
+ image.uri,
223
+ rect,
224
+ config.source,
225
+ state.rotation,
226
+ {
227
+ width: output?.maxWidth,
228
+ height: output?.maxHeight,
229
+ bytes: output?.maxBytes ?? DEFAULT_MAX_BYTES
230
+ },
231
+ format
232
+ );
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."
245
+ });
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,
332
+ {
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,
389
+ {
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
+ ]
403
+ }
404
+ ),
405
+ /* @__PURE__ */ jsx(
406
+ View,
407
+ {
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: [
423
+ {
424
+ 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: [
433
+ {
434
+ position: "absolute",
435
+ left: 0,
436
+ right: 0,
437
+ height: 1,
438
+ top: crop.height * n / 3
439
+ },
440
+ el.gridLine
441
+ ] })
442
+ ] }, n))
443
+ }
444
+ ),
445
+ /* @__PURE__ */ jsx(
446
+ View,
447
+ {
448
+ 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
+ )
464
+ }
465
+ )
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` }]
546
+ }
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` })
554
+ ] })
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,
593
+ {
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 })
606
+ }
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
+ }
636
+
637
+ export { ImageCropper };
638
+ //# sourceMappingURL=index.js.map