react-public-components 1.1.15 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  封装 react 常用的组件,欢迎更多同学提出大众化普遍存在的业务场景。
4
4
 
5
- 本仓库包含三个独立的 React 组件:
5
+ 本仓库包含四个独立的 React 组件:
6
6
 
7
7
  1. **CollapseBox** — 可折叠的内容容器组件
8
8
  2. **Splitter** — 可拖拽调整、可折叠的分屏面板组件
9
9
  3. **DisabledBox** — 禁用态包装容器组件(带锁图标及拦截事件)
10
+ 4. **Masonry** — 瀑布流布局组件(一比一复刻 Ant Design V6 规范)
10
11
 
11
12
  ---
12
13
 
@@ -198,7 +199,65 @@ export default function Demo() {
198
199
 
199
200
  ---
200
201
 
201
- ## 四、目录结构
202
+ ## 四、Masonry 瀑布流组件
203
+
204
+ ### 简介
205
+
206
+ `Masonry` 是一个符合 Ant Design V6 规范的瀑布流布局组件。支持固定或响应式列数配置(`columns`)、水平垂直间距配置(`gutter`)、持续监听子项尺寸变化(`fresh`)、自定义语义化结构样式/类名(`styles`/`classNames`)以及列排序回调(`onLayoutChange`)。
207
+
208
+ ### 引入方式
209
+
210
+ ```tsx
211
+ import { Masonry } from 'react-public-components';
212
+ ```
213
+
214
+ ### Masonry Props
215
+
216
+ | 参数 | 说明 | 类型 | 默认值 |
217
+ | ---------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------- |
218
+ | `classNames` | 自定义内部语义化结构 class,支持对象或函数 | `Record<'root' \| 'item', string> \| ((info: { props }) => Record<'root' \| 'item', string>)` | `-` |
219
+ | `columns` | 列数,固定数字或响应式配置 | `number \| { xs?: number; sm?: number; md?: number; lg?: number; xl?: number; xxl?: number }` | `3` |
220
+ | `fresh` | 是否持续监听子项尺寸变化 | `boolean` | `false` |
221
+ | `gutter` | 间距,固定数字、响应式配置或水平垂直间距配置 | `Gap \| [Gap, Gap]` | `0` |
222
+ | `items` | 瀑布流数据项 | `MasonryItem[]` | `-` |
223
+ | `itemRender` | 自定义数据项渲染函数 | `(item: MasonryItem) => ReactNode` | `-` |
224
+ | `styles` | 语义化结构 style,支持对象和函数形式 | `Record<'root' \| 'item', CSSProperties> \| ((info: { props }) => Record<'root' \| 'item', CSSProperties>)` | `-` |
225
+ | `onLayoutChange` | 列排序计算完成回调 | `(layout: { key: React.Key; column: number }[]) => void` | `-` |
226
+
227
+ ### MasonryItem 属性
228
+
229
+ | 参数 | 说明 | 类型 | 默认值 |
230
+ | ---------- | ------------------------------------------------ | ------------------ | ------ |
231
+ | `children` | 自定义展示内容,相对 `itemRender` 具有更高优先级 | `ReactNode` | `-` |
232
+ | `column` | 自定义指定放置列(0-indexed) | `number` | `-` |
233
+ | `data` | 自定义存储数据 | `T` | `-` |
234
+ | `height` | 高度(手指定时优先使用) | `number` | `-` |
235
+ | `key` | 唯一标识 | `string \| number` | `-` |
236
+
237
+ ### 基础用法
238
+
239
+ ```tsx
240
+ import { Masonry } from 'react-public-components';
241
+
242
+ export default function Demo() {
243
+ return (
244
+ <Masonry
245
+ columns={{ xs: 1, sm: 2, md: 3, lg: 4 }}
246
+ gutter={[16, 16]}
247
+ fresh={true}
248
+ items={[
249
+ { key: '1', height: 120, children: <div>卡片 1</div> },
250
+ { key: '2', height: 180, children: <div>卡片 2</div> },
251
+ { key: '3', height: 150, children: <div>卡片 3</div> },
252
+ ]}
253
+ />
254
+ );
255
+ }
256
+ ```
257
+
258
+ ---
259
+
260
+ ## 五、目录结构
202
261
 
203
262
  ```
204
263
  .
@@ -207,14 +266,17 @@ export default function Demo() {
207
266
  │ └── index.less
208
267
  ├── Splitter/
209
268
  │ └── index.tsx
210
- └── DisabledBox/
269
+ ├── DisabledBox/
270
+ │ ├── index.tsx
271
+ │ └── index.less
272
+ └── Masonry/
211
273
  ├── index.tsx
212
274
  └── index.less
213
275
  ```
214
276
 
215
277
  ---
216
278
 
217
- ## 五、发布到 npm
279
+ ## 六、发布到 npm
218
280
 
219
281
  ### 1. 前置准备
220
282
 
package/dist/index.cjs CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  CollapseBox: () => CollapseBox_default,
34
34
  DisabledBox: () => DisabledBox_default,
35
+ Masonry: () => Masonry_default,
35
36
  Splitter: () => Splitter_default
36
37
  });
