@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 +63 -5
- package/README.md +37 -4
- package/dist/components/FadeImage.d.ts +23 -0
- package/dist/components/FadeImage.js +34 -0
- package/dist/components/FadeImage.js.map +1 -0
- package/dist/components/ImageBox.d.ts +4 -0
- package/dist/components/ImageBox.js +13 -4
- package/dist/components/ImageBox.js.map +1 -1
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FadeImage.tsx +51 -0
- package/src/components/ImageBox.tsx +19 -3
- package/src/exports/client.ts +2 -0
- package/src/index.ts +8 -0
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
|
|
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>`
|
|
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
|
|
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,
|
|
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"}
|
package/dist/exports/client.d.ts
CHANGED
|
@@ -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/dist/exports/client.js
CHANGED
|
@@ -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":"AACA,SAASA,eAAe,QAAQ,iBAAgB;AAGhD,SAASC,aAAa,QAAQ,gBAAe;AAC7C,SAASC,YAAY,QAAQ,0BAAyB;AACtD,SAASC,sBAAsB,QAAQ,kCAAiC;AACxE,SAASC,sBAAsB,QAAQ,0BAAyB;AAChE,SAASC,qBAAqB,QAAQ,yBAAwB;AAC9D,SAASC,2BAA2B,QAAQ,4BAA2B;AACvE,SAASC,+BAA+B,QAAQ,gCAA+B;AAC/E,SAASC,uBAAuB,EAAEC,6BAA6B,QAAQ,4BAA2B;AAGlG,SAASC,2BAA2B,QAAQ,kCAAiC;AAE7E,SAASC,sBAAsB,EAAEC,wBAAwB,QAAQ,2BAA0B;AAE3F,OAAO,MAAMC,iBACX,CAACC,gBACD,CAACC;QACC,MAAMC,
|
|
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.
|
|
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
|
}
|
package/src/exports/client.ts
CHANGED
|
@@ -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 => {
|