@lmb-it/kitsconcerto-native 0.1.288 → 0.1.290

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