vite-smart-img 1.1.14 → 1.1.16

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,365 +1,366 @@
1
- # vite-smart-img
2
-
3
- Smart lazy-loading image component for React + Vite with automatic thumbnail generation, WebP conversion, and resize-aware src upgrades.
4
-
5
- ---
6
-
7
- ## Table of Contents
8
-
9
- - [Features](#features)
10
- - [Installation](#installation)
11
- - [Integration Guide](#integration-guide)
12
- - [1. Vite plugins setup](#1-vite-plugins-setup)
13
- - [2. TypeScript asset types](#2-typescript-asset-types)
14
- - [3. Using SmartImg in components](#3-using-smartimg-in-components)
15
- - [Component API](#component-api)
16
- - [CSS Customization](#css-customization)
17
- - [data-max-size-load attribute](#data-max-size-load-attribute)
18
- - [How it worksdetailed breakdown](#how-it-works--detailed-breakdown)
19
- - [⚠️ WebP source filesnot supported](#️-webp-source-files--not-supported)
20
- - [Debug attributes](#debug-attributes)
21
-
22
- ---
23
-
24
- ## Features
25
-
26
- 1. **SmartImg component** — lazy-loading React component that renders images only when they enter the viewport (≥ 5% visible).
27
- 2. **JPG, PNG, SVG and external URLs** — all handled automatically with format-specific logic.
28
- 3. **SVG and external images**loaded in full as soon as they become visible; marked `data-max-size-load` immediately.
29
- 4. **Responsive thumbnails (prod only)** — selects the smallest thumbnail that covers the element's CSS width × device pixel ratio; upgrades to a larger size after the first resize event.
30
- 5. **Fade-in effect**optional smooth opacity transition via the `opacity` prop.
31
- 6. **Session cache**loaded URLs are cached for the session; re-mounted components restore from cache instantly, without re-triggering fade.
32
- 7. **Intersection-based loading** — images are never fetched until the browser reports 5% visibility.
33
- 8. **Build-time Vite plugin** — hashes, strips metadata, generates thumbnails and WebP variants, and optimizes SVG automatically.
34
- 9. **Automatic dimension reading** both dev and build plugins extract `originalW` / `originalH` from assets and embed them in the module, enabling correct aspect-ratio containers.
35
-
36
- Additional capabilities:
37
- - **WebP detection** via canvas `toDataURL('image/webp')` — detected once on first use, result cached.
38
- - **Debounced resize** `window.resize` is debounced 200 ms; `handleResize` does not fire on every pixel change.
39
- - **Load error state** — broken images show an SVG placeholder (broken-image icon); `data-max-size-load` is set to `"svg-error"` or `"external-error"`.
40
- - **CSS variables**`--smi-bg` (placeholder background color) and `--smi-fade-duration` (transition duration).
41
- - **`className` prop** — pass an extra class to the wrapper `<div>`.
42
- - **Singleton IO and resize listener** one `IntersectionObserver` and one `resize` listener per page, shared by all `SmartImg` instances.
43
- - **Progressive JPEG** all JPEG thumbnails are encoded as progressive, so they begin rendering before fully downloaded.
44
- - **`decoding="async"`**set on the `<img>` element to avoid blocking the main thread during image decode.
45
-
46
- ---
47
-
48
- ## Installation
49
-
50
- ```bash
51
- npm install vite-smart-img
52
- ```
53
-
54
- > **Peer dependencies** — `react >= 18` and `react-dom >= 18` must be present in your project.
55
- >
56
- > **Build dependencies** — the Vite plugins require `sharp` to be installed in your project:
57
- > ```bash
58
- > npm install --save-dev sharp
59
- > ```
60
-
61
- ---
62
-
63
- ## Integration Guide
64
-
65
- ### 1. Vite plugins setup
66
-
67
- Add all four plugins to `vite.config.ts`. Each plugin has a specific role:
68
-
69
- ```ts
70
- // vite.config.ts
71
- import { defineConfig } from 'vite'
72
- import react from '@vitejs/plugin-react'
73
- import {
74
- imageThumbnails,
75
- smartImgSvgAssetBuild,
76
- importedAssetsAlwaysFiles,
77
- smartImgLocalAssetImport,
78
- } from 'vite-smart-img/plugin'
79
-
80
- export default defineConfig({
81
- plugins: [
82
- // Must come before react() and other plugins
83
- importedAssetsAlwaysFiles(), // disables Vite's asset inlining (assetsInlineLimit: 0)
84
- smartImgLocalAssetImport(), // dev only: reads dimensions from JPG/PNG/SVG and exposes { src, originalW, originalH }
85
- imageThumbnails(), // build only: generates thumbnails, WebP variants, strips EXIF, hashes filenames
86
- smartImgSvgAssetBuild(), // build only: optimizes SVG via SVGO and hashes filenames
87
-
88
- react(),
89
- ],
90
- })
91
- ```
92
-
93
- **What each plugin does:**
94
-
95
- | Plugin | When | What |
96
- |---|---|---|
97
- | `importedAssetsAlwaysFiles` | always | Sets `build.assetsInlineLimit: 0` — prevents Vite from inlining small assets as base64, which would break thumbnail URL construction |
98
- | `smartImgLocalAssetImport` | dev only | Intercepts `*.jpg / *.png / *.svg` imports from any `assets/` folder; reads dimensions via sharp (raster) or SVG parser; exports `{ src, originalW, originalH }` |
99
- | `imageThumbnails` | build only | Hashes file content (SHA-256, 8 chars), strips EXIF/metadata, generates thumbnails for each size in `[400, 600, 800 8000]px` that fits within the original width, creates `.jpg/.png` + `.webp` pairs; if a thumbnail would be larger than the cleaned original, the original is used instead |
100
- | `smartImgSvgAssetBuild` | build only | Optimizes SVG via SVGO (multipass, `preset-default`), hashes content, emits as a file |
101
-
102
- ### 2. TypeScript asset types
103
-
104
- Add the following to your `tsconfig.json` so TypeScript understands the shape of imported assets:
105
-
106
- ```json
107
- {
108
- "compilerOptions": {
109
- "types": ["vite-smart-img/vite-asset-types"]
110
- }
111
- }
112
- ```
113
-
114
- Or reference it in a `.d.ts` file in your project:
115
-
116
- ```ts
117
- /// <reference types="vite-smart-img/vite-asset-types" />
118
- ```
119
-
120
- This makes TypeScript recognize that imports from `*/assets/*.jpg`, `*/assets/*.png` and `*/assets/*.svg` return `{ src: string; originalW: number; originalH: number }` instead of a plain string.
121
-
122
- ### 3. Using SmartImg in components
123
-
124
- #### Local asset (JPG / PNG / SVG)
125
-
126
- Import the file directly from an `assets/` folder. The plugins automatically add dimension metadata:
127
-
128
- ```tsx
129
- import { SmartImg } from 'vite-smart-img'
130
- import photo from './assets/photo.jpg'
131
- import logo from './assets/logo.svg'
132
-
133
- export const MyComponent = () => (
134
- <div>
135
- {/* Local raster image — thumbnails in prod, original in dev */}
136
- <SmartImg asset={photo} alt="A photo" opacity />
137
-
138
- {/* Local SVG — always loaded as full original */}
139
- <SmartImg asset={logo} alt="Logo" opacity />
140
- </div>
141
- )
142
- ```
143
-
144
- #### External URL
145
-
146
- Provide dimensions manually (the plugin has no access to external files at build time):
147
-
148
- ```tsx
149
- import { SmartImg } from 'vite-smart-img'
150
-
151
- export const Banner = () => (
152
- <SmartImg
153
- dataSrc="https://example.com/images/banner.jpg"
154
- originalW={1600}
155
- originalH={900}
156
- alt="Banner"
157
- opacity
158
- />
159
- )
160
- ```
161
-
162
- #### Without fade
163
-
164
- Omit the `opacity` prop to show images instantly when they load:
165
-
166
- ```tsx
167
- <SmartImg asset={photo} alt="No fade" />
168
- ```
169
-
170
- #### With extra wrapper class
171
-
172
- ```tsx
173
- <SmartImg asset={photo} alt="Styled" className="my-image-block" opacity />
174
- ```
175
-
176
- The `className` is added to the outer `<div>` wrapper, not the `<img>` itself. Use it for sizing or positioning:
177
-
178
- ```css
179
- .my-image-block {
180
- width: 400px;
181
- border-radius: 12px;
182
- overflow: hidden;
183
- }
184
- ```
185
-
186
- #### Aspect ratio
187
-
188
- The component creates a `padding-top` container based on `originalH / originalW`. This reserves the correct space before the image loads, preventing layout shift (CLS). No explicit height is needed.
189
-
190
- #### Customizing the placeholder and fade
191
-
192
- Override the CSS variables globally or per-component:
193
-
194
- ```css
195
- /* global */
196
- :root {
197
- --smi-bg: #1a1a2e; /* color shown before image loads */
198
- --smi-fade-duration: 600ms; /* fade-in transition duration, default 400ms */
199
- }
200
-
201
- /* scoped */
202
- .hero-image {
203
- --smi-bg: transparent;
204
- --smi-fade-duration: 200ms;
205
- }
206
- ```
207
-
208
- ---
209
-
210
- ## Component API
211
-
212
- ### Props — local asset
213
-
214
- | Prop | Type | Required | Description |
215
- |---|---|---|---|
216
- | `asset` | `SmartImgImportedAsset` | ✓ | Import from `*/assets/*.jpg \| *.png \| *.svg` |
217
- | `alt` | `string` | ✓ | Alt text for the `<img>` |
218
- | `opacity` | `boolean` | | Enable fade-in on load |
219
- | `className` | `string` | — | Extra class on the wrapper `<div>` |
220
-
221
- ### Props — external URL
222
-
223
- | Prop | Type | Required | Description |
224
- |---|---|---|---|
225
- | `dataSrc` | `string` | ✓ | Full URL of the image |
226
- | `originalW` | `number` | ✓ | Original image width in pixels |
227
- | `originalH` | `number` | ✓ | Original image height in pixels |
228
- | `alt` | `string` | ✓ | Alt text for the `<img>` |
229
- | `opacity` | `boolean` | | Enable fade-in on load |
230
- | `className` | `string` | — | Extra class on the wrapper `<div>` |
231
-
232
- ---
233
-
234
- ## CSS Customization
235
-
236
- Styles are injected into `<head>` once on first render. Two CSS variables are available:
237
-
238
- | Variable | Default | Description |
239
- |---|---|---|
240
- | `--smi-bg` | `#f0f0f0` | Background color of the image placeholder before load |
241
- | `--smi-fade-duration` | `400ms` | Duration of the fade-in transition (requires `opacity` prop) |
242
-
243
- ---
244
-
245
- ## data-max-size-load attribute
246
-
247
- After the component confirms the image has been loaded at its optimal size, it sets `data-max-size-load` on the `<img>` element. Use it in CSS to react to the "fully loaded" state:
248
-
249
- ```css
250
- /* show a green dot when image is at max size */
251
- .smi-wrapper:has(img[data-max-size-load]) .my-indicator {
252
- background: green;
253
- }
254
- ```
255
-
256
- **Possible values:**
257
-
258
- | Value | When set |
259
- |---|---|
260
- | `"dev"` | Dev mode — original loaded |
261
- | `"svg"` | SVG fully loaded |
262
- | `"external"` | External URL fully loaded |
263
- | `"800"` / `"1200"` / … | Prod thumbnail size in px confirmed after first resize |
264
- | `"Infinity"` | Prod — original loaded (no suitable thumbnail found) |
265
- | `"svg-error"` | SVG failed to load |
266
- | `"external-error"` | External URL failed to load |
267
-
268
- **Important:** In production, `data-max-size-load` is set **only after the first `resize` event** fires after the image has loaded. This guarantees that all possible viewport sizes and orientations (e.g. portrait → landscape on mobile) have been considered before marking the image as finalized. Re-rendering a component (remounting) does **not** restore this attribute — a new resize event is required.
269
-
270
- ---
271
-
272
- ## How it works — detailed breakdown
273
-
274
- ### Loading flow
275
-
276
- ```
277
- Component mounts
278
-
279
- ├─ Cache hit? → restore src instantly (no fade, no IO)
280
-
281
- └─ No cache → register with IntersectionObserver (threshold 5%)
282
-
283
- └─ Image ≥ 5% visible
284
-
285
- ├─ SVG / external → loadDirectFinal → data-max-size-load="svg|external"
286
-
287
- ├─ Dev (JPG/PNG) → load original → data-max-size-load="dev"
288
-
289
- └─ Prod (JPG/PNG) → pickSize(cssWidth × DPR, originalW)
290
- load thumbnail
291
-
292
- └─ On resize (debounced 200ms)
293
- re-observe visible images
294
- upgrade if larger thumbnail needed
295
- data-max-size-load="<size>"
296
- ```
297
-
298
- ### Thumbnail selection (`pickSize`)
299
-
300
- ```
301
- target = ceil(cssWidth × devicePixelRatio)
302
- pick first size from [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000,
303
- 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000]
304
- where size >= target AND size <= originalW
305
-
306
- → null (no match) means: load the original file directly
307
- ```
308
-
309
- ### Build output structure
310
-
311
- For a file `photo.jpg` (original width 2400px), the plugin emits:
312
-
313
- ```
314
- dist/assets/img/
315
- a1b2c3d4.jpg ← cleaned original (EXIF stripped, orientation corrected)
316
- a1b2c3d4-400.jpg thumbnail 400px wide, progressive JPEG
317
- a1b2c3d4-400.webp WebP version
318
- a1b2c3d4-600.jpg
319
- a1b2c3d4-600.webp
320
-
321
- a1b2c3d4-2000.jpg
322
- a1b2c3d4-2000.webp
323
- (sizes above 2400px are not generated)
324
- ```
325
-
326
- The module exported from the import statement becomes:
327
-
328
- ```ts
329
- { src: '/assets/img/a1b2c3d4.jpg', originalW: 2400, originalH: 1600 }
330
- ```
331
-
332
- ### Session cache
333
-
334
- `loadedCache` is a `Map<dataSrc, { url, size }>` that lives for the duration of the page session. When a `SmartImg` component remounts:
335
-
336
- 1. `useLayoutEffect` reads the cache synchronously before the first paint.
337
- 2. `imgSrc` is set to the cached URL immediately — no BLANK flash, no fade.
338
- 3. If the same image is used in a second component that hasn't loaded it yet, it will get a fade-in when it comes into view (because its `imgSrc` starts as BLANK).
339
-
340
- ---
341
-
342
- ## ⚠️ WebP source files — not supported
343
-
344
- **Do not import `.webp` files as assets.** The plugin only processes `.jpg`, `.jpeg`, `.png` and `.svg` files.
345
-
346
- If you pass a `.webp` file as an import from an `assets/` folder:
347
-
348
- - In **dev**: the `smartImgLocalAssetImport` plugin will not intercept it (no match on extension). Vite will resolve it as a plain URL string, not a `{ src, originalW, originalH }` object — the component will receive broken props and likely throw a runtime error.
349
- - In **build**: `imageThumbnails` will ignore it (`.webp` is not in `SOURCE_EXTS`). The file falls through to Vite's default pipeline, which may inline it as base64 (breaking thumbnail URL construction) or emit it as-is without dimension metadata.
350
-
351
- **Use JPG or PNG as your source files.** The plugin will automatically generate optimized WebP variants alongside them for browsers that support it.
352
-
353
- ---
354
-
355
- ## Debug attributes
356
-
357
- Every `<img>` element rendered by `SmartImg` carries three data attributes for inspection in DevTools:
358
-
359
- | Attribute | Value |
360
- |---|---|
361
- | `data-src` | Original `dataSrc` URL (the source file, not the thumbnail) |
362
- | `data-original-w` | Original image width in pixels |
363
- | `data-original-h` | Original image height in pixels |
364
-
365
- These are read-only metadata; they are not used by the component logic.
1
+ # vite-smart-img
2
+
3
+ Умный компонент ленивой загрузки изображений для **React + Vite**: автоматические превью (thumbnails), конвертация в **WebP**, обновление `src` при изменении размера элемента.
4
+
5
+ ---
6
+
7
+ ## Содержание
8
+
9
+ - [Возможности](#возможности)
10
+ - [Установка](#установка)
11
+ - [Интеграция](#интеграция)
12
+ - [1. Плагины Vite](#1-плагины-vite)
13
+ - [2. Типы для TypeScript](#2-типы-для-typescript)
14
+ - [3. Использование SmartImg в компонентах](#3-использование-smartimg-в-компонентах)
15
+ - [API компонента](#api-компонента)
16
+ - [Кастомизация CSS](#кастомизация-css)
17
+ - [Атрибут data-max-size-load](#атрибут-data-max-size-load)
18
+ - [Как это работаетподробно](#как-это-работает--подробно)
19
+ - [⚠️ Исходные файлы WebPне поддерживаются](#️-исходные-файлы-webp--не-поддерживаются)
20
+ - [Отладочные атрибуты](#отладочные-атрибуты)
21
+
22
+ ---
23
+
24
+ ## Возможности
25
+
26
+ 1. **Компонент SmartImg** — ленивый React-компонент: изображение запрашивается, когда оно попадает в область просмотра (видимость **≥ 5%**).
27
+ 2. **JPG, PNG, SVG и внешние URL** — для каждого формата своя логика.
28
+ 3. **SVG и внешние картинки**после появления в зоне видимости загружаются целиком; сразу выставляется `data-max-size-load`.
29
+ 4. **Адаптивные миниатюры (только prod)** — выбирается наименьшая миниатюра, которая покрывает **CSS-ширину элемента × DPR**; после первого события **resize** при необходимости подгружается более крупный размер.
30
+ 5. **Плавное появление**опционально через проп **`opacity`**.
31
+ 6. **Кэш сессии**загруженные URL запоминаются на время жизни страницы; при повторном монтировании восстановление без повторного fade.
32
+ 7. **IntersectionObserver** — сетевой запрос не уходит, пока браузер не сообщит о видимости **≥ 5%**.
33
+ 8. **Плагин сборки Vite** — хеши имён, очистка метаданных, миниатюры и варианты WebP для растра, оптимизация SVG.
34
+ 9. **Авторазмеры**в dev и build плагины вытаскивают **`originalW` / `originalH`** из ассетов и встраивают в модуль, чтобы контейнер с **aspect-ratio** был корректным.
35
+
36
+ Дополнительно:
37
+
38
+ - **Определение WebP** через `canvas.toDataURL('image/webp')` один раз при первом использовании, результат кэшируется.
39
+ - **Debounced resize** — `window.resize` с задержкой **200 ms**.
40
+ - **Ошибка загрузки**для битой картинки показывается SVG-плейсхолдер; `data-max-size-load` равен **`"svg-error"`** или **`"external-error"`**.
41
+ - **CSS-переменные**`--smi-bg` (фон плейсхолдера), `--smi-fade-duration` (длительность перехода).
42
+ - **Проп `className`**дополнительный класс на обёртке **`<div>`**.
43
+ - **Один IO и один слушатель resize** на страницу для всех экземпляров **SmartImg**.
44
+ - **Progressive JPEG** миниатюры JPEG кодируются как progressive.
45
+ - **`decoding="async"`** на **`<img>`**, чтобы декод не блокировал основной поток.
46
+
47
+ ---
48
+
49
+ ## Установка
50
+
51
+ ```bash
52
+ npm install vite-smart-img
53
+ ```
54
+
55
+ > **Peer dependencies** — в проекте должны быть **`react >= 18`** и **`react-dom >= 18`**.
56
+ >
57
+ > **Для сборки** — плагинам Vite нужен **`sharp`** в devDependencies проекта:
58
+ > ```bash
59
+ > npm install --save-dev sharp
60
+ > ```
61
+
62
+ ---
63
+
64
+ ## Интеграция
65
+
66
+ ### 1. Плагины Vite
67
+
68
+ Подключите все четыре плагина в **`vite.config.ts`**. У каждого своя роль:
69
+
70
+ ```ts
71
+ // vite.config.ts
72
+ import { defineConfig } from 'vite'
73
+ import react from '@vitejs/plugin-react'
74
+ import {
75
+ imageThumbnails,
76
+ smartImgSvgAssetBuild,
77
+ importedAssetsAlwaysFiles,
78
+ smartImgLocalAssetImport,
79
+ } from 'vite-smart-img/plugin'
80
+
81
+ export default defineConfig({
82
+ plugins: [
83
+ // Должны идти до react() и прочих плагинов
84
+ importedAssetsAlwaysFiles(), // отключает inline ассетов Vite (assetsInlineLimit: 0)
85
+ smartImgLocalAssetImport(), // только dev: размеры JPG/PNG/SVG и { src, originalW, originalH }
86
+ imageThumbnails(), // только build: миниатюры, WebP, без EXIF, хеши имён файлов
87
+ smartImgSvgAssetBuild(), // только build: SVGO и хеш для SVG
88
+
89
+ react(),
90
+ ],
91
+ })
92
+ ```
93
+
94
+ **Что делает каждый плагин:**
95
+
96
+ | Плагин | Когда | Назначение |
97
+ |--------|--------|------------|
98
+ | `importedAssetsAlwaysFiles` | всегда | **`build.assetsInlineLimit: 0`** мелкие файлы не уходят в base64 внутри JS, иначе ломается сборка URL миниатюр |
99
+ | `smartImgLocalAssetImport` | только dev | Перехватывает импорты **`*.jpg / *.png / *.svg`** из папок **`assets/`**; **sharp** (растр) или парсер SVG; экспорт **`{ src, originalW, originalH }`** |
100
+ | `imageThumbnails` | только build | Хеш содержимого (SHA-256, 8 символов), снятие EXIF/метаданных, миниатюры по ширинам **[400, 600, 800 8000]**, не шире оригинала; пары **`.jpg/.png` + `.webp`**; если миниатюра получилась больше очищенного оригинала — используется оригинал |
101
+ | `smartImgSvgAssetBuild` | только build | Оптимизация SVG через SVGO (**preset-default**, multipass), хеш, вывод как отдельный файл |
102
+
103
+ ### 2. Типы для TypeScript
104
+
105
+ Добавьте в **`tsconfig.json`**, чтобы TypeScript понимал импорты ассетов:
106
+
107
+ ```json
108
+ {
109
+ "compilerOptions": {
110
+ "types": ["vite-smart-img/vite-asset-types"]
111
+ }
112
+ }
113
+ ```
114
+
115
+ Или в **`*.d.ts`** проекта:
116
+
117
+ ```ts
118
+ /// <reference types="vite-smart-img/vite-asset-types" />
119
+ ```
120
+
121
+ После этого импорты из **`*/assets/*.jpg`**, **`*.png`**, **`*.svg`** имеют тип **`{ src: string; originalW: number; originalH: number }`**, а не просто **`string`**.
122
+
123
+ ### 3. Использование SmartImg в компонентах
124
+
125
+ #### Локальный ассет (JPG / PNG / SVG)
126
+
127
+ Импортируйте файл из папки **`assets/`**. Плагины сами добавят метаданные размеров:
128
+
129
+ ```tsx
130
+ import { SmartImg } from 'vite-smart-img'
131
+ import photo from './assets/photo.jpg'
132
+ import logo from './assets/logo.svg'
133
+
134
+ export const MyComponent = () => (
135
+ <div>
136
+ {/* Локальный растр в prod миниатюры, в dev оригинал */}
137
+ <SmartImg asset={photo} alt="Фотография" opacity />
138
+
139
+ {/* Локальный SVG — всегда полный оригинал */}
140
+ <SmartImg asset={logo} alt="Логотип" opacity />
141
+ </div>
142
+ )
143
+ ```
144
+
145
+ #### Внешний URL
146
+
147
+ Размеры задаются вручную (плагин на этапе сборки не видит чужие файлы):
148
+
149
+ ```tsx
150
+ import { SmartImg } from 'vite-smart-img'
151
+
152
+ export const Banner = () => (
153
+ <SmartImg
154
+ dataSrc="https://example.com/images/banner.jpg"
155
+ originalW={1600}
156
+ originalH={900}
157
+ alt="Баннер"
158
+ opacity
159
+ />
160
+ )
161
+ ```
162
+
163
+ #### Без плавного появления
164
+
165
+ Не передавайте **`opacity`** — картинка покажется сразу после загрузки:
166
+
167
+ ```tsx
168
+ <SmartImg asset={photo} alt="Без fade" />
169
+ ```
170
+
171
+ #### Дополнительный класс обёртки
172
+
173
+ ```tsx
174
+ <SmartImg asset={photo} alt="Стили" className="my-image-block" opacity />
175
+ ```
176
+
177
+ **`className`** вешается на внешний **`<div>`**, не на **`<img>`**. Удобно для размеров и позиционирования:
178
+
179
+ ```css
180
+ .my-image-block {
181
+ width: 400px;
182
+ border-radius: 12px;
183
+ overflow: hidden;
184
+ }
185
+ ```
186
+
187
+ #### Соотношение сторон
188
+
189
+ Компонент создаёт контейнер с **`padding-top`** по формуле **`originalH / originalW`**, чтобы до загрузки зарезервировать место и снизить **CLS**. Отдельная высота не обязательна.
190
+
191
+ #### Плейсхолдер и длительность fade
192
+
193
+ Переопределяйте CSS-переменные глобально или локально:
194
+
195
+ ```css
196
+ /* глобально */
197
+ :root {
198
+ --smi-bg: #1a1a2e; /* фон до загрузки */
199
+ --smi-fade-duration: 600ms; /* длительность fade, по умолчанию 400ms */
200
+ }
201
+
202
+ /* область компонента */
203
+ .hero-image {
204
+ --smi-bg: transparent;
205
+ --smi-fade-duration: 200ms;
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## API компонента
212
+
213
+ ### Пропсы — локальный ассет
214
+
215
+ | Проп | Тип | Обязательно | Описание |
216
+ |------|-----|-------------|----------|
217
+ | `asset` | `SmartImgImportedAsset` | ✓ | Импорт из **`*/assets/*.jpg \| *.png \| *.svg`** |
218
+ | `alt` | `string` | | Текст для атрибута **`alt`** |
219
+ | `opacity` | `boolean` | — | Включить плавное появление |
220
+ | `className` | `string` | — | Доп. класс на обёртке **`<div>`** |
221
+
222
+ ### Пропсы — внешний URL
223
+
224
+ | Проп | Тип | Обязательно | Описание |
225
+ |------|-----|-------------|----------|
226
+ | `dataSrc` | `string` | ✓ | Полный URL изображения |
227
+ | `originalW` | `number` | ✓ | Ширина оригинала в пикселях |
228
+ | `originalH` | `number` | ✓ | Высота оригинала в пикселях |
229
+ | `alt` | `string` | | Текст для **`alt`** |
230
+ | `opacity` | `boolean` | — | Плавное появление |
231
+ | `className` | `string` | — | Класс на обёртке **`<div>`** |
232
+
233
+ ---
234
+
235
+ ## Кастомизация CSS
236
+
237
+ Стили один раз вставляются в **`<head>`** при первом рендере. Доступны две переменные:
238
+
239
+ | Переменная | По умолчанию | Описание |
240
+ |------------|--------------|----------|
241
+ | `--smi-bg` | `#f0f0f0` | Фон плейсхолдера до загрузки |
242
+ | `--smi-fade-duration` | `400ms` | Длительность fade (нужен проп **`opacity`**) |
243
+
244
+ ---
245
+
246
+ ## Атрибут data-max-size-load
247
+
248
+ После подтверждения оптимального размера загрузки компонент выставляет на **`<img>`** атрибут **`data-max-size-load`**. Удобно для CSS «полностью загружено»:
249
+
250
+ ```css
251
+ /* зелёная точка, когда картинка на финальном размере */
252
+ .smi-wrapper:has(img[data-max-size-load]) .my-indicator {
253
+ background: green;
254
+ }
255
+ ```
256
+
257
+ **Возможные значения:**
258
+
259
+ | Значение | Когда |
260
+ |----------|--------|
261
+ | `"dev"` | Режим разработки загружен оригинал |
262
+ | `"svg"` | SVG загружен полностью |
263
+ | `"external"` | Внешний URL загружен |
264
+ | `"800"` / `"1200"` / … | Prod — подтверждённая ширина миниатюры в px после первого resize |
265
+ | `"Infinity"` | Prod загружен оригинал (подходящей миниатюры не нашлось) |
266
+ | `"svg-error"` | Ошибка загрузки SVG |
267
+ | `"external-error"` | Ошибка внешнего URL |
268
+
269
+ **Важно:** в production **`data-max-size-load`** выставляется **только после первого события `resize`**, следующего за загрузкой. Так учитываются смена ориентации и размера viewport. Повторный монтирование компонента **само по себе** этот атрибут не восстанавливает — нужен новый **resize**.
270
+
271
+ ---
272
+
273
+ ## Как это работает — подробно
274
+
275
+ ### Схема загрузки
276
+
277
+ ```
278
+ Монтирование компонента
279
+
280
+ ├─ Попадание в кэш? → мгновенно восстановить src (без fade, без IO)
281
+
282
+ └─ Нет → регистрация в IntersectionObserver (порог 5%)
283
+
284
+ └─ Видимость ≥ 5%
285
+
286
+ ├─ SVG / внешний URL → loadDirectFinal → data-max-size-load="svg|external"
287
+
288
+ ├─ Dev (JPG/PNG) → оригинал → data-max-size-load="dev"
289
+
290
+ └─ Prod (JPG/PNG) pickSize(cssWidth × DPR, originalW)
291
+ → миниатюра
292
+
293
+ └─ При resize (debounce 200ms)
294
+ повторное наблюдение видимых
295
+ при необходимости апгрейд миниатюры
296
+ → data-max-size-load="<size>"
297
+ ```
298
+
299
+ ### Выбор миниатюры (`pickSize`)
300
+
301
+ ```
302
+ target = ceil(cssWidth × devicePixelRatio)
303
+ первый размер из [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000,
304
+ 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000],
305
+ где size >= target И size <= originalW
306
+
307
+ → null: грузить оригинальный файл
308
+ ```
309
+
310
+ ### Структура выхода сборки
311
+
312
+ Для **`photo.jpg`** (оригинальная ширина 2400px) плагин кладёт в **`dist`**:
313
+
314
+ ```
315
+ dist/assets/img/
316
+ a1b2c3d4.jpg очищенный оригинал
317
+ a1b2c3d4-400.jpg миниатюра 400px, progressive JPEG
318
+ a1b2c3d4-400.webp
319
+ a1b2c3d4-600.jpg
320
+ a1b2c3d4-600.webp
321
+
322
+ a1b2c3d4-2000.jpg
323
+ a1b2c3d4-2000.webp
324
+ (размеры шире 2400px не генерируются)
325
+ ```
326
+
327
+ Модуль импорта:
328
+
329
+ ```ts
330
+ { src: '/assets/img/a1b2c3d4.jpg', originalW: 2400, originalH: 1600 }
331
+ ```
332
+
333
+ ### Кэш сессии
334
+
335
+ **`loadedCache`** — **`Map<dataSrc, { url, size }>`** на время сессии страницы. При повторном монтировании **SmartImg**:
336
+
337
+ 1. **`useLayoutEffect`** синхронно читает кэш до первого paint.
338
+ 2. **`imgSrc`** сразу из кэша без вспышки пустоты и без fade.
339
+ 3. Второй экземпляр того же изображения, ещё не загрузившийся, при появлении в viewport может получить fade.
340
+
341
+ ---
342
+
343
+ ## ⚠️ Исходные файлы WebP — не поддерживаются
344
+
345
+ **Не импортируйте `.webp` как ассеты.** Обрабатываются только **`.jpg`**, **`.jpeg`**, **`.png`**, **`.svg`**.
346
+
347
+ Если импортировать **`.webp`** из **`assets/`**:
348
+
349
+ - **dev**: плагин **`smartImgLocalAssetImport`** не перехватит импорт Vite отдаст обычную **строку URL**, не объект **`{ src, originalW, originalH }`** компонент сломается.
350
+ - **build**: **`imageThumbnails`** не трогает **`.webp`**; файл пойдёт в стандартный пайплайн Vite (возможен inline base64 и отсутствие метаданных размеров).
351
+
352
+ **Используйте JPG или PNG как исходники.** WebP для браузеров пакет соберёт сам.
353
+
354
+ ---
355
+
356
+ ## Отладочные атрибуты
357
+
358
+ Каждый **`<img>`** от **SmartImg** в DevTools имеет:
359
+
360
+ | Атрибут | Смысл |
361
+ |---------|--------|
362
+ | `data-src` | Исходный **`dataSrc`** (файл-оригинал, не URL миниатюры) |
363
+ | `data-original-w` | Ширина оригинала |
364
+ | `data-original-h` | Высота оригинала |
365
+
366
+ Только метаданные для отладки; логика компонента на них не опирается.
package/dist/index.js CHANGED
@@ -308,6 +308,7 @@ var SmartImg = (props) => {
308
308
  );
309
309
  const paddingTop = `${(originalH / originalW * 100).toFixed(4)}%`;
310
310
  useLayoutEffect(() => {
311
+ imgRef.current?.removeAttribute("data-max-size-load");
311
312
  const hit = getLoadedUrl(dataSrc);
312
313
  if (!hit) return;
313
314
  hasRealSrc.current = true;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/component/SmartImg.tsx","../src/component/smartImgManager.ts","../src/component/styles.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react'\r\nimport type { SmartImgImportedAsset } from '../smartImgAsset'\r\nimport { register, getLoadedUrl } from './smartImgManager'\r\nimport { injectStyles, cls } from './styles'\r\n\r\n/** 1×1 прозрачный GIF — начальный src пока картинка не загружена */\r\nconst BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'\r\n\r\n/** SVG-заглушка при ошибке загрузки */\r\nconst ERROR_PLACEHOLDER =\r\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%2312121c'/%3E%3Cpath fill='%23444466' d='M21 5v6.59l-3-3.01-4 4.01-4-4-4 3.99-3-2.99V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2zm-3 6.42 3 3.01V19c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2v-6.58l3 2.99 4-4 4 4 4-4.99z'/%3E%3C/svg%3E\"\r\n\r\ntype SmartImgCommon = {\r\n alt: string\r\n /**\r\n * Передай `opacity`, чтобы картинка появлялась плавно (fade-in).\r\n * Без этого пропа — появляется мгновенно.\r\n * При перемонтировании с закешированной картинкой fade не запускается.\r\n */\r\n opacity?: boolean\r\n /** Доп. класс на обёртку. */\r\n className?: string\r\n}\r\n\r\nexport type SmartImgProps = SmartImgCommon &\r\n (\r\n | {\r\n /** Импорт из `.../assets/*` через плагины пакета (`{ src, originalW, originalH }`). */\r\n asset: SmartImgImportedAsset\r\n }\r\n | {\r\n /** Прямой URL (часто внешний) — размеры задаёшь вручную. */\r\n dataSrc: string\r\n originalW: number\r\n originalH: number\r\n }\r\n )\r\n\r\n/**\r\n * «Умная» ленивая загрузка изображений (SmartImg).\r\n *\r\n * - Локальные ассеты: проп **`asset`** из импорта с метаданными из Vite.\r\n * - Внешние URL: **`dataSrc`** + **`originalW`** / **`originalH`**.\r\n */\r\nexport const SmartImg = (props: SmartImgProps) => {\r\n injectStyles()\r\n\r\n const { alt, opacity, className } = props\r\n const { dataSrc, originalW, originalH } =\r\n 'asset' in props\r\n ? {\r\n dataSrc: props.asset.src,\r\n originalW: props.asset.originalW,\r\n originalH: props.asset.originalH,\r\n }\r\n : {\r\n dataSrc: props.dataSrc,\r\n originalW: props.originalW,\r\n originalH: props.originalH,\r\n }\r\n\r\n const wrapperRef = useRef<HTMLDivElement>(null)\r\n const imgRef = useRef<HTMLImageElement>(null)\r\n\r\n const [imgSrc, setImgSrc] = useState<string>(() => {\r\n if (typeof window === 'undefined') return BLANK\r\n return getLoadedUrl(dataSrc)?.url ?? BLANK\r\n })\r\n const [isVisible, setIsVisible] = useState<boolean>(() => {\r\n if (typeof window === 'undefined') return false\r\n return getLoadedUrl(dataSrc) !== null\r\n })\r\n const [isError, setIsError] = useState(false)\r\n\r\n const hasRealSrc = useRef(\r\n typeof window !== 'undefined' && getLoadedUrl(dataSrc) !== null,\r\n )\r\n\r\n const paddingTop = `${((originalH / originalW) * 100).toFixed(4)}%`\r\n\r\n useLayoutEffect(() => {\r\n const hit = getLoadedUrl(dataSrc)\r\n if (!hit) return\r\n hasRealSrc.current = true\r\n setImgSrc(hit.url)\r\n setIsVisible(true)\r\n setIsError(false)\r\n }, [dataSrc])\r\n\r\n useEffect(() => {\r\n const img = imgRef.current\r\n const wrapper = wrapperRef.current\r\n if (!img || !wrapper) return\r\n\r\n if (!getLoadedUrl(dataSrc)) {\r\n hasRealSrc.current = false\r\n setImgSrc(BLANK)\r\n setIsVisible(false)\r\n setIsError(false)\r\n }\r\n\r\n return register(\r\n img,\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n (url) => {\r\n hasRealSrc.current = true\r\n setIsError(false)\r\n setImgSrc(url)\r\n if (!opacity) setIsVisible(true)\r\n },\r\n () => {\r\n hasRealSrc.current = true\r\n setIsError(true)\r\n setImgSrc(ERROR_PLACEHOLDER)\r\n setIsVisible(true)\r\n },\r\n )\r\n }, [dataSrc, opacity, originalW, originalH])\r\n\r\n const imageClassName = [\r\n cls.image,\r\n opacity ? cls.fade : '',\r\n isVisible ? cls.visible : '',\r\n isError ? cls.error : '',\r\n ]\r\n .filter(Boolean)\r\n .join(' ')\r\n\r\n return (\r\n <div\r\n ref={wrapperRef}\r\n className={`${cls.wrapper}${className ? ` ${className}` : ''}`}\r\n style={{ paddingTop }}\r\n >\r\n <img\r\n ref={imgRef}\r\n src={imgSrc}\r\n alt={alt}\r\n data-src={dataSrc}\r\n data-original-w={originalW}\r\n data-original-h={originalH}\r\n className={imageClassName}\r\n onLoad={() => {\r\n if (opacity && hasRealSrc.current) {\r\n setIsVisible(true)\r\n }\r\n }}\r\n decoding=\"async\"\r\n />\r\n </div>\r\n )\r\n}\r\n","/**\r\n * Singleton-менеджер ленивой загрузки изображений (SmartImg).\r\n *\r\n * Один IntersectionObserver и один resize-listener на всю страницу.\r\n * Все компоненты SmartImg регистрируются здесь через register().\r\n */\r\n\r\n// ─── Режим сборки ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * В dev-режиме миниатюр нет — грузим оригинал напрямую.\r\n * Вся логика выбора размера, WebP и resize-апгрейдов пропускается.\r\n * Значение подставляется Vite из import.meta.env.DEV в проекте-потребителе.\r\n */\r\nconst IS_DEV: boolean =\r\n typeof import.meta !== 'undefined' &&\r\n typeof (import.meta as Record<string, unknown>).env === 'object' &&\r\n ((import.meta as { env: Record<string, unknown> }).env['DEV'] as boolean | undefined) === true\r\n\r\n// ─── Доступные размеры миниатюр (ширина в пикселях) ──────────────────────────\r\n\r\nconst SIZES = [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000] as const\r\n\r\n// ─── WebP detection (однократная синхронная проверка через canvas) ────────────\r\n\r\nlet _webpResult: boolean | null = null\r\n\r\nfunction supportsWebP(): boolean {\r\n if (_webpResult !== null) return _webpResult\r\n try {\r\n const c = document.createElement('canvas')\r\n c.width = 1\r\n c.height = 1\r\n _webpResult = c.toDataURL('image/webp').startsWith('data:image/webp')\r\n } catch {\r\n _webpResult = false\r\n }\r\n return _webpResult\r\n}\r\n\r\n// ─── URL helpers ──────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Выбирает размер миниатюры с учётом нужного размера И оригинальных размеров.\r\n *\r\n * Правило: берём первый size из SIZES такой что:\r\n * size >= target (покрывает нужное разрешение)\r\n * size <= originalW (миниатюра с таким размером физически существует)\r\n *\r\n * null означает: грузить оригинальный dataSrc напрямую.\r\n */\r\nfunction pickSize(cssWidth: number, dpr: number, originalW: number): number | null {\r\n const target = Math.ceil(cssWidth * dpr)\r\n return SIZES.find((s) => s >= target && s <= originalW) ?? null\r\n}\r\n\r\n/**\r\n * Строит URL миниатюры из оригинального URL.\r\n * photo.jpg + 1200 + webp → photo-1200.webp\r\n * photo.jpg + 1200 + false → photo-1200.jpg\r\n */\r\nfunction buildUrl(dataSrc: string, size: number, webp: boolean): string {\r\n const base = dataSrc.replace(/\\.[^./]+$/, '')\r\n const originalExt = dataSrc.match(/\\.([^./]+)$/)?.[1] ?? 'jpg'\r\n return `${base}-${size}.${webp ? 'webp' : originalExt}`\r\n}\r\n\r\n/**\r\n * SVG: миниатюры и плагин нарезки не применяются — только оригинал.\r\n * data:... и путь с `.svg` до query/hash.\r\n */\r\nfunction isSvgDataSrc(url: string): boolean {\r\n if (url == null || typeof url !== 'string') return false\r\n if (url.startsWith('data:image/svg')) return true\r\n const base = url.replace(/[?#].*$/, '')\r\n return base.toLowerCase().endsWith('.svg')\r\n}\r\n\r\n/**\r\n * Абсолютный URL с origin, отличным от страницы: нет наших прегенерённых миниатюр.\r\n * Относительные (`/`, `./`, `../`), `data:` и `blob:` не считаем внешними.\r\n */\r\nfunction isExternalDataSrc(url: string): boolean {\r\n if (!url || url.startsWith('data:') || url.startsWith('blob:')) return false\r\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) return false\r\n try {\r\n const resolved = new URL(url, window.location.href)\r\n return resolved.origin !== window.location.origin\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Картинка без цепочки миниатюр SmartImg: сразу оригинал и финальная пометка. */\r\nfunction directLoadKind(dataSrc: string): 'svg' | 'external' | null {\r\n if (isSvgDataSrc(dataSrc)) return 'svg'\r\n if (isExternalDataSrc(dataSrc)) return 'external'\r\n return null\r\n}\r\n\r\n/**\r\n * Загрузка только `dataSrc`, без buildUrl / выбора размера / WebP.\r\n * Вызывается из handleIntersect (после пересечения viewport, threshold 5%).\r\n * На успех: кэш, onLoad, `data-max-size-load` = svg | external.\r\n */\r\nfunction loadDirectFinal(img: HTMLImageElement, state: Entry, kind: 'svg' | 'external'): void {\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', kind)\r\n }\r\n tempImg.onerror = () => {\r\n state.onError()\r\n img.setAttribute('data-max-size-load', `${kind}-error`)\r\n }\r\n tempImg.src = url\r\n}\r\n\r\n/**\r\n * Нужен ли IntersectionObserver для этого dataSrc: false = уже есть подходящая\r\n * версия в loadedCache, повторный `new Image()` и ожидание не требуются.\r\n */\r\nfunction needsIntersectionObserver(\r\n dataSrc: string,\r\n originalW: number,\r\n wrapper: HTMLElement,\r\n cached: CacheEntry | undefined,\r\n): boolean {\r\n if (!cached) return true\r\n if (directLoadKind(dataSrc)) return false\r\n if (IS_DEV) return false\r\n const cssWidth = wrapper.clientWidth || wrapper.offsetWidth\r\n if (cssWidth === 0) return true\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, originalW)\r\n const loaded = cached.size\r\n if (neededSize === null) {\r\n return loaded !== Infinity\r\n }\r\n return neededSize > loaded\r\n}\r\n\r\n/** Пометить завершённую загрузку по данным кэша (без повторного IO). */\r\nfunction applyCachedCompleteMarker(img: HTMLImageElement, dataSrc: string): void {\r\n if (img.hasAttribute('data-max-size-load')) return\r\n const direct = directLoadKind(dataSrc)\r\n if (direct) {\r\n img.setAttribute('data-max-size-load', direct)\r\n return\r\n }\r\n if (IS_DEV) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n return\r\n }\r\n // prod + локальный JPG: маркер ставится только через handleResize() → handleIntersect(),\r\n // не при ре-регистрации компонента (ре-рендер не является ресайзом)\r\n}\r\n\r\n// ─── Кэш загруженных URL ──────────────────────────────────────────────────────\r\n\r\ntype CacheEntry = {\r\n /** Финальный URL который был подставлен в src (миниатюра или оригинал) */\r\n url: string\r\n /** Размер загруженной версии. Infinity = оригинал */\r\n size: number\r\n}\r\n\r\n/**\r\n * Модульный кэш: dataSrc → последний успешно загруженный url + size.\r\n * Живёт в рамках сессии страницы. При перемонтировании компонента\r\n * позволяет сразу показать картинку без fade, не дожидаясь IO.\r\n */\r\nconst loadedCache = new Map<string, CacheEntry>()\r\n\r\n/** Возвращает кэшированный результат для dataSrc, или null если ещё не грузили. */\r\nexport function getLoadedUrl(dataSrc: string): CacheEntry | null {\r\n return loadedCache.get(dataSrc) ?? null\r\n}\r\n\r\n// ─── Типы ─────────────────────────────────────────────────────────────────────\r\n\r\ntype Entry = {\r\n wrapper: HTMLElement\r\n dataSrc: string\r\n /** Оригинальная ширина картинки — ограничивает максимально доступный размер миниатюры */\r\n originalW: number\r\n /** Размер уже загруженной версии. null — картинка ещё не загружалась. */\r\n loadedSize: number | null\r\n /**\r\n * true — картинка была повторно добавлена в observer после resize,\r\n * ждём подтверждения видимости чтобы проверить нужен ли апгрейд.\r\n */\r\n pendingResizeCheck: boolean\r\n onLoad: (url: string) => void\r\n onError: () => void\r\n}\r\n\r\n// ─── Singleton state ──────────────────────────────────────────────────────────\r\n\r\nconst registry = new Map<HTMLImageElement, Entry>()\r\n\r\n/** Элементы, которые сейчас отслеживает IntersectionObserver. */\r\nconst observing = new Set<HTMLImageElement>()\r\n\r\nlet ioInstance: IntersectionObserver | null = null\r\nlet resizeTimer: ReturnType<typeof setTimeout> | null = null\r\nlet resizeAttached = false\r\n\r\n// ─── IntersectionObserver ─────────────────────────────────────────────────────\r\n\r\nfunction getIO(): IntersectionObserver {\r\n if (!ioInstance) {\r\n ioInstance = new IntersectionObserver(handleIntersect, { threshold: 0.05 })\r\n }\r\n return ioInstance\r\n}\r\n\r\n/** Добавляет в observer без дублей. */\r\nfunction safeObserve(img: HTMLImageElement): void {\r\n if (observing.has(img)) return\r\n observing.add(img)\r\n getIO().observe(img)\r\n}\r\n\r\nfunction safeUnobserve(img: HTMLImageElement): void {\r\n if (!observing.has(img)) return\r\n observing.delete(img)\r\n getIO().unobserve(img)\r\n}\r\n\r\nfunction handleIntersect(entries: IntersectionObserverEntry[]): void {\r\n for (const ioEntry of entries) {\r\n if (!ioEntry.isIntersecting) continue\r\n\r\n const img = ioEntry.target as HTMLImageElement\r\n const state = registry.get(img)\r\n if (!state) continue\r\n\r\n safeUnobserve(img)\r\n\r\n const direct = directLoadKind(state.dataSrc)\r\n if (direct) {\r\n loadDirectFinal(img, state, direct)\r\n continue\r\n }\r\n\r\n // ── Dev mode: миниатюр нет, грузим оригинал ──────────────────────────────\r\n if (IS_DEV) {\r\n const hit = loadedCache.get(state.dataSrc)\r\n if (hit) {\r\n state.loadedSize = hit.size\r\n // onLoad всегда вызываем: если компонент уже показывал картинку (imgSrc = реальный URL),\r\n // React получит то же значение → state не обновится → img.src не изменится → fade не мигнёт.\r\n // Если компонент ещё не грузил (imgSrc = BLANK, другой экземпляр) → src сменится → fade-in.\r\n state.onLoad(hit.url)\r\n if (!img.hasAttribute('data-max-size-load')) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n continue\r\n }\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n tempImg.onerror = () => state.onError()\r\n tempImg.src = url\r\n continue\r\n }\r\n\r\n const cssWidth = state.wrapper.clientWidth || state.wrapper.offsetWidth\r\n if (cssWidth === 0) {\r\n safeObserve(img)\r\n continue\r\n }\r\n\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, state.originalW)\r\n const isResizeCheck = state.pendingResizeCheck\r\n state.pendingResizeCheck = false\r\n\r\n const url = neededSize === null\r\n ? state.dataSrc\r\n : buildUrl(state.dataSrc, neededSize, supportsWebP())\r\n\r\n if (isResizeCheck && neededSize !== null && state.loadedSize !== null && neededSize <= state.loadedSize) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n continue\r\n }\r\n\r\n const tempImg = new Image()\r\n\r\n tempImg.onload = () => {\r\n state.loadedSize = neededSize ?? Infinity\r\n loadedCache.set(state.dataSrc, { url, size: state.loadedSize })\r\n state.onLoad(url)\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n }\r\n }\r\n\r\n tempImg.onerror = () => {\r\n state.onError()\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize ?? 0))\r\n }\r\n }\r\n\r\n tempImg.src = url\r\n }\r\n}\r\n\r\n// ─── Resize handler ───────────────────────────────────────────────────────────\r\n\r\nfunction handleResize(): void {\r\n if (IS_DEV) return\r\n\r\n for (const [img, state] of registry) {\r\n if (state.loadedSize === null) continue\r\n if (img.hasAttribute('data-max-size-load')) continue\r\n\r\n state.pendingResizeCheck = true\r\n safeObserve(img)\r\n }\r\n}\r\n\r\nfunction attachResizeListener(): void {\r\n if (resizeAttached) return\r\n resizeAttached = true\r\n window.addEventListener(\r\n 'resize',\r\n () => {\r\n if (resizeTimer) clearTimeout(resizeTimer)\r\n resizeTimer = setTimeout(handleResize, 200)\r\n },\r\n { passive: true },\r\n )\r\n}\r\n\r\n// ─── Public API ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Регистрирует img-элемент в менеджере ленивой загрузки.\r\n * Возвращает функцию очистки — вызвать при размонтировании компонента.\r\n */\r\nexport function register(\r\n img: HTMLImageElement,\r\n wrapper: HTMLElement,\r\n dataSrc: string,\r\n originalW: number,\r\n onLoad: (url: string) => void,\r\n onError: () => void,\r\n): () => void {\r\n if (dataSrc == null || dataSrc === '' || typeof dataSrc !== 'string') {\r\n return () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n }\r\n\r\n attachResizeListener()\r\n\r\n const cached = loadedCache.get(dataSrc)\r\n const observe = needsIntersectionObserver(dataSrc, originalW, wrapper, cached)\r\n // pendingResizeCheck = false: регистрация (в т.ч. при ре-рендере) — не ресайз.\r\n // Флаг true выставляет только handleResize().\r\n const pendingResizeCheck = false\r\n\r\n registry.set(img, {\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n loadedSize: cached?.size ?? null,\r\n pendingResizeCheck,\r\n onLoad,\r\n onError,\r\n })\r\n\r\n const cleanup = () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n\r\n if (cached && !observe) {\r\n applyCachedCompleteMarker(img, dataSrc)\r\n return cleanup\r\n }\r\n\r\n safeObserve(img)\r\n\r\n return cleanup\r\n}\r\n","/**\n * Runtime CSS injection for SmartImg.\n * Injects styles once into <head> on first component use.\n * Uses `smi-` prefix to avoid class name collisions.\n */\n\nconst CSS = `\n.smi-wrapper {\n position: relative;\n width: 100%;\n height: 0;\n overflow: hidden;\n background-color: var(--smi-bg, #f0f0f0);\n}\n.smi-image {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.smi-fade {\n opacity: 0;\n transition: opacity var(--smi-fade-duration, 400ms) ease;\n}\n.smi-fade.smi-visible {\n opacity: 1;\n}\n.smi-error {\n object-fit: contain;\n padding: 15%;\n}\n`\n\nlet injected = false\n\nexport function injectStyles(): void {\n if (typeof document === 'undefined' || injected) return\n injected = true\n const style = document.createElement('style')\n style.setAttribute('data-smi', '')\n style.textContent = CSS\n document.head.appendChild(style)\n}\n\nexport const cls = {\n wrapper: 'smi-wrapper',\n image: 'smi-image',\n fade: 'smi-fade',\n visible: 'smi-visible',\n error: 'smi-error',\n} as const\n"],"mappings":"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","names":["url","tempImg"]}
1
+ {"version":3,"sources":["../src/component/SmartImg.tsx","../src/component/smartImgManager.ts","../src/component/styles.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react'\r\nimport type { SmartImgImportedAsset } from '../smartImgAsset'\r\nimport { register, getLoadedUrl } from './smartImgManager'\r\nimport { injectStyles, cls } from './styles'\r\n\r\n/** 1×1 прозрачный GIF — начальный src пока картинка не загружена */\r\nconst BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'\r\n\r\n/** SVG-заглушка при ошибке загрузки */\r\nconst ERROR_PLACEHOLDER =\r\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%2312121c'/%3E%3Cpath fill='%23444466' d='M21 5v6.59l-3-3.01-4 4.01-4-4-4 3.99-3-2.99V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2zm-3 6.42 3 3.01V19c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2v-6.58l3 2.99 4-4 4 4 4-4.99z'/%3E%3C/svg%3E\"\r\n\r\ntype SmartImgCommon = {\r\n alt: string\r\n /**\r\n * Передай `opacity`, чтобы картинка появлялась плавно (fade-in).\r\n * Без этого пропа — появляется мгновенно.\r\n * При перемонтировании с закешированной картинкой fade не запускается.\r\n */\r\n opacity?: boolean\r\n /** Доп. класс на обёртку. */\r\n className?: string\r\n}\r\n\r\nexport type SmartImgProps = SmartImgCommon &\r\n (\r\n | {\r\n /** Импорт из `.../assets/*` через плагины пакета (`{ src, originalW, originalH }`). */\r\n asset: SmartImgImportedAsset\r\n }\r\n | {\r\n /** Прямой URL (часто внешний) — размеры задаёшь вручную. */\r\n dataSrc: string\r\n originalW: number\r\n originalH: number\r\n }\r\n )\r\n\r\n/**\r\n * «Умная» ленивая загрузка изображений (SmartImg).\r\n *\r\n * - Локальные ассеты: проп **`asset`** из импорта с метаданными из Vite.\r\n * - Внешние URL: **`dataSrc`** + **`originalW`** / **`originalH`**.\r\n */\r\nexport const SmartImg = (props: SmartImgProps) => {\r\n injectStyles()\r\n\r\n const { alt, opacity, className } = props\r\n const { dataSrc, originalW, originalH } =\r\n 'asset' in props\r\n ? {\r\n dataSrc: props.asset.src,\r\n originalW: props.asset.originalW,\r\n originalH: props.asset.originalH,\r\n }\r\n : {\r\n dataSrc: props.dataSrc,\r\n originalW: props.originalW,\r\n originalH: props.originalH,\r\n }\r\n\r\n const wrapperRef = useRef<HTMLDivElement>(null)\r\n const imgRef = useRef<HTMLImageElement>(null)\r\n\r\n const [imgSrc, setImgSrc] = useState<string>(() => {\r\n if (typeof window === 'undefined') return BLANK\r\n return getLoadedUrl(dataSrc)?.url ?? BLANK\r\n })\r\n const [isVisible, setIsVisible] = useState<boolean>(() => {\r\n if (typeof window === 'undefined') return false\r\n return getLoadedUrl(dataSrc) !== null\r\n })\r\n const [isError, setIsError] = useState(false)\r\n\r\n const hasRealSrc = useRef(\r\n typeof window !== 'undefined' && getLoadedUrl(dataSrc) !== null,\r\n )\r\n\r\n const paddingTop = `${((originalH / originalW) * 100).toFixed(4)}%`\r\n\r\n useLayoutEffect(() => {\r\n imgRef.current?.removeAttribute('data-max-size-load')\r\n const hit = getLoadedUrl(dataSrc)\r\n if (!hit) return\r\n hasRealSrc.current = true\r\n setImgSrc(hit.url)\r\n setIsVisible(true)\r\n setIsError(false)\r\n }, [dataSrc])\r\n\r\n useEffect(() => {\r\n const img = imgRef.current\r\n const wrapper = wrapperRef.current\r\n if (!img || !wrapper) return\r\n\r\n if (!getLoadedUrl(dataSrc)) {\r\n hasRealSrc.current = false\r\n setImgSrc(BLANK)\r\n setIsVisible(false)\r\n setIsError(false)\r\n }\r\n\r\n return register(\r\n img,\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n (url) => {\r\n hasRealSrc.current = true\r\n setIsError(false)\r\n setImgSrc(url)\r\n if (!opacity) setIsVisible(true)\r\n },\r\n () => {\r\n hasRealSrc.current = true\r\n setIsError(true)\r\n setImgSrc(ERROR_PLACEHOLDER)\r\n setIsVisible(true)\r\n },\r\n )\r\n }, [dataSrc, opacity, originalW, originalH])\r\n\r\n const imageClassName = [\r\n cls.image,\r\n opacity ? cls.fade : '',\r\n isVisible ? cls.visible : '',\r\n isError ? cls.error : '',\r\n ]\r\n .filter(Boolean)\r\n .join(' ')\r\n\r\n return (\r\n <div\r\n ref={wrapperRef}\r\n className={`${cls.wrapper}${className ? ` ${className}` : ''}`}\r\n style={{ paddingTop }}\r\n >\r\n <img\r\n ref={imgRef}\r\n src={imgSrc}\r\n alt={alt}\r\n data-src={dataSrc}\r\n data-original-w={originalW}\r\n data-original-h={originalH}\r\n className={imageClassName}\r\n onLoad={() => {\r\n if (opacity && hasRealSrc.current) {\r\n setIsVisible(true)\r\n }\r\n }}\r\n decoding=\"async\"\r\n />\r\n </div>\r\n )\r\n}\r\n","/**\r\n * Singleton-менеджер ленивой загрузки изображений (SmartImg).\r\n *\r\n * Один IntersectionObserver и один resize-listener на всю страницу.\r\n * Все компоненты SmartImg регистрируются здесь через register().\r\n */\r\n\r\n// ─── Режим сборки ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * В dev-режиме миниатюр нет — грузим оригинал напрямую.\r\n * Вся логика выбора размера, WebP и resize-апгрейдов пропускается.\r\n * Значение подставляется Vite из import.meta.env.DEV в проекте-потребителе.\r\n */\r\nconst IS_DEV: boolean =\r\n typeof import.meta !== 'undefined' &&\r\n typeof (import.meta as Record<string, unknown>).env === 'object' &&\r\n ((import.meta as { env: Record<string, unknown> }).env['DEV'] as boolean | undefined) === true\r\n\r\n// ─── Доступные размеры миниатюр (ширина в пикселях) ──────────────────────────\r\n\r\nconst SIZES = [400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000] as const\r\n\r\n// ─── WebP detection (однократная синхронная проверка через canvas) ────────────\r\n\r\nlet _webpResult: boolean | null = null\r\n\r\nfunction supportsWebP(): boolean {\r\n if (_webpResult !== null) return _webpResult\r\n try {\r\n const c = document.createElement('canvas')\r\n c.width = 1\r\n c.height = 1\r\n _webpResult = c.toDataURL('image/webp').startsWith('data:image/webp')\r\n } catch {\r\n _webpResult = false\r\n }\r\n return _webpResult\r\n}\r\n\r\n// ─── URL helpers ──────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Выбирает размер миниатюры с учётом нужного размера И оригинальных размеров.\r\n *\r\n * Правило: берём первый size из SIZES такой что:\r\n * size >= target (покрывает нужное разрешение)\r\n * size <= originalW (миниатюра с таким размером физически существует)\r\n *\r\n * null означает: грузить оригинальный dataSrc напрямую.\r\n */\r\nfunction pickSize(cssWidth: number, dpr: number, originalW: number): number | null {\r\n const target = Math.ceil(cssWidth * dpr)\r\n return SIZES.find((s) => s >= target && s <= originalW) ?? null\r\n}\r\n\r\n/**\r\n * Строит URL миниатюры из оригинального URL.\r\n * photo.jpg + 1200 + webp → photo-1200.webp\r\n * photo.jpg + 1200 + false → photo-1200.jpg\r\n */\r\nfunction buildUrl(dataSrc: string, size: number, webp: boolean): string {\r\n const base = dataSrc.replace(/\\.[^./]+$/, '')\r\n const originalExt = dataSrc.match(/\\.([^./]+)$/)?.[1] ?? 'jpg'\r\n return `${base}-${size}.${webp ? 'webp' : originalExt}`\r\n}\r\n\r\n/**\r\n * SVG: миниатюры и плагин нарезки не применяются — только оригинал.\r\n * data:... и путь с `.svg` до query/hash.\r\n */\r\nfunction isSvgDataSrc(url: string): boolean {\r\n if (url == null || typeof url !== 'string') return false\r\n if (url.startsWith('data:image/svg')) return true\r\n const base = url.replace(/[?#].*$/, '')\r\n return base.toLowerCase().endsWith('.svg')\r\n}\r\n\r\n/**\r\n * Абсолютный URL с origin, отличным от страницы: нет наших прегенерённых миниатюр.\r\n * Относительные (`/`, `./`, `../`), `data:` и `blob:` не считаем внешними.\r\n */\r\nfunction isExternalDataSrc(url: string): boolean {\r\n if (!url || url.startsWith('data:') || url.startsWith('blob:')) return false\r\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) return false\r\n try {\r\n const resolved = new URL(url, window.location.href)\r\n return resolved.origin !== window.location.origin\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Картинка без цепочки миниатюр SmartImg: сразу оригинал и финальная пометка. */\r\nfunction directLoadKind(dataSrc: string): 'svg' | 'external' | null {\r\n if (isSvgDataSrc(dataSrc)) return 'svg'\r\n if (isExternalDataSrc(dataSrc)) return 'external'\r\n return null\r\n}\r\n\r\n/**\r\n * Загрузка только `dataSrc`, без buildUrl / выбора размера / WebP.\r\n * Вызывается из handleIntersect (после пересечения viewport, threshold 5%).\r\n * На успех: кэш, onLoad, `data-max-size-load` = svg | external.\r\n */\r\nfunction loadDirectFinal(img: HTMLImageElement, state: Entry, kind: 'svg' | 'external'): void {\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', kind)\r\n }\r\n tempImg.onerror = () => {\r\n state.onError()\r\n img.setAttribute('data-max-size-load', `${kind}-error`)\r\n }\r\n tempImg.src = url\r\n}\r\n\r\n/**\r\n * Нужен ли IntersectionObserver для этого dataSrc: false = уже есть подходящая\r\n * версия в loadedCache, повторный `new Image()` и ожидание не требуются.\r\n */\r\nfunction needsIntersectionObserver(\r\n dataSrc: string,\r\n originalW: number,\r\n wrapper: HTMLElement,\r\n cached: CacheEntry | undefined,\r\n): boolean {\r\n if (!cached) return true\r\n if (directLoadKind(dataSrc)) return false\r\n if (IS_DEV) return false\r\n const cssWidth = wrapper.clientWidth || wrapper.offsetWidth\r\n if (cssWidth === 0) return true\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, originalW)\r\n const loaded = cached.size\r\n if (neededSize === null) {\r\n return loaded !== Infinity\r\n }\r\n return neededSize > loaded\r\n}\r\n\r\n/** Пометить завершённую загрузку по данным кэша (без повторного IO). */\r\nfunction applyCachedCompleteMarker(img: HTMLImageElement, dataSrc: string): void {\r\n if (img.hasAttribute('data-max-size-load')) return\r\n const direct = directLoadKind(dataSrc)\r\n if (direct) {\r\n img.setAttribute('data-max-size-load', direct)\r\n return\r\n }\r\n if (IS_DEV) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n return\r\n }\r\n // prod + локальный JPG: маркер ставится только через handleResize() → handleIntersect(),\r\n // не при ре-регистрации компонента (ре-рендер не является ресайзом)\r\n}\r\n\r\n// ─── Кэш загруженных URL ──────────────────────────────────────────────────────\r\n\r\ntype CacheEntry = {\r\n /** Финальный URL который был подставлен в src (миниатюра или оригинал) */\r\n url: string\r\n /** Размер загруженной версии. Infinity = оригинал */\r\n size: number\r\n}\r\n\r\n/**\r\n * Модульный кэш: dataSrc → последний успешно загруженный url + size.\r\n * Живёт в рамках сессии страницы. При перемонтировании компонента\r\n * позволяет сразу показать картинку без fade, не дожидаясь IO.\r\n */\r\nconst loadedCache = new Map<string, CacheEntry>()\r\n\r\n/** Возвращает кэшированный результат для dataSrc, или null если ещё не грузили. */\r\nexport function getLoadedUrl(dataSrc: string): CacheEntry | null {\r\n return loadedCache.get(dataSrc) ?? null\r\n}\r\n\r\n// ─── Типы ─────────────────────────────────────────────────────────────────────\r\n\r\ntype Entry = {\r\n wrapper: HTMLElement\r\n dataSrc: string\r\n /** Оригинальная ширина картинки — ограничивает максимально доступный размер миниатюры */\r\n originalW: number\r\n /** Размер уже загруженной версии. null — картинка ещё не загружалась. */\r\n loadedSize: number | null\r\n /**\r\n * true — картинка была повторно добавлена в observer после resize,\r\n * ждём подтверждения видимости чтобы проверить нужен ли апгрейд.\r\n */\r\n pendingResizeCheck: boolean\r\n onLoad: (url: string) => void\r\n onError: () => void\r\n}\r\n\r\n// ─── Singleton state ──────────────────────────────────────────────────────────\r\n\r\nconst registry = new Map<HTMLImageElement, Entry>()\r\n\r\n/** Элементы, которые сейчас отслеживает IntersectionObserver. */\r\nconst observing = new Set<HTMLImageElement>()\r\n\r\nlet ioInstance: IntersectionObserver | null = null\r\nlet resizeTimer: ReturnType<typeof setTimeout> | null = null\r\nlet resizeAttached = false\r\n\r\n// ─── IntersectionObserver ─────────────────────────────────────────────────────\r\n\r\nfunction getIO(): IntersectionObserver {\r\n if (!ioInstance) {\r\n ioInstance = new IntersectionObserver(handleIntersect, { threshold: 0.05 })\r\n }\r\n return ioInstance\r\n}\r\n\r\n/** Добавляет в observer без дублей. */\r\nfunction safeObserve(img: HTMLImageElement): void {\r\n if (observing.has(img)) return\r\n observing.add(img)\r\n getIO().observe(img)\r\n}\r\n\r\nfunction safeUnobserve(img: HTMLImageElement): void {\r\n if (!observing.has(img)) return\r\n observing.delete(img)\r\n getIO().unobserve(img)\r\n}\r\n\r\nfunction handleIntersect(entries: IntersectionObserverEntry[]): void {\r\n for (const ioEntry of entries) {\r\n if (!ioEntry.isIntersecting) continue\r\n\r\n const img = ioEntry.target as HTMLImageElement\r\n const state = registry.get(img)\r\n if (!state) continue\r\n\r\n safeUnobserve(img)\r\n\r\n const direct = directLoadKind(state.dataSrc)\r\n if (direct) {\r\n loadDirectFinal(img, state, direct)\r\n continue\r\n }\r\n\r\n // ── Dev mode: миниатюр нет, грузим оригинал ──────────────────────────────\r\n if (IS_DEV) {\r\n const hit = loadedCache.get(state.dataSrc)\r\n if (hit) {\r\n state.loadedSize = hit.size\r\n // onLoad всегда вызываем: если компонент уже показывал картинку (imgSrc = реальный URL),\r\n // React получит то же значение → state не обновится → img.src не изменится → fade не мигнёт.\r\n // Если компонент ещё не грузил (imgSrc = BLANK, другой экземпляр) → src сменится → fade-in.\r\n state.onLoad(hit.url)\r\n if (!img.hasAttribute('data-max-size-load')) {\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n continue\r\n }\r\n const url = state.dataSrc\r\n const tempImg = new Image()\r\n tempImg.onload = () => {\r\n state.loadedSize = Infinity\r\n loadedCache.set(state.dataSrc, { url, size: Infinity })\r\n state.onLoad(url)\r\n img.setAttribute('data-max-size-load', 'dev')\r\n }\r\n tempImg.onerror = () => state.onError()\r\n tempImg.src = url\r\n continue\r\n }\r\n\r\n const cssWidth = state.wrapper.clientWidth || state.wrapper.offsetWidth\r\n if (cssWidth === 0) {\r\n safeObserve(img)\r\n continue\r\n }\r\n\r\n const neededSize = pickSize(cssWidth, window.devicePixelRatio ?? 1, state.originalW)\r\n const isResizeCheck = state.pendingResizeCheck\r\n state.pendingResizeCheck = false\r\n\r\n const url = neededSize === null\r\n ? state.dataSrc\r\n : buildUrl(state.dataSrc, neededSize, supportsWebP())\r\n\r\n if (isResizeCheck && neededSize !== null && state.loadedSize !== null && neededSize <= state.loadedSize) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n continue\r\n }\r\n\r\n const tempImg = new Image()\r\n\r\n tempImg.onload = () => {\r\n state.loadedSize = neededSize ?? Infinity\r\n loadedCache.set(state.dataSrc, { url, size: state.loadedSize })\r\n state.onLoad(url)\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize))\r\n }\r\n }\r\n\r\n tempImg.onerror = () => {\r\n state.onError()\r\n if (isResizeCheck || neededSize === null) {\r\n img.setAttribute('data-max-size-load', String(state.loadedSize ?? 0))\r\n }\r\n }\r\n\r\n tempImg.src = url\r\n }\r\n}\r\n\r\n// ─── Resize handler ───────────────────────────────────────────────────────────\r\n\r\nfunction handleResize(): void {\r\n if (IS_DEV) return\r\n\r\n for (const [img, state] of registry) {\r\n if (state.loadedSize === null) continue\r\n if (img.hasAttribute('data-max-size-load')) continue\r\n\r\n state.pendingResizeCheck = true\r\n safeObserve(img)\r\n }\r\n}\r\n\r\nfunction attachResizeListener(): void {\r\n if (resizeAttached) return\r\n resizeAttached = true\r\n window.addEventListener(\r\n 'resize',\r\n () => {\r\n if (resizeTimer) clearTimeout(resizeTimer)\r\n resizeTimer = setTimeout(handleResize, 200)\r\n },\r\n { passive: true },\r\n )\r\n}\r\n\r\n// ─── Public API ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Регистрирует img-элемент в менеджере ленивой загрузки.\r\n * Возвращает функцию очистки — вызвать при размонтировании компонента.\r\n */\r\nexport function register(\r\n img: HTMLImageElement,\r\n wrapper: HTMLElement,\r\n dataSrc: string,\r\n originalW: number,\r\n onLoad: (url: string) => void,\r\n onError: () => void,\r\n): () => void {\r\n if (dataSrc == null || dataSrc === '' || typeof dataSrc !== 'string') {\r\n return () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n }\r\n\r\n attachResizeListener()\r\n\r\n const cached = loadedCache.get(dataSrc)\r\n const observe = needsIntersectionObserver(dataSrc, originalW, wrapper, cached)\r\n // pendingResizeCheck = false: регистрация (в т.ч. при ре-рендере) — не ресайз.\r\n // Флаг true выставляет только handleResize().\r\n const pendingResizeCheck = false\r\n\r\n registry.set(img, {\r\n wrapper,\r\n dataSrc,\r\n originalW,\r\n loadedSize: cached?.size ?? null,\r\n pendingResizeCheck,\r\n onLoad,\r\n onError,\r\n })\r\n\r\n const cleanup = () => {\r\n safeUnobserve(img)\r\n registry.delete(img)\r\n }\r\n\r\n if (cached && !observe) {\r\n applyCachedCompleteMarker(img, dataSrc)\r\n return cleanup\r\n }\r\n\r\n safeObserve(img)\r\n\r\n return cleanup\r\n}\r\n","/**\n * Runtime CSS injection for SmartImg.\n * Injects styles once into <head> on first component use.\n * Uses `smi-` prefix to avoid class name collisions.\n */\n\nconst CSS = `\n.smi-wrapper {\n position: relative;\n width: 100%;\n height: 0;\n overflow: hidden;\n background-color: var(--smi-bg, #f0f0f0);\n}\n.smi-image {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n.smi-fade {\n opacity: 0;\n transition: opacity var(--smi-fade-duration, 400ms) ease;\n}\n.smi-fade.smi-visible {\n opacity: 1;\n}\n.smi-error {\n object-fit: contain;\n padding: 15%;\n}\n`\n\nlet injected = false\n\nexport function injectStyles(): void {\n if (typeof document === 'undefined' || injected) return\n injected = true\n const style = document.createElement('style')\n style.setAttribute('data-smi', '')\n style.textContent = CSS\n document.head.appendChild(style)\n}\n\nexport const cls = {\n wrapper: 'smi-wrapper',\n image: 'smi-image',\n fade: 'smi-fade',\n visible: 'smi-visible',\n error: 'smi-error',\n} as const\n"],"mappings":"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","names":["url","tempImg"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-smart-img",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "Smart lazy-loading image component for React + Vite with automatic thumbnail generation",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,5 +54,6 @@
54
54
  "tsup": "^8.0.0",
55
55
  "typescript": "^6.0.2",
56
56
  "vite": "^8.0.2"
57
- }
57
+ },
58
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
58
59
  }