@inoo-ch/payload-image-optimizer 1.4.6 → 1.4.8

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/AGENT_DOCS.md CHANGED
@@ -219,7 +219,7 @@ Import from `@inoo-ch/payload-image-optimizer/client`:
219
219
 
220
220
  ### `ImageBox` Component
221
221
 
222
- Drop-in Next.js `<Image>` wrapper with automatic ThumbHash blur placeholders and focal point support.
222
+ Drop-in Next.js `<Image>` wrapper with automatic ThumbHash blur placeholders, focal point support, and smooth fade-in transition.
223
223
 
224
224
  ```tsx
225
225
  import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
@@ -229,21 +229,55 @@ import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
229
229
 
230
230
  // With a plain URL string
231
231
  <ImageBox media="/images/fallback.jpg" alt="Fallback" width={800} height={600} />
232
+
233
+ // Disable fade animation
234
+ <ImageBox media={doc.image} alt="Photo" fade={false} />
235
+
236
+ // Custom fade duration
237
+ <ImageBox media={doc.image} alt="Photo" fadeDuration={300} />
232
238
  ```
233
239
 
234
240
  **Props:** Extends all Next.js `ImageProps` (except `src`), plus:
235
241
 
236
- | Prop | Type | Description |
237
- |------|------|-------------|
238
- | `media` | `MediaResource \| string` | Payload media document or URL string |
239
- | `alt` | `string` | Alt text (overrides `media.alt`) |
242
+ | Prop | Type | Default | Description |
243
+ |------|------|---------|-------------|
244
+ | `media` | `MediaResource \| string` | — | Payload media document or URL string |
245
+ | `alt` | `string` | — | Alt text (overrides `media.alt`) |
246
+ | `fade` | `boolean` | `true` | Enable smooth blur-to-sharp fade transition on load |
247
+ | `fadeDuration` | `number` | `500` | Duration of the fade animation in milliseconds |
240
248
 
241
249
  Automatically applies:
242
250
  - ThumbHash blur placeholder (if available on the media resource)
251
+ - Smooth blur-to-sharp fade transition on image load (disable with `fade={false}`)
243
252
  - Focal point positioning via `objectPosition` (using `focalX`/`focalY`)
244
253
  - Cache-busting via `updatedAt` query parameter
245
254
  - `objectFit: 'cover'` by default (overridable via `style`)
246
255
 
256
+ ### `FadeImage` Component
257
+
258
+ Standalone Next.js `<Image>` wrapper with fade-in transition for use with `getImageOptimizerProps()`. Use this when you have a custom image component and want the fade effect without `ImageBox`.
259
+
260
+ ```tsx
261
+ import { FadeImage, getImageOptimizerProps } from '@inoo-ch/payload-image-optimizer/client'
262
+
263
+ const optimizerProps = getImageOptimizerProps(resource)
264
+
265
+ <FadeImage
266
+ src={resource.url}
267
+ alt=""
268
+ width={800}
269
+ height={600}
270
+ optimizerProps={optimizerProps}
271
+ />
272
+ ```
273
+
274
+ **Props:** Extends all Next.js `ImageProps` (except `placeholder`, `blurDataURL`, `onLoad`), plus:
275
+
276
+ | Prop | Type | Default | Description |
277
+ |------|------|---------|-------------|
278
+ | `optimizerProps` | `ImageOptimizerProps` | — | Props returned by `getImageOptimizerProps()` |
279
+ | `fadeDuration` | `number` | `500` | Duration of the fade animation in milliseconds |
280
+
247
281
  ### `getImageOptimizerProps()` Utility
248
282
 
