xxf_react 0.7.6 → 0.8.1

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.
@@ -2,14 +2,19 @@
2
2
  * 全局缓存拦截器
3
3
  * 用于 xxf_react/http 的 ApiBuilder
4
4
  */
5
- import { CacheInterceptor } from "./CacheInterceptor";
5
+ import { CacheInterceptor } from './CacheInterceptor';
6
6
  /**
7
7
  * 默认缓存拦截器
8
+ *
8
9
  * 只有满足以下条件才缓存:
9
10
  * - HTTP 状态码 = 200
10
- * - Content-Type 包含 application/json
11
- * - data 是有效对象
12
- * - ApiResponse.code = 0 或 200
11
+ * - Content-Type 包含 application/json(不区分大小写)
12
+ * - data 是有效对象或数组
13
+ * - ApiResponse.code 存在且为成功值(0 或 200
14
+ *
15
+ * 特殊处理:
16
+ * - code 字段支持 number 或 string 类型
17
+ * - 所有检查都有异常保护,异常时默认不缓存
13
18
  *
14
19
  * @example
15
20
  * ```ts
@@ -22,17 +27,26 @@ import { CacheInterceptor } from "./CacheInterceptor";
22
27
  export declare const defaultCacheInterceptor: CacheInterceptor;
23
28
  /**
24
29
  * 创建自定义缓存拦截器
25
- * 在默认检查基础上添加自定义逻辑
30
+ *
31
+ * 在默认检查基础上添加自定义逻辑。
32
+ * 默认检查通过后才会执行自定义检查。
26
33
  *
27
34
  * @param customCheck 自定义检查函数,在默认检查通过后执行
35
+ * @returns 组合后的缓存拦截器
28
36
  *
29
37
  * @example
30
38
  * ```ts
39
+ * // 额外检查:只缓存有 data 字段的响应
31
40
  * const myInterceptor = createCacheInterceptor((ctx) => {
32
- * // 额外检查:只缓存有 data 字段的响应
33
41
  * const res = ctx.data as { data?: unknown }
34
42
  * return res.data !== undefined
35
43
  * })
44
+ *
45
+ * // 额外检查:只缓存列表数据
46
+ * const listInterceptor = createCacheInterceptor((ctx) => {
47
+ * const res = ctx.data as { data?: unknown[] }
48
+ * return Array.isArray(res.data) && res.data.length > 0
49
+ * })
36
50
  * ```
37
51
  */
38
52
  export declare function createCacheInterceptor(customCheck?: (ctx: Parameters<CacheInterceptor>[0]) => boolean): CacheInterceptor;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultCacheInterceptor.d.ts","sourceRoot":"","sources":["../../../src/http/interceptor/DefaultCacheInterceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAkBrC,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAClC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,GAChE,gBAAgB,CASlB"}
