xxf_react 0.8.2 → 0.8.3

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.
@@ -3,38 +3,31 @@ import type { XImageProps } from "./XImage.types";
3
3
  /**
4
4
  * 生产级图片组件
5
5
  *
6
- * 基于 next/image 实现,支持:
7
- * - 双层渲染:placeholder 先加载,主图加载完成后 blur 过渡
8
- * - 自动图片优化(Next.js 内置)
6
+ * 基于 react-lazy-load-image-component 实现,支持:
7
+ * - 懒加载 + IntersectionObserver
8
+ * - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
9
9
  * - 错误处理 + 自动重试
10
10
  * - fallback 备用图/组件
11
+ * - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
11
12
  *
12
13
  * @example
13
14
  * ```tsx
14
15
  * // 基础用法
15
16
  * <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
16
17
  *
17
- * // placeholder 模糊过渡(推荐使用小尺寸缩略图,如 w=20
18
- * <XImage
19
- * src="/photo.jpg?w=800"
20
- * placeholder="/photo.jpg?w=20"
21
- * width={400}
22
- * height={300}
23
- * />
18
+ * // 带模糊效果(提供 placeholder URL 自动启用 blur
19
+ * <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
24
20
  *
25
- * // 带错误处理和重试
21
+ * // 自定义占位组件
22
+ * <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
23
+ *
24
+ * // 带错误处理
26
25
  * <XImage
27
26
  * src="/photo.jpg"
28
27
  * fallback="/fallback.jpg"
29
28
  * retryCount={2}
30
29
  * retryDelay={1000}
31
30
  * />
32
- *
33
- * // 自定义占位组件
34
- * <XImage
35
- * src="/photo.jpg"
36
- * placeholder={<Skeleton className="w-full h-full" />}
37
- * />
38
31
  * ```
39
32
  */
40
33
  export declare const XImage: React.NamedExoticComponent<XImageProps & React.RefAttributes<HTMLDivElement | HTMLImageElement>>;