37
38
  module.exports = __toCommonJS(index_exports);
@@ -630,7 +631,8 @@ var CustomSplitter = (props) => {
630
631
  boxSizing: "border-box",
631
632
  display: isHidden ? "none" : "block",
632
633
  height: "100%",
633
- overflow: "auto",
634
+ overflowX: "hidden",
635
+ overflowY: "auto",
634
636
  padding: ((_e = sizes[index]) != null ? _e : 0) < 32 ? 0 : 16,
635
637
  width: "100%"
636
638
  },
@@ -864,10 +866,252 @@ var DisabledBox = (props) => {
864
866
  );
865
867
  };
866
868
  var DisabledBox_default = DisabledBox;
869
+
870
+ // Masonry/index.tsx
871
+ var import_react3 = __toESM(require("react"), 1);
872
+ var import_jsx_runtime5 = require("react/jsx-runtime");
873
+ var BREAKPOINT_ORDER = ["xs", "sm", "md", "lg", "xl", "xxl"];
874
+ function getBreakpoint(width) {
875
+ if (width >= 1600) return "xxl";
876
+ if (width >= 1200) return "xl";
877
+ if (width >= 992) return "lg";
878
+ if (width >= 768) return "md";
879
+ if (width >= 576) return "sm";
880
+ return "xs";
881
+ }
882
+ function resolveResponsiveValue(value, currentBp, defaultValue) {
883
+ if (value === void 0 || value === null) return defaultValue;
884
+ if (typeof value === "object") {
885
+ const obj = value;
886
+ if (obj[currentBp] !== void 0) return obj[currentBp];
887
+ const currentIdx = BREAKPOINT_ORDER.indexOf(currentBp);
888
+ for (let i = currentIdx - 1; i >= 0; i--) {
889
+ const bp = BREAKPOINT_ORDER[i];
890
+ if (obj[bp] !== void 0) return obj[bp];
891
+ }
892
+ for (let i = currentIdx + 1; i < BREAKPOINT_ORDER.length; i++) {
893
+ const bp = BREAKPOINT_ORDER[i];
894
+ if (obj[bp] !== void 0) return obj[bp];
895
+ }
896
+ return defaultValue;
897
+ }
898
+ return value;
899
+ }
900
+ function resolveGutter(gutter, currentBp) {
901
+ if (!gutter) return [0, 0];
902
+ if (Array.isArray(gutter)) {
903
+ const h = resolveResponsiveValue(gutter[0], currentBp, 0);
904
+ const v = resolveResponsiveValue(gutter[1], currentBp, 0);
905
+ return [h, v];
906
+ }
907
+ const g = resolveResponsiveValue(gutter, currentBp, 0);
908
+ return [g, g];
909
+ }
910
+ var Masonry = (props) => {
911
+ const {
912
+ classNames,
913
+ columns = 3,
914
+ fresh = false,
915
+ gutter = 0,
916
+ items,
917
+ itemRender,
918
+ styles,
919
+ onLayoutChange,
920
+ className,
921
+ style,
922
+ children
923
+ } = props;
924
+ const rootRef = (0, import_react3.useRef)(null);
925
+ const [containerWidth, setContainerWidth] = (0, import_react3.useState)(0);
926
+ const [measuredHeights, setMeasuredHeights] = (0, import_react3.useState)({});
927
+ const itemRefs = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
928
+ const normalizedItems = (0, import_react3.useMemo)(() => {
929
+ if (items) return items;
930
+ if (!children) return [];
931
+ return import_react3.default.Children.toArray(children).map((child, index) => {
932
+ var _a;
933
+ if (import_react3.default.isValidElement(child)) {
934
+ const key = (_a = child.key) != null ? _a : `masonry-item-${index}`;
935
+ return {
936
+ key,
937
+ children: child
938
+ };
939
+ }
940
+ return {
941
+ key: `masonry-item-${index}`,
942
+ children: child
943
+ };
944
+ });
945
+ }, [items, children]);
946
+ const currentBreakpoint = (0, import_react3.useMemo)(() => getBreakpoint(containerWidth), [containerWidth]);
947
+ const resolvedColumns = (0, import_react3.useMemo)(() => {
948
+ const count = resolveResponsiveValue(columns, currentBreakpoint, 3);
949
+ return Math.max(1, Math.floor(count));
950
+ }, [columns, currentBreakpoint]);
951
+ const [horizontalGap, verticalGap] = (0, import_react3.useMemo)(
952
+ () => resolveGutter(gutter, currentBreakpoint),
953
+ [gutter, currentBreakpoint]
954
+ );
955
+ (0, import_react3.useEffect)(() => {
956
+ if (!rootRef.current) return;
957
+ const observer = new ResizeObserver((entries) => {
958
+ for (const entry of entries) {
959
+ const width = entry.contentRect.width;
960
+ if (width > 0) {
961
+ setContainerWidth(width);
962
+ }
963
+ }
964
+ });
965
+ observer.observe(rootRef.current);
966
+ return () => observer.disconnect();
967
+ }, []);
968
+ const measureItems = (0, import_react3.useCallback)(() => {
969
+ let hasChange = false;
970
+ const newHeights = {};
971
+ itemRefs.current.forEach((el, key) => {
972
+ if (el) {
973
+ const strKey = String(key);
974
+ const h = el.offsetHeight;
975
+ if (h > 0) {
976
+ newHeights[strKey] = h;
977
+ if (measuredHeights[strKey] !== h) {
978
+ hasChange = true;
979
+ }
980
+ }
981
+ }
982
+ });
983
+ if (hasChange) {
984
+ setMeasuredHeights((prev) => ({ ...prev, ...newHeights }));
985
+ }
986
+ }, [measuredHeights]);
987
+ (0, import_react3.useLayoutEffect)(() => {
988
+ const handle = requestAnimationFrame(() => {
989
+ measureItems();
990
+ });
991
+ return () => cancelAnimationFrame(handle);
992
+ }, [measureItems]);
993
+ (0, import_react3.useEffect)(() => {
994
+ if (!fresh) return;
995
+ const observer = new ResizeObserver(() => {
996
+ measureItems();
997
+ });
998
+ itemRefs.current.forEach((el) => {
999
+ if (el) observer.observe(el);
1000
+ });
1001
+ return () => observer.disconnect();
1002
+ }, [fresh, measureItems, normalizedItems]);
1003
+ const columnWidth = (0, import_react3.useMemo)(() => {
1004
+ if (containerWidth <= 0) return 0;
1005
+ const totalGap = (resolvedColumns - 1) * horizontalGap;
1006
+ return Math.max(0, (containerWidth - totalGap) / resolvedColumns);
1007
+ }, [containerWidth, resolvedColumns, horizontalGap]);
1008
+ const { layoutItems, containerHeight, layoutChangeData } = (0, import_react3.useMemo)(() => {
1009
+ const colHeights = new Array(resolvedColumns).fill(0);
1010
+ const changeData = [];
1011
+ const computedList = normalizedItems.map((item) => {
1012
+ var _a, _b;
1013
+ const h = (_b = (_a = item.height) != null ? _a : measuredHeights[String(item.key)]) != null ? _b : 0;
1014
+ let targetCol = 0;
1015
+ if (typeof item.column === "number" && item.column >= 0 && item.column < resolvedColumns) {
1016
+ targetCol = Math.floor(item.column);
1017
+ } else {
1018
+ let minH = colHeights[0];
1019
+ for (let i = 1; i < resolvedColumns; i++) {
1020
+ if (colHeights[i] < minH) {
1021
+ minH = colHeights[i];
1022
+ targetCol = i;
1023
+ }
1024
+ }
1025
+ }
1026
+ const left = targetCol * (columnWidth + horizontalGap);
1027
+ const top = colHeights[targetCol];
1028
+ colHeights[targetCol] += h + (h > 0 ? verticalGap : 0);
1029
+ changeData.push({ key: item.key, column: targetCol });
1030
+ return {
1031
+ item,
1032
+ left,
1033
+ top,
1034
+ width: columnWidth,
1035
+ height: h,
1036
+ column: targetCol
1037
+ };
1038
+ });
1039
+ const totalH = Math.max(0, ...colHeights);
1040
+ return {
1041
+ layoutItems: computedList,
1042
+ containerHeight: totalH,
1043
+ layoutChangeData: changeData
1044
+ };
1045
+ }, [normalizedItems, resolvedColumns, horizontalGap, verticalGap, columnWidth, measuredHeights]);
1046
+ const prevLayoutRef = (0, import_react3.useRef)("");
1047
+ (0, import_react3.useEffect)(() => {
1048
+ const layoutKey = JSON.stringify(layoutChangeData);
1049
+ if (layoutKey !== prevLayoutRef.current) {
1050
+ prevLayoutRef.current = layoutKey;
1051
+ onLayoutChange == null ? void 0 : onLayoutChange(layoutChangeData);
1052
+ }
1053
+ }, [layoutChangeData, onLayoutChange]);
1054
+ const semanticClassNames = (0, import_react3.useMemo)(() => {
1055
+ var _a;
1056
+ if (typeof classNames === "function") {
1057
+ return (_a = classNames({ props })) != null ? _a : {};
1058
+ }
1059
+ return classNames != null ? classNames : {};
1060
+ }, [classNames, props]);
1061
+ const semanticStyles = (0, import_react3.useMemo)(() => {
1062
+ var _a;
1063
+ if (typeof styles === "function") {
1064
+ return (_a = styles({ props })) != null ? _a : {};
1065
+ }
1066
+ return styles != null ? styles : {};
1067
+ }, [styles, props]);
1068
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1069
+ "div",
1070
+ {
1071
+ ref: rootRef,
1072
+ className: ["masonry_root", semanticClassNames.root, className].filter(Boolean).join(" "),
1073
+ style: {
1074
+ minHeight: containerHeight > 0 ? containerHeight : void 0,
1075
+ height: containerHeight > 0 ? containerHeight : void 0,
1076
+ ...semanticStyles.root,
1077
+ ...style
1078
+ },
1079
+ children: layoutItems.map(({ item, left, top, width, height }) => {
1080
+ var _a;
1081
+ const content = (_a = item.children) != null ? _a : itemRender == null ? void 0 : itemRender(item);
1082
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1083
+ "div",
1084
+ {
1085
+ ref: (node) => {
1086
+ if (node) {
1087
+ itemRefs.current.set(item.key, node);
1088
+ } else {
1089
+ itemRefs.current.delete(item.key);
1090
+ }
1091
+ },
1092
+ className: ["masonry_item", semanticClassNames.item].filter(Boolean).join(" "),
1093
+ style: {
1094
+ width: containerWidth > 0 ? width : "100%",
1095
+ height: height > 0 ? height : void 0,
1096
+ transform: containerWidth > 0 ? `translate3d(${left}px, ${top}px, 0)` : void 0,
1097
+ opacity: containerWidth > 0 ? 1 : 0,
1098
+ ...semanticStyles.item
1099
+ },
1100
+ onLoad: measureItems,
1101
+ children: content
1102
+ },
1103
+ item.key
1104
+ );
1105
+ })
1106
+ }
1107
+ );
1108
+ };
1109
+ var Masonry_default = Masonry;
867
1110
  // Annotate the CommonJS export names for ESM import in node:
868
1111
  0 && (module.exports = {
869
1112
  CollapseBox,
870
1113
  DisabledBox,
1114
+ Masonry,
871
1115
  Splitter
872
1116
  });
873
1117
  //# sourceMappingURL=index.cjs.map