@inoo-ch/payload-image-optimizer 1.4.5 → 1.4.7

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`):
@@ -360,6 +394,7 @@ import type {
360
394
 
361
395
  import type {
362
396
  ImageBoxProps,
397
+ FadeImageProps,
363
398
  ImageOptimizerProps, // return type of getImageOptimizerProps
364
399
  } from '@inoo-ch/payload-image-optimizer/client'
365
400
  ```
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @inoo-ch/payload-image-optimizer
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@inoo-ch/payload-image-optimizer)](https://www.npmjs.com/package/@inoo-ch/payload-image-optimizer)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@inoo-ch/payload-image-optimizer)](https://www.npmjs.com/package/@inoo-ch/payload-image-optimizer)
5
+ [![GitHub](https://img.shields.io/github/license/PascalEugster/payloadcms-plugin-image-optimizer)](https://github.com/PascalEugster/payloadcms-plugin-image-optimizer)
6
+
3
7
  A [Payload CMS](https://payloadcms.com) plugin for automatic image optimization. Converts uploads to WebP/AVIF, resizes to configurable limits, strips EXIF metadata, generates [ThumbHash](https://evanw.github.io/thumbhash/) blur placeholders, and provides bulk regeneration from the admin panel.
4
8
 
5
9
  Built and maintained by [inoo.ch](https://inoo.ch) — a Swiss digital agency crafting modern web experiences.
@@ -13,7 +17,8 @@ Built and maintained by [inoo.ch](https://inoo.ch) — a Swiss digital agency cr
13
17
  - **Bulk regeneration** — Re-process existing images from the admin UI with progress tracking
14
18
  - **Per-collection config** — Override formats, quality, and dimensions per collection
15
19
  - **Admin UI** — Status badges, file size savings, and blur previews in the sidebar
16
- - **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()`
17
22
 
18
23
  ## Requirements
19
24
 
@@ -142,7 +147,7 @@ Payload CMS ships with [sharp](https://sharp.pixelplumbing.com/) built-in and ca
142
147
  | Blur hash placeholders | Requires custom hooks | ThumbHash generated automatically |
143
148
  | Optimization status & savings | Not available | Admin sidebar panel per image |
144
149
  | Bulk re-process existing images | Not available | One-click regeneration with progress tracking |
145
- | Next.js `<Image>` with blur placeholder | Manual wiring | Drop-in `<ImageBox>` component |
150
+ | Next.js `<Image>` with blur placeholder | Manual wiring | Drop-in `<ImageBox>` / `<FadeImage>` components |
146
151
  | Per-collection format/quality overrides | N/A | Supported |
147
152
 
148
153
  ### CPU & Resource Impact
@@ -167,7 +172,7 @@ A **Regenerate Images** button appears in collection list views, allowing you to
167
172
 
168
173
  ## ImageBox Component
169
174
 
170
- The plugin exports an `ImageBox` component — a Next.js `<Image>` wrapper that automatically applies ThumbHash blur placeholders:
175
+ 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:
171
176
 
172
177
  ```tsx
173
178
  import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
@@ -177,10 +182,17 @@ import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
177
182
 
178
183
  // Or use a plain URL string
179
184
  <ImageBox media="/images/photo.jpg" alt="Photo" width={800} height={600} />
185
+
186
+ // Disable fade animation
187
+ <ImageBox media={doc.image} alt="Photo" fade={false} />
188
+
189
+ // Custom fade duration (default: 500ms)
190
+ <ImageBox media={doc.image} alt="Photo" fadeDuration={300} />
180
191
  ```
181
192
 
182
193
  **Features:**
183
194
  - Automatic ThumbHash `blurDataURL` from the media document
195
+ - Smooth blur-to-sharp fade transition on load (enabled by default)
184
196
  - Respects Payload focal point (`focalX` / `focalY`) for `objectPosition`
185
197
  - Lazy loading by default, with `priority` prop for above-the-fold images
186
198
  - Cache busting via `updatedAt` timestamp
@@ -250,7 +262,7 @@ Copy-paste this instruction to your AI coding agent to have it autonomously inte
250
262
  >
251
263
  > 1. Which upload collections should be optimized and with what settings
252
264
  > 2. Whether to use `replaceOriginal` or keep originals alongside variants
253
- > 3. Where to add `<ImageBox>` or `getImageOptimizerProps()` in the frontend for ThumbHash blur placeholders and focal point support
265
+ > 3. Where to add `<ImageBox>`, `<FadeImage>`, or `getImageOptimizerProps()` in the frontend for ThumbHash blur placeholders with smooth fade-in and focal point support
254
266
  > 4. Whether any existing image rendering code should use the optimized variants
255
267
  >
256
268
  > Use the zero-config default (`collections: { <slug>: true }`) unless the project has specific requirements that call for custom settings.
@@ -259,7 +271,7 @@ Copy-paste this instruction to your AI coding agent to have it autonomously inte
259
271
 
260
272
  This plugin is open source and we welcome community involvement:
261
273
 
262
- - **Issues** — Found a bug or have a feature request? [Open an issue](https://github.com/payloadcms-plugins/image-optimizer/issues).
274
+ - **Issues** — Found a bug or have a feature request? [Open an issue](https://github.com/PascalEugster/payloadcms-plugin-image-optimizer/issues).
263
275
  - **Pull Requests** — PRs are welcome! Please open an issue first to discuss larger changes.
264
276
 
265
277
  All changes are reviewed and merged by the package maintainer at [inoo.ch](https://inoo.ch).
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoo-ch/payload-image-optimizer",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
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": [
@@ -16,54 +16,39 @@
16
16
  "resize",
17
17
  "compress"
18
18
  ],
19
- "homepage": "https://github.com/payloadcms-plugins/image-optimizer",
19
+ "homepage": "https://github.com/PascalEugster/payloadcms-plugin-image-optimizer",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/payloadcms-plugins/image-optimizer"
22
+ "url": "https://github.com/PascalEugster/payloadcms-plugin-image-optimizer"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/PascalEugster/payloadcms-plugin-image-optimizer/issues"
23
26
  },
24
27
  "type": "module",
25
28
  "exports": {
26
29
  ".": {
27
30
  "import": "./dist/index.js",
28
- "types": "./src/index.ts",
31
+ "types": "./dist/index.d.ts",
29
32
  "default": "./dist/index.js"
30
33
  },
31
34
  "./client": {
32
35
  "import": "./dist/exports/client.js",
33
- "types": "./src/exports/client.ts",
36
+ "types": "./dist/exports/client.d.ts",
34
37
  "default": "./dist/exports/client.js"
35
38
  },
36
39
  "./rsc": {
37
40
  "import": "./dist/exports/rsc.js",
38
- "types": "./src/exports/rsc.ts",
41
+ "types": "./dist/exports/rsc.d.ts",
39
42
  "default": "./dist/exports/rsc.js"
40
43
  }
41
44
  },
42
- "main": "./src/index.ts",
43
- "types": "./src/index.ts",
45
+ "main": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
44
47
  "files": [
45
48
  "dist",
46
49
  "src",
47
50
  "AGENT_DOCS.md"
48
51
  ],
49
- "scripts": {
50
- "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
51
- "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
52
- "build:types": "tsc --outDir dist --rootDir ./src",
53
- "clean": "rimraf {dist,*.tsbuildinfo}",
54
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
55
- "dev": "next dev dev --turbo",
56
- "dev:generate-importmap": "pnpm dev:payload generate:importmap",
57
- "dev:generate-types": "pnpm dev:payload generate:types",
58
- "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
59
- "generate:importmap": "pnpm dev:generate-importmap",
60
- "generate:types": "pnpm dev:generate-types",
61
- "lint": "eslint",
62
- "lint:fix": "eslint ./src --fix",
63
- "test": "pnpm test:int && pnpm test:e2e",
64
- "test:e2e": "playwright test",
65
- "test:int": "vitest"
66
- },
67
52
  "devDependencies": {
68
53
  "@eslint/eslintrc": "^3.2.0",
69
54
  "@payloadcms/db-mongodb": "3.79.0",
@@ -109,36 +94,26 @@
109
94
  "node": "^18.20.2 || >=20.9.0",
110
95
  "pnpm": "^9 || ^10"
111
96
  },
112
- "publishConfig": {
113
- "exports": {
114
- ".": {
115
- "import": "./dist/index.js",
116
- "types": "./dist/index.d.ts",
117
- "default": "./dist/index.js"
118
- },
119
- "./client": {
120
- "import": "./dist/exports/client.js",
121
- "types": "./dist/exports/client.d.ts",
122
- "default": "./dist/exports/client.js"
123
- },
124
- "./rsc": {
125
- "import": "./dist/exports/rsc.js",
126
- "types": "./dist/exports/rsc.d.ts",
127
- "default": "./dist/exports/rsc.js"
128
- }
129
- },
130
- "main": "./dist/index.js",
131
- "types": "./dist/index.d.ts"
132
- },
133
- "pnpm": {
134
- "onlyBuiltDependencies": [
135
- "sharp",
136
- "esbuild",
137
- "unrs-resolver"
138
- ]
139
- },
140
97
  "registry": "https://registry.npmjs.org/",
141
98
  "dependencies": {
142
99
  "thumbhash": "^0.1.1"
100
+ },
101
+ "scripts": {
102
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
103
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
104
+ "build:types": "tsc --outDir dist --rootDir ./src",
105
+ "clean": "rimraf {dist,*.tsbuildinfo}",
106
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
107
+ "dev": "next dev dev --turbo",
108
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
109
+ "dev:generate-types": "pnpm dev:payload generate:types",
110
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
111
+ "generate:importmap": "pnpm dev:generate-importmap",
112
+ "generate:types": "pnpm dev:generate-types",
113
+ "lint": "eslint",
114
+ "lint:fix": "eslint ./src --fix",
115
+ "test": "pnpm test:int && pnpm test:e2e",
116
+ "test:e2e": "playwright test",
117
+ "test:int": "vitest"
143
118
  }
144
- }
119
+ }
@@ -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'