@@ -1 +1 @@
1
- {"version":3,"file":"XImage.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAQN,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAmBhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,MAAM,kGA4TlB,CAAC;AAEF,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,109 +1,99 @@
1
1
  "use client";
2
- import React, { useState, useEffect, useCallback, useRef, forwardRef, memo, useMemo, } from "react";
3
- import Image from "next/image";
4
- // ============================================================================
5
- // 常量定义
6
- // ============================================================================
7
- /** 过渡动画时长 */
8
- const TRANSITION_DURATION = "0.3s";
9
- /** 占位图模糊程度 */
10
- const PLACEHOLDER_BLUR = "20px";
11
- /** 占位图放大比例(防止模糊边缘露出) */
12
- const PLACEHOLDER_SCALE = 1.1;
13
- // ============================================================================
14
- // 组件实现
15
- // ============================================================================
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";
16
6
  /**
17
7
  * 生产级图片组件
18
8
  *
19
- * 基于 next/image 实现,支持:
20
- * - 双层渲染:placeholder 先加载,主图加载完成后 blur 过渡
21
- * - 自动图片优化(Next.js 内置)
9
+ * 基于 react-lazy-load-image-component 实现,支持:
10
+ * - 懒加载 + IntersectionObserver
11
+ * - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
22
12
  * - 错误处理 + 自动重试
23
13
  * - fallback 备用图/组件
14
+ * - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
24
15
  *
25
16
  * @example
26
17
  * ```tsx
27
18
  * // 基础用法
28
19
  * <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
29
20
  *
30
- * // placeholder 模糊过渡(推荐使用小尺寸缩略图,如 w=20
31
- * <XImage
32
- * src="/photo.jpg?w=800"
33
- * placeholder="/photo.jpg?w=20"
34
- * width={400}
35
- * height={300}
36
- * />
21
+ * // 带模糊效果(提供 placeholder URL 自动启用 blur
22
+ * <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
23
+ *
24
+ * // 自定义占位组件
25
+ * <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
37
26
  *
38
- * // 带错误处理和重试
27
+ * // 带错误处理
39
28
  * <XImage
40
29
  * src="/photo.jpg"
41
30
  * fallback="/fallback.jpg"
42
31
  * retryCount={2}
43
32
  * retryDelay={1000}
44
33
  * />
45
- *
46
- * // 自定义占位组件
47
- * <XImage
48
- * src="/photo.jpg"
49
- * placeholder={<Skeleton className="w-full h-full" />}
50
- * />
51
34
  * ```
52
35
  */
53
36
  export const XImage = memo(forwardRef(function XImage({
54
- // -------- XImage 扩展属性 --------
55
- placeholder, fallback, retryCount = 0, retryDelay = 1000, onStatusChange, wrapperProps, wrapperClassName,
56
- // -------- next/image 常用属性(需要特殊处理) --------
57
- src, alt = "", fill, style, className, onLoad, onError,
58
- // -------- next/image 其余属性(透传) --------
59
- ...imageProps }, ref) {
60
- // ========================================================================
61
- // 解析 placeholder 类型
62
- // ========================================================================
63
- const placeholderSrc = typeof placeholder === "string" ? placeholder : undefined;
64
- const placeholderElement = typeof placeholder !== "string" ? placeholder : undefined;
65
- const hasPlaceholder = Boolean(placeholderSrc || placeholderElement);
66
- // ========================================================================
67
- // 状态管理
68
- // ========================================================================
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
+ // ============ 状态管理 ============
69
70
  const [currentSrc, setCurrentSrc] = useState(src);
70
71
  const [status, setStatus] = useState("idle");
71
72
  const [hasError, setHasError] = useState(false);
72
73
  const [retryAttempt, setRetryAttempt] = useState(0);
73
- const [isLoaded, setIsLoaded] = useState(false);
74
- // ========================================================================
75
74
  // Refs
76
- // ========================================================================
77
75
  const wrapperRef = useRef(null);
78
- const mainImageRef = useRef(null);
79
76
  const retryTimerRef = useRef(null);
80
- // 使用 ref 存储回调,避免 useCallback 依赖问题
81
- const callbacksRef = useRef({ onStatusChange, onLoad, onError });
82
- callbacksRef.current = { onStatusChange, onLoad, onError };
83
- // ========================================================================
84
- // 状态更新辅助函数
85
- // ========================================================================
86
- const updateStatus = useCallback((newStatus) => {
87
- var _a, _b;
88
- setStatus(newStatus);
89
- (_b = (_a = callbacksRef.current).onStatusChange) === null || _b === void 0 ? void 0 : _b.call(_a, newStatus);
90
- }, []);
91
- // ========================================================================
92
- // src 变化时重置状态
93
- // ========================================================================
77
+ const hasCalledOnVisible = useRef(false);
78
+ // 使用 ref 存储回调,避免 effect 依赖问题
79
+ const onStatusChangeRef = useRef(onStatusChange);
80
+ onStatusChangeRef.current = onStatusChange;
81
+ // ============ src 变化重置状态 ============
94
82
  useEffect(() => {
83
+ var _a;
95
84
  setCurrentSrc(src);
96
85
  setHasError(false);
97
86
  setRetryAttempt(0);
98
- setIsLoaded(false);
99
- updateStatus("loading");
87
+ setStatus("idle");
88
+ (_a = onStatusChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStatusChangeRef, "idle");
89
+ hasCalledOnVisible.current = false;
100
90
  // 清理重试定时器
101
91
  if (retryTimerRef.current) {
102
92
  clearTimeout(retryTimerRef.current);
103
93
  retryTimerRef.current = null;
104
94
  }
105
- }, [src, updateStatus]);
106
- // 组件卸载时清理定时器
95
+ }, [src]);
96
+ // 清理定时器
107
97
  useEffect(() => {
108
98
  return () => {
109
99
  if (retryTimerRef.current) {
@@ -111,146 +101,161 @@ src, alt = "", fill, style, className, onLoad, onError,
111
101
  }
112
102
  };
113
103
  }, []);
114
- // ========================================================================
115
- // ref 转发:将内部 img 元素暴露给外部
116
- // ========================================================================
104
+ // ============ ref 转发:从 wrapper 中获取 img 元素 ============
117
105
  useEffect(() => {
118
106
  if (!ref || !wrapperRef.current)
119
107
  return;
120
- // 获取主图 img 元素(双层渲染时取第二个,单层取第一个)
121
- const imgs = wrapperRef.current.querySelectorAll("img");
122
- const targetImg = imgs.length > 1 ? imgs[1] : imgs[0];
123
- if (targetImg) {
108
+ const imgElement = wrapperRef.current.querySelector("img");
109
+ if (imgElement) {
124
110
  if (typeof ref === "function") {
125
- ref(targetImg);
111
+ ref(imgElement);
126
112
  }
127
113
  else {
128
- ref.current = targetImg;
114
+ ref.current = imgElement;
129
115
  }
130
116
  }
131
- }, [ref, isLoaded]); // isLoaded 变化时重新获取,确保 img 已渲染
132
- // ========================================================================
133
- // 事件处理
134
- // ========================================================================
135
- /**
136
- * 主图加载成功
137
- */
138
- const handleLoad = useCallback((e) => {
139
- var _a, _b;
140
- setIsLoaded(true);
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) => {
141
127
  updateStatus("loaded");
142
- (_b = (_a = callbacksRef.current).onLoad) === null || _b === void 0 ? void 0 : _b.call(_a, e);
143
- }, [updateStatus]);
144
- /**
145
- * 主图加载失败 - 处理重试和 fallback 逻辑
146
- */
147
- const handleError = useCallback((e) => {
128
+ onLoad === null || onLoad === void 0 ? void 0 : onLoad(event);
129
+ }, [updateStatus, onLoad]);
130
+ const handleError = useCallback((event) => {
148
131
  setRetryAttempt((currentRetry) => {
149
- // 还有重试次数,延迟后重试
132
+ // 还有重试次数
150
133
  if (currentRetry < retryCount) {
134
+ updateStatus("loading");
151
135
  retryTimerRef.current = setTimeout(() => {
152
- // 添加时间戳绕过缓存
153
- const srcStr = typeof src === "string" ? src : "";
154
- const separator = srcStr.includes("?") ? "&" : "?";
155
- setCurrentSrc(`${srcStr}${separator}_retry=${Date.now()}`);
136
+ // 通过修改 src 触发重新加载(添加时间戳绕过缓存)
137
+ const separator = src.includes("?") ? "&" : "?";
138
+ setCurrentSrc(`${src}${separator}_retry=${Date.now()}`);
156
139
  }, retryDelay);
157
140
  return currentRetry + 1;
158
141
  }
159
- // 重试耗尽,尝试 fallback
142
+ // 重试耗尽,检查 fallback
160
143
  setHasError((currentHasError) => {
161
- var _a, _b;
162
144
  if (fallback && !currentHasError) {
163
145
  if (typeof fallback === "string") {
164
146
  setCurrentSrc(fallback);
165
- return true;
147
+ updateStatus("loading");
166
148
  }
167
- // fallback 是 ReactNode,标记错误状态
168
- updateStatus("error");
169
149
  return true;
170
150
  }
171
- // 无 fallback 或已经在 fallback 状态,触发错误回调
151
+ // 最终失败
172
152
  updateStatus("error");
173
- (_b = (_a = callbacksRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, e);
153
+ onError === null || onError === void 0 ? void 0 : onError(event);
174
154
  return currentHasError;
175
155
  });
176
156
  return currentRetry;
177
157
  });
178
- }, [retryCount, retryDelay, src, fallback, updateStatus]);
179
- // ========================================================================
180
- // 合并 wrapperProps ref
181
- // ========================================================================
182
- const mergedWrapperRef = useCallback((node) => {
183
- wrapperRef.current = node;
184
- // 调用用户传入的 ref
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(通过类型扩展)
185
216
  const userRef = wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.ref;
186
- if (userRef) {
187
- if (typeof userRef === "function") {
188
- userRef(node);
189
- }
190
- else {
191
- userRef.current = node;
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
+ }
192
229
  }
193
- }
194
- }, [wrapperProps]);
195
- // ========================================================================
196
- // 计算样式(使用 useMemo 避免重复创建对象)
197
- // ========================================================================
198
- /** 容器样式 */
199
- const wrapperStyle = useMemo(() => ({
200
- position: "relative",
201
- display: "inline-block",
202
- overflow: "hidden",
203
- ...wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style,
204
- }), [wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style]);
205
- /** 占位图样式(带 blur 效果) */
206
- const placeholderStyle = useMemo(() => ({
207
- ...style,
208
- position: "absolute",
209
- inset: 0,
210
- width: "100%",
211
- height: "100%",
212
- transition: `opacity ${TRANSITION_DURATION} ease-in-out, filter ${TRANSITION_DURATION} ease-in-out`,
213
- opacity: isLoaded ? 0 : 1,
214
- filter: isLoaded ? "blur(0px)" : `blur(${PLACEHOLDER_BLUR})`,
215
- transform: `scale(${PLACEHOLDER_SCALE})`,
216
- pointerEvents: "none", // 防止占位图拦截事件
217
- }), [style, isLoaded]);
218
- /** 主图样式(加载完成后显示) */
219
- const mainImageStyle = useMemo(() => ({
220
- ...style,
221
- position: "absolute",
222
- inset: 0,
223
- width: "100%",
224
- height: "100%",
225
- transition: `opacity ${TRANSITION_DURATION} ease-in-out`,
226
- opacity: isLoaded ? 1 : 0,
227
- }), [style, isLoaded]);
228
- // ========================================================================
229
- // 渲染
230
- // ========================================================================
231
- // 错误状态 + ReactNode fallback
230
+ };
231
+ return {
232
+ ...wrapperProps,
233
+ ref: combinedRef,
234
+ style: wrapperStyle,
235
+ className: wrapperClassName,
236
+ };
237
+ }, [wrapperProps, wrapperStyle, wrapperClassName]);
238
+ // ============ 渲染 fallback ReactNode ============
232
239
  if (hasError && fallback && typeof fallback !== "string") {
233
- return (React.createElement("div", { ref: ref, className: wrapperClassName || className, style: { ...wrapperStyle, ...style }, ...wrapperProps }, fallback));
234
- }
235
- // 有 placeholder(URL 或 ReactElement):双层渲染
236
- if (hasPlaceholder) {
237
- return (React.createElement("div", { ...wrapperProps, ref: mergedWrapperRef, className: wrapperClassName, style: wrapperStyle },
238
- placeholderSrc ? (
239
- // URL 占位图:使用 next/image,priority 立即加载
240
- React.createElement(Image, { src: placeholderSrc, alt: "" // 占位图不需要 alt(屏幕阅读器跳过)
241
- , fill: true, priority: true, unoptimized: true, className: className, style: placeholderStyle, sizes: imageProps.sizes })) : (
242
- // ReactElement 占位组件(如 Skeleton)
243
- React.createElement("div", { style: {
244
- position: "absolute",
245
- inset: 0,
246
- transition: `opacity ${TRANSITION_DURATION} ease-in-out`,
247
- opacity: isLoaded ? 0 : 1,
248
- pointerEvents: "none",
249
- } }, placeholderElement)),
250
- React.createElement(Image, { ...imageProps, ref: mainImageRef, src: currentSrc, alt: alt, fill: true, className: className, style: mainImageStyle, onLoad: handleLoad, onError: handleError })));
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,
246
+ } }, fallback));
251
247
  }