249
283
  For integrating with existing image components (e.g., the Payload website template's `ImageMedia`):
@@ -293,6 +327,29 @@ The plugin registers two Payload job tasks (retries: 2 each):
293
327
  | `imageOptimizer_convertFormats` | After upload (`afterChange` hook) | Generate format variants for a single document |
294
328
  | `imageOptimizer_regenerateDocument` | Bulk regeneration endpoint | Fully re-optimize a single document (resize + thumbhash + all variants) |
295
329
 
330
+ ## Vercel / Serverless Deployment
331
+
332
+ Image processing can exceed the default serverless function timeout. Re-export the plugin's `maxDuration` from the Payload API route:
333
+
334
+ ```ts
335
+ // src/app/(payload)/api/[...slug]/route.ts
336
+ export { maxDuration } from '@inoo-ch/payload-image-optimizer'
337
+ ```
338
+
339
+ This sets a 60-second timeout. Without this, uploads with heavy configs (AVIF + ThumbHash + metadata stripping) may time out on Vercel.
340
+
341
+ ### Large file uploads with Vercel Blob
342
+
343
+ Even with `maxDuration` and `bodySizeLimit`, large uploads hit Vercel's 4.5MB request body limit on serverless functions. If using `@payloadcms/storage-vercel-blob`, enable `clientUploads: true` so files upload directly from the browser to Vercel Blob (up to 5TB), bypassing the server body size limit entirely:
344
+
345
+ ```ts
346
+ vercelBlobStorage({
347
+ collections: { media: true },
348
+ token: process.env.BLOB_READ_WRITE_TOKEN,
349
+ clientUploads: true,
350
+ })
351
+ ```
352
+
296
353
  ## Full Example
297
354
 
298
355
  ```ts
@@ -360,6 +417,7 @@ import type {
360
417
 
361
418
  import type {
362
419
  ImageBoxProps,
420
+ FadeImageProps,
363
421
  ImageOptimizerProps, // return type of getImageOptimizerProps
364
422
  } from '@inoo-ch/payload-image-optimizer/client'
365
423
  ```
package/README.md CHANGED
@@ -17,7 +17,8 @@ Built and maintained by [inoo.ch](https://inoo.ch) — a Swiss digital agency cr
17
17
  - **Bulk regeneration** — Re-process existing images from the admin UI with progress tracking
18
18
  - **Per-collection config** — Override formats, quality, and dimensions per collection
19
19
  - **Admin UI** — Status badges, file size savings, and blur previews in the sidebar
20
- - **ImageBox component** — Drop-in Next.js `<Image>` wrapper with automatic ThumbHash blur
20
+ - **ImageBox component** — Drop-in Next.js `<Image>` wrapper with automatic ThumbHash blur and smooth fade-in
21
+ - **FadeImage component** — Standalone fade-in image for custom setups using `getImageOptimizerProps()`
21
22
 
22
23
  ## Requirements
23
24
 
@@ -132,6 +133,31 @@ collections: {
132
133
 
133
134
  All format conversion runs as async background jobs, so uploads return immediately.
134
135
 
136
+ ### Vercel / Serverless Deployment
137
+
138
+ Image processing (especially AVIF encoding, ThumbHash generation, and metadata stripping) can exceed the default serverless function timeout. The plugin exports a recommended `maxDuration` that you can re-export from your Payload API route:
139
+
140
+ ```ts
141
+ // src/app/(payload)/api/[...slug]/route.ts
142
+ export { maxDuration } from '@inoo-ch/payload-image-optimizer'
143
+ ```
144
+
145
+ This sets a 60-second timeout, which is sufficient for most configurations. Without this, heavy processing configs may cause upload timeouts on Vercel.
146
+
147
+ #### Large file uploads with Vercel Blob
148
+
149
+ Even with `maxDuration` and `bodySizeLimit` configured, large file uploads through the Payload admin still go through the Next.js API route, which hits Vercel's request body size limit (4.5MB on serverless functions). If you're using `@payloadcms/storage-vercel-blob`, enable `clientUploads` to bypass this entirely:
150
+
151
+ ```ts
152
+ vercelBlobStorage({
153
+ collections: { media: true },
154
+ token: process.env.BLOB_READ_WRITE_TOKEN,
155
+ clientUploads: true, // uploads go directly from browser to Vercel Blob
156
+ })
157
+ ```
158
+
159
+ With `clientUploads: true`, files upload directly from the browser to Vercel Blob (up to 5TB) and the server only handles the small JSON metadata payload. This eliminates body size limit errors regardless of file size.
160
+
135
161
  ## How It Differs from Payload's Default Image Handling
136
162
 
137
163
  Payload CMS ships with [sharp](https://sharp.pixelplumbing.com/) built-in and can resize images and generate sizes on upload. This plugin **does not double-process your images** — it intercepts the raw upload in a `beforeChange` hook *before* Payload's own sharp pipeline runs, and writes the optimized buffer back to `req.file.data`. When Payload's built-in `uploadFiles` step kicks in to generate your configured sizes, it works from the already-optimized file, not the raw original.
@@ -146,7 +172,7 @@ Payload CMS ships with [sharp](https://sharp.pixelplumbing.com/) built-in and ca
146
172
  | Blur hash placeholders | Requires custom hooks | ThumbHash generated automatically |
147
173
  | Optimization status & savings | Not available | Admin sidebar panel per image |
148
174
  | Bulk re-process existing images | Not available | One-click regeneration with progress tracking |
149
- | Next.js `<Image>` with blur placeholder | Manual wiring | Drop-in `<ImageBox>` component |
175
+ | Next.js `<Image>` with blur placeholder | Manual wiring | Drop-in `<ImageBox>` / `<FadeImage>` components |
150
176
  | Per-collection format/quality overrides | N/A | Supported |
151
177
 
152
178
  ### CPU & Resource Impact
@@ -171,7 +197,7 @@ A **Regenerate Images** button appears in collection list views, allowing you to
171
197
 
172
198
  ## ImageBox Component
173
199
 
174
- The plugin exports an `ImageBox` component — a Next.js `<Image>` wrapper that automatically applies ThumbHash blur placeholders:
200
+ The plugin exports an `ImageBox` component — a Next.js `<Image>` wrapper that automatically applies ThumbHash blur placeholders with a smooth blur-to-sharp fade transition:
175
201
 
176
202
  ```tsx
177
203
  import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
@@ -181,10 +207,17 @@ import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
181
207
 
182
208
  // Or use a plain URL string
183
209
  <ImageBox media="/images/photo.jpg" alt="Photo" width={800} height={600} />
210
+
211
+ // Disable fade animation
212
+ <ImageBox media={doc.image} alt="Photo" fade={false} />
213
+
214
+ // Custom fade duration (default: 500ms)
215
+ <ImageBox media={doc.image} alt="Photo" fadeDuration={300} />
184
216
  ```
185
217
 
186
218
  **Features:**
187
219
  - Automatic ThumbHash `blurDataURL` from the media document
220
+ - Smooth blur-to-sharp fade transition on load (enabled by default)
188
221
  - Respects Payload focal point (`focalX` / `focalY`) for `objectPosition`
189
222
  - Lazy loading by default, with `priority` prop for above-the-fold images
190
223
  - Cache busting via `updatedAt` timestamp
@@ -254,7 +287,7 @@ Copy-paste this instruction to your AI coding agent to have it autonomously inte
254
287
  >
255
288
  > 1. Which upload collections should be optimized and with what settings
256
289
  > 2. Whether to use `replaceOriginal` or keep originals alongside variants
257
- > 3. Where to add `<ImageBox>` or `getImageOptimizerProps()` in the frontend for ThumbHash blur placeholders and focal point support
290
+ > 3. Where to add `<ImageBox>`, `<FadeImage>`, or `getImageOptimizerProps()` in the frontend for ThumbHash blur placeholders with smooth fade-in and focal point support
258
291
  > 4. Whether any existing image rendering code should use the optimized variants
259
292
  >
260
293
  > Use the zero-config default (`collections: { <slug>: true }`) unless the project has specific requirements that call for custom settings.
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { type ImageProps } from 'next/image';
3
+ import type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js';
4
+ export interface FadeImageProps extends Omit<ImageProps, 'placeholder' | 'blurDataURL' | 'onLoad'> {
5
+ /** Props returned by `getImageOptimizerProps()`. */
6
+ optimizerProps: ImageOptimizerProps;
7
+ /** Duration of the fade animation in milliseconds. Defaults to `500`. */
8
+ fadeDuration?: number;
9
+ }
10
+ /**
11
+ * A Next.js `<Image>` wrapper that applies ThumbHash blur placeholders with a
12
+ * smooth blur-to-sharp fade transition on load.
13
+ *
14
+ * Use this when you call `getImageOptimizerProps()` manually instead of using `ImageBox`:
15
+ *
16
+ * ```tsx
17
+ * import { FadeImage, getImageOptimizerProps } from '@inoo-ch/payload-image-optimizer/client'
18
+ *
19
+ * const optimizerProps = getImageOptimizerProps(resource)
20
+ * <FadeImage src={src} alt="" optimizerProps={optimizerProps} width={800} height={600} />
21
+ * ```
22
+ */
23
+ export declare const FadeImage: React.FC<FadeImageProps>;
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React, { useState } from 'react';
4
+ import NextImage from 'next/image';
5
+ /**
6
+ * A Next.js `<Image>` wrapper that applies ThumbHash blur placeholders with a
7
+ * smooth blur-to-sharp fade transition on load.
8
+ *
9
+ * Use this when you call `getImageOptimizerProps()` manually instead of using `ImageBox`:
10
+ *
11
+ * ```tsx
12
+ * import { FadeImage, getImageOptimizerProps } from '@inoo-ch/payload-image-optimizer/client'
13
+ *
14
+ * const optimizerProps = getImageOptimizerProps(resource)
15
+ * <FadeImage src={src} alt="" optimizerProps={optimizerProps} width={800} height={600} />
16
+ * ```
17
+ */ export const FadeImage = ({ optimizerProps, style, fadeDuration = 500, ...props })=>{
18
+ const [loaded, setLoaded] = useState(false);
19
+ const { blurDataURL, style: optimizerStyle } = optimizerProps;
20
+ return /*#__PURE__*/ _jsx(NextImage, {
21
+ ...props,
22
+ placeholder: blurDataURL ? 'blur' : 'empty',
23
+ blurDataURL: blurDataURL,
24
+ style: {
25
+ ...optimizerStyle,
26
+ ...style,
27
+ filter: loaded ? 'blur(0px)' : 'blur(20px)',
28
+ transition: loaded ? `filter ${fadeDuration}ms ease-in-out` : undefined
29
+ },
30
+ onLoad: ()=>setLoaded(true)
31
+ });
32
+ };
33
+
34
+ //# sourceMappingURL=FadeImage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/FadeImage.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState } from 'react'\nimport NextImage, { type ImageProps } from 'next/image'\nimport type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\n\nexport interface FadeImageProps extends Omit<ImageProps, 'placeholder' | 'blurDataURL' | 'onLoad'> {\n /** Props returned by `getImageOptimizerProps()`. */\n optimizerProps: ImageOptimizerProps\n /** Duration of the fade animation in milliseconds. Defaults to `500`. */\n fadeDuration?: number\n}\n\n/**\n * A Next.js `<Image>` wrapper that applies ThumbHash blur placeholders with a\n * smooth blur-to-sharp fade transition on load.\n *\n * Use this when you call `getImageOptimizerProps()` manually instead of using `ImageBox`:\n *\n * ```tsx\n * import { FadeImage, getImageOptimizerProps } from '@inoo-ch/payload-image-optimizer/client'\n *\n * const optimizerProps = getImageOptimizerProps(resource)\n * <FadeImage src={src} alt=\"\" optimizerProps={optimizerProps} width={800} height={600} />\n * ```\n */\nexport const FadeImage: React.FC<FadeImageProps> = ({\n optimizerProps,\n style,\n fadeDuration = 500,\n ...props\n}) => {\n const [loaded, setLoaded] = useState(false)\n\n const { blurDataURL, style: optimizerStyle } = optimizerProps\n\n return (\n <NextImage\n {...props}\n placeholder={blurDataURL ? 'blur' : 'empty'}\n blurDataURL={blurDataURL}\n style={{\n ...optimizerStyle,\n ...style,\n filter: loaded ? 'blur(0px)' : 'blur(20px)',\n transition: loaded ? `filter ${fadeDuration}ms ease-in-out` : undefined,\n }}\n onLoad={() => setLoaded(true)}\n />\n )\n}\n"],"names":["React","useState","NextImage","FadeImage","optimizerProps","style","fadeDuration","props","loaded","setLoaded","blurDataURL","optimizerStyle","placeholder","filter","transition","undefined","onLoad"],"mappings":"AAAA;;AAEA,OAAOA,SAASC,QAAQ,QAAQ,QAAO;AACvC,OAAOC,eAAoC,aAAY;AAUvD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,YAAsC,CAAC,EAClDC,cAAc,EACdC,KAAK,EACLC,eAAe,GAAG,EAClB,GAAGC,OACJ;IACC,MAAM,CAACC,QAAQC,UAAU,GAAGR,SAAS;IAErC,MAAM,EAAES,WAAW,EAAEL,OAAOM,cAAc,EAAE,GAAGP;IAE/C,qBACE,KAACF;QACE,GAAGK,KAAK;QACTK,aAAaF,cAAc,SAAS;QACpCA,aAAaA;QACbL,OAAO;YACL,GAAGM,cAAc;YACjB,GAAGN,KAAK;YACRQ,QAAQL,SAAS,cAAc;YAC/BM,YAAYN,SAAS,CAAC,OAAO,EAAEF,aAAa,cAAc,CAAC,GAAGS;QAChE;QACAC,QAAQ,IAAMP,UAAU;;AAG9B,EAAC"}
@@ -4,5 +4,9 @@ import type { MediaResource } from '../types.js';
4
4
  export interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {
5
5
  media: MediaResource | string;
6
6
  alt?: string;
7
+ /** Enable smooth blur-to-sharp fade transition on load. Defaults to `true`. */
8
+ fade?: boolean;
9
+ /** Duration of the fade animation in milliseconds. Defaults to `500`. */
10
+ fadeDuration?: number;
7
11
  }
8
12
  export declare const ImageBox: React.FC<ImageBoxProps>;
@@ -1,10 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import React from 'react';
3
+ import React, { useState } from 'react';
4
4
  import NextImage from 'next/image';
5
5
  import { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js';
6
- export const ImageBox = ({ media, alt: altFromProps, fill, sizes, priority, loading: loadingFromProps, style: styleFromProps, ...props })=>{
6
+ export const ImageBox = ({ media, alt: altFromProps, fill, sizes, priority, loading: loadingFromProps, style: styleFromProps, fade = true, fadeDuration = 500, ...props })=>{
7
+ const [loaded, setLoaded] = useState(false);
7
8
  const loading = priority ? undefined : loadingFromProps ?? 'lazy';
9
+ const fadeStyle = fade ? {
10
+ filter: loaded ? 'blur(0px)' : 'blur(20px)',
11
+ transition: loaded ? `filter ${fadeDuration}ms ease-in-out` : undefined
12
+ } : undefined;
8
13
  if (typeof media === 'string') {
9
14
  return /*#__PURE__*/ _jsx(NextImage, {
10
15
  ...props,
@@ -16,10 +21,12 @@ export const ImageBox = ({ media, alt: altFromProps, fill, sizes, priority, load
16
21
  style: {
17
22
  objectFit: 'cover',
18
23
  objectPosition: 'center',
24
+ ...fadeStyle,
19
25
  ...styleFromProps
20
26
  },
21
27
  priority: priority,
22
- loading: loading
28
+ loading: loading,
29
+ onLoad: fade ? ()=>setLoaded(true) : undefined
23
30
  });
24
31
  }
25
32
  const width = media.width ?? undefined;
@@ -39,12 +46,14 @@ export const ImageBox = ({ media, alt: altFromProps, fill, sizes, priority, load
39
46
  style: {
40
47
  objectFit: 'cover',
41
48
  ...optimizerProps.style,
49
+ ...fadeStyle,
42
50
  ...styleFromProps
43
51
  },
44
52
  placeholder: optimizerProps.placeholder,
45
53
  blurDataURL: optimizerProps.blurDataURL,
46
54
  priority: priority,
47
- loading: loading
55
+ loading: loading,
56
+ onLoad: fade ? ()=>setLoaded(true) : undefined
48
57
  });
49
58
  };
50
59
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ImageBox.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\nimport NextImage, { type ImageProps } from 'next/image'\nimport type { MediaResource } from '../types.js'\nimport { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\n\nexport interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {\n media: MediaResource | string\n alt?: string\n}\n\nexport const ImageBox: React.FC<ImageBoxProps> = ({\n media,\n alt: altFromProps,\n fill,\n sizes,\n priority,\n loading: loadingFromProps,\n style: styleFromProps,\n ...props\n}) => {\n const loading = priority ? undefined : (loadingFromProps ?? 'lazy')\n\n if (typeof media === 'string') {\n return (\n <NextImage\n {...props}\n src={media}\n alt={altFromProps || ''}\n quality={80}\n fill={fill}\n sizes={sizes}\n style={{ objectFit: 'cover', objectPosition: 'center', ...styleFromProps }}\n priority={priority}\n loading={loading}\n />\n )\n }\n\n const width = media.width ?? undefined\n const height = media.height ?? undefined\n const alt = altFromProps || (media as any).alt || media.filename || ''\n const src = media.url ? `${media.url}${media.updatedAt ? `?${media.updatedAt}` : ''}` : ''\n\n const optimizerProps = getImageOptimizerProps(media)\n\n return (\n <NextImage\n {...props}\n src={src}\n alt={alt}\n quality={80}\n fill={fill}\n width={!fill ? width : undefined}\n height={!fill ? height : undefined}\n sizes={sizes}\n style={{ objectFit: 'cover', ...optimizerProps.style, ...styleFromProps }}\n placeholder={optimizerProps.placeholder}\n blurDataURL={optimizerProps.blurDataURL}\n priority={priority}\n loading={loading}\n />\n )\n}\n"],"names":["React","NextImage","getImageOptimizerProps","ImageBox","media","alt","altFromProps","fill","sizes","priority","loading","loadingFromProps","style","styleFromProps","props","undefined","src","quality","objectFit","objectPosition","width","height","filename","url","updatedAt","optimizerProps","placeholder","blurDataURL"],"mappings":"AAAA;;AAEA,OAAOA,WAAW,QAAO;AACzB,OAAOC,eAAoC,aAAY;AAEvD,SAASC,sBAAsB,QAAQ,yCAAwC;AAO/E,OAAO,MAAMC,WAAoC,CAAC,EAChDC,KAAK,EACLC,KAAKC,YAAY,EACjBC,IAAI,EACJC,KAAK,EACLC,QAAQ,EACRC,SAASC,gBAAgB,EACzBC,OAAOC,cAAc,EACrB,GAAGC,OACJ;IACC,MAAMJ,UAAUD,WAAWM,YAAaJ,oBAAoB;IAE5D,IAAI,OAAOP,UAAU,UAAU;QAC7B,qBACE,KAACH;YACE,GAAGa,KAAK;YACTE,KAAKZ;YACLC,KAAKC,gBAAgB;YACrBW,SAAS;YACTV,MAAMA;YACNC,OAAOA;YACPI,OAAO;gBAAEM,WAAW;gBAASC,gBAAgB;gBAAU,GAAGN,cAAc;YAAC;YACzEJ,UAAUA;YACVC,SAASA;;IAGf;IAEA,MAAMU,QAAQhB,MAAMgB,KAAK,IAAIL;IAC7B,MAAMM,SAASjB,MAAMiB,MAAM,IAAIN;IAC/B,MAAMV,MAAMC,gBAAgB,AAACF,MAAcC,GAAG,IAAID,MAAMkB,QAAQ,IAAI;IACpE,MAAMN,MAAMZ,MAAMmB,GAAG,GAAG,GAAGnB,MAAMmB,GAAG,GAAGnB,MAAMoB,SAAS,GAAG,CAAC,CAAC,EAAEpB,MAAMoB,SAAS,EAAE,GAAG,IAAI,GAAG;IAExF,MAAMC,iBAAiBvB,uBAAuBE;IAE9C,qBACE,KAACH;QACE,GAAGa,KAAK;QACTE,KAAKA;QACLX,KAAKA;QACLY,SAAS;QACTV,MAAMA;QACNa,OAAO,CAACb,OAAOa,QAAQL;QACvBM,QAAQ,CAACd,OAAOc,SAASN;QACzBP,OAAOA;QACPI,OAAO;YAAEM,WAAW;YAAS,GAAGO,eAAeb,KAAK;YAAE,GAAGC,cAAc;QAAC;QACxEa,aAAaD,eAAeC,WAAW;QACvCC,aAAaF,eAAeE,WAAW;QACvClB,UAAUA;QACVC,SAASA;;AAGf,EAAC"}
1
+ {"version":3,"sources":["../../src/components/ImageBox.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState } from 'react'\nimport NextImage, { type ImageProps } from 'next/image'\nimport type { MediaResource } from '../types.js'\nimport { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\n\nexport interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {\n media: MediaResource | string\n alt?: string\n /** Enable smooth blur-to-sharp fade transition on load. Defaults to `true`. */\n fade?: boolean\n /** Duration of the fade animation in milliseconds. Defaults to `500`. */\n fadeDuration?: number\n}\n\nexport const ImageBox: React.FC<ImageBoxProps> = ({\n media,\n alt: altFromProps,\n fill,\n sizes,\n priority,\n loading: loadingFromProps,\n style: styleFromProps,\n fade = true,\n fadeDuration = 500,\n ...props\n}) => {\n const [loaded, setLoaded] = useState(false)\n const loading = priority ? undefined : (loadingFromProps ?? 'lazy')\n\n const fadeStyle = fade\n ? {\n filter: loaded ? 'blur(0px)' : 'blur(20px)',\n transition: loaded ? `filter ${fadeDuration}ms ease-in-out` : undefined,\n }\n : undefined\n\n if (typeof media === 'string') {\n return (\n <NextImage\n {...props}\n src={media}\n alt={altFromProps || ''}\n quality={80}\n fill={fill}\n sizes={sizes}\n style={{ objectFit: 'cover', objectPosition: 'center', ...fadeStyle, ...styleFromProps }}\n priority={priority}\n loading={loading}\n onLoad={fade ? () => setLoaded(true) : undefined}\n />\n )\n }\n\n const width = media.width ?? undefined\n const height = media.height ?? undefined\n const alt = altFromProps || (media as any).alt || media.filename || ''\n const src = media.url ? `${media.url}${media.updatedAt ? `?${media.updatedAt}` : ''}` : ''\n\n const optimizerProps = getImageOptimizerProps(media)\n\n return (\n <NextImage\n {...props}\n src={src}\n alt={alt}\n quality={80}\n fill={fill}\n width={!fill ? width : undefined}\n height={!fill ? height : undefined}\n sizes={sizes}\n style={{ objectFit: 'cover', ...optimizerProps.style, ...fadeStyle, ...styleFromProps }}\n placeholder={optimizerProps.placeholder}\n blurDataURL={optimizerProps.blurDataURL}\n priority={priority}\n loading={loading}\n onLoad={fade ? () => setLoaded(true) : undefined}\n />\n )\n}\n"],"names":["React","useState","NextImage","getImageOptimizerProps","ImageBox","media","alt","altFromProps","fill","sizes","priority","loading","loadingFromProps","style","styleFromProps","fade","fadeDuration","props","loaded","setLoaded","undefined","fadeStyle","filter","transition","src","quality","objectFit","objectPosition","onLoad","width","height","filename","url","updatedAt","optimizerProps","placeholder","blurDataURL"],"mappings":"AAAA;;AAEA,OAAOA,SAASC,QAAQ,QAAQ,QAAO;AACvC,OAAOC,eAAoC,aAAY;AAEvD,SAASC,sBAAsB,QAAQ,yCAAwC;AAW/E,OAAO,MAAMC,WAAoC,CAAC,EAChDC,KAAK,EACLC,KAAKC,YAAY,EACjBC,IAAI,EACJC,KAAK,EACLC,QAAQ,EACRC,SAASC,gBAAgB,EACzBC,OAAOC,cAAc,EACrBC,OAAO,IAAI,EACXC,eAAe,GAAG,EAClB,GAAGC,OACJ;IACC,MAAM,CAACC,QAAQC,UAAU,GAAGlB,SAAS;IACrC,MAAMU,UAAUD,WAAWU,YAAaR,oBAAoB;IAE5D,MAAMS,YAAYN,OACd;QACEO,QAAQJ,SAAS,cAAc;QAC/BK,YAAYL,SAAS,CAAC,OAAO,EAAEF,aAAa,cAAc,CAAC,GAAGI;IAChE,IACAA;IAEJ,IAAI,OAAOf,UAAU,UAAU;QAC7B,qBACE,KAACH;YACE,GAAGe,KAAK;YACTO,KAAKnB;YACLC,KAAKC,gBAAgB;YACrBkB,SAAS;YACTjB,MAAMA;YACNC,OAAOA;YACPI,OAAO;gBAAEa,WAAW;gBAASC,gBAAgB;gBAAU,GAAGN,SAAS;gBAAE,GAAGP,cAAc;YAAC;YACvFJ,UAAUA;YACVC,SAASA;YACTiB,QAAQb,OAAO,IAAMI,UAAU,QAAQC;;IAG7C;IAEA,MAAMS,QAAQxB,MAAMwB,KAAK,IAAIT;IAC7B,MAAMU,SAASzB,MAAMyB,MAAM,IAAIV;IAC/B,MAAMd,MAAMC,gBAAgB,AAACF,MAAcC,GAAG,IAAID,MAAM0B,QAAQ,IAAI;IACpE,MAAMP,MAAMnB,MAAM2B,GAAG,GAAG,GAAG3B,MAAM2B,GAAG,GAAG3B,MAAM4B,SAAS,GAAG,CAAC,CAAC,EAAE5B,MAAM4B,SAAS,EAAE,GAAG,IAAI,GAAG;IAExF,MAAMC,iBAAiB/B,uBAAuBE;IAE9C,qBACE,KAACH;QACE,GAAGe,KAAK;QACTO,KAAKA;QACLlB,KAAKA;QACLmB,SAAS;QACTjB,MAAMA;QACNqB,OAAO,CAACrB,OAAOqB,QAAQT;QACvBU,QAAQ,CAACtB,OAAOsB,SAASV;QACzBX,OAAOA;QACPI,OAAO;YAAEa,WAAW;YAAS,GAAGQ,eAAerB,KAAK;YAAE,GAAGQ,SAAS;YAAE,GAAGP,cAAc;QAAC;QACtFqB,aAAaD,eAAeC,WAAW;QACvCC,aAAaF,eAAeE,WAAW;QACvC1B,UAAUA;QACVC,SAASA;QACTiB,QAAQb,OAAO,IAAMI,UAAU,QAAQC;;AAG7C,EAAC"}
@@ -1,6 +1,8 @@
1
1
  export { OptimizationStatus } from '../components/OptimizationStatus.js';
2
2
  export { ImageBox } from '../components/ImageBox.js';
3
3
  export type { ImageBoxProps } from '../components/ImageBox.js';
4
+ export { FadeImage } from '../components/FadeImage.js';
5
+ export type { FadeImageProps } from '../components/FadeImage.js';
4
6
  export { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js';
5
7
  export type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js';
6
8
  export { RegenerationButton } from '../components/RegenerationButton.js';
@@ -1,5 +1,6 @@
1
1
  export { OptimizationStatus } from '../components/OptimizationStatus.js';
2
2
  export { ImageBox } from '../components/ImageBox.js';
3
+ export { FadeImage } from '../components/FadeImage.js';
3
4
  export { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js';
4
5
  export { RegenerationButton } from '../components/RegenerationButton.js';
5
6
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { OptimizationStatus } from '../components/OptimizationStatus.js'\nexport { ImageBox } from '../components/ImageBox.js'\nexport type { ImageBoxProps } from '../components/ImageBox.js'\nexport { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\nexport type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\nexport { RegenerationButton } from '../components/RegenerationButton.js'\n"],"names":["OptimizationStatus","ImageBox","getImageOptimizerProps","RegenerationButton"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,sCAAqC;AACxE,SAASC,QAAQ,QAAQ,4BAA2B;AAEpD,SAASC,sBAAsB,QAAQ,yCAAwC;AAE/E,SAASC,kBAAkB,QAAQ,sCAAqC"}
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { OptimizationStatus } from '../components/OptimizationStatus.js'\nexport { ImageBox } from '../components/ImageBox.js'\nexport type { ImageBoxProps } from '../components/ImageBox.js'\nexport { FadeImage } from '../components/FadeImage.js'\nexport type { FadeImageProps } from '../components/FadeImage.js'\nexport { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\nexport type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'\nexport { RegenerationButton } from '../components/RegenerationButton.js'\n"],"names":["OptimizationStatus","ImageBox","FadeImage","getImageOptimizerProps","RegenerationButton"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,sCAAqC;AACxE,SAASC,QAAQ,QAAQ,4BAA2B;AAEpD,SAASC,SAAS,QAAQ,6BAA4B;AAEtD,SAASC,sBAAsB,QAAQ,yCAAwC;AAE/E,SAASC,kBAAkB,QAAQ,sCAAqC"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,11 @@ import type { ImageOptimizerConfig } from './types.js';
3
3
  export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, FieldsOverride } from './types.js';
4
4
  export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js';
5
5
  export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
6
+ /**
7
+ * Recommended maxDuration for the Payload API route on Vercel.
8
+ * Re-export this in your route file:
9
+ *
10
+ * export { maxDuration } from '@inoo-ch/payload-image-optimizer'
11
+ */
12
+ export declare const maxDuration = 60;
6
13
  export declare const imageOptimizer: (pluginOptions: ImageOptimizerConfig) => (config: Config) => Config;
package/dist/index.js CHANGED
@@ -9,6 +9,12 @@ import { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js';
9
9
  import { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js';
10
10
  export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js';
11
11
  export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
12
+ /**
13
+ * Recommended maxDuration for the Payload API route on Vercel.
14
+ * Re-export this in your route file:
15
+ *
16
+ * export { maxDuration } from '@inoo-ch/payload-image-optimizer'
17
+ */ export const maxDuration = 60;
12
18
  export const imageOptimizer = (pluginOptions)=>(config)=>{
13
19
  const resolvedConfig = resolveConfig(pluginOptions);
14
20
  const targetSlugs = Object.keys(resolvedConfig.collections);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ImageOptimizerConfig } from './types.js'\nimport { resolveConfig } from './defaults.js'\nimport { translations } from './translations/index.js'\nimport { getImageOptimizerField } from './fields/imageOptimizerField.js'\nimport { createBeforeChangeHook } from './hooks/beforeChange.js'\nimport { createAfterChangeHook } from './hooks/afterChange.js'\nimport { createConvertFormatsHandler } from './tasks/convertFormats.js'\nimport { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js'\nimport { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js'\n\nexport type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, FieldsOverride } from './types.js'\nexport { defaultImageOptimizerFields } from './fields/imageOptimizerField.js'\n\nexport { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'\n\nexport const imageOptimizer =\n (pluginOptions: ImageOptimizerConfig) =>\n (config: Config): Config => {\n const resolvedConfig = resolveConfig(pluginOptions)\n const targetSlugs = Object.keys(resolvedConfig.collections)\n\n // Inject fields (and hooks when enabled) into targeted upload collections\n const collections = (config.collections || []).map((collection) => {\n if (!targetSlugs.includes(collection.slug)) {\n return collection\n }\n\n // Always inject fields for schema consistency (even when disabled)\n const fields = [...collection.fields, getImageOptimizerField(pluginOptions.fieldsOverride)]\n\n if (resolvedConfig.disabled) {\n return { ...collection, fields }\n }\n\n return {\n ...collection,\n fields,\n hooks: {\n ...collection.hooks,\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n createBeforeChangeHook(resolvedConfig, collection.slug),\n ],\n afterChange: [\n ...(collection.hooks?.afterChange || []),\n createAfterChangeHook(resolvedConfig, collection.slug),\n ],\n },\n admin: {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n beforeListTable: [\n ...(collection.admin?.components?.beforeListTable || []),\n '@inoo-ch/payload-image-optimizer/client#RegenerationButton',\n ],\n },\n },\n }\n })\n\n const i18n = {\n ...config.i18n,\n translations: deepMergeSimple(translations, config.i18n?.translations ?? {}),\n }\n\n // If disabled, return with fields injected but no tasks/endpoints\n if (resolvedConfig.disabled) {\n return { ...config, collections, i18n }\n }\n\n return {\n ...config,\n collections,\n i18n,\n jobs: {\n ...config.jobs,\n tasks: [\n ...(config.jobs?.tasks || []),\n {\n slug: 'imageOptimizer_convertFormats',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'variantsGenerated', type: 'number' },\n ],\n retries: 2,\n handler: createConvertFormatsHandler(resolvedConfig),\n } as any,\n {\n slug: 'imageOptimizer_regenerateDocument',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'status', type: 'text' },\n { name: 'reason', type: 'text' },\n ],\n retries: 2,\n handler: createRegenerateDocumentHandler(resolvedConfig),\n } as any,\n ],\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n path: '/image-optimizer/regenerate',\n method: 'post',\n handler: createRegenerateHandler(resolvedConfig),\n },\n {\n path: '/image-optimizer/regenerate',\n method: 'get',\n handler: createRegenerateStatusHandler(resolvedConfig),\n },\n ],\n }\n }\n"],"names":["deepMergeSimple","resolveConfig","translations","getImageOptimizerField","createBeforeChangeHook","createAfterChangeHook","createConvertFormatsHandler","createRegenerateDocumentHandler","createRegenerateHandler","createRegenerateStatusHandler","defaultImageOptimizerFields","encodeImageToThumbHash","decodeThumbHashToDataURL","imageOptimizer","pluginOptions","config","resolvedConfig","targetSlugs","Object","keys","collections","map","collection","includes","slug","fields","fieldsOverride","disabled","hooks","beforeChange","afterChange","admin","components","beforeListTable","i18n","jobs","tasks","inputSchema","name","type","required","outputSchema","retries","handler","endpoints","path","method"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ImageOptimizerConfig } from './types.js'\nimport { resolveConfig } from './defaults.js'\nimport { translations } from './translations/index.js'\nimport { getImageOptimizerField } from './fields/imageOptimizerField.js'\nimport { createBeforeChangeHook } from './hooks/beforeChange.js'\nimport { createAfterChangeHook } from './hooks/afterChange.js'\nimport { createConvertFormatsHandler } from './tasks/convertFormats.js'\nimport { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js'\nimport { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js'\n\nexport type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, FieldsOverride } from './types.js'\nexport { defaultImageOptimizerFields } from './fields/imageOptimizerField.js'\n\nexport { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'\n\n/**\n * Recommended maxDuration for the Payload API route on Vercel.\n * Re-export this in your route file:\n *\n * export { maxDuration } from '@inoo-ch/payload-image-optimizer'\n */\nexport const maxDuration = 60\n\nexport const imageOptimizer =\n (pluginOptions: ImageOptimizerConfig) =>\n (config: Config): Config => {\n const resolvedConfig = resolveConfig(pluginOptions)\n const targetSlugs = Object.keys(resolvedConfig.collections)\n\n // Inject fields (and hooks when enabled) into targeted upload collections\n const collections = (config.collections || []).map((collection) => {\n if (!targetSlugs.includes(collection.slug)) {\n return collection\n }\n\n // Always inject fields for schema consistency (even when disabled)\n const fields = [...collection.fields, getImageOptimizerField(pluginOptions.fieldsOverride)]\n\n if (resolvedConfig.disabled) {\n return { ...collection, fields }\n }\n\n return {\n ...collection,\n fields,\n hooks: {\n ...collection.hooks,\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n createBeforeChangeHook(resolvedConfig, collection.slug),\n ],\n afterChange: [\n ...(collection.hooks?.afterChange || []),\n createAfterChangeHook(resolvedConfig, collection.slug),\n ],\n },\n admin: {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n beforeListTable: [\n ...(collection.admin?.components?.beforeListTable || []),\n '@inoo-ch/payload-image-optimizer/client#RegenerationButton',\n ],\n },\n },\n }\n })\n\n const i18n = {\n ...config.i18n,\n translations: deepMergeSimple(translations, config.i18n?.translations ?? {}),\n }\n\n // If disabled, return with fields injected but no tasks/endpoints\n if (resolvedConfig.disabled) {\n return { ...config, collections, i18n }\n }\n\n return {\n ...config,\n collections,\n i18n,\n jobs: {\n ...config.jobs,\n tasks: [\n ...(config.jobs?.tasks || []),\n {\n slug: 'imageOptimizer_convertFormats',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'variantsGenerated', type: 'number' },\n ],\n retries: 2,\n handler: createConvertFormatsHandler(resolvedConfig),\n } as any,\n {\n slug: 'imageOptimizer_regenerateDocument',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'status', type: 'text' },\n { name: 'reason', type: 'text' },\n ],\n retries: 2,\n handler: createRegenerateDocumentHandler(resolvedConfig),\n } as any,\n ],\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n path: '/image-optimizer/regenerate',\n method: 'post',\n handler: createRegenerateHandler(resolvedConfig),\n },\n {\n path: '/image-optimizer/regenerate',\n method: 'get',\n handler: createRegenerateStatusHandler(resolvedConfig),\n },\n ],\n }\n }\n"],"names":["deepMergeSimple","resolveConfig","translations","getImageOptimizerField","createBeforeChangeHook","createAfterChangeHook","createConvertFormatsHandler","createRegenerateDocumentHandler","createRegenerateHandler","createRegenerateStatusHandler","defaultImageOptimizerFields","encodeImageToThumbHash","decodeThumbHashToDataURL","maxDuration","imageOptimizer","pluginOptions","config","resolvedConfig","targetSlugs","Object","keys","collections","map","collection","includes","slug","fields","fieldsOverride","disabled","hooks","beforeChange","afterChange","admin","components","beforeListTable","i18n","jobs","tasks","inputSchema","name","type","required","outputSchema","retries","handler","endpoints","path","method"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoo-ch/payload-image-optimizer",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import NextImage, { type ImageProps } from 'next/image'
5
+ import type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
6
+
7
+ export interface FadeImageProps extends Omit<ImageProps, 'placeholder' | 'blurDataURL' | 'onLoad'> {
8
+ /** Props returned by `getImageOptimizerProps()`. */
9
+ optimizerProps: ImageOptimizerProps
10
+ /** Duration of the fade animation in milliseconds. Defaults to `500`. */
11
+ fadeDuration?: number
12
+ }
13
+
14
+ /**
15
+ * A Next.js `<Image>` wrapper that applies ThumbHash blur placeholders with a
16
+ * smooth blur-to-sharp fade transition on load.
17
+ *
18
+ * Use this when you call `getImageOptimizerProps()` manually instead of using `ImageBox`:
19
+ *
20
+ * ```tsx
21
+ * import { FadeImage, getImageOptimizerProps } from '@inoo-ch/payload-image-optimizer/client'
22
+ *
23
+ * const optimizerProps = getImageOptimizerProps(resource)
24
+ * <FadeImage src={src} alt="" optimizerProps={optimizerProps} width={800} height={600} />
25
+ * ```
26
+ */
27
+ export const FadeImage: React.FC<FadeImageProps> = ({
28
+ optimizerProps,
29
+ style,
30
+ fadeDuration = 500,
31
+ ...props
32
+ }) => {
33
+ const [loaded, setLoaded] = useState(false)
34
+
35
+ const { blurDataURL, style: optimizerStyle } = optimizerProps
36
+
37
+ return (
38
+ <NextImage
39
+ {...props}
40
+ placeholder={blurDataURL ? 'blur' : 'empty'}
41
+ blurDataURL={blurDataURL}
42
+ style={{
43
+ ...optimizerStyle,
44
+ ...style,
45
+ filter: loaded ? 'blur(0px)' : 'blur(20px)',
46
+ transition: loaded ? `filter ${fadeDuration}ms ease-in-out` : undefined,
47
+ }}
48
+ onLoad={() => setLoaded(true)}
49
+ />
50
+ )
51
+ }
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import React from 'react'
3
+ import React, { useState } from 'react'
4
4
  import NextImage, { type ImageProps } from 'next/image'
5
5
  import type { MediaResource } from '../types.js'
6
6
  import { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
@@ -8,6 +8,10 @@ import { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
8
8
  export interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {
9
9
  media: MediaResource | string
10
10
  alt?: string
11
+ /** Enable smooth blur-to-sharp fade transition on load. Defaults to `true`. */
12
+ fade?: boolean
13
+ /** Duration of the fade animation in milliseconds. Defaults to `500`. */
14
+ fadeDuration?: number
11
15
  }
12
16
 
13
17
  export const ImageBox: React.FC<ImageBoxProps> = ({
@@ -18,10 +22,20 @@ export const ImageBox: React.FC<ImageBoxProps> = ({
18
22
  priority,
19
23
  loading: loadingFromProps,
20
24
  style: styleFromProps,
25
+ fade = true,
26
+ fadeDuration = 500,
21
27
  ...props
22
28
  }) => {
29
+ const [loaded, setLoaded] = useState(false)
23
30
  const loading = priority ? undefined : (loadingFromProps ?? 'lazy')
24
31
 
32
+ const fadeStyle = fade
33
+ ? {
34
+ filter: loaded ? 'blur(0px)' : 'blur(20px)',
35
+ transition: loaded ? `filter ${fadeDuration}ms ease-in-out` : undefined,
36
+ }
37
+ : undefined
38
+
25
39
  if (typeof media === 'string') {
26
40
  return (
27
41
  <NextImage
@@ -31,9 +45,10 @@ export const ImageBox: React.FC<ImageBoxProps> = ({
31
45
  quality={80}
32
46
  fill={fill}
33
47
  sizes={sizes}
34
- style={{ objectFit: 'cover', objectPosition: 'center', ...styleFromProps }}
48
+ style={{ objectFit: 'cover', objectPosition: 'center', ...fadeStyle, ...styleFromProps }}
35
49
  priority={priority}
36
50
  loading={loading}
51
+ onLoad={fade ? () => setLoaded(true) : undefined}
37
52
  />
38
53
  )
39
54
  }
@@ -55,11 +70,12 @@ export const ImageBox: React.FC<ImageBoxProps> = ({
55
70
  width={!fill ? width : undefined}
56
71
  height={!fill ? height : undefined}
57
72
  sizes={sizes}
58
- style={{ objectFit: 'cover', ...optimizerProps.style, ...styleFromProps }}
73
+ style={{ objectFit: 'cover', ...optimizerProps.style, ...fadeStyle, ...styleFromProps }}
59
74
  placeholder={optimizerProps.placeholder}
60
75
  blurDataURL={optimizerProps.blurDataURL}
61
76
  priority={priority}
62
77
  loading={loading}
78
+ onLoad={fade ? () => setLoaded(true) : undefined}
63
79
  />
64
80
  )
65
81
  }
@@ -1,6 +1,8 @@
1
1
  export { OptimizationStatus } from '../components/OptimizationStatus.js'
2
2
  export { ImageBox } from '../components/ImageBox.js'
3
3
  export type { ImageBoxProps } from '../components/ImageBox.js'
4
+ export { FadeImage } from '../components/FadeImage.js'
5
+ export type { FadeImageProps } from '../components/FadeImage.js'
4
6
  export { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
5
7
  export type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
6
8
  export { RegenerationButton } from '../components/RegenerationButton.js'
package/src/index.ts CHANGED
@@ -16,6 +16,14 @@ export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js'
16
16
 
17
17
  export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'
18
18
 
19
+ /**
20
+ * Recommended maxDuration for the Payload API route on Vercel.
21
+ * Re-export this in your route file:
22
+ *
23
+ * export { maxDuration } from '@inoo-ch/payload-image-optimizer'
24
+ */
25
+ export const maxDuration = 60
26
+
19
27
  export const imageOptimizer =
20
28
  (pluginOptions: ImageOptimizerConfig) =>
21
29
  (config: Config): Config => {