react-pro-image 1.0.1 → 1.1.0

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/README.md CHANGED
@@ -1,55 +1,93 @@
1
1
  <div align="center">
2
-
3
- # react-pro-image
4
-
5
- **One single `<OptimizedImage />` component, a few props, and you get lazy loading, AVIF/WebP auto-format, placeholder crossfade, and error fallback — out of the box.**
6
-
7
- [![npm version](https://img.shields.io/npm/v/react-pro-image.svg?style=flat-square&color=cb3837)](https://www.npmjs.com/package/react-pro-image)
8
- [![bundle size](https://img.shields.io/bundlephobia/minzip/react-pro-image?style=flat-square&color=44cc11)](https://bundlephobia.com/package/react-pro-image)
9
- [![license](https://img.shields.io/npm/l/react-pro-image?style=flat-square&color=blue)](https://github.com/MohamedAlfeky1/react-pro-image/blob/main/LICENSE)
10
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
11
-
2
+ <img
3
+ src="https://mohamedalfeky1.github.io/react-pro-image/img/logo.png"
4
+ alt="React Pro Image logo"
5
+ width="96"
6
+ height="96"
7
+ />
8
+
9
+ <h1>react-pro-image</h1>
10
+
11
+ <p>
12
+ A production-ready React image component for lazy loading, AVIF/WebP format
13
+ negotiation, progressive placeholders, and graceful error fallbacks.
14
+ </p>
15
+
16
+ <p>
17
+ <a href="https://www.npmjs.com/package/react-pro-image">
18
+ <img src="https://img.shields.io/npm/v/react-pro-image.svg?style=flat-square&color=cb3837" alt="npm version" />
19
+ </a>
20
+ <a href="https://bundlephobia.com/package/react-pro-image">
21
+ <img src="https://img.shields.io/bundlephobia/minzip/react-pro-image?style=flat-square&color=44cc11" alt="bundle size" />
22
+ </a>
23
+ <a href="https://github.com/MohamedAlfeky1/react-pro-image/blob/main/LICENSE">
24
+ <img src="https://img.shields.io/npm/l/react-pro-image?style=flat-square&color=blue" alt="license" />
25
+ </a>
26
+ <a href="https://www.typescriptlang.org/">
27
+ <img src="https://img.shields.io/badge/TypeScript-Ready-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript ready" />
28
+ </a>
29
+ </p>
12
30
  </div>
13
31
 
14
- ---
32
+ ## Introduction
15
33
 
16
- ## Features
34
+ `react-pro-image` gives you one `<OptimizedImage />` component for the image
35
+ loading work most React apps repeat by hand.
17
36
 
18
- | Feature | Description |
19
- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
20
- | 🚀 **Lazy Loading** | Images load only when they enter the viewport via `IntersectionObserver` — zero layout shift, zero wasted bandwidth. |
21
- | 🎨 **AVIF / WebP Negotiation** | Automatically detects browser support and serves the smallest modern format. Results are cached in `localStorage`. |
22
- | 🌄 **Placeholder Crossfade** | Show a low-res or blurred placeholder that smoothly fades out once the full image loads. |
23
- | 💥 **Error Fallback** | Gracefully display a fallback image if the primary source fails to load. |
24
- | 🔗 **CDN Auto-Format** | Works with Unsplash, Imgix, Cloudinary, and any CDN that accepts a format query parameter. |
25
- | 📦 **Tree-Shakeable** | ESM + CJS dual builds. Import only what you use. |
26
- | 🔒 **Fully Typed** | Written in TypeScript with strict, exported types for every prop and hook. |
27
- | ⚡ **Zero Dependencies** | Only `react` (≥ 17) as a peer dependency. |
37
+ It can delay off-screen image downloads, choose AVIF or WebP when the browser
38
+ supports them, show a lightweight placeholder while the full image loads, and
39
+ render a fallback image when the primary source fails.
28
40
 
29
- ---
30
-
31
- ## 📦 Installation
41
+ ## Installation
32
42
 
33
43
  ```bash
34
- # npm
35
44
  npm install react-pro-image
45
+ ```
36
46
 
37
- # yarn
47
+ ```bash
38
48
  yarn add react-pro-image
49
+ ```
39
50
 
40
- # pnpm
51
+ ```bash
41
52
  pnpm add react-pro-image
42
53
  ```
43
54
 
44
- > **Peer dependency:** React 17.0.0
55
+ React `>=17.0.0` is required as a peer dependency.
45
56
 
46
- ---
57
+ ## Documentation
58
+
59
+ You can find the React Pro Image documentation [on this website](https://mohamedalfeky1.github.io/react-pro-image/).
60
+
61
+ Check out the [Quick Start](https://mohamedalfeky1.github.io/react-pro-image/getting-started/quick-start) page for a quick overview.
62
+
63
+ The documentation is divided into several sections:
64
+
65
+ - [Overview](https://mohamedalfeky1.github.io/react-pro-image/)
66
+ - [Installation](https://mohamedalfeky1.github.io/react-pro-image/getting-started/installation)
67
+ - [Quick Start](https://mohamedalfeky1.github.io/react-pro-image/getting-started/quick-start)
68
+ - [CDN Images](https://mohamedalfeky1.github.io/react-pro-image/usage-modes/cdn-images)
69
+ - [Self-Hosted Images](https://mohamedalfeky1.github.io/react-pro-image/usage-modes/self-hosted-images)
70
+ - [Progressive Loading](https://mohamedalfeky1.github.io/react-pro-image/loading-reliability/progressive-loading)
71
+ - [Lazy Loading](https://mohamedalfeky1.github.io/react-pro-image/loading-reliability/lazy-loading)
72
+ - [Error Fallbacks](https://mohamedalfeky1.github.io/react-pro-image/loading-reliability/error-fallbacks)
73
+ - [Architecture](https://mohamedalfeky1.github.io/react-pro-image/technical-details/architecture)
74
+ - [AVIF and WebP Negotiation](https://mohamedalfeky1.github.io/react-pro-image/technical-details/avif-webp-negotiation)
75
+ - [Browser Support](https://mohamedalfeky1.github.io/react-pro-image/technical-details/browser-support)
76
+ - [Props Reference](https://mohamedalfeky1.github.io/react-pro-image/api-reference/props-reference)
77
+ - [AutoFormatConfig](https://mohamedalfeky1.github.io/react-pro-image/api-reference/auto-format-config)
78
+ - [Hooks](https://mohamedalfeky1.github.io/react-pro-image/api-reference/hooks)
79
+ - [Exported Types](https://mohamedalfeky1.github.io/react-pro-image/api-reference/exported-types)
80
+ - [Where to Get Support](https://github.com/MohamedAlfeky1/react-pro-image/issues)
81
+ - [Contributing](https://github.com/MohamedAlfeky1/react-pro-image/pulls)
82
+
83
+ You can improve it by sending pull requests to this repository.
47
84
 
48
- ## 🚀 How to use (Quick Start)
85
+ ## Usage Examples
49
86
 
50
- The easiest and recommended way to use `react-pro-image` is with **CDN Auto-Format**.
87
+ ### CDN Images
51
88
 
52
- If your images are hosted on a CDN (like Unsplash, Imgix, or Cloudinary), you don't need to manually create different image formats. Just give the component your image URL, and it will automatically ask the CDN for the best format (AVIF or WebP) that the user's browser supports!
89
+ Use this mode when your images are served from a CDN that accepts a format query
90
+ parameter, such as `fm`, `f`, or `format`.
53
91
 
54
92
  ```tsx
55
93
  import { OptimizedImage } from "react-pro-image";
@@ -57,10 +95,11 @@ import { OptimizedImage } from "react-pro-image";
57
95
  function Hero() {
58
96
  return (
59
97
  <OptimizedImage
60
- autoSrc="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800"
98
+ autoSrc="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800"
61
99
  autoFormat={{ formatKey: "fm", formats: ["avif", "webp"] }}
62
- autoPlaceholder="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=20&blur=10"
63
- alt="Mountain landscape"
100
+ autoPlaceholder="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=20&blur=10"
101
+ autoFallback="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800"
102
+ alt="Sunlit mountain valley with golden light"
64
103
  width={800}
65
104
  height={400}
66
105
  />
@@ -68,324 +107,33 @@ function Hero() {
68
107
  }
69
108
  ```
70
109
 
71
- ### 💡 What do these props mean?
72
-
73
- | Prop | Simple Explanation |
74
- | --- | --- |
75
- | `autoSrc` | The main link to your image on the CDN. The component will automatically add the format parameter to the end of this link. |
76
- | `autoFormat` | Tells the component how your CDN expects the format request. For example, Unsplash uses `fm` (so it becomes `&fm=avif`). We also tell it to try `"avif"` first, then `"webp"`. |
77
- | `autoPlaceholder` | A link to a very tiny, blurry version of the same image. This loads instantly and looks nice while the user waits for the big image to download. It smoothly fades out when the real image is ready. |
78
- | `alt` | Text that describes the image. Important for accessibility (screen readers) and SEO. |
79
- | `width` / `height` | The size of the image container in pixels. |
80
-
81
- ---
82
-
83
- ## 📖 More Ways to Use
84
-
85
- ### 1. Manual Sources (if you host the images yourself)
86
-
87
- If you aren't using a CDN and instead have your images saved in your project (like in a `public` folder), you can pass each format manually.
88
-
89
- The component will automatically check the browser's capabilities and pick the best one:
90
-
91
- ```tsx
92
- <OptimizedImage
93
- src="/photo.jpg"
94
- avifSrc="/photo.avif"
95
- webpSrc="/photo.webp"
96
- placeholder="/photo-tiny.jpg"
97
- fallback="/photo-fallback.jpg"
98
- alt="A beautiful scene"
99
- width={800}
100
- height={400}
101
- />
102
- ```
103
-
104
- **How the component chooses the best image:**
105
- 1. Does the browser support **AVIF** and did you provide `avifSrc`? -> It uses **AVIF**.
106
- 2. Does the browser support **WebP** and did you provide `webpSrc`? -> It uses **WebP**.
107
- 3. Otherwise? -> It falls back to the standard `src` (JPEG/PNG).
108
-
109
- *(You only need to provide the formats you have — `avifSrc` and `webpSrc` are completely optional).*
110
-
111
110
  ---
112
111
 
113
- ### 2. Disabling Lazy Loading
114
-
115
- By default, all images are "lazy-loaded". This means they won't even start downloading until the user scrolls down and the image enters the screen. This saves a lot of data!
112
+ ### Self-Hosted Images
116
113
 
117
- However, for images at the very top of your page (like a hero image), you want them to load immediately. Set `lazy={false}`:
114
+ Use this mode when you host your own JPEG, PNG, AVIF, or WebP files. This
115
+ advanced example provides modern formats, a placeholder, and an error fallback.
118
116
 
119
117
  ```tsx
120
- <OptimizedImage
121
- src="/hero.jpg"
122
- alt="Above the fold hero"
123
- lazy={false}
124
- width={1920}
125
- height={800}
126
- />
127
- ```
128
-
129
- ---
130
-
131
- ### 3. Adjusting the Viewport Trigger (When to start loading)
132
-
133
- You can control exactly *when* the lazy loading starts using `threshold` and `rootMargin`:
134
-
135
- ```tsx
136
- <OptimizedImage
137
- src="/gallery-item.jpg"
138
- alt="Gallery item"
139
- threshold={0.1}
140
- rootMargin="200px"
141
- width={400}
142
- height={300}
143
- />
144
- ```
145
-
146
- - **`threshold={0.1}`**: Start loading when just **10%** of the image area becomes visible on screen. (The default is `0.25` or 25%).
147
- - **`rootMargin="200px"`**: Start loading **200 pixels before** the image even reaches the screen. This is great for making sure images are already downloaded by the time the user scrolls to them!
148
-
149
- ---
150
-
151
- ## 📚 API Reference
152
-
153
- ### `<OptimizedImage />` — Props
154
-
155
- #### Source Props
156
-
157
- > You must provide **either** `src` **or** `autoSrc` — never both.
158
-
159
- | Prop | Type | Default | Description |
160
- | ------------ | ------------------ | ------- | ------------------------------------------------------------------------------------------------------------- |
161
- | `src` | `string` | — | Standard image URL (JPEG, PNG, etc.). **Required** if `autoSrc` is not used. |
162
- | `autoSrc` | `string` | — | CDN image URL. The component appends the format query param automatically. **Required** if `src` is not used. |
163
- | `autoFormat` | `AutoFormatConfig` | — | Format negotiation config. **Required** when using `autoSrc`. |
164
- | `avifSrc` | `string` | — | Optional AVIF source URL. Served if the browser supports AVIF. |
165
- | `webpSrc` | `string` | — | Optional WebP source URL. Served if the browser supports WebP. |
166
-
167
- #### Placeholder Props
168
-
169
- > You may provide **either** `placeholder` **or** `autoPlaceholder` — never both.
170
-
171
- | Prop | Type | Default | Description |
172
- | ----------------- | -------- | ------- | ----------------------------------------------------------------------------------- |
173
- | `placeholder` | `string` | — | URL of a low-res or blurred placeholder image. Fades out once the full image loads. |
174
- | `autoPlaceholder` | `string` | — | CDN-generated placeholder URL. |
175
-
176
- #### Fallback Props
177
-
178
- > You may provide **either** `fallback` **or** `autoFallback` — never both.
179
-
180
- | Prop | Type | Default | Description |
181
- | -------------- | -------- | ------- | -------------------------------------------------------------------------- |
182
- | `fallback` | `string` | — | Image URL displayed if the primary source fails to load. |
183
- | `autoFallback` | `string` | — | CDN fallback URL. Format param is appended automatically via `autoFormat`. |
184
- | `avifFallback` | `string` | — | AVIF override for the fallback image. |
185
- | `webpFallback` | `string` | — | WebP override for the fallback image. |
186
-
187
- #### Layout and Behavior Props
188
-
189
- | Prop | Type | Default | Description |
190
- | ------------ | --------- | ------- | ---------------------------------------------------------- |
191
- | `alt` | `string` | — | Accessible alt text for the image. |
192
- | `width` | `number` | — | Display width of the container in pixels. |
193
- | `height` | `number` | — | Display height of the container in pixels. |
194
- | `className` | `string` | — | CSS class names applied to the outer wrapper div. |
195
- | `lazy` | `boolean` | `true` | Enable or disable lazy loading via IntersectionObserver. |
196
- | `threshold` | `number` | `0.25` | Visibility ratio (0 to 1) required to trigger loading. |
197
- | `rootMargin` | `string` | `"0px"` | CSS-style margin to expand or shrink the observation area. |
198
-
199
- > The component also spreads any additional `HTMLDivElement` attributes onto the outer wrapper.
200
-
201
- ---
202
-
203
- ### AutoFormatConfig
204
-
205
- Configuration object for CDN format negotiation.
206
-
207
- ```ts
208
- interface AutoFormatConfig {
209
- formatKey: string;
210
- formats: ("avif" | "webp")[];
211
- }
212
- ```
213
-
214
- - **formatKey** — The query parameter key used by the CDN (e.g. `"fm"`, `"f"`, `"format"`).
215
- - **formats** — Ordered list of modern formats to try, from most preferred to least (e.g. `["avif", "webp"]`).
216
-
217
- **CDN examples:**
218
-
219
- | CDN | formatKey | Example URL |
220
- | ---------------- | ---------- | ----------------- |
221
- | Unsplash / Imgix | `"fm"` | `...?fm=avif` |
222
- | Cloudinary | `"f"` | `...&f=webp` |
223
- | Custom | `"format"` | `...?format=avif` |
224
-
225
- ---
226
-
227
- ## 🪝 Hooks
228
-
229
- The package exports three composable hooks you can use independently in custom components.
230
-
231
- ### useImageFormatSupport()
232
-
233
- Detects AVIF and WebP support by loading tiny test images. Results are cached in `localStorage` so detection runs only once per browser.
234
-
235
- ```tsx
236
- import { useImageFormatSupport } from "react-pro-image";
237
-
238
- function MyComponent() {
239
- const { avif, webp, ready } = useImageFormatSupport();
240
-
241
- if (!ready) return <p>Checking format support...</p>;
118
+ import { OptimizedImage } from "react-pro-image";
242
119
 
120
+ function GalleryImage() {
243
121
  return (
244
- <img
245
- src={avif ? "/photo.avif" : webp ? "/photo.webp" : "/photo.jpg"}
246
- alt="example"
122
+ <OptimizedImage
123
+ src="/photo.jpg"
124
+ avifSrc="/photo.avif"
125
+ webpSrc="/photo.webp"
126
+ placeholder="/photo-tiny.jpg"
127
+ fallback="/photo-fallback.jpg"
128
+ alt="A scenic landscape"
129
+ width={800}
130
+ height={400}
247
131
  />
248
132
  );
249
133
  }
250
134
  ```
251
135
 
252
- **Returns:**
253
-
254
- | Property | Type | Description |
255
- | -------- | --------- | -------------------------------------- |
256
- | `avif` | `boolean` | `true` if the browser can decode AVIF. |
257
- | `webp` | `boolean` | `true` if the browser can decode WebP. |
258
- | `ready` | `boolean` | `true` once detection is complete. |
259
-
260
- ---
261
-
262
- ### useImageLoader(options)
263
-
264
- Preloads an image off-screen and tracks its load state. Loading is deferred until `isInView` is `true`, enabling lazy-load patterns.
265
-
266
- ```tsx
267
- import { useImageLoader } from "react-pro-image";
268
-
269
- function MyComponent() {
270
- const state = useImageLoader({
271
- src: "/photo.jpg",
272
- avifSrc: "/photo.avif",
273
- isInView: true,
274
- });
275
- // state: "idle" -> "loading" -> "loaded" | "error"
276
- }
277
- ```
278
-
279
- **Options (UseImageLoaderOptions):**
280
-
281
- | Option | Type | Default | Description |
282
- | ------------ | ------------------ | ------- | ---------------------------------------- |
283
- | `src` | `string` | — | Baseline image source. |
284
- | `autoSrc` | `string` | — | CDN image URL for auto-format mode. |
285
- | `autoFormat` | `AutoFormatConfig` | — | Format config (required with `autoSrc`). |
286
- | `avifSrc` | `string` | — | Optional AVIF source (highest priority). |
287
- | `webpSrc` | `string` | — | Optional WebP source (second priority). |
288
- | `isInView` | `boolean` | `false` | When `true`, triggers the preload. |
289
-
290
- **Returns:** `ImageLoadState` — `"idle"` or `"loading"` or `"loaded"` or `"error"`
291
-
292
- ---
293
-
294
- ### useInView(options?)
295
-
296
- Tracks whether a DOM element has entered the viewport using `IntersectionObserver`. One-shot: the observer disconnects after the first intersection.
297
-
298
- ```tsx
299
- import { useInView } from "react-pro-image";
300
-
301
- function MyComponent() {
302
- const { ref, isInView } = useInView({ threshold: 0.25 });
303
-
304
- return (
305
- <div ref={ref}>{isInView && <img src="/photo.jpg" alt="example" />}</div>
306
- );
307
- }
308
- ```
309
-
310
- **Options (UseInViewOptions):**
311
-
312
- | Option | Type | Default | Description |
313
- | ------------ | -------- | ------- | ---------------------------------------------------- |
314
- | `threshold` | `number` | `0.25` | Visibility ratio (0 to 1) required to trigger. |
315
- | `rootMargin` | `string` | `"0px"` | CSS margin to expand or shrink the observation area. |
316
-
317
- **Returns:**
318
-
319
- | Property | Type | Description |
320
- | ---------- | --------------------------- | -------------------------------------------- |
321
- | `ref` | `RefObject<HTMLDivElement>` | Attach to the target element. |
322
- | `isInView` | `boolean` | `true` once the element meets the threshold. |
323
-
324
- ---
325
-
326
- ## 🔤 Exported Types
327
-
328
- All types are exported and available for use in your own components:
329
-
330
- ```ts
331
- import type {
332
- OptimizedImageProps,
333
- AutoFormatConfig,
334
- ImageWithFormatsProps,
335
- UseImageLoaderOptions,
336
- UseInViewOptions,
337
- ImageLoadState,
338
- } from "react-pro-image";
339
- ```
340
-
341
- | Type | Description |
342
- | ----------------------- | -------------------------------------------------------------- |
343
- | `OptimizedImageProps` | Full prop type for the OptimizedImage component. |
344
- | `AutoFormatConfig` | Configuration for CDN format query parameters. |
345
- | `ImageWithFormatsProps` | Props for the internal format-resolving image renderer. |
346
- | `UseImageLoaderOptions` | Options for the useImageLoader hook. |
347
- | `UseInViewOptions` | Options for the useInView hook. |
348
- | `ImageLoadState` | Union type: `"idle"` or `"loading"` or `"loaded"` or `"error"` |
349
-
350
- ---
351
-
352
- ## ⚙️ How It Works
353
-
354
- ```
355
- ┌─────────────────────────────────────────────────────────┐
356
- │ OptimizedImage │
357
- │ │
358
- │ 1. useInView() │
359
- │ - IntersectionObserver watches the container │
360
- │ - Flips isInView to true when threshold is met │
361
- │ - Disconnects after first trigger (one-shot) │
362
- │ │
363
- │ 2. useImageLoader() │
364
- │ - Waits until isInView is true │
365
- │ - useImageFormatSupport() detects AVIF/WebP │
366
- │ - Creates off-screen Image() to preload best format │
367
- │ - State: idle -> loading -> loaded or error │
368
- │ │
369
- │ 3. Render │
370
- │ - Placeholder layer (opacity: 1 -> 0 on load) │
371
- │ - Real image layer (mounted after entering view) │
372
- │ - Fallback layer (shown only on error) │
373
- └─────────────────────────────────────────────────────────┘
374
- ```
375
-
376
- ---
377
-
378
- ## 🌐 Browser Support
379
-
380
- | Feature | Requirement |
381
- | ------------ | ------------------------------------------------------- |
382
- | Lazy Loading | IntersectionObserver — supported in all modern browsers |
383
- | AVIF | Chrome 85+, Firefox 93+, Safari 16.4+ |
384
- | WebP | Chrome 32+, Firefox 65+, Safari 14+ |
385
- | Fallback | Automatic — gracefully falls back to src (JPEG/PNG) |
386
-
387
- ---
388
-
389
- ## 📄 License
136
+ ## Links
390
137
 
391
- [MIT](https://github.com/MohamedAlfeky1/react-pro-image/blob/main/LICENSE) © [MohamedAlfeky1](https://github.com/MohamedAlfeky1)
138
+ - [Documentation](https://mohamedalfeky1.github.io/react-pro-image/)
139
+ - [npm Package](https://www.npmjs.com/package/react-pro-image)
@@ -266,7 +266,13 @@ function ImageWithFormats({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, cus
266
266
  * @see {@link useInView} — viewport detection hook
267
267
  * @see {@link useImageLoader} — off-screen preloading hook
268
268
  */
269
- function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width, height, placeholder, autoPlaceholder, fallback, autoFallback, avifFallback, webpFallback, lazy = true, threshold = .25, rootMargin = "0px", ...rest }) {
269
+ function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width, height, placeholder, autoPlaceholder, fallback, autoFallback, avifFallback, webpFallback, lazy = true, threshold = .25, rootMargin = "0px", style, ...rest }) {
270
+ if (src && autoSrc) throw new Error(`Conflicting props: You cannot provide both 'src' and 'autoSrc'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`);
271
+ if (fallback && autoFallback) throw new Error(`Conflicting props: You cannot provide both 'fallback' and 'autoFallback'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`);
272
+ if (placeholder && autoPlaceholder) throw new Error(`Conflicting props: You cannot provide both 'placeholder' and 'autoPlaceholder'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`);
273
+ const hasWidth = width !== void 0 || style?.width !== void 0;
274
+ const hasHeight = height !== void 0 || style?.height !== void 0;
275
+ if (!hasWidth || !hasHeight) throw new Error(`Missing dimensions: You must provide both 'width' and 'height' (as props or in style) to prevent layout collapse.`);
270
276
  const { ref, isInView } = useInView({
271
277
  threshold,
272
278
  rootMargin
@@ -284,8 +290,10 @@ function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width
284
290
  style: {
285
291
  width,
286
292
  height,
287
- position: "relative"
293
+ position: "relative",
294
+ ...style
288
295
  },
296
+ ...rest,
289
297
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageWithFormats, {
290
298
  avifSrc: avifFallback,
291
299
  webpSrc: webpFallback,
@@ -302,10 +310,11 @@ function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width
302
310
  width,
303
311
  height,
304
312
  position: "relative",
305
- overflow: "hidden"
313
+ overflow: "hidden",
314
+ ...style
306
315
  },
307
316
  ...rest,
308
- children: [placeholder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageWithFormats, {
317
+ children: [(placeholder || autoPlaceholder) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageWithFormats, {
309
318
  src: placeholder,
310
319
  autoSrc: autoPlaceholder,
311
320
  autoFormat,
@@ -1 +1 @@
1
- {"version":3,"file":"react-pro-image.cjs.js","names":[],"sources":["../src/hooks/useImageFormatSupport.ts","../src/hooks/useImageLoader.ts","../src/hooks/useInView.ts","../src/components/OptimizedImage.tsx"],"sourcesContent":["/**\r\n * useImageFormatSupport\r\n *\r\n * A React hook that detects whether the user's browser supports modern image\r\n * formats (AVIF and WebP). It works by attempting to load a tiny test image\r\n * for each format — if the browser can render it, the format is supported.\r\n *\r\n * Results are cached in `localStorage` so the detection only runs once per\r\n * browser, avoiding unnecessary network/decode work on subsequent visits.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { avif, webp, ready } = useImageFormatSupport();\r\n *\r\n * if (!ready) return <p>Checking format support…</p>;\r\n *\r\n * return (\r\n * <img src={avif ? \"/photo.avif\" : webp ? \"/photo.webp\" : \"/photo.jpg\"} />\r\n * );\r\n * ```\r\n *\r\n * @returns An object with three properties:\r\n * - `avif` — `true` if the browser can decode AVIF images\r\n * - `webp` — `true` if the browser can decode WebP images\r\n * - `ready` — `true` once detection is complete (initially `false`)\r\n */\r\nimport { useState, useEffect } from \"react\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Test assets\r\n// ---------------------------------------------------------------------------\r\n// AVIF: We fetch a 1×1 AVIF encoded as a Base64 data-URI to verify the browser can\r\n// actually decode the AVIF format.\r\nconst AVIF_TEST =\r\n \"data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAACEwAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKBzgAPtPlAIED8GqhABwMDIBAAAAA\";\r\n\r\n// WebP: A tiny 1×1 WebP encoded as a Base64 data-URI. WebP files are small\r\n// enough to inline directly, so no network request is needed.\r\nconst WEBP_TEST =\r\n \"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=\";\r\n\r\n// localStorage key used to persist detection results across page loads.\r\nconst CACHE_KEY = \"img-support\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Helper\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Attempts to load an image from `src` and resolves to `true` if the browser\r\n * successfully decoded it, or `false` if loading failed.\r\n *\r\n * How it works:\r\n * 1. Create an off-screen `<img>` element (never added to the DOM).\r\n * 2. Set its `src` to the test image.\r\n * 3. Listen for `onload` (success → true) or `onerror` (failure → false).\r\n *\r\n * The Promise is typed as `Promise<boolean>` so TypeScript knows the resolved\r\n * value is a boolean — without this, it would default to `Promise<unknown>`.\r\n */\r\nfunction canLoadImage(src: string): Promise<boolean> {\r\n return new Promise<boolean>((resolve) => {\r\n const img = new Image();\r\n img.onload = () => resolve(true);\r\n img.onerror = () => resolve(false);\r\n img.src = src;\r\n });\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Hook\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function useImageFormatSupport() {\r\n // State shape:\r\n // avif → does the browser support AVIF? (default: false)\r\n // webp → does the browser support WebP? (default: false)\r\n // ready → has detection finished? (default: false)\r\n //\r\n // Components can check `ready` before acting on `avif`/`webp` to avoid\r\n // a flash of incorrect content while the async check is still running.\r\n const [support, setSupport] = useState({\r\n avif: false,\r\n webp: false,\r\n ready: false,\r\n });\r\n\r\n useEffect(() => {\r\n // --- Cache hit: reuse a previous result from localStorage -------------\r\n const cached = localStorage.getItem(CACHE_KEY);\r\n\r\n if (cached) {\r\n // `cached` is a JSON string like '{\"avif\":true,\"webp\":true}'.\r\n // We spread it into state and set `ready: true` immediately.\r\n setSupport({ ...JSON.parse(cached), ready: true });\r\n return; // skip the network/decode tests entirely\r\n }\r\n\r\n // --- Cache miss: run the format detection tests -----------------------\r\n // `Promise.all` runs both checks in parallel and waits for both to finish.\r\n // The result is an array of two booleans: [avifSupported, webpSupported].\r\n Promise.all([canLoadImage(AVIF_TEST), canLoadImage(WEBP_TEST)]).then(\r\n ([avif, webp]) => {\r\n // Persist so we never re-run the tests on this browser.\r\n localStorage.setItem(CACHE_KEY, JSON.stringify({ avif, webp }));\r\n\r\n // Update React state — triggers a re-render with the final values.\r\n setSupport({ avif, webp, ready: true });\r\n },\r\n );\r\n }, []); // Empty dependency array → runs once on mount, never re-runs.\r\n\r\n return support;\r\n}\r\n","import { useEffect, useState } from \"react\";\r\nimport type { UseImageLoaderOptions } from \"../interfaces\";\r\nimport type { ImageLoadState } from \"../types\";\r\nimport { useImageFormatSupport } from \"./useImageFormatSupport\";\r\n\r\n/**\r\n * Preloads an image off-screen and exposes its load state.\r\n *\r\n * Loading is deferred until `isInView` is `true`, enabling lazy-load\r\n * behaviour when combined with an intersection observer. The hook\r\n * selects the best available format in priority order: AVIF → WebP → original.\r\n *\r\n * @param options.src - Original image URL (required fallback).\r\n * @param options.avifSrc - Optional AVIF source (highest priority).\r\n * @param options.webpSrc - Optional WebP source (second priority).\r\n * @param options.isInView - When `true`, triggers the preload. Pass `true`\r\n * directly to disable lazy behaviour (default `false`).\r\n * @returns Current load state: `\"idle\"` | `\"loading\"` | `\"loaded\"` | `\"error\"`.\r\n *\r\n * @example\r\n * ```tsx\r\n * const state = useImageLoader({ src, isInView: true });\r\n * // state: \"idle\" → \"loading\" → \"loaded\" | \"error\"\r\n * ```\r\n */\r\nexport default function useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView = false,\r\n}: UseImageLoaderOptions) {\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n const [imageState, setImageState] = useState<ImageLoadState>(\"idle\");\r\n\r\n useEffect(() => {\r\n if (!isInView || !ready) return;\r\n\r\n let activeSrc: string | undefined;\r\n\r\n if (autoSrc && autoFormat) {\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n if (bestFormat) {\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n activeSrc = `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`;\r\n } else {\r\n activeSrc = autoSrc;\r\n }\r\n } else {\r\n // --- Manual path: use explicit avifSrc / webpSrc ----------------------\r\n if (avif && avifSrc) {\r\n activeSrc = avifSrc;\r\n } else if (webp && webpSrc) {\r\n activeSrc = webpSrc;\r\n } else {\r\n activeSrc = src;\r\n }\r\n }\r\n\r\n if (!activeSrc) {\r\n setImageState(\"error\");\r\n return;\r\n }\r\n\r\n const img = new Image();\r\n img.onload = () => setImageState(\"loaded\");\r\n img.onerror = () => setImageState(\"error\");\r\n img.src = activeSrc;\r\n\r\n return () => {\r\n img.onload = null;\r\n img.onerror = null;\r\n };\r\n }, [src, autoSrc, autoFormat, avifSrc, webpSrc, avif, webp, ready, isInView]);\r\n\r\n return imageState;\r\n}\r\n","import { useEffect, useRef, useState } from \"react\";\r\nimport type { UseInViewOptions } from \"../interfaces\";\r\n\r\n/**\r\n * Tracks whether a DOM element has entered the viewport using the\r\n * `IntersectionObserver` API. Observation is one-shot: once the element\r\n * becomes visible the observer disconnects automatically.\r\n *\r\n * @param options.threshold - Visibility ratio required to trigger (default `0.25`).\r\n * @param options.rootMargin - CSS-style margin applied to the root viewport (default `\"0px\"`).\r\n * @returns `{ ref, isInView }` — attach `ref` to the target element;\r\n * `isInView` flips to `true` once the threshold is met.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { ref, isInView } = useInView({ threshold: 0.25 });\r\n * return <div ref={ref}>{isInView && <img src={src} />}</div>;\r\n * ```\r\n */\r\nexport default function useInView(options: UseInViewOptions = {}) {\r\n const { threshold = 0.25, rootMargin = \"0px\" } = options;\r\n\r\n const ref = useRef<HTMLDivElement>(null);\r\n const [isInView, setIsInView] = useState(false);\r\n\r\n useEffect(() => {\r\n const element = ref.current;\r\n if (!element) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setIsInView(true);\r\n observer.disconnect(); // One-shot: stop after first intersection\r\n }\r\n },\r\n { threshold, rootMargin },\r\n );\r\n\r\n observer.observe(element);\r\n\r\n return () => observer.disconnect();\r\n }, [threshold, rootMargin]);\r\n\r\n return { ref, isInView };\r\n}\r\n","import { useImageFormatSupport } from \"../hooks/useImageFormatSupport\";\r\nimport useImageLoader from \"../hooks/useImageLoader\";\r\nimport useInView from \"../hooks/useInView\";\r\nimport type { ImageWithFormatsProps, OptimizedImageProps } from \"../interfaces\";\r\n\r\n/**\r\n * Internal component that resolves the best image source based on browser\r\n * format support and renders the appropriate `<img>` tag.\r\n *\r\n * When `autoSrc` + `autoFormat` is provided, it iterates through the\r\n * configured formats in priority order and appends the format query param\r\n * to the URL for the first supported format.\r\n *\r\n * When manual `avifSrc` / `webpSrc` is provided, it picks the best\r\n * supported source directly.\r\n */\r\nfunction ImageWithFormats({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n customStyles,\r\n}: ImageWithFormatsProps) {\r\n /**\r\n * Base styles that position the image as an absolutely-placed cover layer.\r\n * The `transition` enables a smooth opacity crossfade between the\r\n * placeholder and the fully-loaded image.\r\n * Any `customStyles` (e.g. dynamic opacity) are spread on top.\r\n */\r\n const sharedStyles = {\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n width: \"100%\",\r\n height: \"100%\",\r\n objectFit: \"cover\" as const,\r\n ...customStyles,\r\n };\r\n\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n\r\n // --- Auto-format path: build URL with format query param -----------------\r\n if (autoSrc && autoFormat && ready) {\r\n // Helper: appends \"?fm=avif\" or \"&fm=avif\" depending on whether URL already has \"?\"\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n // If a supported format was found, use it; otherwise use autoSrc as-is\r\n return (\r\n <img\r\n src={\r\n bestFormat\r\n ? `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`\r\n : autoSrc\r\n }\r\n alt={alt}\r\n style={sharedStyles}\r\n />\r\n );\r\n }\r\n\r\n // --- Manual path: use explicit avifSrc / webpSrc -------------------------\r\n if (avifSrc && ready && avif) {\r\n return <img src={avifSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n if (webpSrc && ready && webp) {\r\n return <img src={webpSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n return <img src={src} alt={alt} style={sharedStyles} />;\r\n}\r\n\r\n/**\r\n * A performance-focused image component that combines **lazy loading**,\r\n * **placeholder-to-full crossfade**, and **modern format selection**\r\n * (AVIF / WebP) into a single drop-in `<img>` replacement.\r\n *\r\n * ## How it works\r\n *\r\n * 1. **Visibility detection** — An `IntersectionObserver` (via `useInView`)\r\n * watches the container element. No network request is made until the\r\n * configured `threshold` of the element is visible in the viewport.\r\n *\r\n * 2. **Off-screen preload** — Once visible, `useImageLoader` creates a\r\n * hidden `Image()` object to download the best-available format\r\n * (AVIF → WebP → original). The component tracks the load state\r\n * (`idle` → `loading` → `loaded` | `error`).\r\n *\r\n * 3. **Crossfade transition** — The placeholder and real image are rendered\r\n * as stacked layers. When the real image finishes loading, the\r\n * placeholder's opacity is animated to `0`, revealing the full image.\r\n *\r\n * 4. **Error recovery** — If loading fails and a `fallback` src is provided,\r\n * the fallback image is rendered instead.\r\n *\r\n * @see {@link useInView} — viewport detection hook\r\n * @see {@link useImageLoader} — off-screen preloading hook\r\n */\r\nexport default function OptimizedImage({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n width,\r\n height,\r\n placeholder,\r\n autoPlaceholder,\r\n fallback,\r\n autoFallback,\r\n avifFallback,\r\n webpFallback,\r\n lazy = true,\r\n threshold = 0.25,\r\n rootMargin = \"0px\",\r\n ...rest\r\n}: OptimizedImageProps) {\r\n // Attach `ref` to the wrapper so the IntersectionObserver can track it.\r\n // `isInView` flips to `true` once the element meets the visibility threshold\r\n // and stays `true` permanently (one-shot observation).\r\n const { ref, isInView } = useInView({ threshold, rootMargin });\r\n\r\n // Start downloading the real image only after the element enters the viewport.\r\n // When `lazy` is disabled, we pass `true` directly to load immediately.\r\n const imageState = useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView: lazy ? isInView : true,\r\n });\r\n\r\n // If the image failed to load and the consumer provided a fallback,\r\n // render the fallback image (with optional AVIF/WebP variants) and bail out.\r\n if (imageState === \"error\" && (fallback || autoFallback)) {\r\n return (\r\n <div ref={ref} style={{ width, height, position: \"relative\" }}>\r\n <ImageWithFormats\r\n avifSrc={avifFallback}\r\n webpSrc={webpFallback}\r\n src={fallback}\r\n autoSrc={autoFallback}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n />\r\n </div>\r\n );\r\n }\r\n\r\n const isLoaded = imageState === \"loaded\";\r\n\r\n // The container uses `position: relative` + `overflow: hidden` to create\r\n // a stacking context. Both the placeholder and the real image are positioned\r\n // absolutely so they overlap — only their opacity differs.\r\n return (\r\n <div\r\n ref={ref}\r\n style={{\r\n width,\r\n height,\r\n position: \"relative\",\r\n overflow: \"hidden\",\r\n }}\r\n {...rest}\r\n >\r\n {/* Placeholder layer (bottom) — visible immediately, fades out once loaded */}\r\n {placeholder && (\r\n <ImageWithFormats\r\n src={placeholder}\r\n autoSrc={autoPlaceholder}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n customStyles={{\r\n opacity: isLoaded ? 0 : 1,\r\n }}\r\n />\r\n )}\r\n\r\n {/* Real image layer (top) — mounted only after the element enters the viewport */}\r\n {(lazy ? isInView : true) && (\r\n <ImageWithFormats\r\n src={src}\r\n autoSrc={autoSrc}\r\n autoFormat={autoFormat}\r\n avifSrc={avifSrc}\r\n webpSrc={webpSrc}\r\n alt={alt}\r\n />\r\n )}\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"react-pro-image.cjs.js","names":[],"sources":["../src/hooks/useImageFormatSupport.ts","../src/hooks/useImageLoader.ts","../src/hooks/useInView.ts","../src/components/OptimizedImage.tsx"],"sourcesContent":["/**\r\n * useImageFormatSupport\r\n *\r\n * A React hook that detects whether the user's browser supports modern image\r\n * formats (AVIF and WebP). It works by attempting to load a tiny test image\r\n * for each format — if the browser can render it, the format is supported.\r\n *\r\n * Results are cached in `localStorage` so the detection only runs once per\r\n * browser, avoiding unnecessary network/decode work on subsequent visits.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { avif, webp, ready } = useImageFormatSupport();\r\n *\r\n * if (!ready) return <p>Checking format support…</p>;\r\n *\r\n * return (\r\n * <img src={avif ? \"/photo.avif\" : webp ? \"/photo.webp\" : \"/photo.jpg\"} />\r\n * );\r\n * ```\r\n *\r\n * @returns An object with three properties:\r\n * - `avif` — `true` if the browser can decode AVIF images\r\n * - `webp` — `true` if the browser can decode WebP images\r\n * - `ready` — `true` once detection is complete (initially `false`)\r\n */\r\nimport { useState, useEffect } from \"react\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Test assets\r\n// ---------------------------------------------------------------------------\r\n// AVIF: We fetch a 1×1 AVIF encoded as a Base64 data-URI to verify the browser can\r\n// actually decode the AVIF format.\r\nconst AVIF_TEST =\r\n \"data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAACEwAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKBzgAPtPlAIED8GqhABwMDIBAAAAA\";\r\n\r\n// WebP: A tiny 1×1 WebP encoded as a Base64 data-URI. WebP files are small\r\n// enough to inline directly, so no network request is needed.\r\nconst WEBP_TEST =\r\n \"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=\";\r\n\r\n// localStorage key used to persist detection results across page loads.\r\nconst CACHE_KEY = \"img-support\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Helper\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Attempts to load an image from `src` and resolves to `true` if the browser\r\n * successfully decoded it, or `false` if loading failed.\r\n *\r\n * How it works:\r\n * 1. Create an off-screen `<img>` element (never added to the DOM).\r\n * 2. Set its `src` to the test image.\r\n * 3. Listen for `onload` (success → true) or `onerror` (failure → false).\r\n *\r\n * The Promise is typed as `Promise<boolean>` so TypeScript knows the resolved\r\n * value is a boolean — without this, it would default to `Promise<unknown>`.\r\n */\r\nfunction canLoadImage(src: string): Promise<boolean> {\r\n return new Promise<boolean>((resolve) => {\r\n const img = new Image();\r\n img.onload = () => resolve(true);\r\n img.onerror = () => resolve(false);\r\n img.src = src;\r\n });\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Hook\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function useImageFormatSupport() {\r\n // State shape:\r\n // avif → does the browser support AVIF? (default: false)\r\n // webp → does the browser support WebP? (default: false)\r\n // ready → has detection finished? (default: false)\r\n //\r\n // Components can check `ready` before acting on `avif`/`webp` to avoid\r\n // a flash of incorrect content while the async check is still running.\r\n const [support, setSupport] = useState({\r\n avif: false,\r\n webp: false,\r\n ready: false,\r\n });\r\n\r\n useEffect(() => {\r\n // --- Cache hit: reuse a previous result from localStorage -------------\r\n const cached = localStorage.getItem(CACHE_KEY);\r\n\r\n if (cached) {\r\n // `cached` is a JSON string like '{\"avif\":true,\"webp\":true}'.\r\n // We spread it into state and set `ready: true` immediately.\r\n setSupport({ ...JSON.parse(cached), ready: true });\r\n return; // skip the network/decode tests entirely\r\n }\r\n\r\n // --- Cache miss: run the format detection tests -----------------------\r\n // `Promise.all` runs both checks in parallel and waits for both to finish.\r\n // The result is an array of two booleans: [avifSupported, webpSupported].\r\n Promise.all([canLoadImage(AVIF_TEST), canLoadImage(WEBP_TEST)]).then(\r\n ([avif, webp]) => {\r\n // Persist so we never re-run the tests on this browser.\r\n localStorage.setItem(CACHE_KEY, JSON.stringify({ avif, webp }));\r\n\r\n // Update React state — triggers a re-render with the final values.\r\n setSupport({ avif, webp, ready: true });\r\n },\r\n );\r\n }, []); // Empty dependency array → runs once on mount, never re-runs.\r\n\r\n return support;\r\n}\r\n","import { useEffect, useState } from \"react\";\r\nimport type { UseImageLoaderOptions } from \"../interfaces\";\r\nimport type { ImageLoadState } from \"../types\";\r\nimport { useImageFormatSupport } from \"./useImageFormatSupport\";\r\n\r\n/**\r\n * Preloads an image off-screen and exposes its load state.\r\n *\r\n * Loading is deferred until `isInView` is `true`, enabling lazy-load\r\n * behaviour when combined with an intersection observer. The hook\r\n * selects the best available format in priority order: AVIF → WebP → original.\r\n *\r\n * @param options.src - Original image URL (required fallback).\r\n * @param options.avifSrc - Optional AVIF source (highest priority).\r\n * @param options.webpSrc - Optional WebP source (second priority).\r\n * @param options.isInView - When `true`, triggers the preload. Pass `true`\r\n * directly to disable lazy behaviour (default `false`).\r\n * @returns Current load state: `\"idle\"` | `\"loading\"` | `\"loaded\"` | `\"error\"`.\r\n *\r\n * @example\r\n * ```tsx\r\n * const state = useImageLoader({ src, isInView: true });\r\n * // state: \"idle\" → \"loading\" → \"loaded\" | \"error\"\r\n * ```\r\n */\r\nexport default function useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView = false,\r\n}: UseImageLoaderOptions) {\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n const [imageState, setImageState] = useState<ImageLoadState>(\"idle\");\r\n\r\n useEffect(() => {\r\n if (!isInView || !ready) return;\r\n\r\n let activeSrc: string | undefined;\r\n\r\n if (autoSrc && autoFormat) {\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n if (bestFormat) {\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n activeSrc = `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`;\r\n } else {\r\n activeSrc = autoSrc;\r\n }\r\n } else {\r\n // --- Manual path: use explicit avifSrc / webpSrc ----------------------\r\n if (avif && avifSrc) {\r\n activeSrc = avifSrc;\r\n } else if (webp && webpSrc) {\r\n activeSrc = webpSrc;\r\n } else {\r\n activeSrc = src;\r\n }\r\n }\r\n\r\n if (!activeSrc) {\r\n setImageState(\"error\");\r\n return;\r\n }\r\n\r\n const img = new Image();\r\n img.onload = () => setImageState(\"loaded\");\r\n img.onerror = () => setImageState(\"error\");\r\n img.src = activeSrc;\r\n\r\n return () => {\r\n img.onload = null;\r\n img.onerror = null;\r\n };\r\n }, [src, autoSrc, autoFormat, avifSrc, webpSrc, avif, webp, ready, isInView]);\r\n\r\n return imageState;\r\n}\r\n","import { useEffect, useRef, useState } from \"react\";\r\nimport type { UseInViewOptions } from \"../interfaces\";\r\n\r\n/**\r\n * Tracks whether a DOM element has entered the viewport using the\r\n * `IntersectionObserver` API. Observation is one-shot: once the element\r\n * becomes visible the observer disconnects automatically.\r\n *\r\n * @param options.threshold - Visibility ratio required to trigger (default `0.25`).\r\n * @param options.rootMargin - CSS-style margin applied to the root viewport (default `\"0px\"`).\r\n * @returns `{ ref, isInView }` — attach `ref` to the target element;\r\n * `isInView` flips to `true` once the threshold is met.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { ref, isInView } = useInView({ threshold: 0.25 });\r\n * return <div ref={ref}>{isInView && <img src={src} />}</div>;\r\n * ```\r\n */\r\nexport default function useInView(options: UseInViewOptions = {}) {\r\n const { threshold = 0.25, rootMargin = \"0px\" } = options;\r\n\r\n const ref = useRef<HTMLDivElement>(null);\r\n const [isInView, setIsInView] = useState(false);\r\n\r\n useEffect(() => {\r\n const element = ref.current;\r\n if (!element) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setIsInView(true);\r\n observer.disconnect(); // One-shot: stop after first intersection\r\n }\r\n },\r\n { threshold, rootMargin },\r\n );\r\n\r\n observer.observe(element);\r\n\r\n return () => observer.disconnect();\r\n }, [threshold, rootMargin]);\r\n\r\n return { ref, isInView };\r\n}\r\n","import { useImageFormatSupport } from \"../hooks/useImageFormatSupport\";\r\nimport useImageLoader from \"../hooks/useImageLoader\";\r\nimport useInView from \"../hooks/useInView\";\r\nimport type { ImageWithFormatsProps, OptimizedImageProps } from \"../interfaces\";\r\n\r\n/**\r\n * Internal component that resolves the best image source based on browser\r\n * format support and renders the appropriate `<img>` tag.\r\n *\r\n * When `autoSrc` + `autoFormat` is provided, it iterates through the\r\n * configured formats in priority order and appends the format query param\r\n * to the URL for the first supported format.\r\n *\r\n * When manual `avifSrc` / `webpSrc` is provided, it picks the best\r\n * supported source directly.\r\n */\r\nfunction ImageWithFormats({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n customStyles,\r\n}: ImageWithFormatsProps) {\r\n /**\r\n * Base styles that position the image as an absolutely-placed cover layer.\r\n * The `transition` enables a smooth opacity crossfade between the\r\n * placeholder and the fully-loaded image.\r\n * Any `customStyles` (e.g. dynamic opacity) are spread on top.\r\n */\r\n const sharedStyles = {\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n width: \"100%\",\r\n height: \"100%\",\r\n objectFit: \"cover\" as const,\r\n ...customStyles,\r\n };\r\n\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n\r\n // --- Auto-format path: build URL with format query param -----------------\r\n if (autoSrc && autoFormat && ready) {\r\n // Helper: appends \"?fm=avif\" or \"&fm=avif\" depending on whether URL already has \"?\"\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n // If a supported format was found, use it; otherwise use autoSrc as-is\r\n return (\r\n <img\r\n src={\r\n bestFormat\r\n ? `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`\r\n : autoSrc\r\n }\r\n alt={alt}\r\n style={sharedStyles}\r\n />\r\n );\r\n }\r\n\r\n // --- Manual path: use explicit avifSrc / webpSrc -------------------------\r\n if (avifSrc && ready && avif) {\r\n return <img src={avifSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n if (webpSrc && ready && webp) {\r\n return <img src={webpSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n return <img src={src} alt={alt} style={sharedStyles} />;\r\n}\r\n\r\n/**\r\n * A performance-focused image component that combines **lazy loading**,\r\n * **placeholder-to-full crossfade**, and **modern format selection**\r\n * (AVIF / WebP) into a single drop-in `<img>` replacement.\r\n *\r\n * ## How it works\r\n *\r\n * 1. **Visibility detection** — An `IntersectionObserver` (via `useInView`)\r\n * watches the container element. No network request is made until the\r\n * configured `threshold` of the element is visible in the viewport.\r\n *\r\n * 2. **Off-screen preload** — Once visible, `useImageLoader` creates a\r\n * hidden `Image()` object to download the best-available format\r\n * (AVIF → WebP → original). The component tracks the load state\r\n * (`idle` → `loading` → `loaded` | `error`).\r\n *\r\n * 3. **Crossfade transition** — The placeholder and real image are rendered\r\n * as stacked layers. When the real image finishes loading, the\r\n * placeholder's opacity is animated to `0`, revealing the full image.\r\n *\r\n * 4. **Error recovery** — If loading fails and a `fallback` src is provided,\r\n * the fallback image is rendered instead.\r\n *\r\n * @see {@link useInView} — viewport detection hook\r\n * @see {@link useImageLoader} — off-screen preloading hook\r\n */\r\nexport default function OptimizedImage({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n width,\r\n height,\r\n placeholder,\r\n autoPlaceholder,\r\n fallback,\r\n autoFallback,\r\n avifFallback,\r\n webpFallback,\r\n lazy = true,\r\n threshold = 0.25,\r\n rootMargin = \"0px\",\r\n style,\r\n ...rest\r\n}: OptimizedImageProps) {\r\n if (src && autoSrc) {\r\n throw new Error(\r\n `Conflicting props: You cannot provide both 'src' and 'autoSrc'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`,\r\n );\r\n }\r\n\r\n if (fallback && autoFallback) {\r\n throw new Error(\r\n `Conflicting props: You cannot provide both 'fallback' and 'autoFallback'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`,\r\n );\r\n }\r\n\r\n if (placeholder && autoPlaceholder) {\r\n throw new Error(\r\n `Conflicting props: You cannot provide both 'placeholder' and 'autoPlaceholder'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`,\r\n );\r\n }\r\n\r\n const hasWidth = width !== undefined || style?.width !== undefined;\r\n const hasHeight = height !== undefined || style?.height !== undefined;\r\n\r\n if (!hasWidth || !hasHeight) {\r\n throw new Error(\r\n `Missing dimensions: You must provide both 'width' and 'height' (as props or in style) to prevent layout collapse.`,\r\n );\r\n }\r\n\r\n // Attach `ref` to the wrapper so the IntersectionObserver can track it.\r\n // `isInView` flips to `true` once the element meets the visibility threshold\r\n // and stays `true` permanently (one-shot observation).\r\n const { ref, isInView } = useInView({ threshold, rootMargin });\r\n\r\n // Start downloading the real image only after the element enters the viewport.\r\n // When `lazy` is disabled, we pass `true` directly to load immediately.\r\n const imageState = useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView: lazy ? isInView : true,\r\n });\r\n\r\n // If the image failed to load and the consumer provided a fallback,\r\n // render the fallback image (with optional AVIF/WebP variants) and bail out.\r\n if (imageState === \"error\" && (fallback || autoFallback)) {\r\n return (\r\n <div\r\n ref={ref}\r\n style={{\r\n width,\r\n height,\r\n position: \"relative\",\r\n ...style,\r\n }}\r\n {...rest}\r\n >\r\n <ImageWithFormats\r\n avifSrc={avifFallback}\r\n webpSrc={webpFallback}\r\n src={fallback}\r\n autoSrc={autoFallback}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n />\r\n </div>\r\n );\r\n }\r\n\r\n const isLoaded = imageState === \"loaded\";\r\n\r\n // The container uses `position: relative` + `overflow: hidden` to create\r\n // a stacking context. Both the placeholder and the real image are positioned\r\n // absolutely so they overlap — only their opacity differs.\r\n return (\r\n <div\r\n ref={ref}\r\n style={{\r\n width,\r\n height,\r\n position: \"relative\",\r\n overflow: \"hidden\",\r\n ...style,\r\n }}\r\n {...rest}\r\n >\r\n {/* Placeholder layer (bottom) — visible immediately, fades out once loaded */}\r\n {(placeholder || autoPlaceholder) && (\r\n <ImageWithFormats\r\n src={placeholder}\r\n autoSrc={autoPlaceholder}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n customStyles={{\r\n opacity: isLoaded ? 0 : 1,\r\n }}\r\n />\r\n )}\r\n\r\n {/* Real image layer (top) — mounted only after the element enters the viewport */}\r\n {(lazy ? isInView : true) && (\r\n <ImageWithFormats\r\n src={src}\r\n autoSrc={autoSrc}\r\n autoFormat={autoFormat}\r\n avifSrc={avifSrc}\r\n webpSrc={webpSrc}\r\n alt={alt}\r\n />\r\n )}\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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"}
@@ -265,7 +265,13 @@ function ImageWithFormats({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, cus
265
265
  * @see {@link useInView} — viewport detection hook
266
266
  * @see {@link useImageLoader} — off-screen preloading hook
267
267
  */
268
- function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width, height, placeholder, autoPlaceholder, fallback, autoFallback, avifFallback, webpFallback, lazy = true, threshold = .25, rootMargin = "0px", ...rest }) {
268
+ function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width, height, placeholder, autoPlaceholder, fallback, autoFallback, avifFallback, webpFallback, lazy = true, threshold = .25, rootMargin = "0px", style, ...rest }) {
269
+ if (src && autoSrc) throw new Error(`Conflicting props: You cannot provide both 'src' and 'autoSrc'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`);
270
+ if (fallback && autoFallback) throw new Error(`Conflicting props: You cannot provide both 'fallback' and 'autoFallback'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`);
271
+ if (placeholder && autoPlaceholder) throw new Error(`Conflicting props: You cannot provide both 'placeholder' and 'autoPlaceholder'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`);
272
+ const hasWidth = width !== void 0 || style?.width !== void 0;
273
+ const hasHeight = height !== void 0 || style?.height !== void 0;
274
+ if (!hasWidth || !hasHeight) throw new Error(`Missing dimensions: You must provide both 'width' and 'height' (as props or in style) to prevent layout collapse.`);
269
275
  const { ref, isInView } = useInView({
270
276
  threshold,
271
277
  rootMargin
@@ -283,8 +289,10 @@ function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width
283
289
  style: {
284
290
  width,
285
291
  height,
286
- position: "relative"
292
+ position: "relative",
293
+ ...style
287
294
  },
295
+ ...rest,
288
296
  children: /* @__PURE__ */ jsx(ImageWithFormats, {
289
297
  avifSrc: avifFallback,
290
298
  webpSrc: webpFallback,
@@ -301,10 +309,11 @@ function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width
301
309
  width,
302
310
  height,
303
311
  position: "relative",
304
- overflow: "hidden"
312
+ overflow: "hidden",
313
+ ...style
305
314
  },
306
315
  ...rest,
307
- children: [placeholder && /* @__PURE__ */ jsx(ImageWithFormats, {
316
+ children: [(placeholder || autoPlaceholder) && /* @__PURE__ */ jsx(ImageWithFormats, {
308
317
  src: placeholder,
309
318
  autoSrc: autoPlaceholder,
310
319
  autoFormat,
@@ -1 +1 @@
1
- {"version":3,"file":"react-pro-image.es.js","names":[],"sources":["../src/hooks/useImageFormatSupport.ts","../src/hooks/useImageLoader.ts","../src/hooks/useInView.ts","../src/components/OptimizedImage.tsx"],"sourcesContent":["/**\r\n * useImageFormatSupport\r\n *\r\n * A React hook that detects whether the user's browser supports modern image\r\n * formats (AVIF and WebP). It works by attempting to load a tiny test image\r\n * for each format — if the browser can render it, the format is supported.\r\n *\r\n * Results are cached in `localStorage` so the detection only runs once per\r\n * browser, avoiding unnecessary network/decode work on subsequent visits.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { avif, webp, ready } = useImageFormatSupport();\r\n *\r\n * if (!ready) return <p>Checking format support…</p>;\r\n *\r\n * return (\r\n * <img src={avif ? \"/photo.avif\" : webp ? \"/photo.webp\" : \"/photo.jpg\"} />\r\n * );\r\n * ```\r\n *\r\n * @returns An object with three properties:\r\n * - `avif` — `true` if the browser can decode AVIF images\r\n * - `webp` — `true` if the browser can decode WebP images\r\n * - `ready` — `true` once detection is complete (initially `false`)\r\n */\r\nimport { useState, useEffect } from \"react\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Test assets\r\n// ---------------------------------------------------------------------------\r\n// AVIF: We fetch a 1×1 AVIF encoded as a Base64 data-URI to verify the browser can\r\n// actually decode the AVIF format.\r\nconst AVIF_TEST =\r\n \"data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAACEwAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKBzgAPtPlAIED8GqhABwMDIBAAAAA\";\r\n\r\n// WebP: A tiny 1×1 WebP encoded as a Base64 data-URI. WebP files are small\r\n// enough to inline directly, so no network request is needed.\r\nconst WEBP_TEST =\r\n \"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=\";\r\n\r\n// localStorage key used to persist detection results across page loads.\r\nconst CACHE_KEY = \"img-support\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Helper\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Attempts to load an image from `src` and resolves to `true` if the browser\r\n * successfully decoded it, or `false` if loading failed.\r\n *\r\n * How it works:\r\n * 1. Create an off-screen `<img>` element (never added to the DOM).\r\n * 2. Set its `src` to the test image.\r\n * 3. Listen for `onload` (success → true) or `onerror` (failure → false).\r\n *\r\n * The Promise is typed as `Promise<boolean>` so TypeScript knows the resolved\r\n * value is a boolean — without this, it would default to `Promise<unknown>`.\r\n */\r\nfunction canLoadImage(src: string): Promise<boolean> {\r\n return new Promise<boolean>((resolve) => {\r\n const img = new Image();\r\n img.onload = () => resolve(true);\r\n img.onerror = () => resolve(false);\r\n img.src = src;\r\n });\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Hook\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function useImageFormatSupport() {\r\n // State shape:\r\n // avif → does the browser support AVIF? (default: false)\r\n // webp → does the browser support WebP? (default: false)\r\n // ready → has detection finished? (default: false)\r\n //\r\n // Components can check `ready` before acting on `avif`/`webp` to avoid\r\n // a flash of incorrect content while the async check is still running.\r\n const [support, setSupport] = useState({\r\n avif: false,\r\n webp: false,\r\n ready: false,\r\n });\r\n\r\n useEffect(() => {\r\n // --- Cache hit: reuse a previous result from localStorage -------------\r\n const cached = localStorage.getItem(CACHE_KEY);\r\n\r\n if (cached) {\r\n // `cached` is a JSON string like '{\"avif\":true,\"webp\":true}'.\r\n // We spread it into state and set `ready: true` immediately.\r\n setSupport({ ...JSON.parse(cached), ready: true });\r\n return; // skip the network/decode tests entirely\r\n }\r\n\r\n // --- Cache miss: run the format detection tests -----------------------\r\n // `Promise.all` runs both checks in parallel and waits for both to finish.\r\n // The result is an array of two booleans: [avifSupported, webpSupported].\r\n Promise.all([canLoadImage(AVIF_TEST), canLoadImage(WEBP_TEST)]).then(\r\n ([avif, webp]) => {\r\n // Persist so we never re-run the tests on this browser.\r\n localStorage.setItem(CACHE_KEY, JSON.stringify({ avif, webp }));\r\n\r\n // Update React state — triggers a re-render with the final values.\r\n setSupport({ avif, webp, ready: true });\r\n },\r\n );\r\n }, []); // Empty dependency array → runs once on mount, never re-runs.\r\n\r\n return support;\r\n}\r\n","import { useEffect, useState } from \"react\";\r\nimport type { UseImageLoaderOptions } from \"../interfaces\";\r\nimport type { ImageLoadState } from \"../types\";\r\nimport { useImageFormatSupport } from \"./useImageFormatSupport\";\r\n\r\n/**\r\n * Preloads an image off-screen and exposes its load state.\r\n *\r\n * Loading is deferred until `isInView` is `true`, enabling lazy-load\r\n * behaviour when combined with an intersection observer. The hook\r\n * selects the best available format in priority order: AVIF → WebP → original.\r\n *\r\n * @param options.src - Original image URL (required fallback).\r\n * @param options.avifSrc - Optional AVIF source (highest priority).\r\n * @param options.webpSrc - Optional WebP source (second priority).\r\n * @param options.isInView - When `true`, triggers the preload. Pass `true`\r\n * directly to disable lazy behaviour (default `false`).\r\n * @returns Current load state: `\"idle\"` | `\"loading\"` | `\"loaded\"` | `\"error\"`.\r\n *\r\n * @example\r\n * ```tsx\r\n * const state = useImageLoader({ src, isInView: true });\r\n * // state: \"idle\" → \"loading\" → \"loaded\" | \"error\"\r\n * ```\r\n */\r\nexport default function useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView = false,\r\n}: UseImageLoaderOptions) {\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n const [imageState, setImageState] = useState<ImageLoadState>(\"idle\");\r\n\r\n useEffect(() => {\r\n if (!isInView || !ready) return;\r\n\r\n let activeSrc: string | undefined;\r\n\r\n if (autoSrc && autoFormat) {\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n if (bestFormat) {\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n activeSrc = `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`;\r\n } else {\r\n activeSrc = autoSrc;\r\n }\r\n } else {\r\n // --- Manual path: use explicit avifSrc / webpSrc ----------------------\r\n if (avif && avifSrc) {\r\n activeSrc = avifSrc;\r\n } else if (webp && webpSrc) {\r\n activeSrc = webpSrc;\r\n } else {\r\n activeSrc = src;\r\n }\r\n }\r\n\r\n if (!activeSrc) {\r\n setImageState(\"error\");\r\n return;\r\n }\r\n\r\n const img = new Image();\r\n img.onload = () => setImageState(\"loaded\");\r\n img.onerror = () => setImageState(\"error\");\r\n img.src = activeSrc;\r\n\r\n return () => {\r\n img.onload = null;\r\n img.onerror = null;\r\n };\r\n }, [src, autoSrc, autoFormat, avifSrc, webpSrc, avif, webp, ready, isInView]);\r\n\r\n return imageState;\r\n}\r\n","import { useEffect, useRef, useState } from \"react\";\r\nimport type { UseInViewOptions } from \"../interfaces\";\r\n\r\n/**\r\n * Tracks whether a DOM element has entered the viewport using the\r\n * `IntersectionObserver` API. Observation is one-shot: once the element\r\n * becomes visible the observer disconnects automatically.\r\n *\r\n * @param options.threshold - Visibility ratio required to trigger (default `0.25`).\r\n * @param options.rootMargin - CSS-style margin applied to the root viewport (default `\"0px\"`).\r\n * @returns `{ ref, isInView }` — attach `ref` to the target element;\r\n * `isInView` flips to `true` once the threshold is met.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { ref, isInView } = useInView({ threshold: 0.25 });\r\n * return <div ref={ref}>{isInView && <img src={src} />}</div>;\r\n * ```\r\n */\r\nexport default function useInView(options: UseInViewOptions = {}) {\r\n const { threshold = 0.25, rootMargin = \"0px\" } = options;\r\n\r\n const ref = useRef<HTMLDivElement>(null);\r\n const [isInView, setIsInView] = useState(false);\r\n\r\n useEffect(() => {\r\n const element = ref.current;\r\n if (!element) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setIsInView(true);\r\n observer.disconnect(); // One-shot: stop after first intersection\r\n }\r\n },\r\n { threshold, rootMargin },\r\n );\r\n\r\n observer.observe(element);\r\n\r\n return () => observer.disconnect();\r\n }, [threshold, rootMargin]);\r\n\r\n return { ref, isInView };\r\n}\r\n","import { useImageFormatSupport } from \"../hooks/useImageFormatSupport\";\r\nimport useImageLoader from \"../hooks/useImageLoader\";\r\nimport useInView from \"../hooks/useInView\";\r\nimport type { ImageWithFormatsProps, OptimizedImageProps } from \"../interfaces\";\r\n\r\n/**\r\n * Internal component that resolves the best image source based on browser\r\n * format support and renders the appropriate `<img>` tag.\r\n *\r\n * When `autoSrc` + `autoFormat` is provided, it iterates through the\r\n * configured formats in priority order and appends the format query param\r\n * to the URL for the first supported format.\r\n *\r\n * When manual `avifSrc` / `webpSrc` is provided, it picks the best\r\n * supported source directly.\r\n */\r\nfunction ImageWithFormats({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n customStyles,\r\n}: ImageWithFormatsProps) {\r\n /**\r\n * Base styles that position the image as an absolutely-placed cover layer.\r\n * The `transition` enables a smooth opacity crossfade between the\r\n * placeholder and the fully-loaded image.\r\n * Any `customStyles` (e.g. dynamic opacity) are spread on top.\r\n */\r\n const sharedStyles = {\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n width: \"100%\",\r\n height: \"100%\",\r\n objectFit: \"cover\" as const,\r\n ...customStyles,\r\n };\r\n\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n\r\n // --- Auto-format path: build URL with format query param -----------------\r\n if (autoSrc && autoFormat && ready) {\r\n // Helper: appends \"?fm=avif\" or \"&fm=avif\" depending on whether URL already has \"?\"\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n // If a supported format was found, use it; otherwise use autoSrc as-is\r\n return (\r\n <img\r\n src={\r\n bestFormat\r\n ? `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`\r\n : autoSrc\r\n }\r\n alt={alt}\r\n style={sharedStyles}\r\n />\r\n );\r\n }\r\n\r\n // --- Manual path: use explicit avifSrc / webpSrc -------------------------\r\n if (avifSrc && ready && avif) {\r\n return <img src={avifSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n if (webpSrc && ready && webp) {\r\n return <img src={webpSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n return <img src={src} alt={alt} style={sharedStyles} />;\r\n}\r\n\r\n/**\r\n * A performance-focused image component that combines **lazy loading**,\r\n * **placeholder-to-full crossfade**, and **modern format selection**\r\n * (AVIF / WebP) into a single drop-in `<img>` replacement.\r\n *\r\n * ## How it works\r\n *\r\n * 1. **Visibility detection** — An `IntersectionObserver` (via `useInView`)\r\n * watches the container element. No network request is made until the\r\n * configured `threshold` of the element is visible in the viewport.\r\n *\r\n * 2. **Off-screen preload** — Once visible, `useImageLoader` creates a\r\n * hidden `Image()` object to download the best-available format\r\n * (AVIF → WebP → original). The component tracks the load state\r\n * (`idle` → `loading` → `loaded` | `error`).\r\n *\r\n * 3. **Crossfade transition** — The placeholder and real image are rendered\r\n * as stacked layers. When the real image finishes loading, the\r\n * placeholder's opacity is animated to `0`, revealing the full image.\r\n *\r\n * 4. **Error recovery** — If loading fails and a `fallback` src is provided,\r\n * the fallback image is rendered instead.\r\n *\r\n * @see {@link useInView} — viewport detection hook\r\n * @see {@link useImageLoader} — off-screen preloading hook\r\n */\r\nexport default function OptimizedImage({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n width,\r\n height,\r\n placeholder,\r\n autoPlaceholder,\r\n fallback,\r\n autoFallback,\r\n avifFallback,\r\n webpFallback,\r\n lazy = true,\r\n threshold = 0.25,\r\n rootMargin = \"0px\",\r\n ...rest\r\n}: OptimizedImageProps) {\r\n // Attach `ref` to the wrapper so the IntersectionObserver can track it.\r\n // `isInView` flips to `true` once the element meets the visibility threshold\r\n // and stays `true` permanently (one-shot observation).\r\n const { ref, isInView } = useInView({ threshold, rootMargin });\r\n\r\n // Start downloading the real image only after the element enters the viewport.\r\n // When `lazy` is disabled, we pass `true` directly to load immediately.\r\n const imageState = useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView: lazy ? isInView : true,\r\n });\r\n\r\n // If the image failed to load and the consumer provided a fallback,\r\n // render the fallback image (with optional AVIF/WebP variants) and bail out.\r\n if (imageState === \"error\" && (fallback || autoFallback)) {\r\n return (\r\n <div ref={ref} style={{ width, height, position: \"relative\" }}>\r\n <ImageWithFormats\r\n avifSrc={avifFallback}\r\n webpSrc={webpFallback}\r\n src={fallback}\r\n autoSrc={autoFallback}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n />\r\n </div>\r\n );\r\n }\r\n\r\n const isLoaded = imageState === \"loaded\";\r\n\r\n // The container uses `position: relative` + `overflow: hidden` to create\r\n // a stacking context. Both the placeholder and the real image are positioned\r\n // absolutely so they overlap — only their opacity differs.\r\n return (\r\n <div\r\n ref={ref}\r\n style={{\r\n width,\r\n height,\r\n position: \"relative\",\r\n overflow: \"hidden\",\r\n }}\r\n {...rest}\r\n >\r\n {/* Placeholder layer (bottom) — visible immediately, fades out once loaded */}\r\n {placeholder && (\r\n <ImageWithFormats\r\n src={placeholder}\r\n autoSrc={autoPlaceholder}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n customStyles={{\r\n opacity: isLoaded ? 0 : 1,\r\n }}\r\n />\r\n )}\r\n\r\n {/* Real image layer (top) — mounted only after the element enters the viewport */}\r\n {(lazy ? isInView : true) && (\r\n <ImageWithFormats\r\n src={src}\r\n autoSrc={autoSrc}\r\n autoFormat={autoFormat}\r\n avifSrc={avifSrc}\r\n webpSrc={webpSrc}\r\n alt={alt}\r\n />\r\n )}\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"react-pro-image.es.js","names":[],"sources":["../src/hooks/useImageFormatSupport.ts","../src/hooks/useImageLoader.ts","../src/hooks/useInView.ts","../src/components/OptimizedImage.tsx"],"sourcesContent":["/**\r\n * useImageFormatSupport\r\n *\r\n * A React hook that detects whether the user's browser supports modern image\r\n * formats (AVIF and WebP). It works by attempting to load a tiny test image\r\n * for each format — if the browser can render it, the format is supported.\r\n *\r\n * Results are cached in `localStorage` so the detection only runs once per\r\n * browser, avoiding unnecessary network/decode work on subsequent visits.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { avif, webp, ready } = useImageFormatSupport();\r\n *\r\n * if (!ready) return <p>Checking format support…</p>;\r\n *\r\n * return (\r\n * <img src={avif ? \"/photo.avif\" : webp ? \"/photo.webp\" : \"/photo.jpg\"} />\r\n * );\r\n * ```\r\n *\r\n * @returns An object with three properties:\r\n * - `avif` — `true` if the browser can decode AVIF images\r\n * - `webp` — `true` if the browser can decode WebP images\r\n * - `ready` — `true` once detection is complete (initially `false`)\r\n */\r\nimport { useState, useEffect } from \"react\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Test assets\r\n// ---------------------------------------------------------------------------\r\n// AVIF: We fetch a 1×1 AVIF encoded as a Base64 data-URI to verify the browser can\r\n// actually decode the AVIF format.\r\nconst AVIF_TEST =\r\n \"data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAACEwAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKBzgAPtPlAIED8GqhABwMDIBAAAAA\";\r\n\r\n// WebP: A tiny 1×1 WebP encoded as a Base64 data-URI. WebP files are small\r\n// enough to inline directly, so no network request is needed.\r\nconst WEBP_TEST =\r\n \"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=\";\r\n\r\n// localStorage key used to persist detection results across page loads.\r\nconst CACHE_KEY = \"img-support\";\r\n\r\n// ---------------------------------------------------------------------------\r\n// Helper\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Attempts to load an image from `src` and resolves to `true` if the browser\r\n * successfully decoded it, or `false` if loading failed.\r\n *\r\n * How it works:\r\n * 1. Create an off-screen `<img>` element (never added to the DOM).\r\n * 2. Set its `src` to the test image.\r\n * 3. Listen for `onload` (success → true) or `onerror` (failure → false).\r\n *\r\n * The Promise is typed as `Promise<boolean>` so TypeScript knows the resolved\r\n * value is a boolean — without this, it would default to `Promise<unknown>`.\r\n */\r\nfunction canLoadImage(src: string): Promise<boolean> {\r\n return new Promise<boolean>((resolve) => {\r\n const img = new Image();\r\n img.onload = () => resolve(true);\r\n img.onerror = () => resolve(false);\r\n img.src = src;\r\n });\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Hook\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function useImageFormatSupport() {\r\n // State shape:\r\n // avif → does the browser support AVIF? (default: false)\r\n // webp → does the browser support WebP? (default: false)\r\n // ready → has detection finished? (default: false)\r\n //\r\n // Components can check `ready` before acting on `avif`/`webp` to avoid\r\n // a flash of incorrect content while the async check is still running.\r\n const [support, setSupport] = useState({\r\n avif: false,\r\n webp: false,\r\n ready: false,\r\n });\r\n\r\n useEffect(() => {\r\n // --- Cache hit: reuse a previous result from localStorage -------------\r\n const cached = localStorage.getItem(CACHE_KEY);\r\n\r\n if (cached) {\r\n // `cached` is a JSON string like '{\"avif\":true,\"webp\":true}'.\r\n // We spread it into state and set `ready: true` immediately.\r\n setSupport({ ...JSON.parse(cached), ready: true });\r\n return; // skip the network/decode tests entirely\r\n }\r\n\r\n // --- Cache miss: run the format detection tests -----------------------\r\n // `Promise.all` runs both checks in parallel and waits for both to finish.\r\n // The result is an array of two booleans: [avifSupported, webpSupported].\r\n Promise.all([canLoadImage(AVIF_TEST), canLoadImage(WEBP_TEST)]).then(\r\n ([avif, webp]) => {\r\n // Persist so we never re-run the tests on this browser.\r\n localStorage.setItem(CACHE_KEY, JSON.stringify({ avif, webp }));\r\n\r\n // Update React state — triggers a re-render with the final values.\r\n setSupport({ avif, webp, ready: true });\r\n },\r\n );\r\n }, []); // Empty dependency array → runs once on mount, never re-runs.\r\n\r\n return support;\r\n}\r\n","import { useEffect, useState } from \"react\";\r\nimport type { UseImageLoaderOptions } from \"../interfaces\";\r\nimport type { ImageLoadState } from \"../types\";\r\nimport { useImageFormatSupport } from \"./useImageFormatSupport\";\r\n\r\n/**\r\n * Preloads an image off-screen and exposes its load state.\r\n *\r\n * Loading is deferred until `isInView` is `true`, enabling lazy-load\r\n * behaviour when combined with an intersection observer. The hook\r\n * selects the best available format in priority order: AVIF → WebP → original.\r\n *\r\n * @param options.src - Original image URL (required fallback).\r\n * @param options.avifSrc - Optional AVIF source (highest priority).\r\n * @param options.webpSrc - Optional WebP source (second priority).\r\n * @param options.isInView - When `true`, triggers the preload. Pass `true`\r\n * directly to disable lazy behaviour (default `false`).\r\n * @returns Current load state: `\"idle\"` | `\"loading\"` | `\"loaded\"` | `\"error\"`.\r\n *\r\n * @example\r\n * ```tsx\r\n * const state = useImageLoader({ src, isInView: true });\r\n * // state: \"idle\" → \"loading\" → \"loaded\" | \"error\"\r\n * ```\r\n */\r\nexport default function useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView = false,\r\n}: UseImageLoaderOptions) {\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n const [imageState, setImageState] = useState<ImageLoadState>(\"idle\");\r\n\r\n useEffect(() => {\r\n if (!isInView || !ready) return;\r\n\r\n let activeSrc: string | undefined;\r\n\r\n if (autoSrc && autoFormat) {\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n if (bestFormat) {\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n activeSrc = `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`;\r\n } else {\r\n activeSrc = autoSrc;\r\n }\r\n } else {\r\n // --- Manual path: use explicit avifSrc / webpSrc ----------------------\r\n if (avif && avifSrc) {\r\n activeSrc = avifSrc;\r\n } else if (webp && webpSrc) {\r\n activeSrc = webpSrc;\r\n } else {\r\n activeSrc = src;\r\n }\r\n }\r\n\r\n if (!activeSrc) {\r\n setImageState(\"error\");\r\n return;\r\n }\r\n\r\n const img = new Image();\r\n img.onload = () => setImageState(\"loaded\");\r\n img.onerror = () => setImageState(\"error\");\r\n img.src = activeSrc;\r\n\r\n return () => {\r\n img.onload = null;\r\n img.onerror = null;\r\n };\r\n }, [src, autoSrc, autoFormat, avifSrc, webpSrc, avif, webp, ready, isInView]);\r\n\r\n return imageState;\r\n}\r\n","import { useEffect, useRef, useState } from \"react\";\r\nimport type { UseInViewOptions } from \"../interfaces\";\r\n\r\n/**\r\n * Tracks whether a DOM element has entered the viewport using the\r\n * `IntersectionObserver` API. Observation is one-shot: once the element\r\n * becomes visible the observer disconnects automatically.\r\n *\r\n * @param options.threshold - Visibility ratio required to trigger (default `0.25`).\r\n * @param options.rootMargin - CSS-style margin applied to the root viewport (default `\"0px\"`).\r\n * @returns `{ ref, isInView }` — attach `ref` to the target element;\r\n * `isInView` flips to `true` once the threshold is met.\r\n *\r\n * @example\r\n * ```tsx\r\n * const { ref, isInView } = useInView({ threshold: 0.25 });\r\n * return <div ref={ref}>{isInView && <img src={src} />}</div>;\r\n * ```\r\n */\r\nexport default function useInView(options: UseInViewOptions = {}) {\r\n const { threshold = 0.25, rootMargin = \"0px\" } = options;\r\n\r\n const ref = useRef<HTMLDivElement>(null);\r\n const [isInView, setIsInView] = useState(false);\r\n\r\n useEffect(() => {\r\n const element = ref.current;\r\n if (!element) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setIsInView(true);\r\n observer.disconnect(); // One-shot: stop after first intersection\r\n }\r\n },\r\n { threshold, rootMargin },\r\n );\r\n\r\n observer.observe(element);\r\n\r\n return () => observer.disconnect();\r\n }, [threshold, rootMargin]);\r\n\r\n return { ref, isInView };\r\n}\r\n","import { useImageFormatSupport } from \"../hooks/useImageFormatSupport\";\r\nimport useImageLoader from \"../hooks/useImageLoader\";\r\nimport useInView from \"../hooks/useInView\";\r\nimport type { ImageWithFormatsProps, OptimizedImageProps } from \"../interfaces\";\r\n\r\n/**\r\n * Internal component that resolves the best image source based on browser\r\n * format support and renders the appropriate `<img>` tag.\r\n *\r\n * When `autoSrc` + `autoFormat` is provided, it iterates through the\r\n * configured formats in priority order and appends the format query param\r\n * to the URL for the first supported format.\r\n *\r\n * When manual `avifSrc` / `webpSrc` is provided, it picks the best\r\n * supported source directly.\r\n */\r\nfunction ImageWithFormats({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n customStyles,\r\n}: ImageWithFormatsProps) {\r\n /**\r\n * Base styles that position the image as an absolutely-placed cover layer.\r\n * The `transition` enables a smooth opacity crossfade between the\r\n * placeholder and the fully-loaded image.\r\n * Any `customStyles` (e.g. dynamic opacity) are spread on top.\r\n */\r\n const sharedStyles = {\r\n position: \"absolute\" as const,\r\n inset: 0,\r\n width: \"100%\",\r\n height: \"100%\",\r\n objectFit: \"cover\" as const,\r\n ...customStyles,\r\n };\r\n\r\n const { avif, webp, ready } = useImageFormatSupport();\r\n\r\n // --- Auto-format path: build URL with format query param -----------------\r\n if (autoSrc && autoFormat && ready) {\r\n // Helper: appends \"?fm=avif\" or \"&fm=avif\" depending on whether URL already has \"?\"\r\n const separator = autoSrc.includes(\"?\") ? \"&\" : \"?\";\r\n\r\n // Pick the best format: always prefer avif over webp (best quality first)\r\n // The formats array controls which formats are allowed, not the priority\r\n let bestFormat: string | undefined;\r\n\r\n if (avif && autoFormat.formats.includes(\"avif\")) {\r\n bestFormat = \"avif\";\r\n } else if (webp && autoFormat.formats.includes(\"webp\")) {\r\n bestFormat = \"webp\";\r\n }\r\n\r\n // If a supported format was found, use it; otherwise use autoSrc as-is\r\n return (\r\n <img\r\n src={\r\n bestFormat\r\n ? `${autoSrc}${separator}${autoFormat.formatKey}=${bestFormat}`\r\n : autoSrc\r\n }\r\n alt={alt}\r\n style={sharedStyles}\r\n />\r\n );\r\n }\r\n\r\n // --- Manual path: use explicit avifSrc / webpSrc -------------------------\r\n if (avifSrc && ready && avif) {\r\n return <img src={avifSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n if (webpSrc && ready && webp) {\r\n return <img src={webpSrc} alt={alt} style={sharedStyles} />;\r\n }\r\n\r\n return <img src={src} alt={alt} style={sharedStyles} />;\r\n}\r\n\r\n/**\r\n * A performance-focused image component that combines **lazy loading**,\r\n * **placeholder-to-full crossfade**, and **modern format selection**\r\n * (AVIF / WebP) into a single drop-in `<img>` replacement.\r\n *\r\n * ## How it works\r\n *\r\n * 1. **Visibility detection** — An `IntersectionObserver` (via `useInView`)\r\n * watches the container element. No network request is made until the\r\n * configured `threshold` of the element is visible in the viewport.\r\n *\r\n * 2. **Off-screen preload** — Once visible, `useImageLoader` creates a\r\n * hidden `Image()` object to download the best-available format\r\n * (AVIF → WebP → original). The component tracks the load state\r\n * (`idle` → `loading` → `loaded` | `error`).\r\n *\r\n * 3. **Crossfade transition** — The placeholder and real image are rendered\r\n * as stacked layers. When the real image finishes loading, the\r\n * placeholder's opacity is animated to `0`, revealing the full image.\r\n *\r\n * 4. **Error recovery** — If loading fails and a `fallback` src is provided,\r\n * the fallback image is rendered instead.\r\n *\r\n * @see {@link useInView} — viewport detection hook\r\n * @see {@link useImageLoader} — off-screen preloading hook\r\n */\r\nexport default function OptimizedImage({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n alt,\r\n width,\r\n height,\r\n placeholder,\r\n autoPlaceholder,\r\n fallback,\r\n autoFallback,\r\n avifFallback,\r\n webpFallback,\r\n lazy = true,\r\n threshold = 0.25,\r\n rootMargin = \"0px\",\r\n style,\r\n ...rest\r\n}: OptimizedImageProps) {\r\n if (src && autoSrc) {\r\n throw new Error(\r\n `Conflicting props: You cannot provide both 'src' and 'autoSrc'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`,\r\n );\r\n }\r\n\r\n if (fallback && autoFallback) {\r\n throw new Error(\r\n `Conflicting props: You cannot provide both 'fallback' and 'autoFallback'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`,\r\n );\r\n }\r\n\r\n if (placeholder && autoPlaceholder) {\r\n throw new Error(\r\n `Conflicting props: You cannot provide both 'placeholder' and 'autoPlaceholder'. For more info, see: https://github.com/MohamedAlfeky1/react-pro-image`,\r\n );\r\n }\r\n\r\n const hasWidth = width !== undefined || style?.width !== undefined;\r\n const hasHeight = height !== undefined || style?.height !== undefined;\r\n\r\n if (!hasWidth || !hasHeight) {\r\n throw new Error(\r\n `Missing dimensions: You must provide both 'width' and 'height' (as props or in style) to prevent layout collapse.`,\r\n );\r\n }\r\n\r\n // Attach `ref` to the wrapper so the IntersectionObserver can track it.\r\n // `isInView` flips to `true` once the element meets the visibility threshold\r\n // and stays `true` permanently (one-shot observation).\r\n const { ref, isInView } = useInView({ threshold, rootMargin });\r\n\r\n // Start downloading the real image only after the element enters the viewport.\r\n // When `lazy` is disabled, we pass `true` directly to load immediately.\r\n const imageState = useImageLoader({\r\n src,\r\n autoSrc,\r\n autoFormat,\r\n avifSrc,\r\n webpSrc,\r\n isInView: lazy ? isInView : true,\r\n });\r\n\r\n // If the image failed to load and the consumer provided a fallback,\r\n // render the fallback image (with optional AVIF/WebP variants) and bail out.\r\n if (imageState === \"error\" && (fallback || autoFallback)) {\r\n return (\r\n <div\r\n ref={ref}\r\n style={{\r\n width,\r\n height,\r\n position: \"relative\",\r\n ...style,\r\n }}\r\n {...rest}\r\n >\r\n <ImageWithFormats\r\n avifSrc={avifFallback}\r\n webpSrc={webpFallback}\r\n src={fallback}\r\n autoSrc={autoFallback}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n />\r\n </div>\r\n );\r\n }\r\n\r\n const isLoaded = imageState === \"loaded\";\r\n\r\n // The container uses `position: relative` + `overflow: hidden` to create\r\n // a stacking context. Both the placeholder and the real image are positioned\r\n // absolutely so they overlap — only their opacity differs.\r\n return (\r\n <div\r\n ref={ref}\r\n style={{\r\n width,\r\n height,\r\n position: \"relative\",\r\n overflow: \"hidden\",\r\n ...style,\r\n }}\r\n {...rest}\r\n >\r\n {/* Placeholder layer (bottom) — visible immediately, fades out once loaded */}\r\n {(placeholder || autoPlaceholder) && (\r\n <ImageWithFormats\r\n src={placeholder}\r\n autoSrc={autoPlaceholder}\r\n autoFormat={autoFormat}\r\n alt={alt}\r\n customStyles={{\r\n opacity: isLoaded ? 0 : 1,\r\n }}\r\n />\r\n )}\r\n\r\n {/* Real image layer (top) — mounted only after the element enters the viewport */}\r\n {(lazy ? isInView : true) && (\r\n <ImageWithFormats\r\n src={src}\r\n autoSrc={autoSrc}\r\n autoFormat={autoFormat}\r\n avifSrc={avifSrc}\r\n webpSrc={webpSrc}\r\n alt={alt}\r\n />\r\n )}\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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"}
@@ -25,4 +25,4 @@ import { OptimizedImageProps } from '../interfaces';
25
25
  * @see {@link useInView} — viewport detection hook
26
26
  * @see {@link useImageLoader} — off-screen preloading hook
27
27
  */
28
- export default function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width, height, placeholder, autoPlaceholder, fallback, autoFallback, avifFallback, webpFallback, lazy, threshold, rootMargin, ...rest }: OptimizedImageProps): import("react/jsx-runtime").JSX.Element;
28
+ export default function OptimizedImage({ src, autoSrc, autoFormat, avifSrc, webpSrc, alt, width, height, placeholder, autoPlaceholder, fallback, autoFallback, avifFallback, webpFallback, lazy, threshold, rootMargin, style, ...rest }: OptimizedImageProps): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-pro-image",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "One single <OptimizedImage /> component, a few props, and you get lazy loading, AVIF/WebP auto-format, placeholder crossfade, and error fallback — out of the box.",
6
6
  "author": "MohamedAlfeky1",