@lonik/oh-image 1.2.8 → 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
@@ -79,30 +79,168 @@ function useImgLoaded(src) {
79
79
  }
80
80
 
81
81
  //#endregion
82
- //#region src/react/image.tsx
83
- const preload = "preload" in ReactDOM && typeof ReactDOM.preload === "function" ? ReactDOM.preload : null;
84
- function resolveOptions(props) {
85
- const { src, ...rest } = props;
86
- const resolved = { ...rest };
87
- if (typeof src === "object") {
88
- resolved.src = src.src;
89
- resolved.width ??= src.width;
90
- resolved.height ??= src.height;
91
- resolved.srcSet ??= src.srcSets;
92
- resolved.placeholderUrl ??= src.placeholderUrl;
93
- } else resolved.src = src;
94
- if (props.asap) {
95
- resolved.decoding = "async";
96
- resolved.loading = "eager";
97
- resolved.fetchPriority = "high";
98
- if (preload) preload(resolved.src, {
99
- as: "image",
100
- fetchPriority: "high"
101
- });
102
- }
103
- if (props.fill) resolved.sizes ||= "100vw";
82
+ //#region src/react/prop-resolvers.ts
83
+ /**
84
+ * RegExpr to determine whether a src in a srcset is using width descriptors.
85
+ * Should match something like: "100w, 200w".
86
+ */
87
+ const VALID_WIDTH_DESCRIPTOR_SRCSET = /^((\s*\d+w\s*(,|$)){1,})$/;
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);
104
102
  return resolved;
105
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
+ }
126
+ function resolveLoading(prop) {
127
+ if (!prop.asap && prop.loading !== void 0) return prop.loading;
128
+ return prop.asap ? "eager" : "lazy";
129
+ }
130
+ function resolveSizes(prop, resolvedSrcSet, resolvedLoading) {
131
+ const loading = resolvedLoading ?? resolveLoading(prop);
132
+ const srcSet = resolvedSrcSet ?? prop.srcSet;
133
+ let sizes = prop.sizes;
134
+ if (prop.fill) sizes ||= "100vw";
135
+ if (sizes) {
136
+ if (loading === "lazy") sizes = "auto, " + sizes;
137
+ } else if (srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(srcSet) && loading === "lazy") sizes = "auto, 100vw";
138
+ return sizes;
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
+ }
167
+
168
+ //#endregion
169
+ //#region src/react/prop-asserts.ts
170
+ function assertProps(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
+ }
182
+ }
183
+ function assert(assertion, message) {
184
+ if (import.meta.env.DEV) {
185
+ if (assertion()) throw new Error(message || void 0);
186
+ }
187
+ }
188
+ function assertLoadingProp(prop) {
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
+ });
239
+ }
240
+
241
+ //#endregion
242
+ //#region src/react/image.tsx
243
+ const preload = "preload" in ReactDOM && typeof ReactDOM.preload === "function" ? ReactDOM.preload : null;
106
244
  function getPlaceholderStyles(props) {
107
245
  if (!props.placeholderUrl) return {};
108
246
  return {
@@ -121,7 +259,8 @@ function getFillStyles(props) {
121
259
  };
122
260
  }
123
261
  function Image(props) {
124
- const options = resolveOptions(props);
262
+ assertProps(props);
263
+ const options = resolveOptions(props, useImageContext());
125
264
  const [imgRef, isLoaded] = useImgLoaded(options.src);
126
265
  const placeholderStyles = isLoaded ? {} : getPlaceholderStyles(options);
127
266
  const fillStyles = getFillStyles(options);
@@ -130,6 +269,10 @@ function Image(props) {
130
269
  ...fillStyles,
131
270
  ...props.style
132
271
  };
272
+ if (preload && options.asap) preload(options.src, {
273
+ as: "image",
274
+ fetchPriority: "high"
275
+ });
133
276
  return /* @__PURE__ */ jsx("img", {
134
277
  ref: imgRef,
135
278
  className: props.className,
@@ -147,4 +290,126 @@ function Image(props) {
147
290
  }
148
291
 
149
292
  //#endregion
150
- 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.2.8",
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",