1
+ {"version":3,"file":"DefaultCacheInterceptor.d.ts","sourceRoot":"","sources":["../../../src/http/interceptor/DefaultCacheInterceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAuCrC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CAClC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,GAChE,gBAAgB,CAiBlB"}
@@ -2,13 +2,19 @@
2
2
  * 全局缓存拦截器
3
3
  * 用于 xxf_react/http 的 ApiBuilder
4
4
  */
5
+ import { isApiSuccess } from '../../models';
5
6
  /**
6
7
  * 默认缓存拦截器
8
+ *
7
9
  * 只有满足以下条件才缓存:
8
10
  * - HTTP 状态码 = 200
9
- * - Content-Type 包含 application/json
10
- * - data 是有效对象
11
- * - ApiResponse.code = 0 或 200
11
+ * - Content-Type 包含 application/json(不区分大小写)
12
+ * - data 是有效对象或数组
13
+ * - ApiResponse.code 存在且为成功值(0 或 200
14
+ *
15
+ * 特殊处理:
16
+ * - code 字段支持 number 或 string 类型
17
+ * - 所有检查都有异常保护,异常时默认不缓存
12
18
  *
13
19
  * @example
14
20
  * ```ts
@@ -19,45 +25,82 @@
19
25
  * ```
20
26
  */
21
27
  export const defaultCacheInterceptor = (ctx) => {
22
- // 检查 HTTP 状态码
23
- if (ctx.status !== 200) {
24
- return false;
25
- }
26
- // 检查 Content-Type 是否为 JSON
27
- const contentType = ctx.headers.get('Content-Type') || '';
28
- if (!contentType.includes('application/json')) {
29
- return false;
28
+ var _a, _b;
29
+ try {
30
+ // 1. 检查 HTTP 状态码
31
+ if (ctx.status !== 200) {
32
+ return false;
33
+ }
34
+ // 2. 检查 Content-Type 是否为 JSON(不区分大小写)
35
+ const contentType = (_b = (_a = ctx.headers) === null || _a === void 0 ? void 0 : _a.get('Content-Type')) !== null && _b !== void 0 ? _b : '';
36
+ if (!contentType.toLowerCase().includes('application/json')) {
37
+ return false;
38
+ }
39
+ // 3. 检查 data 是否为有效对象或数组
40
+ if (ctx.data === null || ctx.data === undefined) {
41
+ return false;
42
+ }
43
+ if (typeof ctx.data !== 'object') {
44
+ return false;
45
+ }
46
+ // 4. 检查业务码(支持 number 或 string 类型)
47
+ const res = ctx.data;
48
+ if (res.code === undefined || res.code === null) {
49
+ // 没有 code 字段,不符合 ApiResponse 规范,不缓存
50
+ return false;
51
+ }
52
+ // 将 code 转换为 number 进行判断
53
+ const codeNum = typeof res.code === 'string' ? parseInt(res.code, 10) : res.code;
54
+ if (isNaN(codeNum)) {
55
+ return false;
56
+ }
57
+ return isApiSuccess(codeNum);
30
58
  }
31
- // 检查 data 是否为有效对象
32
- if (!ctx.data || typeof ctx.data !== 'object') {
59
+ catch {
60
+ // 任何异常都不缓存,保守策略
33
61
  return false;
34
62
  }
35
- // 检查业务码:code 必须是 0 或 200
36
- const res = ctx.data;
37
- return !(res.code !== 0 && res.code !== 200);
38
63
  };
39
64
  /**
40
65
  * 创建自定义缓存拦截器
41
- * 在默认检查基础上添加自定义逻辑
66
+ *
67
+ * 在默认检查基础上添加自定义逻辑。
68
+ * 默认检查通过后才会执行自定义检查。
42
69
  *
43
70
  * @param customCheck 自定义检查函数,在默认检查通过后执行
71
+ * @returns 组合后的缓存拦截器
44
72
  *
45
73
  * @example
46
74
  * ```ts
75
+ * // 额外检查:只缓存有 data 字段的响应
47
76
  * const myInterceptor = createCacheInterceptor((ctx) => {
48
- * // 额外检查:只缓存有 data 字段的响应
49
77
  * const res = ctx.data as { data?: unknown }
50
78
  * return res.data !== undefined
51
79
  * })
80
+ *
81
+ * // 额外检查:只缓存列表数据
82
+ * const listInterceptor = createCacheInterceptor((ctx) => {
83
+ * const res = ctx.data as { data?: unknown[] }
84
+ * return Array.isArray(res.data) && res.data.length > 0
85
+ * })
52
86
  * ```
53
87
  */
54
88
  export function createCacheInterceptor(customCheck) {
55
89
  return (ctx) => {
56
- // 先执行默认检查
57
- if (!defaultCacheInterceptor(ctx)) {
90
+ try {
91
+ // 先执行默认检查
92
+ if (!defaultCacheInterceptor(ctx)) {
93
+ return false;
94
+ }
95
+ // 执行自定义检查(如果有)
96
+ if (customCheck) {
97
+ return customCheck(ctx);
98
+ }
99
+ return true;
100
+ }
101
+ catch {
102
+ // 自定义检查异常时不缓存
58
103
  return false;
59
104
  }
60
- // 执行自定义检查
61
- return !(customCheck && !customCheck(ctx));
62
105
  };
63
106
  }
@@ -1,19 +1,35 @@
1
- import { ImageProps } from "next/image";
2
- import React, { ReactNode } from "react";
3
- export interface XImageProps extends ImageProps {
4
- /** 图片加载失败时显示的备用图片 URL 或自定义组件
5
- * 可选:urlPlaceholder函数来生成失败的占位图
6
- */
7
- fallback?: string | ReactNode;
8
- }
1
+ import React from "react";
2
+ import type { XImageProps } from "./XImage.types";
9
3
  /**
10
- * 增强的 Image 组件,支持加载失败时显示备用图片或自定义组件
11
- * @param src - 图片地址
12
- * @param fallback - 加载失败时显示的备用图片 URL 或 ReactNode,可选:urlPlaceholder函数来生成失败的占位图
13
- * @param onError - 加载失败回调
14
- * @param className - 样式类名
15
- * @param props - 其他 Next.js Image 支持的属性
4
+ * 生产级图片组件
5
+ *
6
+ * 基于 react-lazy-load-image-component 实现,支持:
7
+ * - 懒加载 + IntersectionObserver
8
+ * - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
9
+ * - 错误处理 + 自动重试
10
+ * - fallback 备用图/组件
11
+ * - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * // 基础用法
16
+ * <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
17
+ *
18
+ * // 带模糊效果(提供 placeholder URL 自动启用 blur)
19
+ * <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
20
+ *
21
+ * // 自定义占位组件
22
+ * <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
23
+ *
24
+ * // 带错误处理
25
+ * <XImage
26
+ * src="/photo.jpg"
27
+ * fallback="/fallback.jpg"
28
+ * retryCount={2}
29
+ * retryDelay={1000}
30
+ * />
31
+ * ```
16
32
  */
17
- export declare function XImage({ src, fallback, onError, ...props }: XImageProps): React.JSX.Element;
33
+ export declare const XImage: React.NamedExoticComponent<XImageProps & React.RefAttributes<HTMLDivElement | HTMLImageElement>>;
18
34
  export default XImage;
19
35
  //# sourceMappingURL=XImage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"XImage.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.tsx"],"names":[],"mappings":"AAEA,OAAc,EAAC,UAAU,EAAC,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,EAAsB,SAAS,EAAC,MAAM,OAAO,CAAC;AAE5D,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC3C;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,EACI,GAAG,EACH,QAAQ,EACR,OAAO,EACP,GAAG,KAAK,EACX,EAAE,WAAW,qBAkDpC;AAED,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"XImage.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KASN,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAIhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,MAAM,kGAoUlB,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * XImage 效果样式管理
3
+ * 自动注入 CSS 到 document.head,用户无需手动导入
4
+ */
5
+ /**
6
+ * 注入效果 CSS 到 document.head
7
+ * - 仅在浏览器环境执行
8
+ * - 只执行一次(多个 XImage 实例共享)
9
+ * - SSR 安全
10
+ */
11
+ export declare function injectEffectStyles(): void;
12
+ /**
13
+ * 移除已注入的效果 CSS(用于测试或清理)
14
+ */
15
+ export declare function removeEffectStyles(): void;
16
+ //# sourceMappingURL=XImage.effects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XImage.effects.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.effects.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8CH;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAczC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAQzC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * XImage 效果样式管理
3
+ * 自动注入 CSS 到 document.head,用户无需手动导入
4
+ */
5
+ // ============ 效果 CSS 定义 ============
6
+ const EFFECT_STYLES = `
7
+ .lazy-load-image-background.blur {
8
+ filter: blur(15px);
9
+ }
10
+ .lazy-load-image-background.blur.lazy-load-image-loaded {
11
+ filter: blur(0);
12
+ transition: filter .3s;
13
+ }
14
+ .lazy-load-image-background.blur > img {
15
+ opacity: 0;
16
+ }
17
+ .lazy-load-image-background.blur.lazy-load-image-loaded > img {
18
+ opacity: 1;
19
+ transition: opacity .3s;
20
+ }
21
+ .lazy-load-image-background.opacity {
22
+ opacity: 0;
23
+ }
24
+ .lazy-load-image-background.opacity.lazy-load-image-loaded {
25
+ opacity: 1;
26
+ transition: opacity .3s;
27
+ }
28
+ .lazy-load-image-background.black-and-white {
29
+ filter: grayscale(1);
30
+ }
31
+ .lazy-load-image-background.black-and-white.lazy-load-image-loaded {
32
+ filter: grayscale(0);
33
+ transition: filter .3s;
34
+ }
35
+ .lazy-load-image-background.black-and-white > img {
36
+ opacity: 0;
37
+ }
38
+ .lazy-load-image-background.black-and-white.lazy-load-image-loaded > img {
39
+ opacity: 1;
40
+ transition: opacity .3s;
41
+ }
42
+ `;
43
+ const STYLE_ID = "ximage-effect-styles";
44
+ // CSS 注入标记,确保只注入一次
45
+ let stylesInjected = false;
46
+ /**
47
+ * 注入效果 CSS 到 document.head
48
+ * - 仅在浏览器环境执行
49
+ * - 只执行一次(多个 XImage 实例共享)
50
+ * - SSR 安全
51
+ */
52
+ export function injectEffectStyles() {
53
+ if (stylesInjected || typeof document === "undefined")
54
+ return;
55
+ // 双重检查:防止多实例并发注入
56
+ if (document.getElementById(STYLE_ID)) {
57
+ stylesInjected = true;
58
+ return;
59
+ }
60
+ const styleElement = document.createElement("style");
61
+ styleElement.id = STYLE_ID;
62
+ styleElement.textContent = EFFECT_STYLES;
63
+ document.head.appendChild(styleElement);
64
+ stylesInjected = true;
65
+ }
66
+ /**
67
+ * 移除已注入的效果 CSS(用于测试或清理)
68
+ */
69
+ export function removeEffectStyles() {
70
+ if (typeof document === "undefined")
71
+ return;
72
+ const styleElement = document.getElementById(STYLE_ID);
73
+ if (styleElement) {
74
+ styleElement.remove();
75
+ stylesInjected = false;
76
+ }
77
+ }
@@ -1,45 +1,261 @@
1
1
  "use client";
2
- import Image from "next/image";
3
- import React, { useState, useEffect } from "react";
2
+ import React, { useState, useEffect, useCallback, useRef, forwardRef, memo, useMemo, useContext, } from "react";
3
+ import { LazyLoadImage } from "react-lazy-load-image-component";
4
+ import { ScrollPositionContext } from "./XImageGallery";
5
+ import { injectEffectStyles } from "./XImage.effects";
4
6
  /**
5
- * 增强的 Image 组件,支持加载失败时显示备用图片或自定义组件
6
- * @param src - 图片地址
7
- * @param fallback - 加载失败时显示的备用图片 URL 或 ReactNode,可选:urlPlaceholder函数来生成失败的占位图
8
- * @param onError - 加载失败回调
9
- * @param className - 样式类名
10
- * @param props - 其他 Next.js Image 支持的属性
7
+ * 生产级图片组件
8
+ *
9
+ * 基于 react-lazy-load-image-component 实现,支持:
10
+ * - 懒加载 + IntersectionObserver
11
+ * - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
12
+ * - 错误处理 + 自动重试
13
+ * - fallback 备用图/组件
14
+ * - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * // 基础用法
19
+ * <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
20
+ *
21
+ * // 带模糊效果(提供 placeholder URL 自动启用 blur)
22
+ * <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
23
+ *
24
+ * // 自定义占位组件
25
+ * <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
26
+ *
27
+ * // 带错误处理
28
+ * <XImage
29
+ * src="/photo.jpg"
30
+ * fallback="/fallback.jpg"
31
+ * retryCount={2}
32
+ * retryDelay={1000}
33
+ * />
34
+ * ```
11
35
  */
12
- export function XImage({ src, fallback, onError, ...props }) {
13
- const [imgSrc, setImgSrc] = useState(src);
36
+ export const XImage = memo(forwardRef(function XImage({
37
+ // 基础属性
38
+ src, alt = "", width, height, className, style, objectFit, objectPosition, fill,
39
+ // 懒加载配置
40
+ lazy = true, threshold = 100, effect, placeholder, useIntersectionObserver = true, delayMethod, delayTime,
41
+ // 错误处理
42
+ fallback, retryCount = 0, retryDelay = 1000,
43
+ // 性能优化
44
+ scrollPosition, visibleByDefault = false,
45
+ // 事件回调
46
+ onLoad, onError, onVisible, onStatusChange,
47
+ // 包裹元素属性
48
+ wrapperProps, wrapperClassName,
49
+ // 原生属性
50
+ loading, decoding = "async", srcSet, sizes, crossOrigin, referrerPolicy, }, ref) {
51
+ // ============ 解析 placeholder 类型 ============
52
+ const isPlaceholderSrc = typeof placeholder === "string";
53
+ const placeholderSrc = isPlaceholderSrc ? placeholder : undefined;
54
+ const placeholderElement = !isPlaceholderSrc ? placeholder : undefined;
55
+ // ============ 智能默认 effect ============
56
+ // 只有当 placeholder 是字符串(URL)时才自动启用 blur
57
+ // ReactElement 类型不自动启用(因为没有图片可以模糊)
58
+ const effectiveEffect = effect !== null && effect !== void 0 ? effect : (isPlaceholderSrc ? "blur" : undefined);
59
+ // ============ 自动注入效果 CSS ============
60
+ // 使用 effect 属性时自动注入,确保 SSR 兼容
61
+ useEffect(() => {
62
+ if (effectiveEffect) {
63
+ injectEffectStyles();
64
+ }
65
+ }, [effectiveEffect]);
66
+ // ============ Context: 自动获取 Gallery 的 scrollPosition ============
67
+ const contextScrollPosition = useContext(ScrollPositionContext);
68
+ const effectiveScrollPosition = scrollPosition !== null && scrollPosition !== void 0 ? scrollPosition : contextScrollPosition;
69
+ // ============ 状态管理 ============
70
+ const [currentSrc, setCurrentSrc] = useState(src);
71
+ const [status, setStatus] = useState("idle");
14
72
  const [hasError, setHasError] = useState(false);
15
- // src 变化时重置状态
73
+ const [retryAttempt, setRetryAttempt] = useState(0);
74
+ // Refs
75
+ const wrapperRef = useRef(null);
76
+ const retryTimerRef = useRef(null);
77
+ const hasCalledOnVisible = useRef(false);
78
+ // 使用 ref 存储回调,避免 effect 依赖问题
79
+ const onStatusChangeRef = useRef(onStatusChange);
80
+ onStatusChangeRef.current = onStatusChange;
81
+ // ============ src 变化重置状态 ============
16
82
  useEffect(() => {
17
- setImgSrc(src);
83
+ var _a;
84
+ setCurrentSrc(src);
18
85
  setHasError(false);
86
+ setRetryAttempt(0);
87
+ setStatus("idle");
88
+ (_a = onStatusChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStatusChangeRef, "idle");
89
+ hasCalledOnVisible.current = false;
90
+ // 清理重试定时器
91
+ if (retryTimerRef.current) {
92
+ clearTimeout(retryTimerRef.current);
93
+ retryTimerRef.current = null;
94
+ }
19
95
  }, [src]);
20
- const handleError = (e) => {
21
- if (fallback && !hasError) {
22
- setHasError(true);
23
- // 如果 fallback 是字符串(URL),则切换图片源
24
- if (typeof fallback === "string") {
25
- setImgSrc(fallback);
26
- return; // fallback 切换时不触发 onError,等 fallback 也失败再触发
96
+ // 清理定时器
97
+ useEffect(() => {
98
+ return () => {
99
+ if (retryTimerRef.current) {
100
+ clearTimeout(retryTimerRef.current);
101
+ }
102
+ };
103
+ }, []);
104
+ // ============ ref 转发:从 wrapper 中获取 img 元素 ============
105
+ useEffect(() => {
106
+ if (!ref || !wrapperRef.current)
107
+ return;
108
+ const imgElement = wrapperRef.current.querySelector("img");
109
+ if (imgElement) {
110
+ if (typeof ref === "function") {
111
+ ref(imgElement);
112
+ }
113
+ else {
114
+ ref.current = imgElement;
27
115
  }
28
116
  }
29
- onError === null || onError === void 0 ? void 0 : onError(e);
30
- };
31
- // fallback ReactNode 且已出错,用容器包裹并应用样式属性
117
+ // 依赖 status 确保图片加载后能获取到正确的 img 元素
118
+ }, [ref, status]);
119
+ // ============ 状态更新辅助 ============
120
+ const updateStatus = useCallback((newStatus) => {
121
+ var _a;
122
+ setStatus(newStatus);
123
+ (_a = onStatusChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStatusChangeRef, newStatus);
124
+ }, []);
125
+ // ============ 事件处理 ============
126
+ const handleLoad = useCallback((event) => {
127
+ updateStatus("loaded");
128
+ onLoad === null || onLoad === void 0 ? void 0 : onLoad(event);
129
+ }, [updateStatus, onLoad]);
130
+ const handleError = useCallback((event) => {
131
+ setRetryAttempt((currentRetry) => {
132
+ // 还有重试次数
133
+ if (currentRetry < retryCount) {
134
+ updateStatus("loading");
135
+ retryTimerRef.current = setTimeout(() => {
136
+ // 通过修改 src 触发重新加载(添加时间戳绕过缓存)
137
+ const separator = src.includes("?") ? "&" : "?";
138
+ setCurrentSrc(`${src}${separator}_retry=${Date.now()}`);
139
+ }, retryDelay);
140
+ return currentRetry + 1;
141
+ }
142
+ // 重试耗尽,检查 fallback
143
+ setHasError((currentHasError) => {
144
+ if (fallback && !currentHasError) {
145
+ if (typeof fallback === "string") {
146
+ setCurrentSrc(fallback);
147
+ updateStatus("loading");
148
+ }
149
+ return true;
150
+ }
151
+ // 最终失败
152
+ updateStatus("error");
153
+ onError === null || onError === void 0 ? void 0 : onError(event);
154
+ return currentHasError;
155
+ });
156
+ return currentRetry;
157
+ });
158
+ }, [retryCount, retryDelay, src, fallback, updateStatus, onError]);
159
+ // beforeLoad: 进入视口,开始加载前调用
160
+ const handleBeforeLoad = useCallback(() => {
161
+ if (!hasCalledOnVisible.current) {
162
+ hasCalledOnVisible.current = true;
163
+ updateStatus("loading");
164
+ onVisible === null || onVisible === void 0 ? void 0 : onVisible();
165
+ }
166
+ }, [updateStatus, onVisible]);
167
+ // ============ 计算样式 ============
168
+ const computedStyle = useMemo(() => {
169
+ const baseStyle = { ...style };
170
+ if (objectFit) {
171
+ baseStyle.objectFit = objectFit;
172
+ }
173
+ if (objectPosition) {
174
+ baseStyle.objectPosition = objectPosition;
175
+ }
176
+ // fill 模式:图片绝对定位填充
177
+ if (fill) {
178
+ baseStyle.position = "absolute";
179
+ baseStyle.width = "100%";
180
+ baseStyle.height = "100%";
181
+ baseStyle.inset = 0;
182
+ baseStyle.objectFit = objectFit || "cover";
183
+ }
184
+ return baseStyle;
185
+ }, [style, objectFit, objectPosition, fill]);
186
+ const wrapperStyle = useMemo(() => {
187
+ const base = { ...wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style };
188
+ // fill 模式下包裹元素需要绝对定位并填满父容器
189
+ if (fill) {
190
+ base.display = "block";
191
+ base.position = "absolute";
192
+ base.inset = 0;
193
+ base.width = "100%";
194
+ base.height = "100%";
195
+ }
196
+ return base;
197
+ }, [fill, wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style]);
198
+ // ============ 解析尺寸(简单计算,不需要 useMemo) ============
199
+ const parsedWidth = fill
200
+ ? undefined
201
+ : typeof width === "number"
202
+ ? width
203
+ : typeof width === "string"
204
+ ? parseInt(width, 10) || undefined
205
+ : undefined;
206
+ const parsedHeight = fill
207
+ ? undefined
208
+ : typeof height === "number"
209
+ ? height
210
+ : typeof height === "string"
211
+ ? parseInt(height, 10) || undefined
212
+ : undefined;
213
+ // ============ 合并 wrapperProps(处理 ref 合并) ============
214
+ const mergedWrapperProps = useMemo(() => {
215
+ // wrapperProps 可能包含 ref(通过类型扩展)
216
+ const userRef = wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.ref;
217
+ // 合并 ref
218
+ const combinedRef = (node) => {
219
+ // 设置内部 ref
220
+ wrapperRef.current = node;
221
+ // 调用用户的 ref
222
+ if (userRef) {
223
+ if (typeof userRef === "function") {
224
+ userRef(node);
225
+ }
226
+ else {
227
+ userRef.current = node;
228
+ }
229
+ }
230
+ };
231
+ return {
232
+ ...wrapperProps,
233
+ ref: combinedRef,
234
+ style: wrapperStyle,
235
+ className: wrapperClassName,
236
+ };
237
+ }, [wrapperProps, wrapperStyle, wrapperClassName]);
238
+ // ============ 渲染 fallback ReactNode ============
32
239
  if (hasError && fallback && typeof fallback !== "string") {
33
- const width = typeof props.width === "number" ? props.width : props.width ? Number(props.width) : undefined;
34
- const height = typeof props.height === "number" ? props.height : props.height ? Number(props.height) : undefined;
35
- return (React.createElement("div", { className: props.className, style: {
36
- width,
37
- height,
38
- position: props.fill ? "absolute" : undefined,
39
- inset: props.fill ? 0 : undefined,
40
- ...props.style,
240
+ return (React.createElement("div", { ref: ref, className: className, style: {
241
+ width: parsedWidth,
242
+ height: parsedHeight,
243
+ position: fill ? "absolute" : undefined,
244
+ inset: fill ? 0 : undefined,
245
+ ...style,
41
246
  } }, fallback));
42
247
  }
43
- return (React.createElement(Image, { ...props, src: imgSrc, onError: handleError }));
44
- }
248
+ // ============ 统一使用 LazyLoadImage ============
249
+ return (React.createElement(LazyLoadImage, { src: currentSrc, alt: alt, width: parsedWidth, height: parsedHeight, className: className, style: computedStyle,
250
+ // 懒加载配置 - lazy=false 时禁用懒加载特性
251
+ threshold: lazy ? threshold : 0, effect: lazy ? effectiveEffect : undefined, placeholderSrc: lazy ? placeholderSrc : undefined, placeholder: lazy ? placeholderElement : undefined, useIntersectionObserver: lazy ? useIntersectionObserver : false, visibleByDefault: !lazy || visibleByDefault, delayMethod: delayMethod, delayTime: delayTime,
252
+ // 性能优化
253
+ scrollPosition: lazy ? effectiveScrollPosition : undefined,
254
+ // 事件
255
+ beforeLoad: lazy ? handleBeforeLoad : undefined, onLoad: handleLoad, onError: handleError,
256
+ // 包裹元素
257
+ wrapperProps: mergedWrapperProps,
258
+ // 原生属性
259
+ loading: lazy ? loading : "eager", decoding: decoding, srcSet: srcSet, sizes: sizes, crossOrigin: crossOrigin, referrerPolicy: referrerPolicy }));
260
+ }));
45
261
  export default XImage;
@@ -0,0 +1,130 @@
1
+ import type { CSSProperties, ReactElement, ReactNode, SyntheticEvent } from "react";
2
+ /**
3
+ * 懒加载效果类型
4
+ * - blur: 模糊渐变效果
5
+ * - opacity: 透明度渐变效果
6
+ * - black-and-white: 黑白到彩色渐变效果
7
+ */
8
+ export type XImageEffect = "blur" | "opacity" | "black-and-white";
9
+ /**
10
+ * 节流/防抖方法类型
11
+ */
12
+ export type DelayMethod = "throttle" | "debounce";
13
+ /**
14
+ * 图片加载状态
15
+ */
16
+ export type XImageStatus = "idle" | "loading" | "loaded" | "error";
17
+ /**
18
+ * 滚动位置类型 (用于 trackWindowScroll HOC)
19
+ */
20
+ export interface ScrollPosition {
21
+ x: number;
22
+ y: number;
23
+ }
24
+ /**
25
+ * XImage 组件 Props
26
+ */
27
+ export interface XImageProps {
28
+ /** 图片源地址 */
29
+ src: string;
30
+ /** 图片描述文本 */
31
+ alt?: string;
32
+ /** 图片宽度 */
33
+ width?: number | string;
34
+ /** 图片高度 */
35
+ height?: number | string;
36
+ /** CSS 类名 */
37
+ className?: string;
38
+ /** 内联样式 */
39
+ style?: CSSProperties;
40
+ /** 图片填充方式 */
41
+ objectFit?: CSSProperties["objectFit"];
42
+ /** 图片对齐方式 */
43
+ objectPosition?: CSSProperties["objectPosition"];
44
+ /** 是否使用 fill 模式(占满父容器) */
45
+ fill?: boolean;
46
+ /** 是否启用懒加载,默认 true */
47
+ lazy?: boolean;
48
+ /** 进入视口前多少像素开始加载,默认 100 */
49
+ threshold?: number;
50
+ /** 加载效果类型(如果提供了 placeholder URL 但未指定 effect,默认为 'blur') */
51
+ effect?: XImageEffect;
52
+ /**
53
+ * 占位内容
54
+ * - string: 占位图片 URL(自动启用 blur 效果)
55
+ * - ReactElement: 自定义占位组件(如 Skeleton,需手动指定 effect)
56
+ */
57
+ placeholder?: string | ReactElement | null;
58
+ /** 是否使用 IntersectionObserver,默认 true */
59
+ useIntersectionObserver?: boolean;
60
+ /** 滚动/resize 事件的节流方法 */
61
+ delayMethod?: DelayMethod;
62
+ /** 节流延迟时间(毫秒) */
63
+ delayTime?: number;
64
+ /**
65
+ * 加载失败时显示的备用内容
66
+ * - string: 备用图片 URL
67
+ * - ReactNode: 自定义错误展示组件
68
+ */
69
+ fallback?: string | ReactNode;
70
+ /** 加载失败后的重试次数,默认 0 */
71
+ retryCount?: number;
72
+ /** 重试间隔(毫秒),默认 1000 */
73
+ retryDelay?: number;
74
+ /**
75
+ * 滚动位置(来自 trackWindowScroll HOC)
76
+ * 当父组件使用 withScrollTracking 包裹时自动传入
77
+ */
78
+ scrollPosition?: ScrollPosition;
79
+ /**
80
+ * 设为 true 则图片立即显示(不触发懒加载)
81
+ * 适用于已在浏览器缓存中的图片
82
+ */
83
+ visibleByDefault?: boolean;
84
+ /** 图片加载完成回调 */
85
+ onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
86
+ /** 图片加载失败回调 */
87
+ onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
88
+ /** 图片进入视口时回调 */
89
+ onVisible?: () => void;
90
+ /** 加载状态变化回调 */
91
+ onStatusChange?: (status: XImageStatus) => void;
92
+ /** 传递给包裹 span 元素的属性 */
93
+ wrapperProps?: React.HTMLAttributes<HTMLSpanElement>;
94
+ /** 包裹元素的类名 */
95
+ wrapperClassName?: string;
96
+ /** 图片加载优先级 */
97
+ loading?: "lazy" | "eager";
98
+ /** 解码方式 */
99
+ decoding?: "async" | "auto" | "sync";
100
+ /** 响应式图片源集 */
101
+ srcSet?: string;
102
+ /** 响应式尺寸 */
103
+ sizes?: string;
104
+ /** 跨域设置 */
105
+ crossOrigin?: "anonymous" | "use-credentials" | "";
106
+ /** referrer 策略 */
107
+ referrerPolicy?: React.HTMLAttributeReferrerPolicy;
108
+ }
109
+ /**
110
+ * XImageGallery 组件 Props
111
+ * 用于包裹多个 XImage 实现统一的滚动监听优化
112
+ */
113
+ export interface XImageGalleryProps {
114
+ children: ReactNode;
115
+ /** 自定义容器类名 */
116
+ className?: string;
117
+ /** 自定义容器样式 */
118
+ style?: CSSProperties;
119
+ /** 节流方法: throttle 或 debounce */
120
+ delayMethod?: DelayMethod;
121
+ /** 节流/防抖延迟时间(毫秒) */
122
+ delayTime?: number;
123
+ }
124
+ /**
125
+ * withScrollTracking HOC 注入的 Props
126
+ */
127
+ export interface WithScrollTrackingProps {
128
+ scrollPosition: ScrollPosition;
129
+ }
130
+ //# sourceMappingURL=XImage.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XImage.types.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ export {};
@@ -0,0 +1,76 @@
1
+ import React, { ComponentType } from "react";
2
+ import type { XImageGalleryProps, ScrollPosition, WithScrollTrackingProps } from "./XImage.types";
3
+ /**
4
+ * ScrollPosition Context
5
+ * 用于在组件树中共享滚动位置,支持深层嵌套的 XImage 自动获取
6
+ */
7
+ export declare const ScrollPositionContext: React.Context<ScrollPosition | undefined>;
8
+ /**
9
+ * Hook: 获取当前的滚动位置
10
+ * 在 XImageGallery 内部的任意深度使用
11
+ */
12
+ export declare function useScrollPosition(): ScrollPosition | undefined;
13
+ /**
14
+ * XImageGallery - 图片画廊/列表性能优化组件
15
+ *
16
+ * 当页面中有大量图片需要懒加载时,使用此组件包裹可以:
17
+ * - 统一管理滚动监听,避免每个图片单独监听 scroll/resize 事件
18
+ * - 通过 trackWindowScroll HOC 实现性能优化
19
+ * - 通过 Context 自动向任意深度的子 XImage 组件传递 scrollPosition
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * // 基础用法 - 包裹多个 XImage(支持任意嵌套深度)
24
+ * <XImageGallery>
25
+ * <div className="grid">
26
+ * {images.map(img => (
27
+ * <div key={img.id} className="card">
28
+ * <XImage src={img.url} effect="blur" />
29
+ * </div>
30
+ * ))}
31
+ * </div>
32
+ * </XImageGallery>
33
+ *
34
+ * // 自定义节流配置
35
+ * <XImageGallery delayMethod="debounce" delayTime={200}>
36
+ * {images.map(img => (
37
+ * <XImage key={img.id} src={img.url} />
38
+ * ))}
39
+ * </XImageGallery>
40
+ * ```
41
+ *
42
+ * @note 注意事项:
43
+ * - 不要在 XImageGallery 内部嵌套另一个 XImageGallery
44
+ * - 内部的 XImage 会自动通过 Context 获取 scrollPosition,无需手动传递
45
+ */
46
+ export declare const XImageGallery: React.NamedExoticComponent<XImageGalleryProps & React.RefAttributes<HTMLDivElement>>;
47
+ /**
48
+ * HOC: 为任意组件添加滚动位置追踪能力
49
+ *
50
+ * 适用于需要自定义容器但又想使用 trackWindowScroll 优化的场景
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * // 自定义图片网格组件
55
+ * const ImageGrid = ({ images, scrollPosition }) => (
56
+ * <div className="grid grid-cols-3">
57
+ * {images.map(img => (
58
+ * <XImage
59
+ * key={img.id}
60
+ * src={img.url}
61
+ * scrollPosition={scrollPosition}
62
+ * />
63
+ * ))}
64
+ * </div>
65
+ * );
66
+ *
67
+ * // 使用 HOC 包裹
68
+ * const OptimizedImageGrid = withScrollTracking(ImageGrid);
69
+ *
70
+ * // 使用
71
+ * <OptimizedImageGrid images={images} />
72
+ * ```
73
+ */
74
+ export declare function withScrollTracking<P extends object>(WrappedComponent: ComponentType<P & WithScrollTrackingProps>): ComponentType<Omit<P, "scrollPosition">>;
75
+ export default XImageGallery;
76
+ //# sourceMappingURL=XImageGallery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"XImageGallery.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImageGallery.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAQ,aAAa,EAAyC,MAAM,OAAO,CAAC;AAE1F,OAAO,KAAK,EACR,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EAC1B,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,2CAAuD,CAAC;AAE1F;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,aAAa,sFAiBzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAC/C,gBAAgB,EAAE,aAAa,CAAC,CAAC,GAAG,uBAAuB,CAAC,GAC7D,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAW1C;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import React, { memo, forwardRef, createContext, useContext } from "react";
3
+ import { trackWindowScroll } from "react-lazy-load-image-component";
4
+ /**
5
+ * ScrollPosition Context
6
+ * 用于在组件树中共享滚动位置,支持深层嵌套的 XImage 自动获取
7
+ */
8
+ export const ScrollPositionContext = createContext(undefined);
9
+ /**
10
+ * Hook: 获取当前的滚动位置
11
+ * 在 XImageGallery 内部的任意深度使用
12
+ */
13
+ export function useScrollPosition() {
14
+ return useContext(ScrollPositionContext);
15
+ }
16
+ const InternalGallery = memo(forwardRef(function InternalGallery({ children, className, style, scrollPosition }, ref) {
17
+ return (React.createElement(ScrollPositionContext.Provider, { value: scrollPosition },
18
+ React.createElement("div", { ref: ref, className: className, style: style }, children)));
19
+ }));
20
+ /**
21
+ * 使用 trackWindowScroll HOC 包裹的 Gallery
22
+ */
23
+ const TrackedGallery = trackWindowScroll(InternalGallery);
24
+ /**
25
+ * XImageGallery - 图片画廊/列表性能优化组件
26
+ *
27
+ * 当页面中有大量图片需要懒加载时,使用此组件包裹可以:
28
+ * - 统一管理滚动监听,避免每个图片单独监听 scroll/resize 事件
29
+ * - 通过 trackWindowScroll HOC 实现性能优化
30
+ * - 通过 Context 自动向任意深度的子 XImage 组件传递 scrollPosition
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * // 基础用法 - 包裹多个 XImage(支持任意嵌套深度)
35
+ * <XImageGallery>
36
+ * <div className="grid">
37
+ * {images.map(img => (
38
+ * <div key={img.id} className="card">
39
+ * <XImage src={img.url} effect="blur" />
40
+ * </div>
41
+ * ))}
42
+ * </div>
43
+ * </XImageGallery>
44
+ *
45
+ * // 自定义节流配置
46
+ * <XImageGallery delayMethod="debounce" delayTime={200}>
47
+ * {images.map(img => (
48
+ * <XImage key={img.id} src={img.url} />
49
+ * ))}
50
+ * </XImageGallery>
51
+ * ```
52
+ *
53
+ * @note 注意事项:
54
+ * - 不要在 XImageGallery 内部嵌套另一个 XImageGallery
55
+ * - 内部的 XImage 会自动通过 Context 获取 scrollPosition,无需手动传递
56
+ */
57
+ export const XImageGallery = memo(forwardRef(function XImageGallery({ children, className, style, delayMethod, delayTime }, ref) {
58
+ return (React.createElement(TrackedGallery, { ref: ref, className: className, style: style, delayMethod: delayMethod, delayTime: delayTime }, children));
59
+ }));
60
+ /**
61
+ * HOC: 为任意组件添加滚动位置追踪能力
62
+ *
63
+ * 适用于需要自定义容器但又想使用 trackWindowScroll 优化的场景
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * // 自定义图片网格组件
68
+ * const ImageGrid = ({ images, scrollPosition }) => (
69
+ * <div className="grid grid-cols-3">
70
+ * {images.map(img => (
71
+ * <XImage
72
+ * key={img.id}
73
+ * src={img.url}
74
+ * scrollPosition={scrollPosition}
75
+ * />
76
+ * ))}
77
+ * </div>
78
+ * );
79
+ *
80
+ * // 使用 HOC 包裹
81
+ * const OptimizedImageGrid = withScrollTracking(ImageGrid);
82
+ *
83
+ * // 使用
84
+ * <OptimizedImageGrid images={images} />
85
+ * ```
86
+ */
87
+ export function withScrollTracking(WrappedComponent) {
88
+ const WithScrollTracking = trackWindowScroll(WrappedComponent);
89
+ // 设置 displayName 便于调试
90
+ const wrappedName = WrappedComponent.displayName || WrappedComponent.name || "Component";
91
+ WithScrollTracking.displayName = `withScrollTracking(${wrappedName})`;
92
+ return WithScrollTracking;
93
+ }
94
+ export default XImageGallery;
@@ -0,0 +1,12 @@
1
+ export { XImage, default as XImageDefault } from "./XImage";
2
+ export { XImageGallery, withScrollTracking, useScrollPosition, ScrollPositionContext, } from "./XImageGallery";
3
+ export type { XImageProps, XImageEffect, XImageStatus, XImageGalleryProps, ScrollPosition, WithScrollTrackingProps, DelayMethod, } from "./XImage.types";
4
+ /**
5
+ * XImage 效果 CSS 已内置,无需手动导入!
6
+ *
7
+ * 直接使用 effect 属性即可:
8
+ * <XImage src="..." effect="blur" />
9
+ * <XImage src="..." effect="opacity" />
10
+ * <XImage src="..." effect="black-and-white" />
11
+ */
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/layout/image/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EACH,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,GACxB,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACR,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,WAAW,GACd,MAAM,gBAAgB,CAAC;AAGxB;;;;;;;GAOG"}
@@ -0,0 +1,4 @@
1
+ "use client";
2
+ // ============ 组件导出 ============
3
+ export { XImage, default as XImageDefault } from "./XImage";
4
+ export { XImageGallery, withScrollTracking, useScrollPosition, ScrollPositionContext, } from "./XImageGallery";
@@ -3,7 +3,7 @@ export * from './resize/core/SizedLayoutContext';
3
3
  export * from './resize/core/ResizeObserverHook';
4
4
  export * from './resize/impl/SizedContainer';
5
5
  export * from './resize/impl/SizedLayout';
6
- export * from './image/XImage';
6
+ export * from './image';
7
7
  export * from './visibility/ElementVisibilityHooks';
8
8
  export * from './virtualized/VirtualizedConfig';
9
9
  export * from './hover/XHover';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,qCAAqC,CAAA;AACnD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,SAAS,CAAA;AACvB,cAAc,qCAAqC,CAAA;AACnD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA"}
@@ -3,7 +3,7 @@ export * from './resize/core/SizedLayoutContext';
3
3
  export * from './resize/core/ResizeObserverHook';
4
4
  export * from './resize/impl/SizedContainer';
5
5
  export * from './resize/impl/SizedLayout';
6
- export * from './image/XImage';
6
+ export * from './image';
7
7
  export * from './visibility/ElementVisibilityHooks';
8
8
  export * from './virtualized/VirtualizedConfig';
9
9
  export * from './hover/XHover';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xxf_react",
3
- "version": "0.7.6",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -74,6 +74,7 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@microsoft/fetch-event-source": "^2.0.1",
77
+ "react-lazy-load-image-component": "^1.6.2",
77
78
  "@use-gesture/react": "^10.3.1",
78
79
  "bowser": "^2.14.1",
79
80
  "broadcast-channel": "^7.3.0",
@@ -96,6 +97,7 @@
96
97
  "devDependencies": {
97
98
  "@types/node": "^25.3.0",
98
99
  "@types/react": "^19.2.14",
100
+ "@types/react-lazy-load-image-component": "^1.6.4",
99
101
  "next": "^16.1.6",
100
102
  "typescript": "^5.9.3"
101
103
  }