vite-smart-img 1.1.15 → 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,366 +1,366 @@
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
- Только метаданные для отладки; логика компонента на них не опирается.
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.15",
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
  }