252
- // placeholder:单层渲染
253
- return (React.createElement("div", { ...wrapperProps, ref: mergedWrapperRef, className: wrapperClassName, style: wrapperStyle },
254
- React.createElement(Image, { ...imageProps, ref: mainImageRef, src: currentSrc, alt: alt, fill: fill, className: className, style: style, onLoad: handleLoad, onError: handleError })));
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 }));
255
260
  }));
256
261
  export default XImage;
@@ -1,110 +1,130 @@
1
- import type { ImageProps } from "next/image";
2
- import type { ReactElement, ReactNode } from "react";
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";
3
13
  /**
4
14
  * 图片加载状态
5
- *
6
- * - `idle`: 初始状态(未开始加载)
7
- * - `loading`: 加载中
8
- * - `loaded`: 加载成功
9
- * - `error`: 加载失败(重试耗尽且无 fallback)
10
15
  */
11
16
  export type XImageStatus = "idle" | "loading" | "loaded" | "error";
17
+ /**
18
+ * 滚动位置类型 (用于 trackWindowScroll HOC)
19
+ */
20
+ export interface ScrollPosition {
21
+ x: number;
22
+ y: number;
23
+ }
12
24
  /**
13
25
  * XImage 组件 Props
14
- *
15
- * 继承 next/image 的所有属性,扩展双层渲染和错误处理能力。
16
- *
17
- * ## 性能建议
18
- *
19
- * 1. **placeholder 使用小尺寸缩略图**:推荐 20-40px 宽度,体积 < 1KB
20
- * ```tsx
21
- * <XImage
22
- * src="/photo.jpg?w=800"
23
- * placeholder="/photo.jpg?w=20"
24
- * />
25
- * ```
26
- *
27
- * 2. **首屏图片使用 priority**:跳过懒加载
28
- * ```tsx
29
- * <XImage src="/hero.jpg" priority />
30
- * ```
31
- *
32
- * 3. **设置正确的 sizes**:避免加载过大图片
33
- * ```tsx
34
- * <XImage
35
- * src="/photo.jpg"
36
- * sizes="(max-width: 768px) 100vw, 50vw"
37
- * />
38
- * ```
39
26
  */
