@strapi/upload 5.50.0 → 5.50.1

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 (140) hide show
  1. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.js +1 -1
  2. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.js.map +1 -1
  3. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.mjs +1 -1
  4. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.mjs.map +1 -1
  5. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromComputerForm.js.map +1 -1
  6. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromComputerForm.mjs.map +1 -1
  7. package/dist/admin/future/pages/Assets/AssetsPage.js +125 -122
  8. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  9. package/dist/admin/future/pages/Assets/AssetsPage.mjs +125 -122
  10. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  11. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +700 -0
  12. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -0
  13. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +678 -0
  14. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -0
  15. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +132 -15
  16. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
  17. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +135 -18
  18. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
  19. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.js +23 -1
  20. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.js.map +1 -1
  21. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.mjs +23 -1
  22. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetPreview.mjs.map +1 -1
  23. package/dist/admin/future/pages/Assets/components/AssetDetails/useCropImg.js +211 -0
  24. package/dist/admin/future/pages/Assets/components/AssetDetails/useCropImg.js.map +1 -0
  25. package/dist/admin/future/pages/Assets/components/AssetDetails/useCropImg.mjs +188 -0
  26. package/dist/admin/future/pages/Assets/components/AssetDetails/useCropImg.mjs.map +1 -0
  27. package/dist/admin/future/pages/Assets/components/AssetsGrid.js +48 -3
  28. package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
  29. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +49 -4
  30. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
  31. package/dist/admin/future/pages/Assets/components/AssetsTable.js +43 -4
  32. package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
  33. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +44 -5
  34. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
  35. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +195 -0
  36. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -0
  37. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +192 -0
  38. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -0
  39. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js +37 -0
  40. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js.map +1 -0
  41. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs +35 -0
  42. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs.map +1 -0
  43. package/dist/admin/future/pages/Assets/components/Dnd/dndIds.js +18 -0
  44. package/dist/admin/future/pages/Assets/components/Dnd/dndIds.js.map +1 -0
  45. package/dist/admin/future/pages/Assets/components/Dnd/dndIds.mjs +13 -0
  46. package/dist/admin/future/pages/Assets/components/Dnd/dndIds.mjs.map +1 -0
  47. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.js +97 -0
  48. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.js.map +1 -0
  49. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.mjs +94 -0
  50. package/dist/admin/future/pages/Assets/components/Dnd/useAssetDnd.mjs.map +1 -0
  51. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.js +13 -4
  52. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.js.map +1 -1
  53. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.mjs +13 -4
  54. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.mjs.map +1 -1
  55. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js +6 -3
  56. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js.map +1 -1
  57. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs +6 -3
  58. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs.map +1 -1
  59. package/dist/admin/future/services/api.js +37 -1
  60. package/dist/admin/future/services/api.js.map +1 -1
  61. package/dist/admin/future/services/api.mjs +37 -2
  62. package/dist/admin/future/services/api.mjs.map +1 -1
  63. package/dist/admin/future/services/folders.js +36 -5
  64. package/dist/admin/future/services/folders.js.map +1 -1
  65. package/dist/admin/future/services/folders.mjs +36 -6
  66. package/dist/admin/future/services/folders.mjs.map +1 -1
  67. package/dist/admin/future/utils/buildBulkMovePayload.js +20 -0
  68. package/dist/admin/future/utils/buildBulkMovePayload.js.map +1 -0
  69. package/dist/admin/future/utils/buildBulkMovePayload.mjs +18 -0
  70. package/dist/admin/future/utils/buildBulkMovePayload.mjs.map +1 -0
  71. package/dist/admin/future/utils/canDropItemOnFolder.js +64 -0
  72. package/dist/admin/future/utils/canDropItemOnFolder.js.map +1 -0
  73. package/dist/admin/future/utils/canDropItemOnFolder.mjs +61 -0
  74. package/dist/admin/future/utils/canDropItemOnFolder.mjs.map +1 -0
  75. package/dist/admin/future/utils/getBulkMoveErrorMessage.js +23 -0
  76. package/dist/admin/future/utils/getBulkMoveErrorMessage.js.map +1 -0
  77. package/dist/admin/future/utils/getBulkMoveErrorMessage.mjs +21 -0
  78. package/dist/admin/future/utils/getBulkMoveErrorMessage.mjs.map +1 -0
  79. package/dist/admin/hooks/useAIMetadataJob.js.map +1 -1
  80. package/dist/admin/hooks/useAIMetadataJob.mjs.map +1 -1
  81. package/dist/admin/hooks/useAssets.js.map +1 -1
  82. package/dist/admin/hooks/useAssets.mjs.map +1 -1
  83. package/dist/admin/hooks/useBulkEdit.js.map +1 -1
  84. package/dist/admin/hooks/useBulkEdit.mjs.map +1 -1
  85. package/dist/admin/hooks/useBulkMove.js.map +1 -1
  86. package/dist/admin/hooks/useBulkMove.mjs.map +1 -1
  87. package/dist/admin/hooks/useBulkRemove.js.map +1 -1
  88. package/dist/admin/hooks/useBulkRemove.mjs.map +1 -1
  89. package/dist/admin/hooks/useEditAsset.js.map +1 -1
  90. package/dist/admin/hooks/useEditAsset.mjs.map +1 -1
  91. package/dist/admin/hooks/useEditFolder.js +4 -3
  92. package/dist/admin/hooks/useEditFolder.js.map +1 -1
  93. package/dist/admin/hooks/useEditFolder.mjs +4 -3
  94. package/dist/admin/hooks/useEditFolder.mjs.map +1 -1
  95. package/dist/admin/hooks/useFolder.js.map +1 -1
  96. package/dist/admin/hooks/useFolder.mjs.map +1 -1
  97. package/dist/admin/hooks/useUpload.js.map +1 -1
  98. package/dist/admin/hooks/useUpload.mjs.map +1 -1
  99. package/dist/admin/pages/SettingsPage/SettingsPage.js +1 -1
  100. package/dist/admin/pages/SettingsPage/SettingsPage.js.map +1 -1
  101. package/dist/admin/pages/SettingsPage/SettingsPage.mjs +1 -1
  102. package/dist/admin/pages/SettingsPage/SettingsPage.mjs.map +1 -1
  103. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetCropEditor.d.ts +10 -0
  104. package/dist/admin/src/future/pages/Assets/components/AssetDetails/useCropImg.d.ts +48 -0
  105. package/dist/admin/src/future/pages/Assets/components/Dnd/AssetsDndProvider.d.ts +12 -0
  106. package/dist/admin/src/future/pages/Assets/components/Dnd/DragOverlayChip.d.ts +6 -0
  107. package/dist/admin/src/future/pages/Assets/components/Dnd/dndIds.d.ts +4 -0
  108. package/dist/admin/src/future/pages/Assets/components/Dnd/useAssetDnd.d.ts +51 -0
  109. package/dist/admin/src/future/services/api.d.ts +24 -6
  110. package/dist/admin/src/future/services/assets.d.ts +1 -1
  111. package/dist/admin/src/future/services/folders.d.ts +12 -2
  112. package/dist/admin/src/future/services/settings.d.ts +1 -1
  113. package/dist/admin/src/future/types/dnd.d.ts +22 -0
  114. package/dist/admin/src/future/utils/buildBulkMovePayload.d.ts +2 -0
  115. package/dist/admin/src/future/utils/canDropItemOnFolder.d.ts +9 -0
  116. package/dist/admin/src/future/utils/getBulkMoveErrorMessage.d.ts +1 -0
  117. package/dist/admin/src/hooks/useAIMetadataJob.d.ts +1 -1
  118. package/dist/admin/src/hooks/useBulkEdit.d.ts +14 -14
  119. package/dist/admin/src/hooks/useBulkMove.d.ts +146 -15
  120. package/dist/admin/src/hooks/useBulkRemove.d.ts +186 -15
  121. package/dist/admin/src/hooks/useEditFolder.d.ts +3 -2
  122. package/dist/admin/translations/en.json.js +28 -0
  123. package/dist/admin/translations/en.json.js.map +1 -1
  124. package/dist/admin/translations/en.json.mjs +28 -0
  125. package/dist/admin/translations/en.json.mjs.map +1 -1
  126. package/dist/admin/translations/pl.json.js +54 -29
  127. package/dist/admin/translations/pl.json.js.map +1 -1
  128. package/dist/admin/translations/pl.json.mjs +54 -29
  129. package/dist/admin/translations/pl.json.mjs.map +1 -1
  130. package/dist/server/controllers/admin-upload.js +3 -2
  131. package/dist/server/controllers/admin-upload.js.map +1 -1
  132. package/dist/server/controllers/admin-upload.mjs +3 -2
  133. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  134. package/dist/server/controllers/content-api.js +6 -5
  135. package/dist/server/controllers/content-api.js.map +1 -1
  136. package/dist/server/controllers/content-api.mjs +6 -5
  137. package/dist/server/controllers/content-api.mjs.map +1 -1
  138. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  139. package/dist/server/src/controllers/content-api.d.ts.map +1 -1
  140. package/package.json +8 -7
