@lonik/oh-image 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.d.ts CHANGED
@@ -2,7 +2,7 @@ import { FormatEnum } from "sharp";
2
2
  import { Plugin } from "vite";
3
3
 
4
4
  //#region src/plugin/types.d.ts
5
- interface PluginConfig extends Required<Pick<ImageOptions, "placeholder" | "bps" | "format">> {
5
+ interface PluginConfig extends Required<Pick<ImageOptions, "placeholder" | "breakpoints" | "format">> {
6
6
  /** Directory name where processed images will be output during build */
7
7
  distDir: string;
8
8
  }
@@ -16,7 +16,27 @@ interface ImageOptions {
16
16
  /** Whether to generate a placeholder image for lazy loading */
17
17
  placeholder?: boolean;
18
18
  /** Breakpoints array - widths in pixels for responsive srcSet generation */
19
- bps?: number[];
19
+ breakpoints?: number[];
20
+ /** Blur the image */
21
+ blur?: number | null;
22
+ /** Flip the image vertically */
23
+ flip?: boolean | null;
24
+ /** Flop the image horizontally */
25
+ flop?: boolean | null;
26
+ /** Rotate the image */
27
+ rotate?: number | null;
28
+ /** Sharpen the image */
29
+ sharpen?: number | null;
30
+ /** Apply median filter */
31
+ median?: number | null;
32
+ /** Apply gamma correction */
33
+ gamma?: number | null;
34
+ /** Negate the image */
35
+ negate?: boolean | null;
36
+ /** Normalize the image */
37
+ normalize?: boolean | null;
38
+ /** Apply threshold */
39
+ threshold?: number | null;
20
40
  }
21
41
  //#endregion
22
42
  //#region src/plugin/plugin.d.ts
package/dist/plugin.js CHANGED
@@ -16,7 +16,19 @@ function queryToOptions(processKey, uri) {
16
16
  parseBooleans: true,
17
17
  parseNumbers: true,
18
18
  arrayFormat: "comma",
19
- types: { bps: "number[]" }
19
+ types: {
20
+ breakpoints: "number[]",
21
+ blur: "number",
22
+ flip: "boolean",
23
+ flop: "boolean",
24
+ rotate: "number",
25
+ sharpen: "number",
26
+ median: "number",
27
+ gamma: "number",
28
+ negate: "boolean",
29
+ normalize: "boolean",
30
+ threshold: "number"
31
+ }
20
32
  });
21
33
  if (processKey in parsed) return {
22
34
  shouldProcess: true,
@@ -94,7 +106,17 @@ function createImageEntries() {
94
106
  width: entry.width,
95
107
  height: entry.height,
96
108
  format: entry.format,
97
- origin: entry.origin
109
+ origin: entry.origin,
110
+ blur: entry.blur,
111
+ flip: entry.flip,
112
+ flop: entry.flop,
113
+ rotate: entry.rotate,
114
+ sharpen: entry.sharpen,
115
+ median: entry.median,
116
+ gamma: entry.gamma,
117
+ negate: entry.negate,
118
+ normalize: entry.normalize,
119
+ threshold: entry.threshold
98
120
  };
99
121
  this.set(identifier, mainEntry);
100
122
  },
@@ -113,7 +135,16 @@ function createImageEntries() {
113
135
  height: placeholderHeight,
114
136
  format: main.format,
115
137
  blur: PLACEHOLDER_BLUR_QUALITY,
116
- origin: main.origin
138
+ origin: main.origin,
139
+ flip: main.flip,
140
+ flop: main.flop,
141
+ rotate: main.rotate,
142
+ sharpen: main.sharpen,
143
+ median: main.median,
144
+ gamma: main.gamma,
145
+ negate: main.negate,
146
+ normalize: main.normalize,
147
+ threshold: main.threshold
117
148
  };
118
149
  this.set(identifier, placeholderEntry);
119
150
  },
@@ -133,6 +164,15 @@ async function processImage(path, options) {
133
164
  });
134
165
  if (options.format) processed = processed.toFormat(options.format);
135
166
  if (options.blur) processed = processed.blur(options.blur);