40
- export interface XImageProps extends Omit<ImageProps, "placeholder" | "ref"> {
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;
41
52
  /**
42
- * 占位内容(用于双层渲染 + blur 过渡)
43
- *
44
- * - `string`: 占位图片 URL(推荐使用小尺寸缩略图)
45
- * - `ReactElement`: 自定义占位组件(如 Skeleton)
46
- *
47
- * @example
48
- * ```tsx
49
- * // URL 占位图(推荐)
50
- * <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
51
- *
52
- * // 自定义 Skeleton
53
- * <XImage src="/photo.jpg" placeholder={<Skeleton />} />
54
- * ```
53
+ * 占位内容
54
+ * - string: 占位图片 URL(自动启用 blur 效果)
55
+ * - ReactElement: 自定义占位组件(如 Skeleton,需手动指定 effect)
55
56
  */
56
57
  placeholder?: string | ReactElement | null;
58
+ /** 是否使用 IntersectionObserver,默认 true */
59
+ useIntersectionObserver?: boolean;
60
+ /** 滚动/resize 事件的节流方法 */
61
+ delayMethod?: DelayMethod;
62
+ /** 节流延迟时间(毫秒) */
63
+ delayTime?: number;
57
64
  /**
58
65
  * 加载失败时显示的备用内容
59
- *
60
- * - `string`: 备用图片 URL(会尝试加载)
61
- * - `ReactNode`: 自定义错误展示组件
62
- *
63
- * @example
64
- * ```tsx
65
- * // 备用图片
66
- * <XImage src="/photo.jpg" fallback="/fallback.jpg" />
67
- *
68
- * // 自定义错误组件
69
- * <XImage src="/photo.jpg" fallback={<ErrorPlaceholder />} />
70
- * ```
66
+ * - string: 备用图片 URL
67
+ * - ReactNode: 自定义错误展示组件
71
68
  */
72
69
  fallback?: string | ReactNode;
73
- /**
74
- * 加载失败后的重试次数
75
- *
76
- * @default 0
77
- */
70
+ /** 加载失败后的重试次数,默认 0 */
78
71
  retryCount?: number;
79
- /**
80
- * 重试间隔(毫秒)
81
- *
82
- * @default 1000
83
- */
72
+ /** 重试间隔(毫秒),默认 1000 */
84
73
  retryDelay?: number;
85
74
  /**
86
- * 加载状态变化回调
87
- *
88
- * @example
89
- * ```tsx
90
- * <XImage
91
- * src="/photo.jpg"
92
- * onStatusChange={(status) => console.log('Status:', status)}
93
- * />
94
- * ```
75
+ * 滚动位置(来自 trackWindowScroll HOC)
76
+ * 当父组件使用 withScrollTracking 包裹时自动传入
95
77
  */
96
- onStatusChange?: (status: XImageStatus) => void;
97
- /**
98
- * 传递给包裹 div 元素的属性
99
- *
100
- * 用于自定义容器的事件处理、数据属性等
101
- */
102
- wrapperProps?: React.HTMLAttributes<HTMLDivElement>;
78
+ scrollPosition?: ScrollPosition;
103
79
  /**
104
- * 包裹元素的类名
105
- *
106
- * 与 className 区分:className 应用于 img,wrapperClassName 应用于外层 div
80
+ * 设为 true 则图片立即显示(不触发懒加载)
81
+ * 适用于已在浏览器缓存中的图片
107
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
+ /** 包裹元素的类名 */
108
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;
109
129
  }
