sanity-plugin-image-resizer 1.1.0 → 1.1.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/_chunks-cjs/resources.js +4 -3
- package/dist/_chunks-cjs/resources.js.map +1 -1
- package/dist/_chunks-es/resources.mjs +4 -3
- package/dist/_chunks-es/resources.mjs.map +1 -1
- package/dist/index.js +28 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/i18n/resources.ts +6 -3
- package/src/tool/ImageResizer.tsx +55 -33
|
@@ -13,10 +13,8 @@ var resources = {
|
|
|
13
13
|
"action.refresh": "Refresh",
|
|
14
14
|
/** Process-all button label ({{count}} = number of pending assets) */
|
|
15
15
|
"action.process-all": "Process All ({{count}})",
|
|
16
|
-
/** Label for the button that
|
|
16
|
+
/** Label for the button that stops processing (with live count) */
|
|
17
17
|
"action.finish-ongoing": "Finish ongoing tasks ({{count}})",
|
|
18
|
-
/** Label for the stop button that cancels the queue immediately */
|
|
19
|
-
"action.stop-all": "Stop All (possible data loss)",
|
|
20
18
|
// ── Status badges ───────────────────────────────────────────────────────
|
|
21
19
|
/** Pending badge ({{count}} = number) */
|
|
22
20
|
"status.pending": "{{count}} pending",
|
|
@@ -40,6 +38,9 @@ var resources = {
|
|
|
40
38
|
"settings.tiff-to-jpg": "Convert TIFF \u2192 JPG (instead of WebP)",
|
|
41
39
|
/** Hint below toggles */
|
|
42
40
|
"settings.apply-hint": "Changes apply on next Refresh.",
|
|
41
|
+
// ── Navigation guard ────────────────────────────────────────────────────
|
|
42
|
+
/** Confirm dialog shown when navigating away during processing */
|
|
43
|
+
"nav.confirm-leave": "Image processing is in progress ({{count}}). Leaving now may cause data loss. Are you sure?",
|
|
43
44
|
// ── Asset card ──────────────────────────────────────────────────────────
|
|
44
45
|
/** Violation badge: TIFF → WebP */
|
|
45
46
|
"violation.tiff-to-webp": "TIFF \u2192 WebP",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resources.js","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {\n // ── Tool ────────────────────────────────────────────────────────────────\n /** Tool title shown in the Studio sidebar */\n 'tool.title': 'Image Resizer',\n\n // ── Header ──────────────────────────────────────────────────────────────\n /** Main heading on the tool page */\n 'header.title': 'Image Resizer',\n /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */\n 'header.description':\n 'Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.',\n\n // ── Actions ─────────────────────────────────────────────────────────────\n /** Refresh button label */\n 'action.refresh': 'Refresh',\n /** Process-all button label ({{count}} = number of pending assets) */\n 'action.process-all': 'Process All ({{count}})',\n /** Label for the button that
|
|
1
|
+
{"version":3,"file":"resources.js","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {\n // ── Tool ────────────────────────────────────────────────────────────────\n /** Tool title shown in the Studio sidebar */\n 'tool.title': 'Image Resizer',\n\n // ── Header ──────────────────────────────────────────────────────────────\n /** Main heading on the tool page */\n 'header.title': 'Image Resizer',\n /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */\n 'header.description':\n 'Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.',\n\n // ── Actions ─────────────────────────────────────────────────────────────\n /** Refresh button label */\n 'action.refresh': 'Refresh',\n /** Process-all button label ({{count}} = number of pending assets) */\n 'action.process-all': 'Process All ({{count}})',\n /** Label for the button that stops processing (with live count) */\n 'action.finish-ongoing': 'Finish ongoing tasks ({{count}})',\n\n // ── Status badges ───────────────────────────────────────────────────────\n /** Pending badge ({{count}} = number) */\n 'status.pending': '{{count}} pending',\n /** Processing badge */\n 'status.processing': '{{count}} processing',\n /** Done badge */\n 'status.done': '{{count}} done',\n /** Failed badge */\n 'status.failed': '{{count}} failed',\n\n // ── Empty / loading states ──────────────────────────────────────────────\n /** Shown while scanning assets */\n 'state.scanning': 'Scanning assets…',\n /** Shown when no violating assets are found */\n 'state.all-good': 'All images meet the requirements.',\n\n // ── Settings dialog ─────────────────────────────────────────────────────\n /** Settings dialog header */\n 'settings.title': 'Conversion Settings',\n /** PNG → WebP toggle label */\n 'settings.png-to-webp': 'Convert PNG → WebP',\n /** TIFF → JPG toggle label */\n 'settings.tiff-to-jpg': 'Convert TIFF → JPG (instead of WebP)',\n /** Hint below toggles */\n 'settings.apply-hint': 'Changes apply on next Refresh.',\n\n // ── Navigation guard ────────────────────────────────────────────────────\n /** Confirm dialog shown when navigating away during processing */\n 'nav.confirm-leave':\n 'Image processing is in progress ({{count}}). Leaving now may cause data loss. Are you sure?',\n\n // ── Asset card ──────────────────────────────────────────────────────────\n /** Violation badge: TIFF → WebP */\n 'violation.tiff-to-webp': 'TIFF → WebP',\n /** Violation badge: TIFF → JPG */\n 'violation.tiff-to-jpg': 'TIFF → JPG',\n /** Violation badge: PNG → WebP */\n 'violation.png-to-webp': 'PNG → WebP',\n /** Violation badge: exceeds max width ({{maxWidth}} = pixel limit) */\n 'violation.width': '> {{maxWidth}}px',\n /** Violation badge: exceeds max size ({{maxSize}} = MB limit) */\n 'violation.size': '> {{maxSize}} MB',\n /** Asset size summary ({{size}} MB — {{width}}px wide) */\n 'asset.summary': '{{size}} MB — {{width}}px wide',\n /** Asset done summary ({{oldSize}} MB → {{newSize}} MB — {{width}}px wide) */\n 'asset.summary-done': '{{oldSize}} MB → {{newSize}} MB{{reduction}} — {{width}}px wide',\n /** Reduction percentage shown after size (e.g. \" (−32%)\") */\n 'asset.reduction': ' (−{{percent}}%)',\n /** Width reduction badge ({{oldWidth}}px → {{newWidth}}px) */\n 'asset.width-reduced': '{{oldWidth}}px → {{newWidth}}px',\n /** Process button */\n 'asset.process': 'Process',\n /** Done badge */\n 'asset.done': 'Done',\n /** Retry button */\n 'asset.retry': 'Retry',\n}\n"],"names":[],"mappings":";AAAA,IAAA,YAAe;AAAA;AAAA;AAAA,EAGX,cAAc;AAAA;AAAA;AAAA,EAId,gBAAgB;AAAA;AAAA,EAEhB,sBACI;AAAA;AAAA;AAAA,EAIJ,kBAAkB;AAAA;AAAA,EAElB,sBAAsB;AAAA;AAAA,EAEtB,yBAAyB;AAAA;AAAA;AAAA,EAIzB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA;AAAA,EAIjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA,EAIlB,kBAAkB;AAAA;AAAA,EAElB,wBAAwB;AAAA;AAAA,EAExB,wBAAwB;AAAA;AAAA,EAExB,uBAAuB;AAAA;AAAA;AAAA,EAIvB,qBACI;AAAA;AAAA;AAAA,EAIJ,0BAA0B;AAAA;AAAA,EAE1B,yBAAyB;AAAA;AAAA,EAEzB,yBAAyB;AAAA;AAAA,EAEzB,mBAAmB;AAAA;AAAA,EAEnB,kBAAkB;AAAA;AAAA,EAElB,iBAAiB;AAAA;AAAA,EAEjB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA;AAAA,EAEnB,uBAAuB;AAAA;AAAA,EAEvB,iBAAiB;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA,EAEd,eAAe;AACnB;;"}
|
|
@@ -12,10 +12,8 @@ var resources = {
|
|
|
12
12
|
"action.refresh": "Refresh",
|
|
13
13
|
/** Process-all button label ({{count}} = number of pending assets) */
|
|
14
14
|
"action.process-all": "Process All ({{count}})",
|
|
15
|
-
/** Label for the button that
|
|
15
|
+
/** Label for the button that stops processing (with live count) */
|
|
16
16
|
"action.finish-ongoing": "Finish ongoing tasks ({{count}})",
|
|
17
|
-
/** Label for the stop button that cancels the queue immediately */
|
|
18
|
-
"action.stop-all": "Stop All (possible data loss)",
|
|
19
17
|
// ── Status badges ───────────────────────────────────────────────────────
|
|
20
18
|
/** Pending badge ({{count}} = number) */
|
|
21
19
|
"status.pending": "{{count}} pending",
|
|
@@ -39,6 +37,9 @@ var resources = {
|
|
|
39
37
|
"settings.tiff-to-jpg": "Convert TIFF \u2192 JPG (instead of WebP)",
|
|
40
38
|
/** Hint below toggles */
|
|
41
39
|
"settings.apply-hint": "Changes apply on next Refresh.",
|
|
40
|
+
// ── Navigation guard ────────────────────────────────────────────────────
|
|
41
|
+
/** Confirm dialog shown when navigating away during processing */
|
|
42
|
+
"nav.confirm-leave": "Image processing is in progress ({{count}}). Leaving now may cause data loss. Are you sure?",
|
|
42
43
|
// ── Asset card ──────────────────────────────────────────────────────────
|
|
43
44
|
/** Violation badge: TIFF → WebP */
|
|
44
45
|
"violation.tiff-to-webp": "TIFF \u2192 WebP",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resources.mjs","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {\n // ── Tool ────────────────────────────────────────────────────────────────\n /** Tool title shown in the Studio sidebar */\n 'tool.title': 'Image Resizer',\n\n // ── Header ──────────────────────────────────────────────────────────────\n /** Main heading on the tool page */\n 'header.title': 'Image Resizer',\n /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */\n 'header.description':\n 'Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.',\n\n // ── Actions ─────────────────────────────────────────────────────────────\n /** Refresh button label */\n 'action.refresh': 'Refresh',\n /** Process-all button label ({{count}} = number of pending assets) */\n 'action.process-all': 'Process All ({{count}})',\n /** Label for the button that
|
|
1
|
+
{"version":3,"file":"resources.mjs","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {\n // ── Tool ────────────────────────────────────────────────────────────────\n /** Tool title shown in the Studio sidebar */\n 'tool.title': 'Image Resizer',\n\n // ── Header ──────────────────────────────────────────────────────────────\n /** Main heading on the tool page */\n 'header.title': 'Image Resizer',\n /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */\n 'header.description':\n 'Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.',\n\n // ── Actions ─────────────────────────────────────────────────────────────\n /** Refresh button label */\n 'action.refresh': 'Refresh',\n /** Process-all button label ({{count}} = number of pending assets) */\n 'action.process-all': 'Process All ({{count}})',\n /** Label for the button that stops processing (with live count) */\n 'action.finish-ongoing': 'Finish ongoing tasks ({{count}})',\n\n // ── Status badges ───────────────────────────────────────────────────────\n /** Pending badge ({{count}} = number) */\n 'status.pending': '{{count}} pending',\n /** Processing badge */\n 'status.processing': '{{count}} processing',\n /** Done badge */\n 'status.done': '{{count}} done',\n /** Failed badge */\n 'status.failed': '{{count}} failed',\n\n // ── Empty / loading states ──────────────────────────────────────────────\n /** Shown while scanning assets */\n 'state.scanning': 'Scanning assets…',\n /** Shown when no violating assets are found */\n 'state.all-good': 'All images meet the requirements.',\n\n // ── Settings dialog ─────────────────────────────────────────────────────\n /** Settings dialog header */\n 'settings.title': 'Conversion Settings',\n /** PNG → WebP toggle label */\n 'settings.png-to-webp': 'Convert PNG → WebP',\n /** TIFF → JPG toggle label */\n 'settings.tiff-to-jpg': 'Convert TIFF → JPG (instead of WebP)',\n /** Hint below toggles */\n 'settings.apply-hint': 'Changes apply on next Refresh.',\n\n // ── Navigation guard ────────────────────────────────────────────────────\n /** Confirm dialog shown when navigating away during processing */\n 'nav.confirm-leave':\n 'Image processing is in progress ({{count}}). Leaving now may cause data loss. Are you sure?',\n\n // ── Asset card ──────────────────────────────────────────────────────────\n /** Violation badge: TIFF → WebP */\n 'violation.tiff-to-webp': 'TIFF → WebP',\n /** Violation badge: TIFF → JPG */\n 'violation.tiff-to-jpg': 'TIFF → JPG',\n /** Violation badge: PNG → WebP */\n 'violation.png-to-webp': 'PNG → WebP',\n /** Violation badge: exceeds max width ({{maxWidth}} = pixel limit) */\n 'violation.width': '> {{maxWidth}}px',\n /** Violation badge: exceeds max size ({{maxSize}} = MB limit) */\n 'violation.size': '> {{maxSize}} MB',\n /** Asset size summary ({{size}} MB — {{width}}px wide) */\n 'asset.summary': '{{size}} MB — {{width}}px wide',\n /** Asset done summary ({{oldSize}} MB → {{newSize}} MB — {{width}}px wide) */\n 'asset.summary-done': '{{oldSize}} MB → {{newSize}} MB{{reduction}} — {{width}}px wide',\n /** Reduction percentage shown after size (e.g. \" (−32%)\") */\n 'asset.reduction': ' (−{{percent}}%)',\n /** Width reduction badge ({{oldWidth}}px → {{newWidth}}px) */\n 'asset.width-reduced': '{{oldWidth}}px → {{newWidth}}px',\n /** Process button */\n 'asset.process': 'Process',\n /** Done badge */\n 'asset.done': 'Done',\n /** Retry button */\n 'asset.retry': 'Retry',\n}\n"],"names":[],"mappings":"AAAA,IAAA,YAAe;AAAA;AAAA;AAAA,EAGX,cAAc;AAAA;AAAA;AAAA,EAId,gBAAgB;AAAA;AAAA,EAEhB,sBACI;AAAA;AAAA;AAAA,EAIJ,kBAAkB;AAAA;AAAA,EAElB,sBAAsB;AAAA;AAAA,EAEtB,yBAAyB;AAAA;AAAA;AAAA,EAIzB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA;AAAA,EAIjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA,EAIlB,kBAAkB;AAAA;AAAA,EAElB,wBAAwB;AAAA;AAAA,EAExB,wBAAwB;AAAA;AAAA,EAExB,uBAAuB;AAAA;AAAA;AAAA,EAIvB,qBACI;AAAA;AAAA;AAAA,EAIJ,0BAA0B;AAAA;AAAA,EAE1B,yBAAyB;AAAA;AAAA,EAEzB,yBAAyB;AAAA;AAAA,EAEzB,mBAAmB;AAAA;AAAA,EAEnB,kBAAkB;AAAA;AAAA,EAElB,iBAAiB;AAAA;AAAA,EAEjB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA;AAAA,EAEnB,uBAAuB;AAAA;AAAA,EAEvB,iBAAiB;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA,EAEd,eAAe;AACnB;"}
|
package/dist/index.js
CHANGED
|
@@ -247,7 +247,7 @@ function ImageResizerView() {
|
|
|
247
247
|
react.useEffect(() => {
|
|
248
248
|
assetsRef.current = assets;
|
|
249
249
|
}, [assets]);
|
|
250
|
-
const cancelRef = react.useRef(!1), fetchAssets = react.useCallback(async () => {
|
|
250
|
+
const cancelRef = react.useRef(!1), processingCountRef = react.useRef(0), confirmMsgRef = react.useRef(""), fetchAssets = react.useCallback(async () => {
|
|
251
251
|
setLoading(!0);
|
|
252
252
|
try {
|
|
253
253
|
const raw = await client.fetch(
|
|
@@ -342,19 +342,9 @@ function ImageResizerView() {
|
|
|
342
342
|
}
|
|
343
343
|
};
|
|
344
344
|
await Promise.all(Array.from({ length: CONCURRENCY }, () => next())), setProcessingAll(!1);
|
|
345
|
-
}, [processAsset])
|
|
346
|
-
react.useCallback(() => {
|
|
345
|
+
}, [processAsset]), stopProcessing = react.useCallback(() => {
|
|
347
346
|
cancelRef.current = !0;
|
|
348
|
-
}, []), react.
|
|
349
|
-
if (!processingAll) return;
|
|
350
|
-
const handler = (e) => {
|
|
351
|
-
e.preventDefault();
|
|
352
|
-
};
|
|
353
|
-
return window.addEventListener("beforeunload", handler), () => window.removeEventListener("beforeunload", handler);
|
|
354
|
-
}, [processingAll]), react.useEffect(() => () => {
|
|
355
|
-
cancelRef.current = !0;
|
|
356
|
-
}, []);
|
|
357
|
-
const counts = react.useMemo(
|
|
347
|
+
}, []), counts = react.useMemo(
|
|
358
348
|
() => ({
|
|
359
349
|
pending: assets.filter((a) => a.status === "idle").length,
|
|
360
350
|
processing: assets.filter((a) => a.status === "processing").length,
|
|
@@ -363,7 +353,27 @@ function ImageResizerView() {
|
|
|
363
353
|
}),
|
|
364
354
|
[assets]
|
|
365
355
|
);
|
|
366
|
-
return
|
|
356
|
+
return react.useEffect(() => {
|
|
357
|
+
processingCountRef.current = counts.processing, confirmMsgRef.current = t("nav.confirm-leave", { count: counts.processing });
|
|
358
|
+
}, [counts.processing, t]), react.useEffect(() => {
|
|
359
|
+
const handler = (e) => {
|
|
360
|
+
processingCountRef.current !== 0 && (e.preventDefault(), e.returnValue = "");
|
|
361
|
+
};
|
|
362
|
+
return window.addEventListener("beforeunload", handler), () => window.removeEventListener("beforeunload", handler);
|
|
363
|
+
}, []), react.useEffect(() => {
|
|
364
|
+
const original = history.pushState.bind(history);
|
|
365
|
+
return history.pushState = function(...args) {
|
|
366
|
+
if (processingCountRef.current > 0) {
|
|
367
|
+
if (!window.confirm(confirmMsgRef.current)) return;
|
|
368
|
+
cancelRef.current = !0;
|
|
369
|
+
}
|
|
370
|
+
original(...args);
|
|
371
|
+
}, () => {
|
|
372
|
+
history.pushState = original;
|
|
373
|
+
};
|
|
374
|
+
}, []), react.useEffect(() => () => {
|
|
375
|
+
cancelRef.current = !0;
|
|
376
|
+
}, []), /* @__PURE__ */ jsxRuntime.jsxs(
|
|
367
377
|
ui.Container,
|
|
368
378
|
{
|
|
369
379
|
width: 4,
|
|
@@ -406,7 +416,7 @@ function ImageResizerView() {
|
|
|
406
416
|
disabled: loading || processingAll
|
|
407
417
|
}
|
|
408
418
|
),
|
|
409
|
-
counts.pending > 0 &&
|
|
419
|
+
counts.pending > 0 && counts.processing === 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
410
420
|
ui.Button,
|
|
411
421
|
{
|
|
412
422
|
text: t("action.process-all", { count: counts.pending }),
|
|
@@ -415,17 +425,16 @@ function ImageResizerView() {
|
|
|
415
425
|
disabled: loading
|
|
416
426
|
}
|
|
417
427
|
),
|
|
418
|
-
|
|
428
|
+
counts.processing > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
419
429
|
ui.Button,
|
|
420
430
|
{
|
|
421
431
|
text: t("action.finish-ongoing", {
|
|
422
432
|
count: counts.processing
|
|
423
433
|
}),
|
|
424
434
|
tone: "caution",
|
|
425
|
-
|
|
426
|
-
disabled: !0
|
|
435
|
+
onClick: stopProcessing
|
|
427
436
|
}
|
|
428
|
-
)
|
|
437
|
+
)
|
|
429
438
|
] })
|
|
430
439
|
] }),
|
|
431
440
|
!loading && assets.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, wrap: "wrap", children: [
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator, type SanityClient } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * a reference points to `oldId` (either via `asset._ref` or a direct\n * `_ref`). Returns a flat `{ path: newRef }` object compatible with\n * `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n // Match image fields with asset._ref\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n // Match direct references (e.g. inside media.tag arrays)\n if (\n record._type === 'reference' &&\n record._ref === oldId\n ) {\n const refPath = path || '_ref'\n return { [refPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n/**\n * Metadata fields to preserve when replacing an image asset.\n * Covers built-in Sanity asset fields and sanity-plugin-media tags.\n */\nconst ASSET_METADATA_FIELDS = [\n 'title',\n 'description',\n 'altText',\n 'creditLine',\n 'source',\n 'opt',\n] as const\n\n/**\n * Copies user-editable metadata (title, description, alt text, credit,\n * source, and opt.media.tags) from the old asset to the new one.\n */\nexport async function copyAssetMetadata(\n client: SanityClient,\n oldAssetId: string,\n newAssetId: string,\n): Promise<void> {\n const projection = ASSET_METADATA_FIELDS.join(', ')\n const oldMeta = await client.fetch(\n `*[_id == $id][0]{ ${projection} }`,\n { id: oldAssetId },\n )\n if (!oldMeta) return\n\n // Build a flat patch with only the fields that actually exist\n const patch: Record<string, unknown> = {}\n for (const field of ASSET_METADATA_FIELDS) {\n if (oldMeta[field] !== undefined && oldMeta[field] !== null) {\n patch[field] = oldMeta[field]\n }\n }\n\n if (Object.keys(patch).length > 0) {\n await client.patch(newAssetId).set(patch).commit()\n }\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport { useTranslation } from 'sanity'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\nimport { imageResizerLocaleNamespace } from '../../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(\n settings: ConversionSettings,\n t: (key: string) => string\n): string {\n const parts: string[] = []\n parts.push(\n settings.tiffToJpg\n ? t('violation.tiff-to-jpg')\n : t('violation.tiff-to-webp')\n )\n if (settings.pngToWebp) parts.push(t('violation.png-to-webp'))\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n t,\n}: {\n type: Violation\n settings: ConversionSettings\n t: (key: string, params?: Record<string, unknown>) => string\n}) {\n let label: string\n if (type === 'format') {\n label = formatViolationLabel(settings, t)\n } else if (type === 'width') {\n label = t('violation.width', { maxWidth: IMAGE_MAX_WIDTH })\n } else {\n label = t('violation.size', { maxSize: MAX_SIZE_MB })\n }\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary-done', {\n oldSize: (asset.size / 1024 / 1024).toFixed(1),\n newSize: (asset.newSize! / 1024 / 1024).toFixed(1),\n reduction:\n sizeReduction !== null && sizeReduction > 0\n ? t('asset.reduction', { percent: sizeReduction })\n : '',\n width: asset.newWidth,\n })}\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {t('asset.width-reduced', {\n oldWidth: asset.width,\n newWidth: asset.newWidth,\n })}\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} t={t} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary', {\n size: (asset.size / 1024 / 1024).toFixed(1),\n width: asset.width,\n })}\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text={t('asset.process')}\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && (\n <Badge tone=\"positive\">{t('asset.done')}</Badge>\n )}\n {asset.status === 'error' && (\n <Button\n text={t('asset.retry')}\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient, useTranslation } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n copyAssetMetadata,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\nimport { imageResizerLocaleNamespace } from '../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // Cancellation flag — checked between items in the batch loop\n const cancelRef = useRef(false)\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )] | order(size desc) [0...500] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset\n await copyAssetMetadata(client, asset._id, newAsset._id)\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n cancelRef.current = false\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n if (cancelRef.current) break\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n /** Cancels the batch queue (in-flight items finish, no new ones start). */\n const stopProcessing = useCallback(() => {\n cancelRef.current = true\n }, [])\n\n // ── navigation guards ───────────────────────────────────────────────────\n\n // Warn before browser close / refresh while processing\n useEffect(() => {\n if (!processingAll) return\n const handler = (e: BeforeUnloadEvent) => {\n e.preventDefault()\n }\n window.addEventListener('beforeunload', handler)\n return () => window.removeEventListener('beforeunload', handler)\n }, [processingAll])\n\n // Cancel queue when the component unmounts (in-app navigation)\n useEffect(() => {\n return () => {\n cancelRef.current = true\n }\n }, [])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>{t('header.title')}</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n {t('header.description', {\n maxWidth: IMAGE_MAX_WIDTH,\n maxSize: MAX_SIZE_MB,\n })}\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text={t('action.refresh')}\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && !processingAll && (\n <Button\n text={t('action.process-all', { count: counts.pending })}\n tone=\"primary\"\n onClick={processAll}\n disabled={loading}\n />\n )}\n {processingAll && (\n <Flex gap={2}>\n <Button\n text={t('action.finish-ongoing', {\n count: counts.processing,\n })}\n tone=\"caution\"\n mode=\"ghost\"\n disabled\n />\n {/* <Button\n text={t('action.stop-all')}\n tone=\"critical\"\n onClick={stopProcessing}\n /> */}\n </Flex>\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">\n {t('status.pending', { count: counts.pending })}\n </Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">\n {t('status.processing', { count: counts.processing })}\n </Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">\n {t('status.done', { count: counts.done })}\n </Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">\n {t('status.failed', { count: counts.error })}\n </Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>{t('state.scanning')}</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">{t('state.all-good')}</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header={t('settings.title')}\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n {t('settings.png-to-webp')}\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n {t('settings.tiff-to-jpg')}\n </Label>\n </Flex>\n <Text size={1} muted>\n {t('settings.apply-hint')}\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport {\n imageResizerUsEnglishLocaleBundle,\n imageResizerLocaleNamespace,\n} from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n i18n: {\n title: {\n key: 'tool.title',\n ns: imageResizerLocaleNamespace,\n },\n },\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["defineLocaleResourceBundle","IMAGE_ACCEPT","IMAGE_MAX_SIZE","IMAGE_MAX_WIDTH","MAX_SIZE_MB","jsx","Badge","useTranslation","Card","jsxs","Flex","Box","Stack","Text","Fragment","Button","Spinner","useClient","useState","useCallback","useRef","useEffect","useMemo","Container","Heading","CogIcon","Dialog","Switch","Label","definePlugin"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator, type SanityClient } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * a reference points to `oldId` (either via `asset._ref` or a direct\n * `_ref`). Returns a flat `{ path: newRef }` object compatible with\n * `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n // Match image fields with asset._ref\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n // Match direct references (e.g. inside media.tag arrays)\n if (\n record._type === 'reference' &&\n record._ref === oldId\n ) {\n const refPath = path || '_ref'\n return { [refPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n/**\n * Metadata fields to preserve when replacing an image asset.\n * Covers built-in Sanity asset fields and sanity-plugin-media tags.\n */\nconst ASSET_METADATA_FIELDS = [\n 'title',\n 'description',\n 'altText',\n 'creditLine',\n 'source',\n 'opt',\n] as const\n\n/**\n * Copies user-editable metadata (title, description, alt text, credit,\n * source, and opt.media.tags) from the old asset to the new one.\n */\nexport async function copyAssetMetadata(\n client: SanityClient,\n oldAssetId: string,\n newAssetId: string,\n): Promise<void> {\n const projection = ASSET_METADATA_FIELDS.join(', ')\n const oldMeta = await client.fetch(\n `*[_id == $id][0]{ ${projection} }`,\n { id: oldAssetId },\n )\n if (!oldMeta) return\n\n // Build a flat patch with only the fields that actually exist\n const patch: Record<string, unknown> = {}\n for (const field of ASSET_METADATA_FIELDS) {\n if (oldMeta[field] !== undefined && oldMeta[field] !== null) {\n patch[field] = oldMeta[field]\n }\n }\n\n if (Object.keys(patch).length > 0) {\n await client.patch(newAssetId).set(patch).commit()\n }\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport { useTranslation } from 'sanity'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\nimport { imageResizerLocaleNamespace } from '../../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(\n settings: ConversionSettings,\n t: (key: string) => string\n): string {\n const parts: string[] = []\n parts.push(\n settings.tiffToJpg\n ? t('violation.tiff-to-jpg')\n : t('violation.tiff-to-webp')\n )\n if (settings.pngToWebp) parts.push(t('violation.png-to-webp'))\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n t,\n}: {\n type: Violation\n settings: ConversionSettings\n t: (key: string, params?: Record<string, unknown>) => string\n}) {\n let label: string\n if (type === 'format') {\n label = formatViolationLabel(settings, t)\n } else if (type === 'width') {\n label = t('violation.width', { maxWidth: IMAGE_MAX_WIDTH })\n } else {\n label = t('violation.size', { maxSize: MAX_SIZE_MB })\n }\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary-done', {\n oldSize: (asset.size / 1024 / 1024).toFixed(1),\n newSize: (asset.newSize! / 1024 / 1024).toFixed(1),\n reduction:\n sizeReduction !== null && sizeReduction > 0\n ? t('asset.reduction', { percent: sizeReduction })\n : '',\n width: asset.newWidth,\n })}\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {t('asset.width-reduced', {\n oldWidth: asset.width,\n newWidth: asset.newWidth,\n })}\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} t={t} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary', {\n size: (asset.size / 1024 / 1024).toFixed(1),\n width: asset.width,\n })}\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text={t('asset.process')}\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && (\n <Badge tone=\"positive\">{t('asset.done')}</Badge>\n )}\n {asset.status === 'error' && (\n <Button\n text={t('asset.retry')}\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient, useTranslation } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n copyAssetMetadata,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\nimport { imageResizerLocaleNamespace } from '../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // Cancellation flag — checked between items in the batch loop\n const cancelRef = useRef(false)\n\n // Ref mirrors counts.processing for use inside non-React callbacks\n const processingCountRef = useRef(0)\n // Ref for the i18n confirm message, updated reactively\n const confirmMsgRef = useRef('')\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )] | order(size desc) [0...500] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset\n await copyAssetMetadata(client, asset._id, newAsset._id)\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n cancelRef.current = false\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n if (cancelRef.current) break\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n /** Cancels the batch queue (in-flight items finish, no new ones start). */\n const stopProcessing = useCallback(() => {\n cancelRef.current = true\n }, [])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── navigation guards ───────────────────────────────────────────────────\n\n // Keep refs in sync for use inside non-React callbacks\n useEffect(() => {\n processingCountRef.current = counts.processing\n confirmMsgRef.current = t('nav.confirm-leave', { count: counts.processing })\n }, [counts.processing, t])\n\n // Warn before browser close / refresh while processing\n useEffect(() => {\n const handler = (e: BeforeUnloadEvent) => {\n if (processingCountRef.current === 0) return\n e.preventDefault()\n e.returnValue = ''\n }\n window.addEventListener('beforeunload', handler)\n return () => window.removeEventListener('beforeunload', handler)\n }, [])\n\n // Intercept in-app navigation (Sanity router uses history.pushState)\n useEffect(() => {\n const original = history.pushState.bind(history)\n history.pushState = function (\n ...args: Parameters<typeof history.pushState>\n ) {\n if (processingCountRef.current > 0) {\n // eslint-disable-next-line no-alert\n if (!window.confirm(confirmMsgRef.current)) return\n cancelRef.current = true\n }\n original(...args)\n }\n return () => {\n history.pushState = original\n }\n }, [])\n\n // Cancel queue when the component unmounts (e.g. forced navigation)\n useEffect(() => {\n return () => {\n cancelRef.current = true\n }\n }, [])\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>{t('header.title')}</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n {t('header.description', {\n maxWidth: IMAGE_MAX_WIDTH,\n maxSize: MAX_SIZE_MB,\n })}\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text={t('action.refresh')}\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && counts.processing === 0 && (\n <Button\n text={t('action.process-all', { count: counts.pending })}\n tone=\"primary\"\n onClick={processAll}\n disabled={loading}\n />\n )}\n {counts.processing > 0 && (\n <Button\n text={t('action.finish-ongoing', {\n count: counts.processing,\n })}\n tone=\"caution\"\n onClick={stopProcessing}\n />\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">\n {t('status.pending', { count: counts.pending })}\n </Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">\n {t('status.processing', { count: counts.processing })}\n </Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">\n {t('status.done', { count: counts.done })}\n </Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">\n {t('status.failed', { count: counts.error })}\n </Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>{t('state.scanning')}</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">{t('state.all-good')}</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header={t('settings.title')}\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n {t('settings.png-to-webp')}\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n {t('settings.tiff-to-jpg')}\n </Label>\n </Flex>\n <Text size={1} muted>\n {t('settings.apply-hint')}\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport {\n imageResizerUsEnglishLocaleBundle,\n imageResizerLocaleNamespace,\n} from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n i18n: {\n title: {\n key: 'tool.title',\n ns: imageResizerLocaleNamespace,\n },\n },\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["defineLocaleResourceBundle","IMAGE_ACCEPT","IMAGE_MAX_SIZE","IMAGE_MAX_WIDTH","MAX_SIZE_MB","jsx","Badge","useTranslation","Card","jsxs","Flex","Box","Stack","Text","Fragment","Button","Spinner","useClient","useState","useCallback","useRef","useEffect","useMemo","Container","Heading","CogIcon","Dialog","Switch","Label","definePlugin"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -245,7 +245,7 @@ function ImageResizerView() {
|
|
|
245
245
|
useEffect(() => {
|
|
246
246
|
assetsRef.current = assets;
|
|
247
247
|
}, [assets]);
|
|
248
|
-
const cancelRef = useRef(!1), fetchAssets = useCallback(async () => {
|
|
248
|
+
const cancelRef = useRef(!1), processingCountRef = useRef(0), confirmMsgRef = useRef(""), fetchAssets = useCallback(async () => {
|
|
249
249
|
setLoading(!0);
|
|
250
250
|
try {
|
|
251
251
|
const raw = await client.fetch(
|
|
@@ -340,19 +340,9 @@ function ImageResizerView() {
|
|
|
340
340
|
}
|
|
341
341
|
};
|
|
342
342
|
await Promise.all(Array.from({ length: CONCURRENCY }, () => next())), setProcessingAll(!1);
|
|
343
|
-
}, [processAsset])
|
|
344
|
-
useCallback(() => {
|
|
343
|
+
}, [processAsset]), stopProcessing = useCallback(() => {
|
|
345
344
|
cancelRef.current = !0;
|
|
346
|
-
}, []),
|
|
347
|
-
if (!processingAll) return;
|
|
348
|
-
const handler = (e) => {
|
|
349
|
-
e.preventDefault();
|
|
350
|
-
};
|
|
351
|
-
return window.addEventListener("beforeunload", handler), () => window.removeEventListener("beforeunload", handler);
|
|
352
|
-
}, [processingAll]), useEffect(() => () => {
|
|
353
|
-
cancelRef.current = !0;
|
|
354
|
-
}, []);
|
|
355
|
-
const counts = useMemo(
|
|
345
|
+
}, []), counts = useMemo(
|
|
356
346
|
() => ({
|
|
357
347
|
pending: assets.filter((a) => a.status === "idle").length,
|
|
358
348
|
processing: assets.filter((a) => a.status === "processing").length,
|
|
@@ -361,7 +351,27 @@ function ImageResizerView() {
|
|
|
361
351
|
}),
|
|
362
352
|
[assets]
|
|
363
353
|
);
|
|
364
|
-
return
|
|
354
|
+
return useEffect(() => {
|
|
355
|
+
processingCountRef.current = counts.processing, confirmMsgRef.current = t("nav.confirm-leave", { count: counts.processing });
|
|
356
|
+
}, [counts.processing, t]), useEffect(() => {
|
|
357
|
+
const handler = (e) => {
|
|
358
|
+
processingCountRef.current !== 0 && (e.preventDefault(), e.returnValue = "");
|
|
359
|
+
};
|
|
360
|
+
return window.addEventListener("beforeunload", handler), () => window.removeEventListener("beforeunload", handler);
|
|
361
|
+
}, []), useEffect(() => {
|
|
362
|
+
const original = history.pushState.bind(history);
|
|
363
|
+
return history.pushState = function(...args) {
|
|
364
|
+
if (processingCountRef.current > 0) {
|
|
365
|
+
if (!window.confirm(confirmMsgRef.current)) return;
|
|
366
|
+
cancelRef.current = !0;
|
|
367
|
+
}
|
|
368
|
+
original(...args);
|
|
369
|
+
}, () => {
|
|
370
|
+
history.pushState = original;
|
|
371
|
+
};
|
|
372
|
+
}, []), useEffect(() => () => {
|
|
373
|
+
cancelRef.current = !0;
|
|
374
|
+
}, []), /* @__PURE__ */ jsxs(
|
|
365
375
|
Container,
|
|
366
376
|
{
|
|
367
377
|
width: 4,
|
|
@@ -404,7 +414,7 @@ function ImageResizerView() {
|
|
|
404
414
|
disabled: loading || processingAll
|
|
405
415
|
}
|
|
406
416
|
),
|
|
407
|
-
counts.pending > 0 &&
|
|
417
|
+
counts.pending > 0 && counts.processing === 0 && /* @__PURE__ */ jsx(
|
|
408
418
|
Button,
|
|
409
419
|
{
|
|
410
420
|
text: t("action.process-all", { count: counts.pending }),
|
|
@@ -413,17 +423,16 @@ function ImageResizerView() {
|
|
|
413
423
|
disabled: loading
|
|
414
424
|
}
|
|
415
425
|
),
|
|
416
|
-
|
|
426
|
+
counts.processing > 0 && /* @__PURE__ */ jsx(
|
|
417
427
|
Button,
|
|
418
428
|
{
|
|
419
429
|
text: t("action.finish-ongoing", {
|
|
420
430
|
count: counts.processing
|
|
421
431
|
}),
|
|
422
432
|
tone: "caution",
|
|
423
|
-
|
|
424
|
-
disabled: !0
|
|
433
|
+
onClick: stopProcessing
|
|
425
434
|
}
|
|
426
|
-
)
|
|
435
|
+
)
|
|
427
436
|
] })
|
|
428
437
|
] }),
|
|
429
438
|
!loading && assets.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 3, wrap: "wrap", children: [
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator, type SanityClient } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * a reference points to `oldId` (either via `asset._ref` or a direct\n * `_ref`). Returns a flat `{ path: newRef }` object compatible with\n * `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n // Match image fields with asset._ref\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n // Match direct references (e.g. inside media.tag arrays)\n if (\n record._type === 'reference' &&\n record._ref === oldId\n ) {\n const refPath = path || '_ref'\n return { [refPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n/**\n * Metadata fields to preserve when replacing an image asset.\n * Covers built-in Sanity asset fields and sanity-plugin-media tags.\n */\nconst ASSET_METADATA_FIELDS = [\n 'title',\n 'description',\n 'altText',\n 'creditLine',\n 'source',\n 'opt',\n] as const\n\n/**\n * Copies user-editable metadata (title, description, alt text, credit,\n * source, and opt.media.tags) from the old asset to the new one.\n */\nexport async function copyAssetMetadata(\n client: SanityClient,\n oldAssetId: string,\n newAssetId: string,\n): Promise<void> {\n const projection = ASSET_METADATA_FIELDS.join(', ')\n const oldMeta = await client.fetch(\n `*[_id == $id][0]{ ${projection} }`,\n { id: oldAssetId },\n )\n if (!oldMeta) return\n\n // Build a flat patch with only the fields that actually exist\n const patch: Record<string, unknown> = {}\n for (const field of ASSET_METADATA_FIELDS) {\n if (oldMeta[field] !== undefined && oldMeta[field] !== null) {\n patch[field] = oldMeta[field]\n }\n }\n\n if (Object.keys(patch).length > 0) {\n await client.patch(newAssetId).set(patch).commit()\n }\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport { useTranslation } from 'sanity'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\nimport { imageResizerLocaleNamespace } from '../../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(\n settings: ConversionSettings,\n t: (key: string) => string\n): string {\n const parts: string[] = []\n parts.push(\n settings.tiffToJpg\n ? t('violation.tiff-to-jpg')\n : t('violation.tiff-to-webp')\n )\n if (settings.pngToWebp) parts.push(t('violation.png-to-webp'))\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n t,\n}: {\n type: Violation\n settings: ConversionSettings\n t: (key: string, params?: Record<string, unknown>) => string\n}) {\n let label: string\n if (type === 'format') {\n label = formatViolationLabel(settings, t)\n } else if (type === 'width') {\n label = t('violation.width', { maxWidth: IMAGE_MAX_WIDTH })\n } else {\n label = t('violation.size', { maxSize: MAX_SIZE_MB })\n }\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary-done', {\n oldSize: (asset.size / 1024 / 1024).toFixed(1),\n newSize: (asset.newSize! / 1024 / 1024).toFixed(1),\n reduction:\n sizeReduction !== null && sizeReduction > 0\n ? t('asset.reduction', { percent: sizeReduction })\n : '',\n width: asset.newWidth,\n })}\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {t('asset.width-reduced', {\n oldWidth: asset.width,\n newWidth: asset.newWidth,\n })}\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} t={t} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary', {\n size: (asset.size / 1024 / 1024).toFixed(1),\n width: asset.width,\n })}\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text={t('asset.process')}\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && (\n <Badge tone=\"positive\">{t('asset.done')}</Badge>\n )}\n {asset.status === 'error' && (\n <Button\n text={t('asset.retry')}\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient, useTranslation } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n copyAssetMetadata,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\nimport { imageResizerLocaleNamespace } from '../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // Cancellation flag — checked between items in the batch loop\n const cancelRef = useRef(false)\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )] | order(size desc) [0...500] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset\n await copyAssetMetadata(client, asset._id, newAsset._id)\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n cancelRef.current = false\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n if (cancelRef.current) break\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n /** Cancels the batch queue (in-flight items finish, no new ones start). */\n const stopProcessing = useCallback(() => {\n cancelRef.current = true\n }, [])\n\n // ── navigation guards ───────────────────────────────────────────────────\n\n // Warn before browser close / refresh while processing\n useEffect(() => {\n if (!processingAll) return\n const handler = (e: BeforeUnloadEvent) => {\n e.preventDefault()\n }\n window.addEventListener('beforeunload', handler)\n return () => window.removeEventListener('beforeunload', handler)\n }, [processingAll])\n\n // Cancel queue when the component unmounts (in-app navigation)\n useEffect(() => {\n return () => {\n cancelRef.current = true\n }\n }, [])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>{t('header.title')}</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n {t('header.description', {\n maxWidth: IMAGE_MAX_WIDTH,\n maxSize: MAX_SIZE_MB,\n })}\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text={t('action.refresh')}\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && !processingAll && (\n <Button\n text={t('action.process-all', { count: counts.pending })}\n tone=\"primary\"\n onClick={processAll}\n disabled={loading}\n />\n )}\n {processingAll && (\n <Flex gap={2}>\n <Button\n text={t('action.finish-ongoing', {\n count: counts.processing,\n })}\n tone=\"caution\"\n mode=\"ghost\"\n disabled\n />\n {/* <Button\n text={t('action.stop-all')}\n tone=\"critical\"\n onClick={stopProcessing}\n /> */}\n </Flex>\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">\n {t('status.pending', { count: counts.pending })}\n </Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">\n {t('status.processing', { count: counts.processing })}\n </Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">\n {t('status.done', { count: counts.done })}\n </Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">\n {t('status.failed', { count: counts.error })}\n </Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>{t('state.scanning')}</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">{t('state.all-good')}</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header={t('settings.title')}\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n {t('settings.png-to-webp')}\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n {t('settings.tiff-to-jpg')}\n </Label>\n </Flex>\n <Text size={1} muted>\n {t('settings.apply-hint')}\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport {\n imageResizerUsEnglishLocaleBundle,\n imageResizerLocaleNamespace,\n} from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n i18n: {\n title: {\n key: 'tool.title',\n ns: imageResizerLocaleNamespace,\n },\n },\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["MAX_SIZE_MB"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/i18n/index.ts","../src/helpers.ts","../src/tool/components/AssetCard.tsx","../src/tool/ImageResizer.tsx","../src/plugin.tsx"],"sourcesContent":["import { defineLocaleResourceBundle } from 'sanity'\n\nexport const imageResizerLocaleNamespace = 'image-resizer'\n\nexport const imageResizerUsEnglishLocaleBundle = defineLocaleResourceBundle({\n locale: 'en-US',\n namespace: imageResizerLocaleNamespace,\n resources: () => import('./resources'),\n})\n","import { type CustomValidator, type SanityClient } from 'sanity'\n\n// ─── types ──────────────────────────────────────────────────────────────────\n\n/** @public */\nexport interface ImageResizerOptions {\n /** Accepted image MIME types (comma-separated). Default: 'image/jpeg, image/png, image/gif, image/webp' */\n imageAccept?: string\n /** Max file size in bytes. Default: 20 * 1024 * 1024 (20 MB) */\n imageMaxSize?: number\n /** Max image width in pixels. Default: 10000 */\n imageMaxWidth?: number\n}\n\nexport type Violation = 'format' | 'width' | 'size'\nexport type ProcessStatus = 'idle' | 'processing' | 'done' | 'error'\n\n/** User-configurable format conversion toggles */\nexport interface ConversionSettings {\n /** Convert PNG images to WebP */\n pngToWebp: boolean\n /** Convert TIFF images to JPG (instead of WebP) */\n tiffToJpg: boolean\n}\n\nexport const DEFAULT_SETTINGS: ConversionSettings = {\n pngToWebp: false,\n tiffToJpg: false,\n}\n\nexport interface ImageAsset {\n _id: string\n url: string\n originalFilename: string\n mimeType: string\n size: number\n width: number\n violations: Violation[]\n status: ProcessStatus\n error?: string\n /** Post-processing result info */\n newUrl?: string\n newSize?: number\n newWidth?: number\n newFilename?: string\n}\n\n// ─── plugin config state ────────────────────────────────────────────────────\n\nconst DEFAULTS: Required<ImageResizerOptions> = {\n imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n imageMaxSize: 20 * 1024 * 1024,\n imageMaxWidth: 6000,\n}\n\n/** @public */\nexport let IMAGE_ACCEPT = DEFAULTS.imageAccept\n/** @public */\nexport let IMAGE_MAX_SIZE = DEFAULTS.imageMaxSize\n/** @public */\nexport let IMAGE_MAX_WIDTH = DEFAULTS.imageMaxWidth\n\nexport function applyConfig(options?: ImageResizerOptions) {\n const resolved = { ...DEFAULTS, ...options }\n IMAGE_ACCEPT = resolved.imageAccept\n IMAGE_MAX_SIZE = resolved.imageMaxSize\n IMAGE_MAX_WIDTH = resolved.imageMaxWidth\n}\n\n// ─── constants ──────────────────────────────────────────────────────────────\n\n/** Sanity Image API format parameter for each output MIME type */\nexport const MIME_FORMAT_MAP: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** Quality steps to try when the image exceeds the size budget (high → low) */\nexport const QUALITY_STEPS = [92, 80, 70, 60, 50, 40]\n\n/** Number of assets to process in parallel during batch runs */\nexport const CONCURRENCY = 3\n\n// ─── helpers ────────────────────────────────────────────────────────────────\n\n/** Checks which resize constraints the asset violates. */\nexport function getViolations(\n asset: Pick<ImageAsset, 'mimeType' | 'size' | 'width'>,\n settings: ConversionSettings\n): Violation[] {\n const v: Violation[] = []\n if (asset.mimeType === 'image/tiff') v.push('format')\n if (settings.pngToWebp && asset.mimeType === 'image/png') v.push('format')\n if (asset.width > IMAGE_MAX_WIDTH) v.push('width')\n if (asset.size > IMAGE_MAX_SIZE) v.push('size')\n return v\n}\n\n/**\n * Determines the Sanity Image API `fm` parameter for the output,\n * taking user conversion settings into account.\n */\nexport function outputFormat(\n inputMimeType: string,\n settings: ConversionSettings\n): string {\n if (inputMimeType === 'image/tiff') return settings.tiffToJpg ? 'jpg' : 'webp'\n if (inputMimeType === 'image/png' && settings.pngToWebp) return 'webp'\n return MIME_FORMAT_MAP[inputMimeType] ?? 'png'\n}\n\n/**\n * Uses the Sanity Image API to resize/re-encode an image server-side.\n * Appends URL parameters (`w`, `fm`, `q`) and progressively lowers\n * quality until the downloaded blob fits within `IMAGE_MAX_SIZE`.\n */\nexport async function processImage(\n url: string,\n inputMimeType: string,\n currentWidth: number,\n settings: ConversionSettings\n): Promise<{ blob: Blob; outFormat: string }> {\n const outFormat = outputFormat(inputMimeType, settings)\n\n const transformUrl = new URL(url)\n transformUrl.searchParams.set('fm', outFormat)\n transformUrl.searchParams.set(\n 'w',\n String(Math.min(currentWidth, IMAGE_MAX_WIDTH))\n )\n\n const maxSizeMB = IMAGE_MAX_SIZE / 1024 / 1024\n\n for (const q of QUALITY_STEPS) {\n transformUrl.searchParams.set('q', String(q))\n\n const res = await fetch(transformUrl.toString())\n if (!res.ok) throw new Error(`Fetch failed (HTTP ${res.status})`)\n\n const blob = await res.blob()\n if (blob.size <= IMAGE_MAX_SIZE) {\n return { blob, outFormat }\n }\n }\n\n throw new Error(`Cannot compress image below ${maxSizeMB} MB`)\n}\n\n/**\n * Recursively traverses a document tree and collects all paths where\n * a reference points to `oldId` (either via `asset._ref` or a direct\n * `_ref`). Returns a flat `{ path: newRef }` object compatible with\n * `client.patch().set()`.\n */\nexport function buildReplacementPatch(\n obj: unknown,\n oldId: string,\n newId: string,\n path = ''\n): Record<string, { _type: 'reference'; _ref: string }> {\n if (!obj || typeof obj !== 'object') return {}\n\n if (Array.isArray(obj)) {\n return obj.reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, item, i) => {\n const key =\n item && typeof item === 'object' && typeof item._key === 'string'\n ? `_key==\"${item._key}\"`\n : String(i)\n const childPath = path ? `${path}[${key}]` : `[${key}]`\n return Object.assign(\n acc,\n buildReplacementPatch(item, oldId, newId, childPath)\n )\n },\n {}\n )\n }\n\n const record = obj as Record<string, unknown>\n\n // Match image fields with asset._ref\n if ((record.asset as any)?._ref === oldId) {\n const assetPath = path ? `${path}.asset` : 'asset'\n return { [assetPath]: { _type: 'reference', _ref: newId } }\n }\n\n // Match direct references (e.g. inside media.tag arrays)\n if (\n record._type === 'reference' &&\n record._ref === oldId\n ) {\n const refPath = path || '_ref'\n return { [refPath]: { _type: 'reference', _ref: newId } }\n }\n\n return Object.keys(record)\n .filter((k) => !k.startsWith('_'))\n .reduce<Record<string, { _type: 'reference'; _ref: string }>>(\n (acc, key) => {\n const childPath = path ? `${path}.${key}` : key\n return Object.assign(\n acc,\n buildReplacementPatch(record[key], oldId, newId, childPath)\n )\n },\n {}\n )\n}\n\n/**\n * Metadata fields to preserve when replacing an image asset.\n * Covers built-in Sanity asset fields and sanity-plugin-media tags.\n */\nconst ASSET_METADATA_FIELDS = [\n 'title',\n 'description',\n 'altText',\n 'creditLine',\n 'source',\n 'opt',\n] as const\n\n/**\n * Copies user-editable metadata (title, description, alt text, credit,\n * source, and opt.media.tags) from the old asset to the new one.\n */\nexport async function copyAssetMetadata(\n client: SanityClient,\n oldAssetId: string,\n newAssetId: string,\n): Promise<void> {\n const projection = ASSET_METADATA_FIELDS.join(', ')\n const oldMeta = await client.fetch(\n `*[_id == $id][0]{ ${projection} }`,\n { id: oldAssetId },\n )\n if (!oldMeta) return\n\n // Build a flat patch with only the fields that actually exist\n const patch: Record<string, unknown> = {}\n for (const field of ASSET_METADATA_FIELDS) {\n if (oldMeta[field] !== undefined && oldMeta[field] !== null) {\n patch[field] = oldMeta[field]\n }\n }\n\n if (Object.keys(patch).length > 0) {\n await client.patch(newAssetId).set(patch).commit()\n }\n}\n\n// ─── validation ─────────────────────────────────────────────────────────────\n\n/** @public */\nexport const validateImageSize: CustomValidator = async (value: any, context) => {\n if (!value?.asset?._ref) return true\n\n const client = context.getClient({ apiVersion: '2025-02-19' })\n const asset = await client.fetch(\n `*[_id == $id][0]{ size, \"width\": metadata.dimensions.width, mimeType }`,\n { id: value.asset._ref }\n )\n\n if (!asset) return true\n\n const allowedMimeTypes = IMAGE_ACCEPT.split(',').map((s) => s.trim())\n if (asset.mimeType && !allowedMimeTypes.includes(asset.mimeType)) {\n return `File type \"${asset.mimeType}\" is not allowed. Accepted types: ${allowedMimeTypes.join(', ')}`\n }\n\n if (asset.size && asset.size > IMAGE_MAX_SIZE) {\n const sizeMB = (asset.size / (1024 * 1024)).toFixed(1)\n const maxMB = (IMAGE_MAX_SIZE / (1024 * 1024)).toFixed(0)\n return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`\n }\n\n if (asset.width && asset.width > IMAGE_MAX_WIDTH) {\n return `Image width (${asset.width}px) exceeds the maximum of ${IMAGE_MAX_WIDTH}px`\n }\n\n return true\n}\n","import {\n Badge,\n Box,\n Button,\n Card,\n Flex,\n Spinner,\n Stack,\n Text,\n} from '@sanity/ui'\nimport { useTranslation } from 'sanity'\nimport {\n type ConversionSettings,\n type ImageAsset,\n type ProcessStatus,\n type Violation,\n IMAGE_MAX_WIDTH,\n IMAGE_MAX_SIZE,\n} from '../../helpers'\nimport { imageResizerLocaleNamespace } from '../../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\n/** Builds a human-readable label for the format violation badge. */\nfunction formatViolationLabel(\n settings: ConversionSettings,\n t: (key: string) => string\n): string {\n const parts: string[] = []\n parts.push(\n settings.tiffToJpg\n ? t('violation.tiff-to-jpg')\n : t('violation.tiff-to-webp')\n )\n if (settings.pngToWebp) parts.push(t('violation.png-to-webp'))\n return parts.join(', ')\n}\n\n/** Displays a caution badge indicating which constraint was violated. */\nfunction ViolationBadge({\n type,\n settings,\n t,\n}: {\n type: Violation\n settings: ConversionSettings\n t: (key: string, params?: Record<string, unknown>) => string\n}) {\n let label: string\n if (type === 'format') {\n label = formatViolationLabel(settings, t)\n } else if (type === 'width') {\n label = t('violation.width', { maxWidth: IMAGE_MAX_WIDTH })\n } else {\n label = t('violation.size', { maxSize: MAX_SIZE_MB })\n }\n return (\n <Badge tone=\"caution\" size={1}>\n {label}\n </Badge>\n )\n}\n\n/** Resolves the visual tone for an asset card based on its processing status. */\nfunction statusTone(status: ProcessStatus) {\n const map: Record<\n ProcessStatus,\n 'positive' | 'critical' | 'primary' | 'default'\n > = {\n done: 'positive',\n error: 'critical',\n processing: 'primary',\n idle: 'default',\n }\n return map[status]\n}\n\n/** Renders a single asset row with thumbnail, info, badges and action button. */\nexport function AssetCard({\n asset,\n onProcess,\n settings,\n}: {\n asset: ImageAsset\n onProcess: (asset: ImageAsset) => void\n settings: ConversionSettings\n}) {\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const isDone = asset.status === 'done' && asset.newUrl\n const thumbUrl = isDone ? asset.newUrl! : asset.url\n const sizeReduction =\n isDone && asset.newSize != null\n ? Math.round((1 - asset.newSize / asset.size) * 100)\n : null\n\n return (\n <Card\n key={asset._id}\n tone={statusTone(asset.status)}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Flex gap={3} align=\"center\">\n {/* Thumbnail — 64×64 with 2× source for retina */}\n <Box style={{ width: 64, height: 64, flexShrink: 0 }}>\n <img\n src={`${thumbUrl}?w=128&h=128&fit=crop&auto=format`}\n alt=\"\"\n loading=\"lazy\"\n style={{\n width: 64,\n height: 64,\n objectFit: 'cover',\n borderRadius: 4,\n }}\n />\n </Box>\n\n {/* File info + violation badges */}\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Box style={{ width: '100%', minWidth: 0 }}>\n <Text size={1} weight=\"semibold\">\n {isDone\n ? asset.newFilename || asset.originalFilename || asset._id\n : asset.originalFilename || asset._id}\n </Text>\n </Box>\n {isDone ? (\n <>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary-done', {\n oldSize: (asset.size / 1024 / 1024).toFixed(1),\n newSize: (asset.newSize! / 1024 / 1024).toFixed(1),\n reduction:\n sizeReduction !== null && sizeReduction > 0\n ? t('asset.reduction', { percent: sizeReduction })\n : '',\n width: asset.newWidth,\n })}\n </Text>\n <Flex gap={2} wrap=\"wrap\">\n {asset.newWidth != null && asset.newWidth < asset.width && (\n <Badge tone=\"positive\" size={1}>\n {t('asset.width-reduced', {\n oldWidth: asset.width,\n newWidth: asset.newWidth,\n })}\n </Badge>\n )}\n </Flex>\n </>\n ) : (\n <>\n <Flex gap={2} wrap=\"wrap\">\n {asset.violations.map((v) => (\n <ViolationBadge key={v} type={v} settings={settings} t={t} />\n ))}\n </Flex>\n <Text size={1} muted style={{ wordBreak: 'break-word' }}>\n {t('asset.summary', {\n size: (asset.size / 1024 / 1024).toFixed(1),\n width: asset.width,\n })}\n </Text>\n </>\n )}\n {asset.status === 'error' && (\n <Text\n size={1}\n style={{\n color: 'var(--card-badge-critical-dot-color)',\n wordBreak: 'break-word',\n }}\n >\n {asset.error}\n </Text>\n )}\n </Stack>\n\n {/* Action button — contextual per status */}\n <Box style={{ flexShrink: 0 }}>\n {asset.status === 'idle' && (\n <Button\n text={t('asset.process')}\n mode=\"ghost\"\n tone=\"primary\"\n onClick={() => onProcess(asset)}\n />\n )}\n {asset.status === 'processing' && <Spinner />}\n {asset.status === 'done' && (\n <Badge tone=\"positive\">{t('asset.done')}</Badge>\n )}\n {asset.status === 'error' && (\n <Button\n text={t('asset.retry')}\n mode=\"ghost\"\n tone=\"critical\"\n onClick={() => onProcess(asset)}\n />\n )}\n </Box>\n </Flex>\n </Card>\n )\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useClient, useTranslation } from 'sanity'\nimport {\n Badge,\n Box,\n Button,\n Card,\n Container,\n Dialog,\n Flex,\n Heading,\n Label,\n Spinner,\n Stack,\n Switch,\n Text,\n} from '@sanity/ui'\nimport { CogIcon } from '@sanity/icons'\nimport {\n type ConversionSettings,\n type ImageAsset,\n CONCURRENCY,\n DEFAULT_SETTINGS,\n IMAGE_MAX_SIZE,\n IMAGE_MAX_WIDTH,\n buildReplacementPatch,\n copyAssetMetadata,\n getViolations,\n processImage,\n} from '../helpers'\nimport { AssetCard } from './components/AssetCard'\nimport { imageResizerLocaleNamespace } from '../i18n'\n\n/** Human-readable size limit for display purposes */\nconst MAX_SIZE_MB = IMAGE_MAX_SIZE / 1024 / 1024\n\nconst KV_SETTINGS_KEY = 'image-resizer-settings'\n\nfunction loadSettings(): ConversionSettings {\n try {\n const raw = localStorage.getItem(KV_SETTINGS_KEY)\n if (raw) {\n const parsed = JSON.parse(raw)\n return {\n pngToWebp:\n typeof parsed.pngToWebp === 'boolean'\n ? parsed.pngToWebp\n : DEFAULT_SETTINGS.pngToWebp,\n tiffToJpg:\n typeof parsed.tiffToJpg === 'boolean'\n ? parsed.tiffToJpg\n : DEFAULT_SETTINGS.tiffToJpg,\n }\n }\n } catch {\n // ignore corrupt data\n }\n return DEFAULT_SETTINGS\n}\n\n/**\n * Studio tool that scans all image assets for constraint violations\n * (TIFF format, oversized width/filesize) and lets editors batch-resize\n * them in-place — re-encoding, resizing and re-linking references.\n */\nexport function ImageResizerView() {\n const client = useClient({ apiVersion: '2025-02-19' })\n const { t } = useTranslation(imageResizerLocaleNamespace)\n const [assets, setAssets] = useState<ImageAsset[]>([])\n const [loading, setLoading] = useState(true)\n const [processingAll, setProcessingAll] = useState(false)\n const [settings, setSettings] = useState<ConversionSettings>(loadSettings)\n const [showSettings, setShowSettings] = useState(false)\n\n /** Wraps setSettings to also persist to localStorage. */\n const updateSettings = useCallback(\n (updater: (prev: ConversionSettings) => ConversionSettings) => {\n setSettings((prev) => {\n const next = updater(prev)\n localStorage.setItem(KV_SETTINGS_KEY, JSON.stringify(next))\n return next\n })\n },\n []\n )\n\n // Ref keeps processAll's sequential loop in sync with latest state\n const assetsRef = useRef(assets)\n useEffect(() => {\n assetsRef.current = assets\n }, [assets])\n\n // Cancellation flag — checked between items in the batch loop\n const cancelRef = useRef(false)\n\n // Ref mirrors counts.processing for use inside non-React callbacks\n const processingCountRef = useRef(0)\n // Ref for the i18n confirm message, updated reactively\n const confirmMsgRef = useRef('')\n\n // ── data fetching ───────────────────────────────────────────────────────\n\n /** Fetches all image assets that violate at least one constraint. */\n const fetchAssets = useCallback(async () => {\n setLoading(true)\n try {\n const raw = await client.fetch<\n Omit<ImageAsset, 'violations' | 'status'>[]\n >(\n `*[_type == \"sanity.imageAsset\" && (\n mimeType == \"image/tiff\" ||\n metadata.dimensions.width > ${IMAGE_MAX_WIDTH} ||\n size > ${IMAGE_MAX_SIZE}\n )] | order(size desc) [0...500] {\n _id, url, originalFilename, mimeType, size,\n \"width\": metadata.dimensions.width\n }`\n )\n setAssets(\n raw.map((a) => ({\n ...a,\n violations: getViolations(a, settings),\n status: 'idle',\n }))\n )\n } finally {\n setLoading(false)\n }\n }, [client, settings])\n\n useEffect(() => {\n fetchAssets()\n }, [fetchAssets])\n\n // ── single-asset processing ─────────────────────────────────────────────\n\n /** Helper to update a single asset's state by ID. */\n const updateAsset = useCallback(\n (id: string, patch: Partial<ImageAsset>) =>\n setAssets((prev) =>\n prev.map((a) => (a._id === id ? { ...a, ...patch } : a))\n ),\n []\n )\n\n /**\n * Processes one asset end-to-end:\n * 1. Re-encode / resize via Sanity Image API transformations\n * 2. Upload the transformed image as a new asset\n * 3. Find & patch all documents that reference the old asset\n * 4. Delete the old asset\n */\n const processAsset = useCallback(\n async (asset: ImageAsset) => {\n updateAsset(asset._id, { status: 'processing', error: undefined })\n\n try {\n // 1 — Resize / convert via Sanity Image API\n const { blob, outFormat } = await processImage(\n asset.url,\n asset.mimeType,\n asset.width,\n settings\n )\n\n // Skip replacement if the new file is bigger than the original\n if (blob.size >= asset.size) {\n updateAsset(asset._id, {\n status: 'error',\n error: `Skipped: resized file (${(blob.size / 1024 / 1024).toFixed(1)} MB) is not smaller than original (${(asset.size / 1024 / 1024).toFixed(1)} MB)`,\n })\n return\n }\n\n // 2 — Upload replacement asset\n const baseName =\n asset.originalFilename?.replace(/\\.[^.]+$/, '') || 'image'\n const newAsset = await client.assets.upload('image', blob, {\n filename: `${baseName}.${outFormat}`,\n contentType: `image/${outFormat}`,\n })\n\n // 2b — Copy metadata (tags, alt text, credits, etc.) to the new asset\n await copyAssetMetadata(client, asset._id, newAsset._id)\n\n // 3 — Re-link all referencing documents\n const refs = await client.fetch<{ _id: string }[]>(\n `*[references($id)]{ _id }`,\n { id: asset._id }\n )\n\n for (const { _id } of refs) {\n const doc = await client.getDocument(_id)\n if (!doc) continue\n const patch = buildReplacementPatch(doc, asset._id, newAsset._id)\n if (Object.keys(patch).length > 0) {\n await client.patch(_id).set(patch).commit()\n }\n }\n\n // 4 — Delete the old asset now that all references point to the new one\n await client.delete(asset._id)\n\n // Fetch the new asset's metadata for display\n const newMeta = await client.fetch<{\n url: string\n size: number\n width: number\n originalFilename: string\n }>(\n `*[_id == $id][0]{ url, size, originalFilename, \"width\": metadata.dimensions.width }`,\n { id: newAsset._id }\n )\n\n updateAsset(asset._id, {\n status: 'done',\n newUrl: newMeta?.url ?? newAsset.url,\n newSize: newMeta?.size ?? blob.size,\n newWidth: newMeta?.width ?? Math.min(asset.width, IMAGE_MAX_WIDTH),\n newFilename: newMeta?.originalFilename ?? `${baseName}.${outFormat}`,\n })\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n updateAsset(asset._id, { status: 'error', error: message })\n }\n },\n [client, updateAsset, settings]\n )\n\n // ── batch processing ────────────────────────────────────────────────────\n\n /**\n * Processes all pending / failed assets with up to `CONCURRENCY`\n * tasks running in parallel using a simple worker-pool pattern.\n */\n const processAll = useCallback(async () => {\n cancelRef.current = false\n setProcessingAll(true)\n const pending = assetsRef.current.filter(\n (a) => a.status === 'idle' || a.status === 'error'\n )\n\n let idx = 0\n const next = async (): Promise<void> => {\n while (idx < pending.length) {\n if (cancelRef.current) break\n const asset = pending[idx++]\n await processAsset(asset)\n }\n }\n\n // Spawn `CONCURRENCY` workers that all pull from the same queue\n await Promise.all(Array.from({ length: CONCURRENCY }, () => next()))\n\n setProcessingAll(false)\n }, [processAsset])\n\n /** Cancels the batch queue (in-flight items finish, no new ones start). */\n const stopProcessing = useCallback(() => {\n cancelRef.current = true\n }, [])\n\n // ── derived state (memoised) ────────────────────────────────────────────\n\n /** Aggregated status counts used for badges and conditional rendering. */\n const counts = useMemo(\n () => ({\n pending: assets.filter((a) => a.status === 'idle').length,\n processing: assets.filter((a) => a.status === 'processing').length,\n done: assets.filter((a) => a.status === 'done').length,\n error: assets.filter((a) => a.status === 'error').length,\n }),\n [assets]\n )\n\n // ── navigation guards ───────────────────────────────────────────────────\n\n // Keep refs in sync for use inside non-React callbacks\n useEffect(() => {\n processingCountRef.current = counts.processing\n confirmMsgRef.current = t('nav.confirm-leave', { count: counts.processing })\n }, [counts.processing, t])\n\n // Warn before browser close / refresh while processing\n useEffect(() => {\n const handler = (e: BeforeUnloadEvent) => {\n if (processingCountRef.current === 0) return\n e.preventDefault()\n e.returnValue = ''\n }\n window.addEventListener('beforeunload', handler)\n return () => window.removeEventListener('beforeunload', handler)\n }, [])\n\n // Intercept in-app navigation (Sanity router uses history.pushState)\n useEffect(() => {\n const original = history.pushState.bind(history)\n history.pushState = function (\n ...args: Parameters<typeof history.pushState>\n ) {\n if (processingCountRef.current > 0) {\n // eslint-disable-next-line no-alert\n if (!window.confirm(confirmMsgRef.current)) return\n cancelRef.current = true\n }\n original(...args)\n }\n return () => {\n history.pushState = original\n }\n }, [])\n\n // Cancel queue when the component unmounts (e.g. forced navigation)\n useEffect(() => {\n return () => {\n cancelRef.current = true\n }\n }, [])\n\n // ── render ──────────────────────────────────────────────────────────────\n\n return (\n <Container\n width={4}\n padding={4}\n style={{ width: 'calc(100vw - 1.25rem * 2)' }}\n >\n <Stack space={5}>\n {/* ── Header ─────────────────────────────────────────────────── */}\n <Flex align=\"flex-start\" justify=\"space-between\" gap={4} wrap=\"wrap\">\n <Stack space={2} style={{ flex: 1, minWidth: 0 }}>\n <Heading size={2}>{t('header.title')}</Heading>\n <Card\n size={1}\n tone=\"transparent\"\n style={{ wordBreak: 'break-word' }}\n >\n {t('header.description', {\n maxWidth: IMAGE_MAX_WIDTH,\n maxSize: MAX_SIZE_MB,\n })}\n </Card>\n </Stack>\n <Flex gap={2} align=\"center\" wrap=\"wrap\" style={{ flexShrink: 0 }}>\n <Button\n icon={CogIcon}\n mode=\"ghost\"\n onClick={() => setShowSettings(true)}\n disabled={processingAll}\n />\n <Button\n text={t('action.refresh')}\n mode=\"ghost\"\n onClick={fetchAssets}\n disabled={loading || processingAll}\n />\n {counts.pending > 0 && counts.processing === 0 && (\n <Button\n text={t('action.process-all', { count: counts.pending })}\n tone=\"primary\"\n onClick={processAll}\n disabled={loading}\n />\n )}\n {counts.processing > 0 && (\n <Button\n text={t('action.finish-ongoing', {\n count: counts.processing,\n })}\n tone=\"caution\"\n onClick={stopProcessing}\n />\n )}\n </Flex>\n </Flex>\n\n {/* ── Status badges ──────────────────────────────────────────── */}\n {!loading && assets.length > 0 && (\n <Flex gap={3} wrap=\"wrap\">\n {counts.pending > 0 && (\n <Badge tone=\"caution\">\n {t('status.pending', { count: counts.pending })}\n </Badge>\n )}\n {counts.processing > 0 && (\n <Badge tone=\"primary\">\n {t('status.processing', { count: counts.processing })}\n </Badge>\n )}\n {counts.done > 0 && (\n <Badge tone=\"positive\">\n {t('status.done', { count: counts.done })}\n </Badge>\n )}\n {counts.error > 0 && (\n <Badge tone=\"critical\">\n {t('status.failed', { count: counts.error })}\n </Badge>\n )}\n </Flex>\n )}\n\n {/* ── Asset list ─────────────────────────────────────────────── */}\n {loading ? (\n <Flex padding={6} justify=\"center\" align=\"center\" gap={3}>\n <Spinner />\n <Text muted>{t('state.scanning')}</Text>\n </Flex>\n ) : assets.length === 0 ? (\n <Card padding={5} radius={2} tone=\"positive\" border>\n <Text align=\"center\">{t('state.all-good')}</Text>\n </Card>\n ) : (\n <Stack space={2}>\n {assets.map((asset) => (\n <AssetCard\n key={asset._id}\n asset={asset}\n onProcess={processAsset}\n settings={settings}\n />\n ))}\n </Stack>\n )}\n </Stack>\n\n {/* ── Settings dialog ────────────────────────────────────────── */}\n {showSettings && (\n <Dialog\n id=\"image-resizer-settings\"\n header={t('settings.title')}\n onClose={() => setShowSettings(false)}\n width={1}\n >\n <Box padding={4}>\n <Stack space={4}>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"png-to-webp\"\n checked={settings.pngToWebp}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, pngToWebp: checked }))\n }}\n />\n <Label htmlFor=\"png-to-webp\" style={{ cursor: 'pointer' }}>\n {t('settings.png-to-webp')}\n </Label>\n </Flex>\n <Flex align=\"center\" gap={3}>\n <Switch\n id=\"tiff-to-jpg\"\n checked={settings.tiffToJpg}\n onChange={(e) => {\n const checked = e.currentTarget.checked\n updateSettings((s) => ({ ...s, tiffToJpg: checked }))\n }}\n />\n <Label htmlFor=\"tiff-to-jpg\" style={{ cursor: 'pointer' }}>\n {t('settings.tiff-to-jpg')}\n </Label>\n </Flex>\n <Text size={1} muted>\n {t('settings.apply-hint')}\n </Text>\n </Stack>\n </Box>\n </Dialog>\n )}\n </Container>\n )\n}\n","import { definePlugin } from 'sanity'\nimport {\n imageResizerUsEnglishLocaleBundle,\n imageResizerLocaleNamespace,\n} from './i18n'\nimport { applyConfig, type ImageResizerOptions } from './helpers'\nimport { ImageResizerView } from './tool/ImageResizer'\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { imageResizerPlugin } from 'sanity-plugin-image-resizer'\n *\n * export default defineConfig({\n * // ...\n * plugins: [\n * imageResizerPlugin({\n * imageAccept: 'image/jpeg, image/png, image/gif, image/webp',\n * imageMaxSize: 20 * 1024 * 1024,\n * imageMaxWidth: 6000,\n * }),\n * ],\n * })\n * ```\n */\n\n/**\n * @public\n */\nexport const imageResizerPlugin = definePlugin<ImageResizerOptions | void>(\n (options) => {\n applyConfig(options ?? undefined)\n\n return {\n name: 'sanity-plugin-image-resizer',\n\n tools: (prev) => {\n return [\n ...prev,\n {\n name: 'image-resizer',\n title: 'Image Resizer',\n component: ImageResizerView,\n i18n: {\n title: {\n key: 'tool.title',\n ns: imageResizerLocaleNamespace,\n },\n },\n },\n ]\n },\n\n i18n: {\n bundles: [imageResizerUsEnglishLocaleBundle],\n },\n }\n }\n)\n"],"names":["MAX_SIZE_MB"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/i18n/resources.ts
CHANGED
|
@@ -15,10 +15,8 @@ export default {
|
|
|
15
15
|
'action.refresh': 'Refresh',
|
|
16
16
|
/** Process-all button label ({{count}} = number of pending assets) */
|
|
17
17
|
'action.process-all': 'Process All ({{count}})',
|
|
18
|
-
/** Label for the button that
|
|
18
|
+
/** Label for the button that stops processing (with live count) */
|
|
19
19
|
'action.finish-ongoing': 'Finish ongoing tasks ({{count}})',
|
|
20
|
-
/** Label for the stop button that cancels the queue immediately */
|
|
21
|
-
'action.stop-all': 'Stop All (possible data loss)',
|
|
22
20
|
|
|
23
21
|
// ── Status badges ───────────────────────────────────────────────────────
|
|
24
22
|
/** Pending badge ({{count}} = number) */
|
|
@@ -46,6 +44,11 @@ export default {
|
|
|
46
44
|
/** Hint below toggles */
|
|
47
45
|
'settings.apply-hint': 'Changes apply on next Refresh.',
|
|
48
46
|
|
|
47
|
+
// ── Navigation guard ────────────────────────────────────────────────────
|
|
48
|
+
/** Confirm dialog shown when navigating away during processing */
|
|
49
|
+
'nav.confirm-leave':
|
|
50
|
+
'Image processing is in progress ({{count}}). Leaving now may cause data loss. Are you sure?',
|
|
51
|
+
|
|
49
52
|
// ── Asset card ──────────────────────────────────────────────────────────
|
|
50
53
|
/** Violation badge: TIFF → WebP */
|
|
51
54
|
'violation.tiff-to-webp': 'TIFF → WebP',
|
|
@@ -93,6 +93,11 @@ export function ImageResizerView() {
|
|
|
93
93
|
// Cancellation flag — checked between items in the batch loop
|
|
94
94
|
const cancelRef = useRef(false)
|
|
95
95
|
|
|
96
|
+
// Ref mirrors counts.processing for use inside non-React callbacks
|
|
97
|
+
const processingCountRef = useRef(0)
|
|
98
|
+
// Ref for the i18n confirm message, updated reactively
|
|
99
|
+
const confirmMsgRef = useRef('')
|
|
100
|
+
|
|
96
101
|
// ── data fetching ───────────────────────────────────────────────────────
|
|
97
102
|
|
|
98
103
|
/** Fetches all image assets that violate at least one constraint. */
|
|
@@ -255,37 +260,62 @@ export function ImageResizerView() {
|
|
|
255
260
|
cancelRef.current = true
|
|
256
261
|
}, [])
|
|
257
262
|
|
|
263
|
+
// ── derived state (memoised) ────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/** Aggregated status counts used for badges and conditional rendering. */
|
|
266
|
+
const counts = useMemo(
|
|
267
|
+
() => ({
|
|
268
|
+
pending: assets.filter((a) => a.status === 'idle').length,
|
|
269
|
+
processing: assets.filter((a) => a.status === 'processing').length,
|
|
270
|
+
done: assets.filter((a) => a.status === 'done').length,
|
|
271
|
+
error: assets.filter((a) => a.status === 'error').length,
|
|
272
|
+
}),
|
|
273
|
+
[assets]
|
|
274
|
+
)
|
|
275
|
+
|
|
258
276
|
// ── navigation guards ───────────────────────────────────────────────────
|
|
259
277
|
|
|
278
|
+
// Keep refs in sync for use inside non-React callbacks
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
processingCountRef.current = counts.processing
|
|
281
|
+
confirmMsgRef.current = t('nav.confirm-leave', { count: counts.processing })
|
|
282
|
+
}, [counts.processing, t])
|
|
283
|
+
|
|
260
284
|
// Warn before browser close / refresh while processing
|
|
261
285
|
useEffect(() => {
|
|
262
|
-
if (!processingAll) return
|
|
263
286
|
const handler = (e: BeforeUnloadEvent) => {
|
|
287
|
+
if (processingCountRef.current === 0) return
|
|
264
288
|
e.preventDefault()
|
|
289
|
+
e.returnValue = ''
|
|
265
290
|
}
|
|
266
291
|
window.addEventListener('beforeunload', handler)
|
|
267
292
|
return () => window.removeEventListener('beforeunload', handler)
|
|
268
|
-
}, [
|
|
293
|
+
}, [])
|
|
269
294
|
|
|
270
|
-
//
|
|
295
|
+
// Intercept in-app navigation (Sanity router uses history.pushState)
|
|
271
296
|
useEffect(() => {
|
|
297
|
+
const original = history.pushState.bind(history)
|
|
298
|
+
history.pushState = function (
|
|
299
|
+
...args: Parameters<typeof history.pushState>
|
|
300
|
+
) {
|
|
301
|
+
if (processingCountRef.current > 0) {
|
|
302
|
+
// eslint-disable-next-line no-alert
|
|
303
|
+
if (!window.confirm(confirmMsgRef.current)) return
|
|
304
|
+
cancelRef.current = true
|
|
305
|
+
}
|
|
306
|
+
original(...args)
|
|
307
|
+
}
|
|
272
308
|
return () => {
|
|
273
|
-
|
|
309
|
+
history.pushState = original
|
|
274
310
|
}
|
|
275
311
|
}, [])
|
|
276
312
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
processing: assets.filter((a) => a.status === 'processing').length,
|
|
284
|
-
done: assets.filter((a) => a.status === 'done').length,
|
|
285
|
-
error: assets.filter((a) => a.status === 'error').length,
|
|
286
|
-
}),
|
|
287
|
-
[assets]
|
|
288
|
-
)
|
|
313
|
+
// Cancel queue when the component unmounts (e.g. forced navigation)
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
return () => {
|
|
316
|
+
cancelRef.current = true
|
|
317
|
+
}
|
|
318
|
+
}, [])
|
|
289
319
|
|
|
290
320
|
// ── render ──────────────────────────────────────────────────────────────
|
|
291
321
|
|
|
@@ -324,7 +354,7 @@ export function ImageResizerView() {
|
|
|
324
354
|
onClick={fetchAssets}
|
|
325
355
|
disabled={loading || processingAll}
|
|
326
356
|
/>
|
|
327
|
-
{counts.pending > 0 &&
|
|
357
|
+
{counts.pending > 0 && counts.processing === 0 && (
|
|
328
358
|
<Button
|
|
329
359
|
text={t('action.process-all', { count: counts.pending })}
|
|
330
360
|
tone="primary"
|
|
@@ -332,22 +362,14 @@ export function ImageResizerView() {
|
|
|
332
362
|
disabled={loading}
|
|
333
363
|
/>
|
|
334
364
|
)}
|
|
335
|
-
{
|
|
336
|
-
<
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
disabled
|
|
344
|
-
/>
|
|
345
|
-
{/* <Button
|
|
346
|
-
text={t('action.stop-all')}
|
|
347
|
-
tone="critical"
|
|
348
|
-
onClick={stopProcessing}
|
|
349
|
-
/> */}
|
|
350
|
-
</Flex>
|
|
365
|
+
{counts.processing > 0 && (
|
|
366
|
+
<Button
|
|
367
|
+
text={t('action.finish-ongoing', {
|
|
368
|
+
count: counts.processing,
|
|
369
|
+
})}
|
|
370
|
+
tone="caution"
|
|
371
|
+
onClick={stopProcessing}
|
|
372
|
+
/>
|
|
351
373
|
)}
|
|
352
374
|
</Flex>
|
|
353
375
|
</Flex>
|