@@ -0,0 +1,700 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+ var strapiAdmin = require('@strapi/admin/strapi-admin');
6
+ var designSystem = require('@strapi/design-system');
7
+ var icons = require('@strapi/icons');
8
+ var reactIntl = require('react-intl');
9
+ var styledComponents = require('styled-components');
10
+ var files = require('../../../../utils/files.js');
11
+ var translations = require('../../../../utils/translations.js');
12
+ var useCropImg = require('./useCropImg.js');
13
+
14
+ function _interopNamespace(e) {
15
+ if (e && e.__esModule) return e;
16
+ var n = Object.create(null);
17
+ if (e) {
18
+ Object.keys(e).forEach(function (k) {
19
+ if (k !== 'default') {
20
+ var d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: function () { return e[k]; }
24
+ });
25
+ }
26
+ });
27
+ }
28
+ n.default = e;
29
+ return Object.freeze(n);
30
+ }
31
+
32
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
33
+
34
+ const FOCAL_DIAMETER_REM = 5.6;
35
+ const HANDLE_PX = 12;
36
+ /* -------------------------------------------------------------------------------------------------
37
+ * Styled
38
+ * -----------------------------------------------------------------------------------------------*/ // Full-viewport takeover with a small inset so the editor reads as a card.
39
+ const Overlay = styledComponents.styled(designSystem.Flex)`
40
+ position: fixed;
41
+ z-index: 1200;
42
+ flex-direction: column;
43
+ top: ${({ theme })=>theme.spaces[1]};
44
+ left: ${({ theme })=>theme.spaces[1]};
45
+ right: ${({ theme })=>theme.spaces[1]};
46
+ bottom: ${({ theme })=>theme.spaces[1]};
47
+ border-radius: ${({ theme })=>theme.borderRadius};
48
+ border: 1px solid ${({ theme })=>theme.colors.neutral150};
49
+ background: ${({ theme })=>theme.colors.neutral0};
50
+ `;
51
+ const HeaderBar = styledComponents.styled(designSystem.Flex)`
52
+ width: 100%;
53
+ gap: ${({ theme })=>theme.spaces[2]};
54
+ padding: ${({ theme })=>`${theme.spaces[3]} ${theme.spaces[5]}`};
55
+ border-bottom: 1px solid ${({ theme })=>theme.colors.neutral150};
56
+ background: ${({ theme })=>theme.colors.neutral0};
57
+ `;
58
+ // Fills the remaining space between header/footer; checkerboard pattern.
59
+ const Body = styledComponents.styled(designSystem.Box)`
60
+ width: 100%;
61
+ position: relative;
62
+ flex: 1;
63
+ min-height: 0;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ overflow: hidden;
68
+ background: repeating-conic-gradient(
69
+ ${({ theme })=>theme.colors.neutral100} 0% 25%,
70
+ ${({ theme })=>theme.colors.neutral0} 0% 50%
71
+ )
72
+ 50% / 20px 20px;
73
+ `;
74
+ // Wrapper sized to the image's natural aspect ratio so the <img> inside fills
75
+ // it without letterboxing. Percentage-based positioning of the crop overlay
76
+ // then maps directly to natural-px coordinates.
77
+ const CropArea = styledComponents.styled.div`
78
+ position: relative;
79
+ max-width: 100%;
80
+ max-height: 100%;
81
+ ${({ $aspect })=>$aspect ? `aspect-ratio: ${$aspect};` : ''}
82
+
83
+ img {
84
+ display: block;
85
+ width: 100%;
86
+ height: 100%;
87
+ user-select: none;
88
+ -webkit-user-drag: none;
89
+ }
90
+ `;
91
+ // Crop selection rectangle, positioned by % of CropArea. Inverts the dim
92
+ // outside via box-shadow so the focused area shows full color.
93
+ const CropOverlay = styledComponents.styled.div`
94
+ position: absolute;
95
+ border: 1px dashed ${({ theme })=>theme.colors.primary600};
96
+ box-shadow: 0 0 0 9999px rgba(33, 33, 52, 0.5);
97
+ cursor: move;
98
+ `;
99
+ const ResizeHandle = styledComponents.styled.button`
100
+ position: absolute;
101
+ width: ${HANDLE_PX}px;
102
+ height: ${HANDLE_PX}px;
103
+ margin: -${HANDLE_PX / 2}px;
104
+ padding: 0;
105
+ border: 1px solid ${({ theme })=>theme.colors.primary600};
106
+ border-radius: 2px;
107
+ background: ${({ theme })=>theme.colors.neutral0};
108
+ cursor: ${({ $cursor })=>$cursor};
109
+ `;
110
+ const FocalPointHandle = styledComponents.styled.button`
111
+ position: absolute;
112
+ width: ${FOCAL_DIAMETER_REM}rem;
113
+ height: ${FOCAL_DIAMETER_REM}rem;
114
+ margin: ${-FOCAL_DIAMETER_REM / 2}rem 0 0 ${-FOCAL_DIAMETER_REM / 2}rem;
115
+ border-radius: 50%;
116
+ border: 1px solid ${({ theme })=>theme.colors.neutral800};
117
+ background: transparent;
118
+ cursor: grab;
119
+ padding: 0;
120
+
121
+ &::before {
122
+ content: '';
123
+ position: absolute;
124
+ top: 50%;
125
+ left: 50%;
126
+ width: 16px;
127
+ height: 16px;
128
+ border-radius: 50%;
129
+ background: rgba(0, 0, 0, 0.16);
130
+ transform: translate(-50%, -50%);
131
+ }
132
+ &::after {
133
+ content: '';
134
+ position: absolute;
135
+ top: 50%;
136
+ left: 50%;
137
+ width: 6px;
138
+ height: 6px;
139
+ border-radius: 50%;
140
+ background: ${({ theme })=>theme.colors.neutral800};
141
+ transform: translate(-50%, -50%);
142
+ }
143
+
144
+ &:active {
145
+ cursor: grabbing;
146
+ }
147
+ `;
148
+ const InfoBox = styledComponents.styled(designSystem.Box)`
149
+ position: absolute;
150
+ right: ${({ theme })=>theme.spaces[1]};
151
+ bottom: ${({ theme })=>theme.spaces[1]};
152
+ width: 100%;
153
+ max-width: 32rem;
154
+ padding: ${({ theme })=>theme.spaces[3]};
155
+ border-radius: ${({ theme })=>theme.borderRadius};
156
+ background: ${({ theme })=>theme.colorScheme === 'dark' ? theme.colors.neutral150 : theme.colors.neutral900};
157
+ z-index: 20;
158
+ `;
159
+ const FooterBar = styledComponents.styled(designSystem.Flex)`
160
+ width: 100%;
161
+ justify-content: space-between;
162
+ padding: ${({ theme })=>`${theme.spaces[3]} ${theme.spaces[5]}`};
163
+ border-top: 1px solid ${({ theme })=>theme.colors.neutral150};
164
+ background: ${({ theme })=>theme.colors.neutral0};
165
+ `;
166
+ const FieldRow = styledComponents.styled(designSystem.Field.Root)`
167
+ flex-direction: row;
168
+ align-items: center;
169
+ `;
170
+ const FieldNumberInput = styledComponents.styled(designSystem.NumberInput)`
171
+ width: 8.4rem;
172
+ `;
173
+ const LabelIcon = styledComponents.styled(designSystem.Field.Label)`
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ width: 1.6rem;
178
+ height: 1.6rem;
179
+ `;
180
+ const BackgroundPreserveRatioContainer = styledComponents.styled(designSystem.Box)`
181
+ position: absolute;
182
+ top: 50%;
183
+ left: 0;
184
+ transform: translateY(-50%);
185
+
186
+ svg {
187
+ display: block;
188
+ }
189
+ `;
190
+ const BackgroundPreserveRatio = ()=>/*#__PURE__*/ jsxRuntime.jsx(BackgroundPreserveRatioContainer, {
191
+ children: /*#__PURE__*/ jsxRuntime.jsx("svg", {
192
+ width: "17",
193
+ height: "49",
194
+ viewBox: "0 0 17 49",
195
+ fill: "none",
196
+ xmlns: "http://www.w3.org/2000/svg",
197
+ children: /*#__PURE__*/ jsxRuntime.jsx("path", {
198
+ d: "M0.5 0.5H8.5C12.9183 0.5 16.5 4.08172 16.5 8.5M0.5 48.5H8.5C12.9183 48.5 16.5 44.9183 16.5 40.5",
199
+ stroke: "#666687",
200
+ strokeLinecap: "round"
201
+ })
202
+ })
203
+ });
204
+ const AssetCropEditor = ({ asset, isBusy = false, onClose, onApply, onSaveAsCopy })=>{
205
+ const { formatMessage } = reactIntl.useIntl();
206
+ const { toggleNotification } = strapiAdmin.useNotification();
207
+ // The InfoBox is a fixed dark surface in both themes, so its text can't use a
208
+ // single neutral token (those invert with the scheme). Pick the scheme's white
209
+ // and a muted grey explicitly.
210
+ const theme = styledComponents.useTheme();
211
+ const isDark = theme.colorScheme === 'dark';
212
+ const infoTextColor = isDark ? 'neutral1000' : 'neutral0';
213
+ const infoMutedColor = isDark ? 'neutral600' : 'neutral200';
214
+ const imgRef = React__namespace.useRef(null);
215
+ const cropAreaRef = React__namespace.useRef(null);
216
+ const { init, crop, naturalSize, aspectRatio, setCropSize, setCropPosition, setAspectRatio, produceFile, width, height } = useCropImg.useCropImg();
217
+ const [aspectLocked, setAspectLocked] = React__namespace.useState(false);
218
+ // Focal point as a percentage of the crop area (matches the {x,y} contract).
219
+ const [focal, setFocal] = React__namespace.useState(asset.focalPoint ?? {
220
+ x: 50,
221
+ y: 50
222
+ });
223
+ // Append `updatedAt` as a cache-buster: a replaced asset is served at the same
224
+ // URL, so without this, reopening the editor shows the browser-cached old image
225
+ // (mirrors AssetPreview).
226
+ const rawImageUrl = files.prefixFileUrlWithBackendUrl(asset.url);
227
+ const cacheKey = asset.updatedAt ? new Date(asset.updatedAt).getTime() : undefined;
228
+ const imageUrl = cacheKey !== undefined ? `${rawImageUrl}${rawImageUrl.includes('?') ? '&' : '?'}v=${cacheKey}` : rawImageUrl;
229
+ const handleImageLoad = ()=>{
230
+ if (imgRef.current) {
231
+ init(imgRef.current);
232
+ }
233
+ };
234
+ // Map a pointer event to natural-px coordinates inside the image.
235
+ const pointerToNatural = (event)=>{
236
+ const rect = cropAreaRef.current?.getBoundingClientRect();
237
+ if (!rect || !naturalSize.width || !naturalSize.height) return null;
238
+ const ratioX = naturalSize.width / rect.width;
239
+ const ratioY = naturalSize.height / rect.height;
240
+ return {
241
+ x: (event.clientX - rect.left) * ratioX,
242
+ y: (event.clientY - rect.top) * ratioY
243
+ };
244
+ };
245
+ // Drag the entire crop rectangle.
246
+ const handleMovePointerDown = (event)=>{
247
+ event.preventDefault();
248
+ event.stopPropagation();
249
+ const start = pointerToNatural(event);
250
+ if (!start) return;
251
+ const startCrop = {
252
+ ...crop
253
+ };
254
+ const move = (e)=>{
255
+ const next = pointerToNatural(e);
256
+ if (!next) return;
257
+ setCropPosition({
258
+ x: startCrop.x + (next.x - start.x),
259
+ y: startCrop.y + (next.y - start.y)
260
+ });
261
+ };
262
+ const up = ()=>{
263
+ window.removeEventListener('pointermove', move);
264
+ window.removeEventListener('pointerup', up);
265
+ };
266
+ window.addEventListener('pointermove', move);
267
+ window.addEventListener('pointerup', up);
268
+ };
269
+ // Resize from a corner; the opposite corner stays anchored.
270
+ const handleResizePointerDown = (corner)=>(event)=>{
271
+ event.preventDefault();
272
+ event.stopPropagation();
273
+ const startCrop = {
274
+ ...crop
275
+ };
276
+ const anchorX = corner === 'tl' || corner === 'bl' ? startCrop.x + startCrop.width : startCrop.x;
277
+ const anchorY = corner === 'tl' || corner === 'tr' ? startCrop.y + startCrop.height : startCrop.y;
278
+ const move = (e)=>{
279
+ const point = pointerToNatural(e);
280
+ if (!point) return;
281
+ const { x, y, width: w, height: h } = useCropImg.resolveCornerResize({
282
+ anchorX,
283
+ anchorY,
284
+ point,
285
+ aspectRatio: aspectLocked ? aspectRatio : null
286
+ });
287
+ setCropPosition({
288
+ x,
289
+ y
290
+ });
291
+ setCropSize({
292
+ width: w,
293
+ height: h
294
+ });
295
+ };
296
+ const up = ()=>{
297
+ window.removeEventListener('pointermove', move);
298
+ window.removeEventListener('pointerup', up);
299
+ };
300
+ window.addEventListener('pointermove', move);
301
+ window.addEventListener('pointerup', up);
302
+ };
303
+ const toggleAspectLock = ()=>{
304
+ setAspectLocked((locked)=>{
305
+ const next = !locked;
306
+ setAspectRatio(next && height ? width / height : null);
307
+ return next;
308
+ });
309
+ };
310
+ // Drag the focal handle within the crop rectangle, clamped 0–100.
311
+ const handleFocalPointerDown = (event)=>{
312
+ event.preventDefault();
313
+ event.stopPropagation();
314
+ const move = (e)=>{
315
+ const point = pointerToNatural(e);
316
+ if (!point) return;
317
+ // Convert pointer to a percentage of the current crop area.
318
+ const px = (point.x - crop.x) / crop.width * 100;
319
+ const py = (point.y - crop.y) / crop.height * 100;
320
+ setFocal({
321
+ x: Math.round(Math.min(100, Math.max(0, px))),
322
+ y: Math.round(Math.min(100, Math.max(0, py)))
323
+ });
324
+ };
325
+ const up = ()=>{
326
+ window.removeEventListener('pointermove', move);
327
+ window.removeEventListener('pointerup', up);
328
+ };
329
+ window.addEventListener('pointermove', move);
330
+ window.addEventListener('pointerup', up);
331
+ };
332
+ const focalPxX = Math.round(focal.x / 100 * width);
333
+ const focalPxY = Math.round(focal.y / 100 * height);
334
+ const setFocalPx = (axis, value)=>{
335
+ const dim = axis === 'x' ? width : height;
336
+ if (!dim) return;
337
+ const pct = Math.min(100, Math.max(0, value / dim * 100));
338
+ setFocal((prev)=>({
339
+ ...prev,
340
+ [axis]: Math.round(pct)
341
+ }));
342
+ };
343
+ const cropPercents = naturalSize.width && naturalSize.height ? {
344
+ left: crop.x / naturalSize.width * 100,
345
+ top: crop.y / naturalSize.height * 100,
346
+ width: crop.width / naturalSize.width * 100,
347
+ height: crop.height / naturalSize.height * 100
348
+ } : null;
349
+ // The crop is only usable once the image has loaded and `init()` has seeded
350
+ // the natural size. Until then the produce/export path would reject.
351
+ const isReady = cropPercents !== null;
352
+ const handleAction = async (action)=>{
353
+ if (!isReady) return;
354
+ let file;
355
+ try {
356
+ file = await produceFile(asset.name, asset.mime ?? 'image/png', asset.updatedAt);
357
+ } catch {
358
+ toggleNotification({
359
+ type: 'danger',
360
+ message: formatMessage({
361
+ id: translations.getTranslationKey('asset-details.crop.export-error'),
362
+ defaultMessage: 'Could not process the cropped image.'
363
+ })
364
+ });
365
+ return;
366
+ }
367
+ const roundedFocal = {
368
+ x: Math.round(focal.x),
369
+ y: Math.round(focal.y)
370
+ };
371
+ if (action === 'apply') {
372
+ onApply(file, roundedFocal);
373
+ } else {
374
+ onSaveAsCopy(file, roundedFocal);
375
+ }
376
+ };
377
+ return /*#__PURE__*/ jsxRuntime.jsx(designSystem.Portal, {
378
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.FocusTrap, {
379
+ onEscape: onClose,
380
+ children: /*#__PURE__*/ jsxRuntime.jsxs(Overlay, {
381
+ children: [
382
+ /*#__PURE__*/ jsxRuntime.jsxs(HeaderBar, {
383
+ alignItems: "center",
384
+ children: [
385
+ /*#__PURE__*/ jsxRuntime.jsx(icons.Crop, {
386
+ "aria-hidden": true
387
+ }),
388
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
389
+ variant: "omega",
390
+ fontWeight: "bold",
391
+ children: formatMessage({
392
+ id: translations.getTranslationKey('asset-details.crop.title'),
393
+ defaultMessage: 'Crop & Focus area'
394
+ })
395
+ })
396
+ ]
397
+ }),
398
+ /*#__PURE__*/ jsxRuntime.jsxs(Body, {
399
+ children: [
400
+ /*#__PURE__*/ jsxRuntime.jsxs(CropArea, {
401
+ ref: cropAreaRef,
402
+ $aspect: naturalSize.width && naturalSize.height ? naturalSize.width / naturalSize.height : undefined,
403
+ children: [
404
+ /*#__PURE__*/ jsxRuntime.jsx("img", {
405
+ ref: imgRef,
406
+ src: imageUrl,
407
+ alt: asset.name,
408
+ crossOrigin: "anonymous",
409
+ onLoad: handleImageLoad,
410
+ draggable: false
411
+ }),
412
+ cropPercents ? /*#__PURE__*/ jsxRuntime.jsxs(CropOverlay, {
413
+ style: {
414
+ left: `${cropPercents.left}%`,
415
+ top: `${cropPercents.top}%`,
416
+ width: `${cropPercents.width}%`,
417
+ height: `${cropPercents.height}%`
418
+ },
419
+ onPointerDown: handleMovePointerDown,
420
+ children: [
421
+ /*#__PURE__*/ jsxRuntime.jsx(ResizeHandle, {
422
+ type: "button",
423
+ "aria-label": formatMessage({
424
+ id: translations.getTranslationKey('asset-details.crop.resize.top-left'),
425
+ defaultMessage: 'Resize top-left'
426
+ }),
427
+ $cursor: "nwse-resize",
428
+ style: {
429
+ left: 0,
430
+ top: 0
431
+ },
432
+ onPointerDown: handleResizePointerDown('tl')
433
+ }),
434
+ /*#__PURE__*/ jsxRuntime.jsx(ResizeHandle, {
435
+ type: "button",
436
+ "aria-label": formatMessage({
437
+ id: translations.getTranslationKey('asset-details.crop.resize.top-right'),
438
+ defaultMessage: 'Resize top-right'
439
+ }),
440
+ $cursor: "nesw-resize",
441
+ style: {
442
+ right: 0,
443
+ top: 0
444
+ },
445
+ onPointerDown: handleResizePointerDown('tr')
446
+ }),
447
+ /*#__PURE__*/ jsxRuntime.jsx(ResizeHandle, {
448
+ type: "button",
449
+ "aria-label": formatMessage({
450
+ id: translations.getTranslationKey('asset-details.crop.resize.bottom-left'),
451
+ defaultMessage: 'Resize bottom-left'
452
+ }),
453
+ $cursor: "nesw-resize",
454
+ style: {
455
+ left: 0,
456
+ bottom: 0
457
+ },
458
+ onPointerDown: handleResizePointerDown('bl')
459
+ }),
460
+ /*#__PURE__*/ jsxRuntime.jsx(ResizeHandle, {
461
+ type: "button",
462
+ "aria-label": formatMessage({
463
+ id: translations.getTranslationKey('asset-details.crop.resize.bottom-right'),
464
+ defaultMessage: 'Resize bottom-right'
465
+ }),
466
+ $cursor: "nwse-resize",
467
+ style: {
468
+ right: 0,
469
+ bottom: 0
470
+ },
471
+ onPointerDown: handleResizePointerDown('br')
472
+ }),
473
+ /*#__PURE__*/ jsxRuntime.jsx(FocalPointHandle, {
474
+ type: "button",
475
+ "aria-label": formatMessage({
476
+ id: translations.getTranslationKey('asset-details.crop.focal-point'),
477
+ defaultMessage: 'Focal point'
478
+ }),
479
+ style: {
480
+ left: `${focal.x}%`,
481
+ top: `${focal.y}%`
482
+ },
483
+ onPointerDown: handleFocalPointerDown
484
+ })
485
+ ]
486
+ }) : null
487
+ ]
488
+ }),
489
+ /*#__PURE__*/ jsxRuntime.jsxs(InfoBox, {
490
+ children: [
491
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
492
+ direction: "column",
493
+ alignItems: "stretch",
494
+ gap: 1,
495
+ paddingBottom: 3,
496
+ children: [
497
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
498
+ variant: "omega",
499
+ fontWeight: "bold",
500
+ textColor: infoTextColor,
501
+ children: formatMessage({
502
+ id: translations.getTranslationKey('asset-details.crop.title'),
503
+ defaultMessage: 'Crop & Focus area'
504
+ })
505
+ }),
506
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
507
+ variant: "pi",
508
+ textColor: infoMutedColor,
509
+ children: formatMessage({
510
+ id: translations.getTranslationKey('asset-details.crop.hint'),
511
+ defaultMessage: 'Set the crop area with the rectangle. Pin the always-visible area with the circle.'
512
+ })
513
+ })
514
+ ]
515
+ }),
516
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
517
+ gap: 6,
518
+ alignItems: "center",
519
+ children: [
520
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
521
+ alignItems: "center",
522
+ gap: 2,
523
+ children: [
524
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
525
+ direction: "column",
526
+ gap: 2,
527
+ children: [
528
+ /*#__PURE__*/ jsxRuntime.jsxs(FieldRow, {
529
+ name: "crop-width",
530
+ gap: 2,
531
+ children: [
532
+ /*#__PURE__*/ jsxRuntime.jsx(LabelIcon, {
533
+ textColor: infoTextColor,
534
+ children: /*#__PURE__*/ jsxRuntime.jsx(icons.ArrowsHorizontal, {})
535
+ }),
536
+ /*#__PURE__*/ jsxRuntime.jsx(FieldNumberInput, {
537
+ "aria-label": formatMessage({
538
+ id: translations.getTranslationKey('asset-details.crop.width'),
539
+ defaultMessage: 'Width (px)'
540
+ }),
541
+ value: width,
542
+ min: 1,
543
+ max: naturalSize.width || undefined,
544
+ onValueChange: (next)=>{
545
+ if (next !== undefined) setCropSize({
546
+ width: next
547
+ });
548
+ }
549
+ })
550
+ ]
551
+ }),
552
+ /*#__PURE__*/ jsxRuntime.jsxs(FieldRow, {
553
+ name: "crop-height",
554
+ gap: 2,
555
+ children: [
556
+ /*#__PURE__*/ jsxRuntime.jsx(LabelIcon, {
557
+ textColor: infoTextColor,
558
+ children: /*#__PURE__*/ jsxRuntime.jsx(icons.ArrowsVertical, {})
559
+ }),
560
+ /*#__PURE__*/ jsxRuntime.jsx(FieldNumberInput, {
561
+ "aria-label": formatMessage({
562
+ id: translations.getTranslationKey('asset-details.crop.height'),
563
+ defaultMessage: 'Height (px)'
564
+ }),
565
+ value: height,
566
+ min: 1,
567
+ max: naturalSize.height || undefined,
568
+ onValueChange: (next)=>{
569
+ if (next !== undefined) setCropSize({
570
+ height: next
571
+ });
572
+ }
573
+ })
574
+ ]
575
+ })
576
+ ]
577
+ }),
578
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
579
+ position: "relative",
580
+ children: [
581
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.IconButton, {
582
+ label: formatMessage({
583
+ id: translations.getTranslationKey('asset-details.crop.aspect-lock'),
584
+ defaultMessage: 'Lock aspect ratio'
585
+ }),
586
+ variant: aspectLocked ? 'secondary' : 'ghost',
587
+ onClick: toggleAspectLock,
588
+ children: /*#__PURE__*/ jsxRuntime.jsx(icons.Link, {})
589
+ }),
590
+ /*#__PURE__*/ jsxRuntime.jsx(BackgroundPreserveRatio, {})
591
+ ]
592
+ })
593
+ ]
594
+ }),
595
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
596
+ direction: "column",
597
+ gap: 2,
598
+ marginLeft: 'auto',
599
+ children: [
600
+ /*#__PURE__*/ jsxRuntime.jsxs(FieldRow, {
601
+ name: "focal-x",
602
+ gap: 2,
603
+ children: [
604
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Label, {
605
+ textColor: infoTextColor,
606
+ children: formatMessage({
607
+ id: translations.getTranslationKey('asset-details.crop.focal-x-axis'),
608
+ defaultMessage: 'X'
609
+ })
610
+ }),
611
+ /*#__PURE__*/ jsxRuntime.jsx(FieldNumberInput, {
612
+ "aria-label": formatMessage({
613
+ id: translations.getTranslationKey('asset-details.crop.focal-x'),
614
+ defaultMessage: 'Focal point X (px)'
615
+ }),
616
+ value: focalPxX,
617
+ onValueChange: (next)=>{
618
+ if (next !== undefined) setFocalPx('x', next);
619
+ }
620
+ })
621
+ ]
622
+ }),
623
+ /*#__PURE__*/ jsxRuntime.jsxs(FieldRow, {
624
+ name: "focal-y",
625
+ gap: 2,
626
+ children: [
627
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Field.Label, {
628
+ textColor: infoTextColor,
629
+ children: formatMessage({
630
+ id: translations.getTranslationKey('asset-details.crop.focal-y-axis'),
631
+ defaultMessage: 'Y'
632
+ })
633
+ }),
634
+ /*#__PURE__*/ jsxRuntime.jsx(FieldNumberInput, {
635
+ "aria-label": formatMessage({
636
+ id: translations.getTranslationKey('asset-details.crop.focal-y'),
637
+ defaultMessage: 'Focal point Y (px)'
638
+ }),
639
+ value: focalPxY,
640
+ onValueChange: (next)=>{
641
+ if (next !== undefined) setFocalPx('y', next);
642
+ }
643
+ })
644
+ ]
645
+ })
646
+ ]
647
+ })
648
+ ]
649
+ })
650
+ ]
651
+ })
652
+ ]
653
+ }),
654
+ /*#__PURE__*/ jsxRuntime.jsxs(FooterBar, {
655
+ alignItems: "center",
656
+ children: [
657
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
658
+ variant: "tertiary",
659
+ onClick: onClose,
660
+ disabled: isBusy,
661
+ children: formatMessage({
662
+ id: 'app.components.Button.cancel',
663
+ defaultMessage: 'Cancel'
664
+ })
665
+ }),
666
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
667
+ gap: 2,
668
+ children: [
669
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
670
+ variant: "secondary",
671
+ onClick: ()=>handleAction('copy'),
672
+ loading: isBusy,
673
+ disabled: !isReady,
674
+ children: formatMessage({
675
+ id: translations.getTranslationKey('asset-details.crop.save-as-copy'),
676
+ defaultMessage: 'Save as copy'
677
+ })
678
+ }),
679
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
680
+ variant: "default",
681
+ onClick: ()=>handleAction('apply'),
682
+ loading: isBusy,
683
+ disabled: !isReady,
684
+ children: formatMessage({
685
+ id: translations.getTranslationKey('asset-details.crop.apply'),
686
+ defaultMessage: 'Apply'
687
+ })
688
+ })
689
+ ]
690
+ })
691
+ ]
692
+ })
693
+ ]
694
+ })
695
+ })
696
+ });
697
+ };
698
+
699
+ exports.AssetCropEditor = AssetCropEditor;
700
+ //# sourceMappingURL=AssetCropEditor.js.map