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/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