110
130
  //# sourceMappingURL=XImage.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"XImage.types.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,UAAU,EAAE,aAAa,GAAG,KAAK,CAAC;IACxE;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;IAE3C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAEhD;;;;OAIG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAEpD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B"}
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,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;
@@ -1,3 +1,12 @@
1
1
  export { XImage, default as XImageDefault } from "./XImage";
2
- export type { XImageProps, XImageStatus } from "./XImage.types";
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
+ */
3
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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;AAG5D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
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"}
@@ -1,3 +1,4 @@
1
1
  "use client";
2
2
  // ============ 组件导出 ============
3
3
  export { XImage, default as XImageDefault } from "./XImage";
4
+ export { XImageGallery, withScrollTracking, useScrollPosition, ScrollPositionContext, } from "./XImageGallery";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xxf_react",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,11 +70,11 @@
70
70
  "build": "tsc"
71
71
  },
72
72
  "peerDependencies": {
73
- "react": ">=18",
74
- "next": ">=13"
73
+ "react": ">=18"
75
74
  },
76
75
  "dependencies": {
77
76
  "@microsoft/fetch-event-source": "^2.0.1",
77
+ "react-lazy-load-image-component": "^1.6.2",
78
78
  "@use-gesture/react": "^10.3.1",
79
79
  "bowser": "^2.14.1",
80
80
  "broadcast-channel": "^7.3.0",
@@ -97,6 +97,7 @@
97
97
  "devDependencies": {
98
98
  "@types/node": "^25.3.0",
99
99
  "@types/react": "^19.2.14",
100
+ "@types/react-lazy-load-image-component": "^1.6.4",
100
101
  "next": "^16.1.6",
101
102
  "typescript": "^5.9.3"
102
103
  }