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