167
+ if (options.flip) processed = processed.flip();
168
+ if (options.flop) processed = processed.flop();
169
+ if (options.rotate) processed = processed.rotate(options.rotate);
170
+ if (options.sharpen) processed = processed.sharpen(options.sharpen);
171
+ if (options.median) processed = processed.median(options.median);
172
+ if (options.gamma) processed = processed.gamma(options.gamma);
173
+ if (options.negate) processed = processed.negate();
174
+ if (options.normalize) processed = processed.normalize();
175
+ if (options.threshold) processed = processed.threshold(options.threshold);
136
176
  return await processed.toBuffer();
137
177
  }
138
178
 
@@ -141,7 +181,7 @@ async function processImage(path, options) {
141
181
  const DEFAULT_IMAGE_FORMAT = "webp";
142
182
  const DEFAULT_CONFIGS = {
143
183
  distDir: "oh-images",
144
- bps: [
184
+ breakpoints: [
145
185
  16,
146
186
  48,
147
187
  96,
@@ -229,7 +269,17 @@ function ohImage(options) {
229
269
  width: mergedOptions.width,
230
270
  height: mergedOptions.height,
231
271
  format: mergedOptions.format,
232
- origin
272
+ origin,
273
+ blur: mergedOptions.blur,
274
+ flip: mergedOptions.flip,
275
+ flop: mergedOptions.flop,
276
+ rotate: mergedOptions.rotate,
277
+ sharpen: mergedOptions.sharpen,
278
+ median: mergedOptions.median,
279
+ gamma: mergedOptions.gamma,
280
+ negate: mergedOptions.negate,
281
+ normalize: mergedOptions.normalize,
282
+ threshold: mergedOptions.threshold
233
283
  });
234
284
  const src = {
235
285
  width: metadata.width,
@@ -237,24 +287,43 @@ function ohImage(options) {
237
287
  src: mainIdentifier,
238
288
  srcSets: ""
239
289
  };
240
- if (parsed.options?.placeholder) {
290
+ if (mergedOptions.placeholder) {
241
291
  const placeholderIdentifier = identifier.placeholder(DEFAULT_IMAGE_FORMAT);
242
292
  imageEntries.createPlaceholderEntry(placeholderIdentifier, {
243
293
  width: metadata.width,
244
294
  height: metadata.height,
245
295
  format: DEFAULT_IMAGE_FORMAT,
246
- origin
296
+ origin,
297
+ flip: mergedOptions.flip,
298
+ flop: mergedOptions.flop,
299
+ rotate: mergedOptions.rotate,
300
+ sharpen: mergedOptions.sharpen,
301
+ median: mergedOptions.median,
302
+ gamma: mergedOptions.gamma,
303
+ negate: mergedOptions.negate,
304
+ normalize: mergedOptions.normalize,
305
+ threshold: mergedOptions.threshold
247
306
  });
248
307
  src.placeholderUrl = placeholderIdentifier;
249
308
  }
250
- if (mergedOptions.bps) {
309
+ if (mergedOptions.breakpoints) {
251
310
  const srcSets = [];
252
- for (const breakpoint of mergedOptions.bps) {
311
+ for (const breakpoint of mergedOptions.breakpoints) {
253
312
  const srcSetIdentifier = identifier.srcSet(DEFAULT_IMAGE_FORMAT, breakpoint);
254
313
  imageEntries.createSrcSetEntry(srcSetIdentifier, {
255
314
  width: breakpoint,
256
315
  format: DEFAULT_IMAGE_FORMAT,
257
- origin
316
+ origin,
317
+ blur: mergedOptions.blur,
318
+ flip: mergedOptions.flip,
319
+ flop: mergedOptions.flop,
320
+ rotate: mergedOptions.rotate,
321
+ sharpen: mergedOptions.sharpen,
322
+ median: mergedOptions.median,
323
+ gamma: mergedOptions.gamma,
324
+ negate: mergedOptions.negate,
325
+ normalize: mergedOptions.normalize,
326
+ threshold: mergedOptions.threshold
258
327
  });
259
328
  srcSets.push(`${srcSetIdentifier} ${breakpoint}w`);
260
329
  }
package/dist/react.d.ts CHANGED
@@ -2,8 +2,15 @@ import { ImgHTMLAttributes } from "react";
2
2
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/react/types.d.ts
5
+ interface ImageLoaderOptions {
6
+ src: string;
7
+ width?: number | null | undefined;
8
+ height?: number | null | undefined;
9
+ isPlaceholder?: boolean;
10
+ }
11
+ type ImageLoader = (options: ImageLoaderOptions) => string;
5
12
  type ImageSrcType = string | ImageSrc;
6
- interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "fetchPriority" | "decoding" | "loading" | "height" | "width" | "srcSet" | "className" | "sizes" | "style">> {
13
+ interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "fetchPriority" | "decoding" | "loading" | "srcSet" | "className" | "sizes" | "style">> {
7
14
  /** Alternative text for the image, required for accessibility. Use an empty string for decorative images. */
8
15
  alt: string;
9
16
  /** Configures the Image component to load the image immediately. */
@@ -17,6 +24,10 @@ interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "
17
24
  * styles such that the image fills its containing element.
18
25
  */
19
26
  fill?: boolean;
27
+ loader?: ImageLoader | null;
28
+ width?: number | undefined;
29
+ height?: number | undefined;
30
+ breakpoints?: number[];
20
31
  }
21
32
  //#endregion
22
33
  //#region src/react/image.d.ts
@@ -51,4 +62,58 @@ declare function Image(props: ImageProps): react_jsx_runtime0.JSX.Element;
51
62
  */
52
63
  declare function useImgLoaded(src: string | undefined): [(img: HTMLImageElement | null) => void, boolean];
53
64
  //#endregion
54
- export { Image, type ImageProps, useImgLoaded };
65
+ //#region src/react/loaders/cloudflare-context.d.ts
66
+ interface CloudflareLoaderOptions {
67
+ path: string;
68
+ placeholder: boolean;
69
+ format: string;
70
+ params?: Record<string, string>;
71
+ placeholderParams?: Record<string, string>;
72
+ breakpoints?: number[];
73
+ }
74
+ declare function useCloudflareContext(): CloudflareLoaderOptions;
75
+ declare function CloudflareLoaderProvider({
76
+ children,
77
+ ...props
78
+ }: {
79
+ children: React.ReactNode;
80
+ } & Partial<CloudflareLoaderOptions>): react_jsx_runtime0.JSX.Element;
81
+ //#endregion
82
+ //#region src/react/loaders/cloudflare-loader.d.ts
83
+ declare function useCloudflareLoader(options?: Partial<CloudflareLoaderOptions>): ImageLoader;
84
+ //#endregion
85
+ //#region src/react/loaders/imgproxy-context.d.ts
86
+ interface ImgproxyLoaderOptions {
87
+ path: string;
88
+ placeholder: boolean;
89
+ format: string;
90
+ params?: Record<string, string>;
91
+ placeholderParams?: Record<string, string>;
92
+ breakpoints?: number[];
93
+ paramsSeparator?: string;
94
+ }
95
+ declare function useImgproxyContext(): ImgproxyLoaderOptions;
96
+ declare function ImgproxyLoaderProvider({
97
+ children,
98
+ ...props
99
+ }: {
100
+ children: React.ReactNode;
101
+ } & Partial<ImgproxyLoaderOptions>): react_jsx_runtime0.JSX.Element;
102
+ //#endregion
103
+ //#region src/react/loaders/imgproxy-loader.d.ts
104
+ declare function useImgproxyLoader(options?: Partial<ImgproxyLoaderOptions>): ImageLoader;
105
+ //#endregion
106
+ //#region src/react/image-context.d.ts
107
+ interface ImageContextValue extends Pick<ImageProps, "loading"> {
108
+ breakpoints: number[];
109
+ loader: ImageLoader | null;
110
+ }
111
+ declare function useImageContext(): ImageContextValue;
112
+ declare function ImageProvider({
113
+ children,
114
+ ...props
115
+ }: {
116
+ children: React.ReactNode;
117
+ } & Partial<ImageContextValue>): react_jsx_runtime0.JSX.Element;
118
+ //#endregion
119
+ export { type CloudflareLoaderOptions, CloudflareLoaderProvider, Image, type ImageLoader, type ImageLoaderOptions, type ImageProps, ImageProvider, type ImageSrcType, type ImgproxyLoaderOptions, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader };
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as ReactDOM from "react-dom";
2
- import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/react/use-img-loaded.ts
@@ -85,51 +85,157 @@ function useImgLoaded(src) {
85
85
  * Should match something like: "100w, 200w".
86
86
  */
87
87
  const VALID_WIDTH_DESCRIPTOR_SRCSET = /^((\s*\d+w\s*(,|$)){1,})$/;
88
- function resolveOptions(prop) {
89
- const { src, ...rest } = prop;
90
- const resolved = { ...rest };
91
- if (typeof src === "object") {
92
- resolved.src = src.src;
93
- resolved.width ??= src.width;
94
- resolved.height ??= src.height;
95
- resolved.srcSet ??= src.srcSets;
96
- resolved.placeholderUrl ??= src.placeholderUrl;
97
- } else resolved.src = src;
98
- if (prop.asap) {
99
- resolved.decoding = "async";
100
- resolved.loading = "eager";
101
- resolved.fetchPriority = "high";
102
- }
103
- resolved.sizes = resolveSizes(prop);
104
- resolved.loading = resolveLoading(prop);
88
+ function resolveOptions(prop, defaultOptions) {
89
+ const resolved = {
90
+ ...defaultOptions,
91
+ ...prop
92
+ };
93
+ resolved.decoding = resolveDecoding(resolved);
94
+ resolved.fetchPriority = resolveFetchPriority(resolved);
95
+ resolved.loading = resolveLoading(resolved);
96
+ resolved.srcSet = resolveSrcSet(resolved);
97
+ resolved.sizes = resolveSizes(resolved, resolved.srcSet, resolved.loading);
98
+ resolved.placeholderUrl = resolvePlaceholderURL(resolved);
99
+ resolved.height = resolveHeight(resolved);
100
+ resolved.width = resolveWidth(resolved);
101
+ resolved.src = resolveSrc(resolved);
105
102
  return resolved;
106
103
  }
104
+ function resolveDecoding(prop) {
105
+ return prop.asap ? "async" : prop.decoding;
106
+ }
107
+ function resolveFetchPriority(prop) {
108
+ if (prop.asap) return "high";
109
+ return prop.fetchPriority ?? "auto";
110
+ }
111
+ function resolveSrcSet(prop) {
112
+ if (prop.srcSet) return prop.srcSet;
113
+ if (typeof prop.src === "object") return prop.src.srcSets;
114
+ if (!prop.breakpoints) return;
115
+ const baseSrc = prop.src;
116
+ const entries = [];
117
+ for (const breakpoint of prop.breakpoints) if (prop.loader) entries.push(`${prop.loader({
118
+ src: baseSrc,
119
+ width: breakpoint,
120
+ height: prop.height,
121
+ isPlaceholder: false
122
+ })} ${breakpoint}w`);
123
+ if (entries.length === 0) return;
124
+ return entries.join(", ");
125
+ }
107
126
  function resolveLoading(prop) {
108
127
  if (!prop.asap && prop.loading !== void 0) return prop.loading;
109
128
  return prop.asap ? "eager" : "lazy";
110
129
  }
111
- function resolveSizes(prop) {
112
- const loading = resolveLoading(prop);
130
+ function resolveSizes(prop, resolvedSrcSet, resolvedLoading) {
131
+ const loading = resolvedLoading ?? resolveLoading(prop);
132
+ const srcSet = resolvedSrcSet ?? prop.srcSet;
113
133
  let sizes = prop.sizes;
114
134
  if (prop.fill) sizes ||= "100vw";
115
135
  if (sizes) {
116
136
  if (loading === "lazy") sizes = "auto, " + sizes;
117
- } else if (prop.srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(prop.srcSet) && loading === "lazy") sizes = "auto, 100vw";
137
+ } else if (srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(srcSet) && loading === "lazy") sizes = "auto, 100vw";
118
138
  return sizes;
119
139
  }
140
+ function resolveSrc(prop) {
141
+ if (typeof prop.src === "object") return prop.src.src;
142
+ if (prop.loader) return prop.loader({
143
+ src: prop.src,
144
+ width: prop.width,
145
+ height: prop.height
146
+ });
147
+ return prop.src;
148
+ }
149
+ function resolveWidth(prop) {
150
+ if (prop.width) return prop.width;
151
+ if (typeof prop.src === "object") return prop.src.width;
152
+ }
153
+ function resolveHeight(prop) {
154
+ if (prop.height) return prop.height;
155
+ if (typeof prop.src === "object") return prop.src.height;
156
+ }
157
+ function resolvePlaceholderURL(prop) {
158
+ if (prop.placeholderUrl) return prop.placeholderUrl;
159
+ if (typeof prop.src === "object") return prop.src.placeholderUrl;
160
+ if (prop.loader) return prop.loader({
161
+ isPlaceholder: true,
162
+ src: prop.src,
163
+ width: prop.width,
164
+ height: prop.height
165
+ });
166
+ }
120
167
 
121
168
  //#endregion
122
169
  //#region src/react/prop-asserts.ts
123
170
  function assertProps(prop) {
124
- assertLoadingProp(prop);
171
+ try {
172
+ assertLoadingProp(prop);
173
+ assertDecodingProp(prop);
174
+ assertFetchPriorityProp(prop);
175
+ assertBreakpointsProp(prop);
176
+ assertFillProp(prop);
177
+ assertDimensionsProp(prop);
178
+ } catch (err) {
179
+ const message = err instanceof Error ? err.message : err;
180
+ console.warn(message);
181
+ }
125
182
  }
126
183
  function assert(assertion, message) {
127
184
  if (import.meta.env.DEV) {
128
- if (assertion()) throw new Error(message);
185
+ if (assertion()) throw new Error(message || void 0);
129
186
  }
130
187
  }
131
188
  function assertLoadingProp(prop) {
132
- assert(() => prop.loading && prop.asap, `Do not use \`loading\` on a asap image — asap images are always eagerly loaded.`);
189
+ assert(() => prop.loading && prop.asap, import.meta.env.DEV && `Do not use \`loading\` on a asap image — asap images are always eagerly loaded.`);
190
+ }
191
+ function assertDecodingProp(prop) {
192
+ assert(() => prop.decoding && prop.asap, import.meta.env.DEV && `Do not use \`decoding\` on a asap image — asap images always use async decoding.`);
193
+ }
194
+ function assertFetchPriorityProp(prop) {
195
+ assert(() => prop.fetchPriority && prop.asap, import.meta.env.DEV && `Do not use \`fetchPriority\` on a asap image — asap images always use high fetch priority.`);
196
+ }
197
+ function assertBreakpointsProp(prop) {
198
+ assert(() => prop.breakpoints && typeof prop.src === "object", import.meta.env.DEV && `Do not use \`breakpoints\` when \`src\` is an imported image — the image's built-in srcSets are used instead.`);
199
+ assert(() => prop.breakpoints && typeof prop.src === "string" && !prop.loader, import.meta.env.DEV && `Do not use \`breakpoints\` without a \`loader\` — breakpoints require a loader to generate srcSet entries.`);
200
+ }
201
+ function assertFillProp(prop) {
202
+ assert(() => prop.fill && (prop.width !== void 0 || prop.height !== void 0), import.meta.env.DEV && `Do not use \`width\` or \`height\` with \`fill\` — fill mode makes the image fill its container.`);
203
+ }
204
+ function assertDimensionsProp(prop) {
205
+ assert(() => typeof prop.src === "string" && !prop.fill && prop.width === void 0 && prop.height === void 0, import.meta.env.DEV && `Image is missing \`width\` and \`height\` props. Either provide dimensions, use \`fill\`, or use an imported image source.`);
206
+ }
207
+
208
+ //#endregion
209
+ //#region src/react/image-context.tsx
210
+ const ImageContext = createContext({
211
+ breakpoints: [
212
+ 16,
213
+ 48,
214
+ 96,
215
+ 128,
216
+ 384,
217
+ 640,
218
+ 750,
219
+ 828,
220
+ 1080,
221
+ 1200,
222
+ 1920
223
+ ],
224
+ loading: "lazy",
225
+ loader: null
226
+ });
227
+ function useImageContext() {
228
+ return useContext(ImageContext);
229
+ }
230
+ function ImageProvider({ children, ...props }) {
231
+ const ctx = useImageContext();
232
+ return /* @__PURE__ */ jsx(ImageContext.Provider, {
233
+ value: {
234
+ ...ctx,
235
+ ...props
236
+ },
237
+ children
238
+ });
133
239
  }
134
240
 
135
241
  //#endregion
@@ -154,7 +260,7 @@ function getFillStyles(props) {
154
260
  }
155
261
  function Image(props) {
156
262
  assertProps(props);
157
- const options = resolveOptions(props);
263
+ const options = resolveOptions(props, useImageContext());
158
264
  const [imgRef, isLoaded] = useImgLoaded(options.src);
159
265
  const placeholderStyles = isLoaded ? {} : getPlaceholderStyles(options);
160
266
  const fillStyles = getFillStyles(options);
@@ -184,4 +290,126 @@ function Image(props) {
184
290
  }
185
291
 
186
292
  //#endregion
187
- export { Image, useImgLoaded };
293
+ //#region src/react/loaders/image-loader-utils.ts
294
+ function normalizeLoaderParams(params, separator) {
295
+ return Object.entries(params).map(([key, value]) => `${key}${separator}${value}`);
296
+ }
297
+ function isAbsoluteUrl(src) {
298
+ return /^https?:\/\//.test(src);
299
+ }
300
+ function assertPath(path) {
301
+ assert(() => !path?.trim(), import.meta.env.DEV && `Path is required`);
302
+ assert(() => {
303
+ try {
304
+ new URL(path);
305
+ return !isAbsoluteUrl(path);
306
+ } catch {
307
+ return true;
308
+ }
309
+ }, import.meta.env.DEV && `Path is invalid url: ${path}`);
310
+ }
311
+
312
+ //#endregion
313
+ //#region src/react/loaders/cloudflare-context.tsx
314
+ const CloudflareContext = createContext({
315
+ path: "",
316
+ placeholder: true,
317
+ format: "auto",
318
+ placeholderParams: { quality: "low" }
319
+ });
320
+ function useCloudflareContext() {
321
+ return useContext(CloudflareContext);
322
+ }
323
+ function CloudflareLoaderProvider({ children, ...props }) {
324
+ const ctx = useCloudflareContext();
325
+ return /* @__PURE__ */ jsx(CloudflareContext.Provider, {
326
+ value: {
327
+ ...ctx,
328
+ ...props
329
+ },
330
+ children
331
+ });
332
+ }
333
+
334
+ //#endregion
335
+ //#region src/react/loaders/cloudflare-loader.ts
336
+ function useCloudflareLoader(options) {
337
+ const resolvedOptions = {
338
+ ...useCloudflareContext(),
339
+ ...options
340
+ };
341
+ assertPath(resolvedOptions.path);
342
+ return (imageOptions) => {
343
+ const parts = [];
344
+ const format = resolvedOptions.format;
345
+ if (format) parts.push(`format=${format}`);
346
+ if (imageOptions.width) parts.push(`width=${imageOptions.width}`);
347
+ if (imageOptions.height) parts.push(`height=${imageOptions.height}`);
348
+ if (resolvedOptions.params) parts.push(...normalizeLoaderParams(resolvedOptions.params, "="));
349
+ if (imageOptions.isPlaceholder) {
350
+ if (resolvedOptions.placeholderParams) {
351
+ const placeholderParams = normalizeLoaderParams(resolvedOptions.placeholderParams, "=");
352
+ parts.push(...placeholderParams);
353
+ }
354
+ } else if (resolvedOptions.params) {
355
+ const params = normalizeLoaderParams(resolvedOptions.params, "=");
356
+ parts.push(...params);
357
+ }
358
+ const processingOptions = parts.join(",");
359
+ return `${resolvedOptions.path}/cdn-cgi/image/${processingOptions}/${imageOptions.src}`;
360
+ };
361
+ }
362
+
363
+ //#endregion
364
+ //#region src/react/loaders/imgproxy-context.tsx
365
+ const ImgproxyContext = createContext({
366
+ path: "",
367
+ placeholder: true,
368
+ format: "webp",
369
+ placeholderParams: { quality: "1" }
370
+ });
371
+ function useImgproxyContext() {
372
+ return useContext(ImgproxyContext);
373
+ }
374
+ function ImgproxyLoaderProvider({ children, ...props }) {
375
+ const ctx = useImgproxyContext();
376
+ return /* @__PURE__ */ jsx(ImgproxyContext.Provider, {
377
+ value: {
378
+ ...ctx,
379
+ ...props
380
+ },
381
+ children
382
+ });
383
+ }
384
+
385
+ //#endregion
386
+ //#region src/react/loaders/imgproxy-loader.ts
387
+ function useImgproxyLoader(options) {
388
+ const resolvedOptions = {
389
+ ...useImgproxyContext(),
390
+ ...options
391
+ };
392
+ assertPath(resolvedOptions.path);
393
+ return (imageOptions) => {
394
+ const parts = [];
395
+ const format = resolvedOptions.format;
396
+ const paramsSeparator = resolvedOptions.paramsSeparator ?? "/";
397
+ if (format) parts.push(`format:${format}`);
398
+ if (imageOptions.width) parts.push(`width:${imageOptions.width}`);
399
+ if (imageOptions.height) parts.push(`height:${imageOptions.height}`);
400
+ if (imageOptions.isPlaceholder) {
401
+ if (resolvedOptions.placeholderParams) {
402
+ const placeholderParams = normalizeLoaderParams(resolvedOptions.placeholderParams, ":");
403
+ parts.push(...placeholderParams);
404
+ }
405
+ } else if (resolvedOptions.params) {
406
+ const params = normalizeLoaderParams(resolvedOptions.params, ":");
407
+ parts.push(...params);
408
+ }
409
+ const processingOptions = parts.join(paramsSeparator);
410
+ return `${resolvedOptions.path}/${processingOptions}/plain/${imageOptions.src}`;
411
+ };
412
+ }
413
+
414
+ //#endregion
415
+ export { CloudflareLoaderProvider, Image, ImageProvider, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lonik/oh-image",
3
3
  "type": "module",
4
- "version": "1.3.0",
4
+ "version": "2.0.0",
5
5
  "description": "A React component library for optimized image handling.",
6
6
  "author": "Luka Onikadze <lukonik@gmail.com>",
7
7
  "license": "MIT",
@@ -41,6 +41,7 @@
41
41
  "play:build": "vite build",
42
42
  "play:preview": "vite preview",
43
43
  "test": "vitest",
44
+ "coverage": "vitest --coverage",
44
45
  "typecheck": "tsc --noEmit",
45
46
  "release": "bumpp && pnpm publish",
46
47
  "prepublishOnly": "pnpm run build"
@@ -60,10 +61,13 @@
60
61
  "@types/react": "^18",
61
62
  "@types/react-dom": "^18",
62
63
  "@vitejs/plugin-react": "^5.1.2",
64
+ "@vitest/coverage-v8": "4.0.18",
63
65
  "bumpp": "^10.3.2",
64
66
  "eslint": "^9.39.2",
65
67
  "eslint-config-prettier": "^10.1.8",
66
68
  "eslint-plugin-react": "^7.37.5",
69
+ "eslint-plugin-react-hooks": "^7.0.1",
70
+ "eslint-plugin-react-refresh": "^0.5.0",
67
71
  "globals": "^17.3.0",
68
72
  "happy-dom": "^20.6.0",
69
73
  "tsdown": "^0.18.1",