sanity-plugin-image-field 0.0.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.
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/dist/index.cjs +1512 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +1513 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1512 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ImageFieldInput: () => ImageFieldInput,
|
|
24
|
+
defineImageField: () => defineImageField,
|
|
25
|
+
imageFieldFormInput: () => imageFieldFormInput
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/ImageFieldInput.tsx
|
|
30
|
+
var import_react4 = require("react");
|
|
31
|
+
var import_ui2 = require("@sanity/ui");
|
|
32
|
+
var import_icons2 = require("@sanity/icons");
|
|
33
|
+
var import_sanity4 = require("sanity");
|
|
34
|
+
|
|
35
|
+
// src/cropMath.ts
|
|
36
|
+
var import_sanity = require("sanity");
|
|
37
|
+
|
|
38
|
+
// src/utils.ts
|
|
39
|
+
var validateAspectRatio = (input) => {
|
|
40
|
+
if (typeof input === "number") {
|
|
41
|
+
if (!Number.isFinite(input) || input <= 0) {
|
|
42
|
+
throw new Error("aspectRatio must be a finite number greater than 0.");
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(input)) {
|
|
47
|
+
if (input.length === 0) {
|
|
48
|
+
throw new Error("aspectRatio must not be an empty array.");
|
|
49
|
+
}
|
|
50
|
+
if (input.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"aspectRatio array values must be finite numbers greater than 0."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isFinite(input.min) || !Number.isFinite(input.max)) {
|
|
58
|
+
throw new Error("aspectRatio min/max must be finite numbers.");
|
|
59
|
+
}
|
|
60
|
+
if (input.min <= 0 || input.max <= 0) {
|
|
61
|
+
throw new Error("aspectRatio min and max must be greater than 0.");
|
|
62
|
+
}
|
|
63
|
+
if (input.max < input.min) {
|
|
64
|
+
throw new Error("aspectRatio max must be greater than or equal to min.");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
68
|
+
var clampRatio = (ratio, range) => clamp(ratio, range.min, range.max);
|
|
69
|
+
var nearestRatio = (ratio, ratios) => ratios.reduce(
|
|
70
|
+
(best, candidate) => Math.abs(candidate - ratio) < Math.abs(best - ratio) ? candidate : best
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// src/cropMath.ts
|
|
74
|
+
var MIN_CROP_SIZE = 24;
|
|
75
|
+
var FREE_ASPECT_RATIO_RANGE = {
|
|
76
|
+
min: 0.01,
|
|
77
|
+
max: 100
|
|
78
|
+
};
|
|
79
|
+
var resolveAspectRatio = (input) => {
|
|
80
|
+
if (input === void 0) {
|
|
81
|
+
return { aspectRatioRange: FREE_ASPECT_RATIO_RANGE };
|
|
82
|
+
}
|
|
83
|
+
if (typeof input === "number") {
|
|
84
|
+
return { aspectRatioRange: { min: input, max: input } };
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(input)) {
|
|
87
|
+
const snapRatios = Array.from(new Set(input)).toSorted((a, b) => a - b);
|
|
88
|
+
return {
|
|
89
|
+
aspectRatioRange: {
|
|
90
|
+
min: Math.min(...snapRatios),
|
|
91
|
+
max: Math.max(...snapRatios)
|
|
92
|
+
},
|
|
93
|
+
snapRatios
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { aspectRatioRange: { min: input.min, max: input.max } };
|
|
97
|
+
};
|
|
98
|
+
var largestRectForRatio = (source, ratio) => {
|
|
99
|
+
let width = source.width;
|
|
100
|
+
let height = width / ratio;
|
|
101
|
+
if (height > source.height) {
|
|
102
|
+
height = source.height;
|
|
103
|
+
width = height * ratio;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
x: (source.width - width) / 2,
|
|
107
|
+
y: (source.height - height) / 2,
|
|
108
|
+
width,
|
|
109
|
+
height
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
var defaultCrop = (source, range, snapRatios) => {
|
|
113
|
+
const sourceRatio = source.width / source.height;
|
|
114
|
+
const ratio = snapRatios ? nearestRatio(sourceRatio, snapRatios) : clampRatio(sourceRatio, range);
|
|
115
|
+
return largestRectForRatio(source, ratio);
|
|
116
|
+
};
|
|
117
|
+
var moveRect = (rect, deltaX, deltaY, source) => ({
|
|
118
|
+
...rect,
|
|
119
|
+
x: clamp(rect.x + deltaX, 0, source.width - rect.width),
|
|
120
|
+
y: clamp(rect.y + deltaY, 0, source.height - rect.height)
|
|
121
|
+
});
|
|
122
|
+
var resize = (rect, dir, pointer, source, range, opts = {}) => {
|
|
123
|
+
const { lockRatio = false, mirror = false, snapRatios } = opts;
|
|
124
|
+
const centeredX = mirror || dir.dx === 0;
|
|
125
|
+
const centeredY = mirror || dir.dy === 0;
|
|
126
|
+
const anchorX = centeredX ? rect.x + rect.width / 2 : dir.dx > 0 ? rect.x : rect.x + rect.width;
|
|
127
|
+
const anchorY = centeredY ? rect.y + rect.height / 2 : dir.dy > 0 ? rect.y : rect.y + rect.height;
|
|
128
|
+
const pointerX = clamp(pointer.x, 0, source.width);
|
|
129
|
+
const pointerY = clamp(pointer.y, 0, source.height);
|
|
130
|
+
const availWidth = centeredX ? 2 * Math.min(anchorX, source.width - anchorX) : dir.dx > 0 ? source.width - anchorX : anchorX;
|
|
131
|
+
const availHeight = centeredY ? 2 * Math.min(anchorY, source.height - anchorY) : dir.dy > 0 ? source.height - anchorY : anchorY;
|
|
132
|
+
const desiredWidth = (centeredX ? 2 : 1) * Math.abs(pointerX - anchorX);
|
|
133
|
+
const desiredHeight = (centeredY ? 2 : 1) * Math.abs(pointerY - anchorY);
|
|
134
|
+
const askedRatio = lockRatio ? rect.width / rect.height : dir.dx === 0 ? desiredHeight === 0 ? range.max : rect.width / desiredHeight : dir.dy === 0 ? desiredWidth / rect.height : desiredHeight === 0 ? range.max : desiredWidth / desiredHeight;
|
|
135
|
+
const ratio = snapRatios ? nearestRatio(askedRatio, snapRatios) : clampRatio(askedRatio, range);
|
|
136
|
+
const targetWidth = dir.dx !== 0 ? desiredWidth : desiredHeight * ratio;
|
|
137
|
+
let width = Math.min(targetWidth, availWidth);
|
|
138
|
+
let height = width / ratio;
|
|
139
|
+
if (height > availHeight) {
|
|
140
|
+
height = availHeight;
|
|
141
|
+
width = height * ratio;
|
|
142
|
+
}
|
|
143
|
+
const minWidth = Math.min(MIN_CROP_SIZE, availWidth, availHeight * ratio);
|
|
144
|
+
if (width < minWidth) {
|
|
145
|
+
width = minWidth;
|
|
146
|
+
height = width / ratio;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
x: centeredX ? anchorX - width / 2 : dir.dx > 0 ? anchorX : anchorX - width,
|
|
150
|
+
y: centeredY ? anchorY - height / 2 : dir.dy > 0 ? anchorY : anchorY - height,
|
|
151
|
+
width,
|
|
152
|
+
height
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
var applyRatio = (rect, ratio, source) => {
|
|
156
|
+
const centerX = rect.x + rect.width / 2;
|
|
157
|
+
const centerY = rect.y + rect.height / 2;
|
|
158
|
+
const width = Math.min(rect.width, rect.height * ratio);
|
|
159
|
+
const height = width / ratio;
|
|
160
|
+
return {
|
|
161
|
+
x: clamp(centerX - width / 2, 0, source.width - width),
|
|
162
|
+
y: clamp(centerY - height / 2, 0, source.height - height),
|
|
163
|
+
width,
|
|
164
|
+
height
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
var toNormalizedPoint = (pointer, box) => ({
|
|
168
|
+
x: clamp((pointer.x - box.left) / box.width, 0, 1),
|
|
169
|
+
y: clamp((pointer.y - box.top) / box.height, 0, 1)
|
|
170
|
+
});
|
|
171
|
+
var hotspotToFocalPoint = (hotspot, rect, source) => ({
|
|
172
|
+
x: clamp((hotspot.x * source.width - rect.x) / rect.width, 0, 1),
|
|
173
|
+
y: clamp((hotspot.y * source.height - rect.y) / rect.height, 0, 1)
|
|
174
|
+
});
|
|
175
|
+
var focalPointToHotspot = (point, rect, source, base) => ({
|
|
176
|
+
...base,
|
|
177
|
+
x: (rect.x + point.x * rect.width) / source.width,
|
|
178
|
+
y: (rect.y + point.y * rect.height) / source.height,
|
|
179
|
+
width: Math.min(base.width, rect.width / source.width),
|
|
180
|
+
height: Math.min(base.height, rect.height / source.height)
|
|
181
|
+
});
|
|
182
|
+
var pixelRectToCrop = (rect, source) => ({
|
|
183
|
+
_type: "sanity.imageCrop",
|
|
184
|
+
top: Math.max(0, rect.y / source.height),
|
|
185
|
+
left: Math.max(0, rect.x / source.width),
|
|
186
|
+
right: Math.max(0, (source.width - rect.x - rect.width) / source.width),
|
|
187
|
+
bottom: Math.max(0, (source.height - rect.y - rect.height) / source.height)
|
|
188
|
+
});
|
|
189
|
+
var cropToPixelRect = (crop, source) => ({
|
|
190
|
+
x: crop.left * source.width,
|
|
191
|
+
y: crop.top * source.height,
|
|
192
|
+
width: (1 - crop.left - crop.right) * source.width,
|
|
193
|
+
height: (1 - crop.top - crop.bottom) * source.height
|
|
194
|
+
});
|
|
195
|
+
var croppedDimensions = (rect) => ({
|
|
196
|
+
width: Math.round(rect.width),
|
|
197
|
+
height: Math.round(rect.height)
|
|
198
|
+
});
|
|
199
|
+
var dimensionsAtLeast = (dimensions, threshold) => dimensions.width >= threshold.width && dimensions.height >= threshold.height;
|
|
200
|
+
var formatAspectRatio = (ratio) => {
|
|
201
|
+
for (let height = 1; height <= 16; height++) {
|
|
202
|
+
const width = ratio * height;
|
|
203
|
+
const rounded = Math.round(width);
|
|
204
|
+
if (rounded >= 1 && Math.abs(width - rounded) < 0.02) {
|
|
205
|
+
return `${rounded}:${height}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return `${ratio.toFixed(2)}:1`;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/CropDialog.tsx
|
|
212
|
+
var import_react3 = require("react");
|
|
213
|
+
var import_ui = require("@sanity/ui");
|
|
214
|
+
var import_icons = require("@sanity/icons");
|
|
215
|
+
var import_sanity3 = require("sanity");
|
|
216
|
+
|
|
217
|
+
// src/CropArea.tsx
|
|
218
|
+
var import_react2 = require("react");
|
|
219
|
+
|
|
220
|
+
// src/useContentWidth.ts
|
|
221
|
+
var import_react = require("react");
|
|
222
|
+
function useContentWidth(ref) {
|
|
223
|
+
const [width, setWidth] = (0, import_react.useState)(0);
|
|
224
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
225
|
+
const element = ref.current;
|
|
226
|
+
if (!element) {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
230
|
+
if (!entry) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
setWidth(entry.contentRect.width);
|
|
234
|
+
});
|
|
235
|
+
observer.observe(element);
|
|
236
|
+
return () => {
|
|
237
|
+
observer.disconnect();
|
|
238
|
+
};
|
|
239
|
+
}, [ref]);
|
|
240
|
+
return width;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/CropArea.tsx
|
|
244
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
245
|
+
var HANDLES = [
|
|
246
|
+
{ dx: -1, dy: -1 },
|
|
247
|
+
{ dx: 0, dy: -1 },
|
|
248
|
+
{ dx: 1, dy: -1 },
|
|
249
|
+
{ dx: 1, dy: 0 },
|
|
250
|
+
{ dx: 1, dy: 1 },
|
|
251
|
+
{ dx: 0, dy: 1 },
|
|
252
|
+
{ dx: -1, dy: 1 },
|
|
253
|
+
{ dx: -1, dy: 0 }
|
|
254
|
+
];
|
|
255
|
+
var HANDLE_SIZE = 14;
|
|
256
|
+
var BAR_LENGTH = 22;
|
|
257
|
+
var BAR_THICKNESS = 8;
|
|
258
|
+
var HOTSPOT_SIZE = 22;
|
|
259
|
+
var KEYBOARD_STEP = 0.02;
|
|
260
|
+
var CROP_ARIA_LABEL = "Crop selection. Use arrow keys to move, hold Shift with arrow keys to resize.";
|
|
261
|
+
function usePointerDrag(onMove) {
|
|
262
|
+
const dragRef = (0, import_react2.useRef)(null);
|
|
263
|
+
const start = (0, import_react2.useCallback)(
|
|
264
|
+
(event, state, { stopPropagation = false } = {}) => {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
if (stopPropagation) {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
}
|
|
269
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
270
|
+
dragRef.current = state;
|
|
271
|
+
},
|
|
272
|
+
[]
|
|
273
|
+
);
|
|
274
|
+
const handlePointerMove = (0, import_react2.useCallback)(
|
|
275
|
+
(event) => {
|
|
276
|
+
const state = dragRef.current;
|
|
277
|
+
if (state === null) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
onMove(state, event);
|
|
281
|
+
},
|
|
282
|
+
[onMove]
|
|
283
|
+
);
|
|
284
|
+
const handleLostPointerCapture = (0, import_react2.useCallback)(() => {
|
|
285
|
+
dragRef.current = null;
|
|
286
|
+
}, []);
|
|
287
|
+
return { start, handlePointerMove, handleLostPointerCapture };
|
|
288
|
+
}
|
|
289
|
+
var CropArea = ({
|
|
290
|
+
imageUrl,
|
|
291
|
+
sourceDimensions: sourceDimensions2,
|
|
292
|
+
range,
|
|
293
|
+
snapRatios,
|
|
294
|
+
rect,
|
|
295
|
+
onChange,
|
|
296
|
+
focalPoint,
|
|
297
|
+
onFocalPointChange,
|
|
298
|
+
maxHeight = 460
|
|
299
|
+
}) => {
|
|
300
|
+
const frameRef = (0, import_react2.useRef)(null);
|
|
301
|
+
const imageBoxRef = (0, import_react2.useRef)(null);
|
|
302
|
+
const frameWidth = useContentWidth(frameRef);
|
|
303
|
+
const scale = displayScale(sourceDimensions2, frameWidth, maxHeight);
|
|
304
|
+
const displayWidth = sourceDimensions2.width * scale;
|
|
305
|
+
const displayHeight = sourceDimensions2.height * scale;
|
|
306
|
+
const cropBox = {
|
|
307
|
+
left: rect.x * scale,
|
|
308
|
+
top: rect.y * scale,
|
|
309
|
+
width: rect.width * scale,
|
|
310
|
+
height: rect.height * scale
|
|
311
|
+
};
|
|
312
|
+
const pointerToSource = (0, import_react2.useCallback)(
|
|
313
|
+
(event) => {
|
|
314
|
+
const box = imageBoxRef.current?.getBoundingClientRect();
|
|
315
|
+
if (!box || scale === 0) {
|
|
316
|
+
return { x: 0, y: 0 };
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
x: (event.clientX - box.left) / scale,
|
|
320
|
+
y: (event.clientY - box.top) / scale
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
[scale]
|
|
324
|
+
);
|
|
325
|
+
const pointerToFocal = (0, import_react2.useCallback)(
|
|
326
|
+
(event) => {
|
|
327
|
+
const box = imageBoxRef.current?.getBoundingClientRect();
|
|
328
|
+
if (!box || cropBox.width === 0 || cropBox.height === 0) {
|
|
329
|
+
return { x: 0, y: 0 };
|
|
330
|
+
}
|
|
331
|
+
return toNormalizedPoint(
|
|
332
|
+
{ x: event.clientX, y: event.clientY },
|
|
333
|
+
{
|
|
334
|
+
left: box.left + cropBox.left,
|
|
335
|
+
top: box.top + cropBox.top,
|
|
336
|
+
width: cropBox.width,
|
|
337
|
+
height: cropBox.height
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
[cropBox.left, cropBox.top, cropBox.width, cropBox.height]
|
|
342
|
+
);
|
|
343
|
+
const onCropMove = (0, import_react2.useCallback)(
|
|
344
|
+
(drag, event) => {
|
|
345
|
+
const pointer = pointerToSource(event);
|
|
346
|
+
const next = drag.mode === "move" ? moveRect(
|
|
347
|
+
drag.startRect,
|
|
348
|
+
pointer.x - drag.startPointer.x,
|
|
349
|
+
pointer.y - drag.startPointer.y,
|
|
350
|
+
sourceDimensions2
|
|
351
|
+
) : (
|
|
352
|
+
// Modifier keys are read at move time so they can change mid-drag.
|
|
353
|
+
// Shift locks the ratio; Option/Alt resizes from center. With
|
|
354
|
+
// snapRatios, the crop snaps to the nearest allowed ratio.
|
|
355
|
+
resize(drag.startRect, drag.dir, pointer, sourceDimensions2, range, {
|
|
356
|
+
lockRatio: event.shiftKey,
|
|
357
|
+
mirror: event.altKey,
|
|
358
|
+
snapRatios
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
onChange(next);
|
|
362
|
+
},
|
|
363
|
+
[onChange, pointerToSource, range, snapRatios, sourceDimensions2]
|
|
364
|
+
);
|
|
365
|
+
const cropDrag = usePointerDrag(onCropMove);
|
|
366
|
+
const onFocalMove = (0, import_react2.useCallback)(
|
|
367
|
+
(_active, event) => {
|
|
368
|
+
if (!onFocalPointChange) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
onFocalPointChange(pointerToFocal(event));
|
|
372
|
+
},
|
|
373
|
+
[onFocalPointChange, pointerToFocal]
|
|
374
|
+
);
|
|
375
|
+
const focalDrag = usePointerDrag(onFocalMove);
|
|
376
|
+
const handleKeyDown = (event) => {
|
|
377
|
+
const stepX = sourceDimensions2.width * KEYBOARD_STEP;
|
|
378
|
+
const stepY = sourceDimensions2.height * KEYBOARD_STEP;
|
|
379
|
+
if (event.shiftKey) {
|
|
380
|
+
const ratio = rect.width / rect.height;
|
|
381
|
+
const widthSteps = /* @__PURE__ */ new Map([
|
|
382
|
+
["ArrowRight", stepX],
|
|
383
|
+
["ArrowLeft", -stepX],
|
|
384
|
+
["ArrowDown", stepY * ratio],
|
|
385
|
+
["ArrowUp", -stepY * ratio]
|
|
386
|
+
]);
|
|
387
|
+
const widthStep = widthSteps.get(event.key);
|
|
388
|
+
if (widthStep === void 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
const east = rect.x + rect.width;
|
|
393
|
+
const south = rect.y + rect.height;
|
|
394
|
+
onChange(
|
|
395
|
+
resize(
|
|
396
|
+
rect,
|
|
397
|
+
{ dx: 1, dy: 1 },
|
|
398
|
+
{ x: east + widthStep, y: south },
|
|
399
|
+
sourceDimensions2,
|
|
400
|
+
range,
|
|
401
|
+
{ lockRatio: true, snapRatios }
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const moves = /* @__PURE__ */ new Map([
|
|
407
|
+
["ArrowLeft", [-stepX, 0]],
|
|
408
|
+
["ArrowRight", [stepX, 0]],
|
|
409
|
+
["ArrowUp", [0, -stepY]],
|
|
410
|
+
["ArrowDown", [0, stepY]]
|
|
411
|
+
]);
|
|
412
|
+
const delta = moves.get(event.key);
|
|
413
|
+
if (!delta) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
onChange(moveRect(rect, delta[0], delta[1], sourceDimensions2));
|
|
418
|
+
};
|
|
419
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
420
|
+
"div",
|
|
421
|
+
{
|
|
422
|
+
ref: frameRef,
|
|
423
|
+
style: {
|
|
424
|
+
width: "100%",
|
|
425
|
+
display: "flex",
|
|
426
|
+
justifyContent: "center",
|
|
427
|
+
userSelect: "none",
|
|
428
|
+
touchAction: "none"
|
|
429
|
+
},
|
|
430
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
431
|
+
"div",
|
|
432
|
+
{
|
|
433
|
+
ref: imageBoxRef,
|
|
434
|
+
style: {
|
|
435
|
+
position: "relative",
|
|
436
|
+
width: displayWidth,
|
|
437
|
+
height: displayHeight,
|
|
438
|
+
maxWidth: "100%"
|
|
439
|
+
},
|
|
440
|
+
children: [
|
|
441
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
442
|
+
"img",
|
|
443
|
+
{
|
|
444
|
+
src: imageUrl,
|
|
445
|
+
alt: "",
|
|
446
|
+
draggable: false,
|
|
447
|
+
style: {
|
|
448
|
+
display: "block",
|
|
449
|
+
width: "100%",
|
|
450
|
+
height: "100%",
|
|
451
|
+
objectFit: "fill"
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
),
|
|
455
|
+
scale > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
457
|
+
"div",
|
|
458
|
+
{
|
|
459
|
+
"aria-hidden": true,
|
|
460
|
+
style: {
|
|
461
|
+
position: "absolute",
|
|
462
|
+
inset: 0,
|
|
463
|
+
overflow: "hidden",
|
|
464
|
+
pointerEvents: "none"
|
|
465
|
+
},
|
|
466
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
467
|
+
"div",
|
|
468
|
+
{
|
|
469
|
+
style: {
|
|
470
|
+
position: "absolute",
|
|
471
|
+
...cropBox,
|
|
472
|
+
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.55)"
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
),
|
|
478
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
479
|
+
"div",
|
|
480
|
+
{
|
|
481
|
+
role: "group",
|
|
482
|
+
"aria-label": CROP_ARIA_LABEL,
|
|
483
|
+
tabIndex: 0,
|
|
484
|
+
onPointerDown: (event) => {
|
|
485
|
+
cropDrag.start(event, {
|
|
486
|
+
mode: "move",
|
|
487
|
+
startRect: rect,
|
|
488
|
+
startPointer: pointerToSource(event)
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
onPointerMove: cropDrag.handlePointerMove,
|
|
492
|
+
onLostPointerCapture: cropDrag.handleLostPointerCapture,
|
|
493
|
+
onKeyDown: handleKeyDown,
|
|
494
|
+
style: {
|
|
495
|
+
position: "absolute",
|
|
496
|
+
...cropBox,
|
|
497
|
+
cursor: "move",
|
|
498
|
+
outline: "1px solid rgba(255, 255, 255, 0.9)"
|
|
499
|
+
},
|
|
500
|
+
children: [
|
|
501
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThirdsGuides, {}),
|
|
502
|
+
HANDLES.map((dir) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
503
|
+
Handle,
|
|
504
|
+
{
|
|
505
|
+
dir,
|
|
506
|
+
onPointerDown: (event) => {
|
|
507
|
+
cropDrag.start(
|
|
508
|
+
event,
|
|
509
|
+
{ mode: "resize", dir, startRect: rect },
|
|
510
|
+
{ stopPropagation: true }
|
|
511
|
+
);
|
|
512
|
+
},
|
|
513
|
+
onPointerMove: cropDrag.handlePointerMove,
|
|
514
|
+
onLostPointerCapture: cropDrag.handleLostPointerCapture
|
|
515
|
+
},
|
|
516
|
+
`${dir.dx},${dir.dy}`
|
|
517
|
+
))
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
),
|
|
521
|
+
focalPoint && onFocalPointChange && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
522
|
+
HotspotMarker,
|
|
523
|
+
{
|
|
524
|
+
left: cropBox.left + focalPoint.x * cropBox.width,
|
|
525
|
+
top: cropBox.top + focalPoint.y * cropBox.height,
|
|
526
|
+
onPointerDown: (event) => {
|
|
527
|
+
focalDrag.start(event, true, { stopPropagation: true });
|
|
528
|
+
},
|
|
529
|
+
onPointerMove: focalDrag.handlePointerMove,
|
|
530
|
+
onLostPointerCapture: focalDrag.handleLostPointerCapture
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
] })
|
|
534
|
+
]
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
};
|
|
540
|
+
var displayScale = (sourceDimensions2, frameWidth, maxHeight) => {
|
|
541
|
+
if (frameWidth === 0) {
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
return Math.min(
|
|
545
|
+
frameWidth / sourceDimensions2.width,
|
|
546
|
+
maxHeight / sourceDimensions2.height
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
var ThirdsGuides = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
550
|
+
"div",
|
|
551
|
+
{
|
|
552
|
+
style: {
|
|
553
|
+
position: "absolute",
|
|
554
|
+
inset: 0,
|
|
555
|
+
pointerEvents: "none",
|
|
556
|
+
backgroundImage: `
|
|
557
|
+
linear-gradient(to right, transparent 33.33%, rgba(255, 255, 255, 0.35) 33.33%, rgba(255, 255, 255, 0.35) calc(33.33% + 1px), transparent calc(33.33% + 1px), transparent 66.66%, rgba(255, 255, 255, 0.35) 66.66%, rgba(255, 255, 255, 0.35) calc(66.66% + 1px), transparent calc(66.66% + 1px)),
|
|
558
|
+
linear-gradient(to bottom, transparent 33.33%, rgba(255, 255, 255, 0.35) 33.33%, rgba(255, 255, 255, 0.35) calc(33.33% + 1px), transparent calc(33.33% + 1px), transparent 66.66%, rgba(255, 255, 255, 0.35) 66.66%, rgba(255, 255, 255, 0.35) calc(66.66% + 1px), transparent calc(66.66% + 1px))
|
|
559
|
+
`
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
var cursorForDirection = (dir) => {
|
|
564
|
+
if (dir.dx === 0) {
|
|
565
|
+
return "ns-resize";
|
|
566
|
+
}
|
|
567
|
+
if (dir.dy === 0) {
|
|
568
|
+
return "ew-resize";
|
|
569
|
+
}
|
|
570
|
+
return dir.dx === dir.dy ? "nwse-resize" : "nesw-resize";
|
|
571
|
+
};
|
|
572
|
+
var Handle = ({
|
|
573
|
+
dir,
|
|
574
|
+
onPointerDown,
|
|
575
|
+
onPointerMove,
|
|
576
|
+
onLostPointerCapture
|
|
577
|
+
}) => {
|
|
578
|
+
const isCorner = dir.dx !== 0 && dir.dy !== 0;
|
|
579
|
+
const horizontalBar = dir.dx === 0;
|
|
580
|
+
const width = isCorner ? HANDLE_SIZE : horizontalBar ? BAR_LENGTH : BAR_THICKNESS;
|
|
581
|
+
const height = isCorner ? HANDLE_SIZE : horizontalBar ? BAR_THICKNESS : BAR_LENGTH;
|
|
582
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
583
|
+
"div",
|
|
584
|
+
{
|
|
585
|
+
onPointerDown,
|
|
586
|
+
onPointerMove,
|
|
587
|
+
onLostPointerCapture,
|
|
588
|
+
style: {
|
|
589
|
+
position: "absolute",
|
|
590
|
+
left: `${(dir.dx + 1) * 50}%`,
|
|
591
|
+
top: `${(dir.dy + 1) * 50}%`,
|
|
592
|
+
transform: "translate(-50%, -50%)",
|
|
593
|
+
width,
|
|
594
|
+
height,
|
|
595
|
+
borderRadius: isCorner ? "50%" : BAR_THICKNESS / 2,
|
|
596
|
+
background: "#fff",
|
|
597
|
+
boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.4)",
|
|
598
|
+
cursor: cursorForDirection(dir),
|
|
599
|
+
touchAction: "none"
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
};
|
|
604
|
+
var HotspotMarker = ({
|
|
605
|
+
left,
|
|
606
|
+
top,
|
|
607
|
+
onPointerDown,
|
|
608
|
+
onPointerMove,
|
|
609
|
+
onLostPointerCapture
|
|
610
|
+
}) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
611
|
+
"div",
|
|
612
|
+
{
|
|
613
|
+
title: "Focal point",
|
|
614
|
+
onPointerDown,
|
|
615
|
+
onPointerMove,
|
|
616
|
+
onLostPointerCapture,
|
|
617
|
+
style: {
|
|
618
|
+
position: "absolute",
|
|
619
|
+
left,
|
|
620
|
+
top,
|
|
621
|
+
transform: "translate(-50%, -50%)",
|
|
622
|
+
width: HOTSPOT_SIZE,
|
|
623
|
+
height: HOTSPOT_SIZE,
|
|
624
|
+
borderRadius: "50%",
|
|
625
|
+
border: "2px solid #fff",
|
|
626
|
+
background: "rgba(255, 255, 255, 0.2)",
|
|
627
|
+
boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.5), 0 0 6px rgba(0, 0, 0, 0.5)",
|
|
628
|
+
cursor: "grab",
|
|
629
|
+
touchAction: "none"
|
|
630
|
+
},
|
|
631
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
632
|
+
"div",
|
|
633
|
+
{
|
|
634
|
+
style: {
|
|
635
|
+
position: "absolute",
|
|
636
|
+
left: "50%",
|
|
637
|
+
top: "50%",
|
|
638
|
+
transform: "translate(-50%, -50%)",
|
|
639
|
+
width: 4,
|
|
640
|
+
height: 4,
|
|
641
|
+
borderRadius: "50%",
|
|
642
|
+
background: "#fff",
|
|
643
|
+
boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.5)"
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
// src/imageUrl.ts
|
|
651
|
+
var import_url_builder = require("@sanity-image/url-builder");
|
|
652
|
+
var import_sanity2 = require("sanity");
|
|
653
|
+
var sourceDimensions = (assetRef) => (0, import_url_builder.parseImageId)(assetRef).dimensions;
|
|
654
|
+
var isSvg = (ref) => /^image-[a-f0-9]+(?:-\d+x\d+)?-svg$/.test(ref);
|
|
655
|
+
var isCroppable = (assetRef) => Boolean(assetRef && /-\d+x\d+-\w+$/.test(assetRef) && !isSvg(assetRef));
|
|
656
|
+
var isImageAssetRef = (ref) => {
|
|
657
|
+
if (isSvg(ref)) {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const { assetId, format } = (0, import_url_builder.parseImageId)(ref);
|
|
662
|
+
return Boolean(assetId && format);
|
|
663
|
+
} catch {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
var croppedDimensionsFromValue = (value) => {
|
|
668
|
+
if (!isCroppable(value?.asset?._ref)) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
const source = sourceDimensions(value.asset._ref);
|
|
672
|
+
if (!value.crop) {
|
|
673
|
+
return source;
|
|
674
|
+
}
|
|
675
|
+
return croppedDimensions(cropToPixelRect(value.crop, source));
|
|
676
|
+
};
|
|
677
|
+
var cdnBaseUrl = (source) => `https://cdn.sanity.io/images/${source.projectId}/${source.dataset}/`;
|
|
678
|
+
var sourceImageUrl = (assetRef, source) => {
|
|
679
|
+
const { assetId, dimensions, format } = (0, import_url_builder.parseImageId)(assetRef);
|
|
680
|
+
const filename = `${assetId}-${dimensions.width}x${dimensions.height}.${format}`;
|
|
681
|
+
return `${cdnBaseUrl(source)}${filename}`;
|
|
682
|
+
};
|
|
683
|
+
var croppedImageUrl = (value, source, displayWidth) => {
|
|
684
|
+
const assetRef = value.asset?._ref;
|
|
685
|
+
if (!assetRef) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (isSvg(assetRef)) {
|
|
689
|
+
const assetId = assetRef.slice("image-".length, -"-svg".length);
|
|
690
|
+
return `${cdnBaseUrl(source)}${assetId}.svg`;
|
|
691
|
+
}
|
|
692
|
+
const { src } = (0, import_url_builder.buildSrc)({
|
|
693
|
+
baseUrl: cdnBaseUrl(source),
|
|
694
|
+
id: assetRef,
|
|
695
|
+
width: Math.round(displayWidth),
|
|
696
|
+
crop: value.crop
|
|
697
|
+
});
|
|
698
|
+
return src;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/CropDialog.tsx
|
|
702
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
703
|
+
var CENTERED_HOTSPOT = {
|
|
704
|
+
_type: "sanity.imageHotspot",
|
|
705
|
+
x: 0.5,
|
|
706
|
+
y: 0.5,
|
|
707
|
+
width: 0.01,
|
|
708
|
+
height: 0.01
|
|
709
|
+
};
|
|
710
|
+
var CropDialog = ({
|
|
711
|
+
assetRef,
|
|
712
|
+
imageSource,
|
|
713
|
+
sourceDimensions: sourceDimensions2,
|
|
714
|
+
config,
|
|
715
|
+
initialCrop,
|
|
716
|
+
hotspotEnabled,
|
|
717
|
+
initialHotspot,
|
|
718
|
+
onConfirm,
|
|
719
|
+
onClose
|
|
720
|
+
}) => {
|
|
721
|
+
const {
|
|
722
|
+
aspectRatioRange,
|
|
723
|
+
snapRatios,
|
|
724
|
+
recommendedDimensions,
|
|
725
|
+
requiredDimensions
|
|
726
|
+
} = config;
|
|
727
|
+
const [rect, setRect] = (0, import_react3.useState)(() => {
|
|
728
|
+
if (!initialCrop) {
|
|
729
|
+
return defaultCrop(sourceDimensions2, aspectRatioRange, snapRatios);
|
|
730
|
+
}
|
|
731
|
+
const loaded = cropToPixelRect(initialCrop, sourceDimensions2);
|
|
732
|
+
return snapRatios ? applyRatio(
|
|
733
|
+
loaded,
|
|
734
|
+
nearestRatio(loaded.width / loaded.height, snapRatios),
|
|
735
|
+
sourceDimensions2
|
|
736
|
+
) : loaded;
|
|
737
|
+
});
|
|
738
|
+
const [focalPoint, setFocalPoint] = (0, import_react3.useState)(
|
|
739
|
+
() => initialHotspot ? hotspotToFocalPoint(initialHotspot, rect, sourceDimensions2) : { x: 0.5, y: 0.5 }
|
|
740
|
+
);
|
|
741
|
+
const dimensions = croppedDimensions(rect);
|
|
742
|
+
const belowRequired = requiredDimensions && !dimensionsAtLeast(dimensions, requiredDimensions);
|
|
743
|
+
const belowRecommended = recommendedDimensions && !dimensionsAtLeast(dimensions, recommendedDimensions);
|
|
744
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
745
|
+
import_ui.Dialog,
|
|
746
|
+
{
|
|
747
|
+
id: "aspect-ratio-crop-dialog",
|
|
748
|
+
header: "Crop image",
|
|
749
|
+
width: 2,
|
|
750
|
+
onClose,
|
|
751
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Flex, { padding: 3, justify: "space-between", align: "center", gap: 3, children: [
|
|
752
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
753
|
+
import_ui.Button,
|
|
754
|
+
{
|
|
755
|
+
mode: "bleed",
|
|
756
|
+
icon: import_icons.ResetIcon,
|
|
757
|
+
text: "Reset crop",
|
|
758
|
+
fontSize: 1,
|
|
759
|
+
onClick: () => {
|
|
760
|
+
setRect(
|
|
761
|
+
defaultCrop(sourceDimensions2, aspectRatioRange, snapRatios)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
),
|
|
766
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Flex, { gap: 2, children: [
|
|
767
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui.Button, { mode: "ghost", text: "Cancel", onClick: onClose }),
|
|
768
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
769
|
+
import_ui.Button,
|
|
770
|
+
{
|
|
771
|
+
tone: "primary",
|
|
772
|
+
text: "Confirm crop",
|
|
773
|
+
disabled: belowRequired,
|
|
774
|
+
onClick: () => {
|
|
775
|
+
onConfirm({
|
|
776
|
+
crop: pixelRectToCrop(rect, sourceDimensions2),
|
|
777
|
+
hotspot: hotspotEnabled ? focalPointToHotspot(
|
|
778
|
+
focalPoint,
|
|
779
|
+
rect,
|
|
780
|
+
sourceDimensions2,
|
|
781
|
+
initialHotspot ?? CENTERED_HOTSPOT
|
|
782
|
+
) : void 0
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
] })
|
|
788
|
+
] }),
|
|
789
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Stack, { gap: 3, padding: 3, children: [
|
|
790
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
791
|
+
CropArea,
|
|
792
|
+
{
|
|
793
|
+
imageUrl: sourceImageUrl(assetRef, imageSource),
|
|
794
|
+
sourceDimensions: sourceDimensions2,
|
|
795
|
+
range: aspectRatioRange,
|
|
796
|
+
snapRatios,
|
|
797
|
+
rect,
|
|
798
|
+
onChange: setRect,
|
|
799
|
+
focalPoint: hotspotEnabled ? focalPoint : void 0,
|
|
800
|
+
onFocalPointChange: hotspotEnabled ? setFocalPoint : void 0
|
|
801
|
+
}
|
|
802
|
+
),
|
|
803
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Flex, { justify: "space-between", align: "center", gap: 3, wrap: "wrap", children: [
|
|
804
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Text, { size: 1, muted: true, children: [
|
|
805
|
+
"Allowed aspect ratio: ",
|
|
806
|
+
describeAspectRatio(config)
|
|
807
|
+
] }),
|
|
808
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Text, { size: 1, weight: "medium", children: [
|
|
809
|
+
dimensions.width,
|
|
810
|
+
" \xD7 ",
|
|
811
|
+
dimensions.height,
|
|
812
|
+
" px"
|
|
813
|
+
] })
|
|
814
|
+
] }),
|
|
815
|
+
hotspotEnabled && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui.Text, { size: 1, muted: true, children: "Drag the circle to set the focal point. It governs how cover-mode crops frame the image downstream, not this preview." }),
|
|
816
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
817
|
+
DimensionStatusMessage,
|
|
818
|
+
{
|
|
819
|
+
requiredDimensions,
|
|
820
|
+
recommendedDimensions,
|
|
821
|
+
belowRequired,
|
|
822
|
+
belowRecommended
|
|
823
|
+
}
|
|
824
|
+
)
|
|
825
|
+
] })
|
|
826
|
+
}
|
|
827
|
+
);
|
|
828
|
+
};
|
|
829
|
+
var StatusMessage = ({
|
|
830
|
+
tone,
|
|
831
|
+
icon,
|
|
832
|
+
message
|
|
833
|
+
}) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui.Card, { tone, padding: 3, radius: 2, border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_ui.Flex, { gap: 3, align: "center", children: [
|
|
834
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui.Text, { size: 1, children: icon }),
|
|
835
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui.Box, { flex: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ui.Text, { size: 1, textOverflow: "ellipsis", children: message }) })
|
|
836
|
+
] }) });
|
|
837
|
+
var DimensionStatusMessage = ({
|
|
838
|
+
requiredDimensions,
|
|
839
|
+
recommendedDimensions,
|
|
840
|
+
belowRequired,
|
|
841
|
+
belowRecommended
|
|
842
|
+
}) => {
|
|
843
|
+
if (requiredDimensions && belowRequired) {
|
|
844
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
845
|
+
StatusMessage,
|
|
846
|
+
{
|
|
847
|
+
tone: "critical",
|
|
848
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.ErrorOutlineIcon, {}),
|
|
849
|
+
message: `Crop must be at least ${describeDimensions(requiredDimensions)}.`
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (recommendedDimensions && belowRecommended) {
|
|
854
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
855
|
+
StatusMessage,
|
|
856
|
+
{
|
|
857
|
+
tone: "caution",
|
|
858
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.WarningOutlineIcon, {}),
|
|
859
|
+
message: `For best results, keep the crop at least ${describeDimensions(
|
|
860
|
+
recommendedDimensions
|
|
861
|
+
)}.`
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
if (requiredDimensions || recommendedDimensions) {
|
|
866
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
867
|
+
StatusMessage,
|
|
868
|
+
{
|
|
869
|
+
tone: "positive",
|
|
870
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons.CheckmarkCircleIcon, {}),
|
|
871
|
+
message: "Crop size looks good."
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
return null;
|
|
876
|
+
};
|
|
877
|
+
var describeDimensions = (dimensions) => `${dimensions.width} \xD7 ${dimensions.height} px`;
|
|
878
|
+
var describeAspectRatio = ({
|
|
879
|
+
aspectRatioRange,
|
|
880
|
+
snapRatios
|
|
881
|
+
}) => {
|
|
882
|
+
if (snapRatios) {
|
|
883
|
+
if (snapRatios.length <= 3) {
|
|
884
|
+
return snapRatios.map(formatAspectRatio).join(", ");
|
|
885
|
+
}
|
|
886
|
+
const lo = formatAspectRatio(Math.min(...snapRatios));
|
|
887
|
+
const hi = formatAspectRatio(Math.max(...snapRatios));
|
|
888
|
+
return `${snapRatios.length} ratios (${lo} \u2013 ${hi})`;
|
|
889
|
+
}
|
|
890
|
+
return aspectRatioRange.min === aspectRatioRange.max ? formatAspectRatio(aspectRatioRange.min) : `${formatAspectRatio(aspectRatioRange.min)} \u2013 ${formatAspectRatio(
|
|
891
|
+
aspectRatioRange.max
|
|
892
|
+
)}`;
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// src/upload.ts
|
|
896
|
+
var acceptsType = (mimeType, accept) => {
|
|
897
|
+
const type = mimeType.trim().toLowerCase();
|
|
898
|
+
const patterns = accept.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
899
|
+
if (patterns.length === 0) {
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
return patterns.some((pattern) => {
|
|
903
|
+
if (pattern === "*" || pattern === "*/*") {
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
if (pattern.endsWith("/*")) {
|
|
907
|
+
return type.startsWith(pattern.slice(0, -1));
|
|
908
|
+
}
|
|
909
|
+
return type === pattern;
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
var isTextEntryTarget = (target) => {
|
|
913
|
+
if (!(target instanceof HTMLElement)) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
const attribute = target.getAttribute("contenteditable");
|
|
920
|
+
return target.isContentEditable || attribute === "" || attribute === "true";
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/ImageFieldInput.tsx
|
|
924
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
925
|
+
var API_VERSION = "2025-02-19";
|
|
926
|
+
var ImageFieldInput = ({
|
|
927
|
+
value,
|
|
928
|
+
onChange,
|
|
929
|
+
path,
|
|
930
|
+
members,
|
|
931
|
+
readOnly,
|
|
932
|
+
schemaType,
|
|
933
|
+
...renderObjectProps
|
|
934
|
+
}) => {
|
|
935
|
+
const client = (0, import_sanity4.useClient)({ apiVersion: API_VERSION });
|
|
936
|
+
const { projectId, dataset } = client.config();
|
|
937
|
+
const assetSources = (0, import_sanity4.useWorkspace)().form.image.assetSources;
|
|
938
|
+
const imageFieldOptions = schemaType.options?.imageField;
|
|
939
|
+
const aspectRatio = imageFieldOptions?.aspectRatio;
|
|
940
|
+
const hasAspectConstraint = aspectRatio !== void 0;
|
|
941
|
+
const hotspotEnabled = Boolean(schemaType.options?.hotspot);
|
|
942
|
+
const resolved = resolveAspectRatio(aspectRatio);
|
|
943
|
+
const config = {
|
|
944
|
+
aspectRatioRange: resolved.aspectRatioRange,
|
|
945
|
+
snapRatios: resolved.snapRatios,
|
|
946
|
+
recommendedDimensions: imageFieldOptions?.recommendedDimensions,
|
|
947
|
+
requiredDimensions: imageFieldOptions?.requiredDimensions
|
|
948
|
+
};
|
|
949
|
+
const accept = schemaType.options?.accept ?? "image/*";
|
|
950
|
+
const [upload, setUpload] = (0, import_react4.useState)({ status: "idle" });
|
|
951
|
+
const [dialogOpen, setDialogOpen] = (0, import_react4.useState)(false);
|
|
952
|
+
const [pendingAsset, setPendingAsset] = (0, import_react4.useState)(null);
|
|
953
|
+
const [draggingOver, setDraggingOver] = (0, import_react4.useState)(false);
|
|
954
|
+
const [browsingSource, setBrowsingSource] = (0, import_react4.useState)(null);
|
|
955
|
+
const fileInputRef = (0, import_react4.useRef)(null);
|
|
956
|
+
const dragDepth = (0, import_react4.useRef)(0);
|
|
957
|
+
const subscription = (0, import_react4.useRef)(null);
|
|
958
|
+
(0, import_react4.useEffect)(() => () => subscription.current?.unsubscribe(), []);
|
|
959
|
+
const assetRef = value?.asset?._ref;
|
|
960
|
+
const assetCrop = value?.crop;
|
|
961
|
+
const assetHotspot = value?.hotspot;
|
|
962
|
+
const activeAssetRef = pendingAsset ?? assetRef;
|
|
963
|
+
const dialogSource = activeAssetRef && isCroppable(activeAssetRef) ? sourceDimensions(activeAssetRef) : null;
|
|
964
|
+
const imageSource = projectId && dataset ? { projectId, dataset } : null;
|
|
965
|
+
const customFieldMembers = members.filter(isCustomFieldMember);
|
|
966
|
+
const commitAsset = (ref, extraPatches = []) => {
|
|
967
|
+
onChange([
|
|
968
|
+
(0, import_sanity4.setIfMissing)({ _type: "image" }),
|
|
969
|
+
(0, import_sanity4.set)({ _type: "reference", _ref: ref }, ["asset"]),
|
|
970
|
+
...extraPatches
|
|
971
|
+
]);
|
|
972
|
+
setUpload({ status: "idle" });
|
|
973
|
+
};
|
|
974
|
+
const applyAsset = (selectedAssetRef) => {
|
|
975
|
+
if (isSvg(selectedAssetRef)) {
|
|
976
|
+
commitAsset(selectedAssetRef, [(0, import_sanity4.unset)(["crop"])]);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const dimensions = isCroppable(selectedAssetRef) ? sourceDimensions(selectedAssetRef) : null;
|
|
980
|
+
const openEditor = Boolean(dimensions) && (hasAspectConstraint || hotspotEnabled);
|
|
981
|
+
if (openEditor) {
|
|
982
|
+
setPendingAsset(selectedAssetRef);
|
|
983
|
+
setUpload({ status: "idle" });
|
|
984
|
+
setDialogOpen(true);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (dimensions && config.requiredDimensions && !dimensionsAtLeast(dimensions, config.requiredDimensions)) {
|
|
988
|
+
setUpload({
|
|
989
|
+
status: "error",
|
|
990
|
+
message: `That image is too small. It must be at least ${config.requiredDimensions.width} \xD7 ${config.requiredDimensions.height} px.`
|
|
991
|
+
});
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
commitAsset(selectedAssetRef, [(0, import_sanity4.unset)(["crop"])]);
|
|
995
|
+
};
|
|
996
|
+
const uploadFile = (file) => {
|
|
997
|
+
if (!acceptsType(file.type, accept)) {
|
|
998
|
+
setUpload({
|
|
999
|
+
status: "error",
|
|
1000
|
+
message: "That file type isn't accepted here. Choose a supported image."
|
|
1001
|
+
});
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
setUpload({ status: "uploading", progress: 0 });
|
|
1005
|
+
subscription.current?.unsubscribe();
|
|
1006
|
+
subscription.current = client.observable.assets.upload("image", file, { filename: file.name }).subscribe({
|
|
1007
|
+
next: (event) => {
|
|
1008
|
+
if (event.type === "progress") {
|
|
1009
|
+
setUpload({ status: "uploading", progress: event.percent });
|
|
1010
|
+
} else {
|
|
1011
|
+
applyAsset(event.body.document._id);
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
error: () => {
|
|
1015
|
+
setUpload({
|
|
1016
|
+
status: "error",
|
|
1017
|
+
message: "Upload failed. Please try again."
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
};
|
|
1022
|
+
const selectAsset = (assets) => {
|
|
1023
|
+
setBrowsingSource(null);
|
|
1024
|
+
const chosen = assets.at(0);
|
|
1025
|
+
if (chosen?.kind === "assetDocumentId" && typeof chosen.value === "string" && isImageAssetRef(chosen.value)) {
|
|
1026
|
+
applyAsset(chosen.value);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
setUpload({
|
|
1030
|
+
status: "error",
|
|
1031
|
+
message: "That selection can't be used here. Choose an uploaded image."
|
|
1032
|
+
});
|
|
1033
|
+
};
|
|
1034
|
+
const confirmCropAndHotspot = ({
|
|
1035
|
+
crop,
|
|
1036
|
+
hotspot
|
|
1037
|
+
}) => {
|
|
1038
|
+
const assetPatches = pendingAsset ? [
|
|
1039
|
+
(0, import_sanity4.setIfMissing)({ _type: "image" }),
|
|
1040
|
+
(0, import_sanity4.set)({ _type: "reference", _ref: pendingAsset }, ["asset"])
|
|
1041
|
+
] : [];
|
|
1042
|
+
onChange([
|
|
1043
|
+
...assetPatches,
|
|
1044
|
+
(0, import_sanity4.set)(crop, ["crop"]),
|
|
1045
|
+
...hotspot ? [(0, import_sanity4.set)(hotspot, ["hotspot"])] : []
|
|
1046
|
+
]);
|
|
1047
|
+
setPendingAsset(null);
|
|
1048
|
+
setDialogOpen(false);
|
|
1049
|
+
};
|
|
1050
|
+
const removeImage = () => {
|
|
1051
|
+
const keys = Object.keys(value ?? {});
|
|
1052
|
+
const hasMemberFields = keys.some((key) => !RESERVED_KEYS.has(key));
|
|
1053
|
+
const isArrayElement = typeof path.at(-1) !== "string";
|
|
1054
|
+
const removeKeys = ["asset", "media"].concat(keys.filter((key) => ASSET_BOUND_KEYS.has(key))).map((key) => (0, import_sanity4.unset)([key]));
|
|
1055
|
+
onChange(hasMemberFields || isArrayElement ? removeKeys : (0, import_sanity4.unset)());
|
|
1056
|
+
setUpload({ status: "idle" });
|
|
1057
|
+
};
|
|
1058
|
+
const onFilePicked = (event) => {
|
|
1059
|
+
const file = event.target.files?.[0];
|
|
1060
|
+
if (file) {
|
|
1061
|
+
uploadFile(file);
|
|
1062
|
+
}
|
|
1063
|
+
event.target.value = "";
|
|
1064
|
+
};
|
|
1065
|
+
const handlePaste = (event) => {
|
|
1066
|
+
if (readOnly || upload.status === "uploading") {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (isTextEntryTarget(event.target)) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const file = Array.from(event.clipboardData.files).find(
|
|
1073
|
+
(item) => acceptsType(item.type, accept)
|
|
1074
|
+
);
|
|
1075
|
+
if (!file) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
event.preventDefault();
|
|
1079
|
+
uploadFile(file);
|
|
1080
|
+
};
|
|
1081
|
+
const dropHandlers = useDropHandlers({
|
|
1082
|
+
disabled: readOnly || upload.status === "uploading",
|
|
1083
|
+
dragDepth,
|
|
1084
|
+
setDraggingOver,
|
|
1085
|
+
onFile: uploadFile
|
|
1086
|
+
});
|
|
1087
|
+
if (!imageSource) {
|
|
1088
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Card, { padding: 4, radius: 2, tone: "critical", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Text, { size: 1, children: "Image input unavailable: missing project configuration." }) });
|
|
1089
|
+
}
|
|
1090
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Stack, { gap: 3, ...dropHandlers, onPaste: handlePaste, children: [
|
|
1091
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1092
|
+
"input",
|
|
1093
|
+
{
|
|
1094
|
+
ref: fileInputRef,
|
|
1095
|
+
type: "file",
|
|
1096
|
+
accept,
|
|
1097
|
+
hidden: true,
|
|
1098
|
+
"aria-label": "Upload image",
|
|
1099
|
+
onChange: onFilePicked
|
|
1100
|
+
}
|
|
1101
|
+
),
|
|
1102
|
+
upload.status === "uploading" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(UploadingCard, { progress: upload.progress }) : value?.asset ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1103
|
+
ConfirmedPreview,
|
|
1104
|
+
{
|
|
1105
|
+
value,
|
|
1106
|
+
imageSource,
|
|
1107
|
+
config,
|
|
1108
|
+
croppable: Boolean(assetRef && isCroppable(assetRef)),
|
|
1109
|
+
readOnly,
|
|
1110
|
+
draggingOver,
|
|
1111
|
+
onEditCrop: () => {
|
|
1112
|
+
setDialogOpen(true);
|
|
1113
|
+
},
|
|
1114
|
+
onReplace: () => fileInputRef.current?.click(),
|
|
1115
|
+
onRemove: removeImage,
|
|
1116
|
+
selectAffordance: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1117
|
+
SelectExistingButton,
|
|
1118
|
+
{
|
|
1119
|
+
sources: assetSources,
|
|
1120
|
+
disabled: readOnly,
|
|
1121
|
+
fontSize: 1,
|
|
1122
|
+
onBrowse: setBrowsingSource
|
|
1123
|
+
}
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1127
|
+
EmptyDropzone,
|
|
1128
|
+
{
|
|
1129
|
+
readOnly,
|
|
1130
|
+
draggingOver,
|
|
1131
|
+
onUploadClick: () => fileInputRef.current?.click(),
|
|
1132
|
+
selectAffordance: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1133
|
+
SelectExistingButton,
|
|
1134
|
+
{
|
|
1135
|
+
sources: assetSources,
|
|
1136
|
+
disabled: readOnly,
|
|
1137
|
+
onBrowse: setBrowsingSource
|
|
1138
|
+
}
|
|
1139
|
+
)
|
|
1140
|
+
}
|
|
1141
|
+
),
|
|
1142
|
+
upload.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Card, { padding: 3, radius: 2, tone: "critical", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Text, { size: 1, children: upload.message }) }),
|
|
1143
|
+
customFieldMembers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1144
|
+
import_sanity4.ObjectInputMembers,
|
|
1145
|
+
{
|
|
1146
|
+
members: customFieldMembers,
|
|
1147
|
+
...renderObjectProps
|
|
1148
|
+
}
|
|
1149
|
+
),
|
|
1150
|
+
browsingSource && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1151
|
+
browsingSource.component,
|
|
1152
|
+
{
|
|
1153
|
+
assetSource: browsingSource,
|
|
1154
|
+
action: "select",
|
|
1155
|
+
assetType: "image",
|
|
1156
|
+
accept,
|
|
1157
|
+
selectionType: "single",
|
|
1158
|
+
selectedAssets: [],
|
|
1159
|
+
dialogHeaderTitle: "Select image",
|
|
1160
|
+
onClose: () => {
|
|
1161
|
+
setBrowsingSource(null);
|
|
1162
|
+
},
|
|
1163
|
+
onSelect: selectAsset
|
|
1164
|
+
}
|
|
1165
|
+
),
|
|
1166
|
+
dialogOpen && activeAssetRef && dialogSource && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1167
|
+
CropDialog,
|
|
1168
|
+
{
|
|
1169
|
+
assetRef: activeAssetRef,
|
|
1170
|
+
imageSource,
|
|
1171
|
+
sourceDimensions: dialogSource,
|
|
1172
|
+
config,
|
|
1173
|
+
initialCrop: pendingAsset ? void 0 : assetCrop,
|
|
1174
|
+
hotspotEnabled,
|
|
1175
|
+
initialHotspot: pendingAsset ? void 0 : assetHotspot,
|
|
1176
|
+
onConfirm: confirmCropAndHotspot,
|
|
1177
|
+
onClose: () => {
|
|
1178
|
+
setPendingAsset(null);
|
|
1179
|
+
setDialogOpen(false);
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
activeAssetRef
|
|
1183
|
+
)
|
|
1184
|
+
] });
|
|
1185
|
+
};
|
|
1186
|
+
var UploadingCard = ({ progress }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Card, { padding: 5, radius: 2, border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Flex, { direction: "column", align: "center", gap: 3, children: [
|
|
1187
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Spinner, { muted: true }),
|
|
1188
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Text, { size: 1, muted: true, children: [
|
|
1189
|
+
"Uploading\u2026 ",
|
|
1190
|
+
Math.round(progress),
|
|
1191
|
+
"%"
|
|
1192
|
+
] })
|
|
1193
|
+
] }) });
|
|
1194
|
+
var EmptyDropzone = ({
|
|
1195
|
+
readOnly,
|
|
1196
|
+
draggingOver,
|
|
1197
|
+
onUploadClick,
|
|
1198
|
+
selectAffordance
|
|
1199
|
+
}) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1200
|
+
import_ui2.Card,
|
|
1201
|
+
{
|
|
1202
|
+
padding: 5,
|
|
1203
|
+
radius: 2,
|
|
1204
|
+
border: true,
|
|
1205
|
+
tone: draggingOver ? "primary" : "default",
|
|
1206
|
+
style: { borderStyle: "dashed" },
|
|
1207
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Flex, { direction: "column", align: "center", gap: 4, children: [
|
|
1208
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Text, { size: 4, muted: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.ImageIcon, {}) }),
|
|
1209
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Stack, { gap: 2, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Text, { size: 1, muted: true, align: "center", children: draggingOver ? "Drop the image to upload" : "Drag and drop an image here, or" }) }),
|
|
1210
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Inline, { gap: 2, children: [
|
|
1211
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1212
|
+
import_ui2.Button,
|
|
1213
|
+
{
|
|
1214
|
+
icon: import_icons2.UploadIcon,
|
|
1215
|
+
text: "Upload",
|
|
1216
|
+
mode: "ghost",
|
|
1217
|
+
disabled: readOnly,
|
|
1218
|
+
onClick: onUploadClick
|
|
1219
|
+
}
|
|
1220
|
+
),
|
|
1221
|
+
selectAffordance
|
|
1222
|
+
] })
|
|
1223
|
+
] })
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
var ConfirmedPreview = ({
|
|
1227
|
+
value,
|
|
1228
|
+
imageSource,
|
|
1229
|
+
config,
|
|
1230
|
+
croppable,
|
|
1231
|
+
readOnly,
|
|
1232
|
+
draggingOver,
|
|
1233
|
+
onEditCrop,
|
|
1234
|
+
onReplace,
|
|
1235
|
+
onRemove,
|
|
1236
|
+
selectAffordance
|
|
1237
|
+
}) => {
|
|
1238
|
+
const previewUrl = croppedImageUrl(value, imageSource, 640);
|
|
1239
|
+
const dimensions = croppedDimensionsFromValue(value);
|
|
1240
|
+
const dimensionIssue = dimensions ? getDimensionIssue(dimensions, config) : void 0;
|
|
1241
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Stack, { gap: 3, children: [
|
|
1242
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1243
|
+
import_ui2.Card,
|
|
1244
|
+
{
|
|
1245
|
+
radius: 2,
|
|
1246
|
+
border: true,
|
|
1247
|
+
tone: draggingOver ? "primary" : "default",
|
|
1248
|
+
style: { overflow: "hidden" },
|
|
1249
|
+
children: previewUrl ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Flex, { justify: "center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1250
|
+
"img",
|
|
1251
|
+
{
|
|
1252
|
+
src: previewUrl,
|
|
1253
|
+
alt: "",
|
|
1254
|
+
style: { display: "block", maxWidth: "100%", maxHeight: 300 }
|
|
1255
|
+
}
|
|
1256
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Box, { padding: 4, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Text, { size: 1, muted: true, children: "Preview unavailable." }) })
|
|
1257
|
+
}
|
|
1258
|
+
),
|
|
1259
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Flex, { justify: "space-between", align: "center", gap: 3, wrap: "wrap", children: [
|
|
1260
|
+
dimensions && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Text, { size: 1, muted: true, children: [
|
|
1261
|
+
dimensions.width,
|
|
1262
|
+
" \xD7 ",
|
|
1263
|
+
dimensions.height,
|
|
1264
|
+
" px"
|
|
1265
|
+
] }),
|
|
1266
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui2.Inline, { gap: 2, children: [
|
|
1267
|
+
croppable && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1268
|
+
import_ui2.Button,
|
|
1269
|
+
{
|
|
1270
|
+
icon: import_icons2.CropIcon,
|
|
1271
|
+
text: "Edit crop",
|
|
1272
|
+
mode: "ghost",
|
|
1273
|
+
fontSize: 1,
|
|
1274
|
+
disabled: readOnly,
|
|
1275
|
+
onClick: onEditCrop
|
|
1276
|
+
}
|
|
1277
|
+
),
|
|
1278
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1279
|
+
import_ui2.Button,
|
|
1280
|
+
{
|
|
1281
|
+
icon: import_icons2.ImageIcon,
|
|
1282
|
+
text: "Replace",
|
|
1283
|
+
mode: "ghost",
|
|
1284
|
+
fontSize: 1,
|
|
1285
|
+
disabled: readOnly,
|
|
1286
|
+
onClick: onReplace
|
|
1287
|
+
}
|
|
1288
|
+
),
|
|
1289
|
+
selectAffordance,
|
|
1290
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1291
|
+
import_ui2.Button,
|
|
1292
|
+
{
|
|
1293
|
+
icon: import_icons2.TrashIcon,
|
|
1294
|
+
text: "Remove",
|
|
1295
|
+
mode: "bleed",
|
|
1296
|
+
tone: "critical",
|
|
1297
|
+
fontSize: 1,
|
|
1298
|
+
disabled: readOnly,
|
|
1299
|
+
onClick: onRemove
|
|
1300
|
+
}
|
|
1301
|
+
)
|
|
1302
|
+
] })
|
|
1303
|
+
] }),
|
|
1304
|
+
dimensionIssue && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Card, { padding: 3, radius: 2, tone: dimensionIssue.tone, border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Text, { size: 1, children: dimensionIssue.message }) })
|
|
1305
|
+
] });
|
|
1306
|
+
};
|
|
1307
|
+
var SelectExistingButton = ({
|
|
1308
|
+
sources,
|
|
1309
|
+
disabled,
|
|
1310
|
+
fontSize,
|
|
1311
|
+
onBrowse
|
|
1312
|
+
}) => {
|
|
1313
|
+
const [firstSource] = sources;
|
|
1314
|
+
if (!firstSource) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
if (sources.length === 1) {
|
|
1318
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1319
|
+
import_ui2.Button,
|
|
1320
|
+
{
|
|
1321
|
+
icon: import_icons2.ImagesIcon,
|
|
1322
|
+
text: "Select existing",
|
|
1323
|
+
mode: "ghost",
|
|
1324
|
+
fontSize,
|
|
1325
|
+
disabled,
|
|
1326
|
+
onClick: () => {
|
|
1327
|
+
onBrowse(firstSource);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1333
|
+
import_ui2.MenuButton,
|
|
1334
|
+
{
|
|
1335
|
+
id: "image-field-select-existing",
|
|
1336
|
+
button: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1337
|
+
import_ui2.Button,
|
|
1338
|
+
{
|
|
1339
|
+
icon: import_icons2.ImagesIcon,
|
|
1340
|
+
text: "Select existing",
|
|
1341
|
+
mode: "ghost",
|
|
1342
|
+
fontSize,
|
|
1343
|
+
disabled
|
|
1344
|
+
}
|
|
1345
|
+
),
|
|
1346
|
+
menu: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui2.Menu, { children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1347
|
+
import_ui2.MenuItem,
|
|
1348
|
+
{
|
|
1349
|
+
text: source.title ?? source.name,
|
|
1350
|
+
icon: source.icon,
|
|
1351
|
+
fontSize,
|
|
1352
|
+
onClick: () => {
|
|
1353
|
+
onBrowse(source);
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
source.name
|
|
1357
|
+
)) })
|
|
1358
|
+
}
|
|
1359
|
+
);
|
|
1360
|
+
};
|
|
1361
|
+
var ASSET_MANAGED_FIELDS = /* @__PURE__ */ new Set(["asset", "media", "crop", "hotspot"]);
|
|
1362
|
+
var isCustomFieldMember = (member) => member.kind !== "field" || !ASSET_MANAGED_FIELDS.has(member.name);
|
|
1363
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
1364
|
+
"_type",
|
|
1365
|
+
"_key",
|
|
1366
|
+
"_upload",
|
|
1367
|
+
"asset",
|
|
1368
|
+
"crop",
|
|
1369
|
+
"hotspot",
|
|
1370
|
+
"media"
|
|
1371
|
+
]);
|
|
1372
|
+
var ASSET_BOUND_KEYS = /* @__PURE__ */ new Set(["crop", "hotspot", "_upload"]);
|
|
1373
|
+
var getDimensionIssue = (dimensions, config) => {
|
|
1374
|
+
const { requiredDimensions, recommendedDimensions } = config;
|
|
1375
|
+
if (requiredDimensions && !dimensionsAtLeast(dimensions, requiredDimensions)) {
|
|
1376
|
+
return {
|
|
1377
|
+
tone: "critical",
|
|
1378
|
+
message: `Cropped image is below the required ${requiredDimensions.width} \xD7 ${requiredDimensions.height} px. Edit the crop to enlarge it.`
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
if (recommendedDimensions && !dimensionsAtLeast(dimensions, recommendedDimensions)) {
|
|
1382
|
+
return {
|
|
1383
|
+
tone: "caution",
|
|
1384
|
+
message: `Cropped image is below the recommended ${recommendedDimensions.width} \xD7 ${recommendedDimensions.height} px.`
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
return void 0;
|
|
1388
|
+
};
|
|
1389
|
+
var hasFiles = (event) => Array.from(event.dataTransfer.types).includes("Files");
|
|
1390
|
+
var useDropHandlers = ({
|
|
1391
|
+
disabled,
|
|
1392
|
+
dragDepth,
|
|
1393
|
+
setDraggingOver,
|
|
1394
|
+
onFile
|
|
1395
|
+
}) => {
|
|
1396
|
+
return {
|
|
1397
|
+
onDragEnter: (event) => {
|
|
1398
|
+
if (disabled || !hasFiles(event)) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
event.preventDefault();
|
|
1402
|
+
dragDepth.current += 1;
|
|
1403
|
+
setDraggingOver(true);
|
|
1404
|
+
},
|
|
1405
|
+
onDragOver: (event) => {
|
|
1406
|
+
if (disabled || !hasFiles(event)) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
event.preventDefault();
|
|
1410
|
+
event.dataTransfer.dropEffect = "copy";
|
|
1411
|
+
},
|
|
1412
|
+
onDragLeave: (event) => {
|
|
1413
|
+
if (disabled) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
event.preventDefault();
|
|
1417
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
1418
|
+
if (dragDepth.current === 0) {
|
|
1419
|
+
setDraggingOver(false);
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
onDrop: (event) => {
|
|
1423
|
+
if (disabled) {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
event.preventDefault();
|
|
1427
|
+
dragDepth.current = 0;
|
|
1428
|
+
setDraggingOver(false);
|
|
1429
|
+
const file = event.dataTransfer.files.item(0);
|
|
1430
|
+
if (file) {
|
|
1431
|
+
onFile(file);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// src/defineImageField.ts
|
|
1438
|
+
var import_sanity5 = require("sanity");
|
|
1439
|
+
var defineImageField = ({
|
|
1440
|
+
aspectRatio,
|
|
1441
|
+
recommendedDimensions,
|
|
1442
|
+
requiredDimensions,
|
|
1443
|
+
options,
|
|
1444
|
+
validation,
|
|
1445
|
+
...overrides
|
|
1446
|
+
}) => {
|
|
1447
|
+
if (aspectRatio !== void 0) {
|
|
1448
|
+
validateAspectRatio(aspectRatio);
|
|
1449
|
+
}
|
|
1450
|
+
const imageField = {
|
|
1451
|
+
aspectRatio,
|
|
1452
|
+
recommendedDimensions,
|
|
1453
|
+
requiredDimensions
|
|
1454
|
+
};
|
|
1455
|
+
return (0, import_sanity5.defineField)({
|
|
1456
|
+
...overrides,
|
|
1457
|
+
type: "image",
|
|
1458
|
+
options: { ...options, imageField },
|
|
1459
|
+
components: { input: ImageFieldInput },
|
|
1460
|
+
validation: (rule, context) => {
|
|
1461
|
+
const rules = [];
|
|
1462
|
+
if (requiredDimensions) {
|
|
1463
|
+
rules.push(
|
|
1464
|
+
rule.custom(
|
|
1465
|
+
(value) => validateCroppedSize(value, requiredDimensions, "error")
|
|
1466
|
+
)
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
if (recommendedDimensions) {
|
|
1470
|
+
rules.push(
|
|
1471
|
+
rule.custom(
|
|
1472
|
+
(value) => validateCroppedSize(value, recommendedDimensions, "warning")
|
|
1473
|
+
).warning()
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
if (validation) {
|
|
1477
|
+
const userRules = validation(rule, context);
|
|
1478
|
+
rules.push(...Array.isArray(userRules) ? userRules : [userRules]);
|
|
1479
|
+
}
|
|
1480
|
+
return rules;
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
};
|
|
1484
|
+
var validateCroppedSize = (value, threshold, level) => {
|
|
1485
|
+
const dimensions = croppedDimensionsFromValue(value);
|
|
1486
|
+
if (!dimensions || dimensionsAtLeast(dimensions, threshold)) {
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
const verb = level === "warning" ? "should" : "must";
|
|
1490
|
+
const suffix = level === "warning" ? " for best results" : "";
|
|
1491
|
+
return `Cropped image ${verb} be at least ${threshold.width} \xD7 ${threshold.height} px${suffix}`;
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/formComponents.tsx
|
|
1495
|
+
var import_sanity6 = require("sanity");
|
|
1496
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1497
|
+
var isImageInput = (props) => {
|
|
1498
|
+
for (let current = props.schemaType; current; current = current.type ?? void 0) {
|
|
1499
|
+
if (current.name === "image") {
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return false;
|
|
1504
|
+
};
|
|
1505
|
+
var imageFieldFormInput = (props) => isImageInput(props) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ImageFieldInput, { ...props }) : props.renderDefault(props);
|
|
1506
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1507
|
+
0 && (module.exports = {
|
|
1508
|
+
ImageFieldInput,
|
|
1509
|
+
defineImageField,
|
|
1510
|
+
imageFieldFormInput
|
|
1511
|
+
});
|
|
1512
|
+
//# sourceMappingURL=index.cjs.map
|