react-public-components 1.1.16 → 1.1.18

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,108 @@ 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
+ ## 五、BorderBeam 边框流光组件
261
+
262
+ ### 简介
263
+
264
+ `BorderBeam` 是一个给任意容器边框添加持续流动高亮效果的组件。使用现代 CSS Mask 遮罩与 CSS Motion Path 动画,支持单色与多点渐变色配置、多条流光均匀分布、流光尺寸与线宽调节,以及贴合自定义宿主容器。
265
+
266
+ ### 引入方式
267
+
268
+ ```tsx
269
+ import { BorderBeam } from 'react-public-components';
270
+ ```
271
+
272
+ ### API
273
+
274
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 全局配置 |
275
+ | ----------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------ | ----- | -------- |
276
+ | `children` | 装饰内容 | `ReactNode` | `-` | 6.4.0 | × |
277
+ | `color` | 流光颜色配置,支持单色字符串或渐变停靠点数组。`percent` 使用 `0 ~ 100` 的输入区间,组件会在内部为尾部透明过渡预留空间 | `string \| { color: string; percent: number }[]` | `-` | 6.4.0 | × |
278
+ | `count` | 流光数量 | `number` | `1` | 6.6.0 | × |
279
+ | `duration` | 流光完成一圈动画的时间,单位秒 | `number` | `6` | 6.5.0 | × |
280
+ | `lineWidth` | 流光线宽,数字类型按像素处理 | `number \| string` | `1px` | 6.5.0 | × |
281
+ | `outset` | 流光层相对容器边缘的外扩距离,遇到裁剪容器时可设为 `0` | `number \| string` | `-` | 6.4.0 | × |
282
+ | `size` | 流光可见段的尺寸,数字类型按像素处理 | `number \| string` | `100` | 6.5.0 | × |
283
+
284
+ ### 基础用法
285
+
286
+ ```tsx
287
+ import { BorderBeam } from 'react-public-components';
288
+
289
+ export default function Demo() {
290
+ return (
291
+ <BorderBeam size={100} duration={6} color="#1677ff">
292
+ <div style={{ padding: 24, borderRadius: 12, border: '1px solid #eee' }}>
293
+ <h3>Workspace overview</h3>
294
+ <p>Review task status, deployment health, and recent automation activity in one panel.</p>
295
+ </div>
296
+ </BorderBeam>
297
+ );
298
+ }
299
+ ```
300
+
301
+ ---
302
+
303
+ ## 六、目录结构
202
304
 
203
305
  ```
204
306
  .
@@ -207,14 +309,20 @@ export default function Demo() {
207
309
  │ └── index.less
208
310
  ├── Splitter/
209
311
  │ └── index.tsx
210
- └── DisabledBox/
312
+ ├── DisabledBox/
313
+ │ ├── index.tsx
314
+ │ └── index.less
315
+ ├── Masonry/
316
+ │ ├── index.tsx
317
+ │ └── index.less
318
+ └── BorderBeam/
211
319
  ├── index.tsx
212
320
  └── index.less
213
321
  ```
214
322
 
215
323
  ---
216
324
 
217
- ## 五、发布到 npm
325
+ ## 七、发布到 npm
218
326
 
219
327
  ### 1. 前置准备
220
328
 
package/dist/index.cjs CHANGED
@@ -30,8 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BorderBeam: () => BorderBeam_default,
33
34
  CollapseBox: () => CollapseBox_default,
34
35
  DisabledBox: () => DisabledBox_default,
36
+ Masonry: () => Masonry_default,
35
37
  Splitter: () => Splitter_default
36
38
  });
37
39
  module.exports = __toCommonJS(index_exports);
@@ -865,10 +867,351 @@ var DisabledBox = (props) => {
865
867
  );
866
868
  };
867
869
  var DisabledBox_default = DisabledBox;
870
+
871
+ // Masonry/index.tsx
872
+ var import_react3 = __toESM(require("react"), 1);
873
+ var import_jsx_runtime5 = require("react/jsx-runtime");
874
+ var BREAKPOINT_ORDER = ["xs", "sm", "md", "lg", "xl", "xxl"];
875
+ function getBreakpoint(width) {
876
+ if (width >= 1600) return "xxl";
877
+ if (width >= 1200) return "xl";
878
+ if (width >= 992) return "lg";
879
+ if (width >= 768) return "md";
880
+ if (width >= 576) return "sm";
881
+ return "xs";
882
+ }
883
+ function resolveResponsiveValue(value, currentBp, defaultValue) {
884
+ if (value === void 0 || value === null) return defaultValue;
885
+ if (typeof value === "object") {
886
+ const obj = value;
887
+ if (obj[currentBp] !== void 0) return obj[currentBp];
888
+ const currentIdx = BREAKPOINT_ORDER.indexOf(currentBp);
889
+ for (let i = currentIdx - 1; i >= 0; i--) {
890
+ const bp = BREAKPOINT_ORDER[i];
891
+ if (obj[bp] !== void 0) return obj[bp];
892
+ }
893
+ for (let i = currentIdx + 1; i < BREAKPOINT_ORDER.length; i++) {
894
+ const bp = BREAKPOINT_ORDER[i];
895
+ if (obj[bp] !== void 0) return obj[bp];
896
+ }
897
+ return defaultValue;
898
+ }
899
+ return value;
900
+ }
901
+ function resolveGutter(gutter, currentBp) {
902
+ if (!gutter) return [0, 0];
903
+ if (Array.isArray(gutter)) {
904
+ const h = resolveResponsiveValue(gutter[0], currentBp, 0);
905
+ const v = resolveResponsiveValue(gutter[1], currentBp, 0);
906
+ return [h, v];
907
+ }
908
+ const g = resolveResponsiveValue(gutter, currentBp, 0);
909
+ return [g, g];
910
+ }
911
+ var Masonry = (props) => {
912
+ const {
913
+ classNames,
914
+ columns = 3,
915
+ fresh = false,
916
+ gutter = 0,
917
+ items,
918
+ itemRender,
919
+ styles,
920
+ onLayoutChange,
921
+ className,
922
+ style,
923
+ children
924
+ } = props;
925
+ const rootRef = (0, import_react3.useRef)(null);
926
+ const [containerWidth, setContainerWidth] = (0, import_react3.useState)(0);
927
+ const [measuredHeights, setMeasuredHeights] = (0, import_react3.useState)({});
928
+ const itemRefs = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
929
+ const normalizedItems = (0, import_react3.useMemo)(() => {
930
+ if (items) return items;
931
+ if (!children) return [];
932
+ return import_react3.default.Children.toArray(children).map((child, index) => {
933
+ var _a;
934
+ if (import_react3.default.isValidElement(child)) {
935
+ const key = (_a = child.key) != null ? _a : `masonry-item-${index}`;
936
+ return {
937
+ key,
938
+ children: child
939
+ };
940
+ }
941
+ return {
942
+ key: `masonry-item-${index}`,
943
+ children: child
944
+ };
945
+ });
946
+ }, [items, children]);
947
+ const currentBreakpoint = (0, import_react3.useMemo)(() => getBreakpoint(containerWidth), [containerWidth]);
948
+ const resolvedColumns = (0, import_react3.useMemo)(() => {
949
+ const count = resolveResponsiveValue(columns, currentBreakpoint, 3);
950
+ return Math.max(1, Math.floor(count));
951
+ }, [columns, currentBreakpoint]);
952
+ const [horizontalGap, verticalGap] = (0, import_react3.useMemo)(
953
+ () => resolveGutter(gutter, currentBreakpoint),
954
+ [gutter, currentBreakpoint]
955
+ );
956
+ (0, import_react3.useEffect)(() => {
957
+ if (!rootRef.current) return;
958
+ const observer = new ResizeObserver((entries) => {
959
+ for (const entry of entries) {
960
+ const width = entry.contentRect.width;
961
+ if (width > 0) {
962
+ setContainerWidth(width);
963
+ }
964
+ }
965
+ });
966
+ observer.observe(rootRef.current);
967
+ return () => observer.disconnect();
968
+ }, []);
969
+ const measureItems = (0, import_react3.useCallback)(() => {
970
+ let hasChange = false;
971
+ const newHeights = {};
972
+ itemRefs.current.forEach((el, key) => {
973
+ if (el) {
974
+ const strKey = String(key);
975
+ const h = el.offsetHeight;
976
+ if (h > 0) {
977
+ newHeights[strKey] = h;
978
+ if (measuredHeights[strKey] !== h) {
979
+ hasChange = true;
980
+ }
981
+ }
982
+ }
983
+ });
984
+ if (hasChange) {
985
+ setMeasuredHeights((prev) => ({ ...prev, ...newHeights }));
986
+ }
987
+ }, [measuredHeights]);
988
+ (0, import_react3.useLayoutEffect)(() => {
989
+ const handle = requestAnimationFrame(() => {
990
+ measureItems();
991
+ });
992
+ return () => cancelAnimationFrame(handle);
993
+ }, [measureItems]);
994
+ (0, import_react3.useEffect)(() => {
995
+ if (!fresh) return;
996
+ const observer = new ResizeObserver(() => {
997
+ measureItems();
998
+ });
999
+ itemRefs.current.forEach((el) => {
1000
+ if (el) observer.observe(el);
1001
+ });
1002
+ return () => observer.disconnect();
1003
+ }, [fresh, measureItems, normalizedItems]);
1004
+ const columnWidth = (0, import_react3.useMemo)(() => {
1005
+ if (containerWidth <= 0) return 0;
1006
+ const totalGap = (resolvedColumns - 1) * horizontalGap;
1007
+ return Math.max(0, (containerWidth - totalGap) / resolvedColumns);
1008
+ }, [containerWidth, resolvedColumns, horizontalGap]);
1009
+ const { layoutItems, containerHeight, layoutChangeData } = (0, import_react3.useMemo)(() => {
1010
+ const colHeights = new Array(resolvedColumns).fill(0);
1011
+ const changeData = [];
1012
+ const computedList = normalizedItems.map((item) => {
1013
+ var _a, _b;
1014
+ const h = (_b = (_a = item.height) != null ? _a : measuredHeights[String(item.key)]) != null ? _b : 0;
1015
+ let targetCol = 0;
1016
+ if (typeof item.column === "number" && item.column >= 0 && item.column < resolvedColumns) {
1017
+ targetCol = Math.floor(item.column);
1018
+ } else {
1019
+ let minH = colHeights[0];
1020
+ for (let i = 1; i < resolvedColumns; i++) {
1021
+ if (colHeights[i] < minH) {
1022
+ minH = colHeights[i];
1023
+ targetCol = i;
1024
+ }
1025
+ }
1026
+ }
1027
+ const left = targetCol * (columnWidth + horizontalGap);
1028
+ const top = colHeights[targetCol];
1029
+ colHeights[targetCol] += h + (h > 0 ? verticalGap : 0);
1030
+ changeData.push({ key: item.key, column: targetCol });
1031
+ return {
1032
+ item,
1033
+ left,
1034
+ top,
1035
+ width: columnWidth,
1036
+ height: h,
1037
+ column: targetCol
1038
+ };
1039
+ });
1040
+ const totalH = Math.max(0, ...colHeights);
1041
+ return {
1042
+ layoutItems: computedList,
1043
+ containerHeight: totalH,
1044
+ layoutChangeData: changeData
1045
+ };
1046
+ }, [normalizedItems, resolvedColumns, horizontalGap, verticalGap, columnWidth, measuredHeights]);
1047
+ const prevLayoutRef = (0, import_react3.useRef)("");
1048
+ (0, import_react3.useEffect)(() => {
1049
+ const layoutKey = JSON.stringify(layoutChangeData);
1050
+ if (layoutKey !== prevLayoutRef.current) {
1051
+ prevLayoutRef.current = layoutKey;
1052
+ onLayoutChange == null ? void 0 : onLayoutChange(layoutChangeData);
1053
+ }
1054
+ }, [layoutChangeData, onLayoutChange]);
1055
+ const semanticClassNames = (0, import_react3.useMemo)(() => {
1056
+ var _a;
1057
+ if (typeof classNames === "function") {
1058
+ return (_a = classNames({ props })) != null ? _a : {};
1059
+ }
1060
+ return classNames != null ? classNames : {};
1061
+ }, [classNames, props]);
1062
+ const semanticStyles = (0, import_react3.useMemo)(() => {
1063
+ var _a;
1064
+ if (typeof styles === "function") {
1065
+ return (_a = styles({ props })) != null ? _a : {};
1066
+ }
1067
+ return styles != null ? styles : {};
1068
+ }, [styles, props]);
1069
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1070
+ "div",
1071
+ {
1072
+ ref: rootRef,
1073
+ className: ["masonry_root", semanticClassNames.root, className].filter(Boolean).join(" "),
1074
+ style: {
1075
+ minHeight: containerHeight > 0 ? containerHeight : void 0,
1076
+ height: containerHeight > 0 ? containerHeight : void 0,
1077
+ ...semanticStyles.root,
1078
+ ...style
1079
+ },
1080
+ children: layoutItems.map(({ item, left, top, width, height }) => {
1081
+ var _a;
1082
+ const content = (_a = item.children) != null ? _a : itemRender == null ? void 0 : itemRender(item);
1083
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1084
+ "div",
1085
+ {
1086
+ ref: (node) => {
1087
+ if (node) {
1088
+ itemRefs.current.set(item.key, node);
1089
+ } else {
1090
+ itemRefs.current.delete(item.key);
1091
+ }
1092
+ },
1093
+ className: ["masonry_item", semanticClassNames.item].filter(Boolean).join(" "),
1094
+ style: {
1095
+ width: containerWidth > 0 ? width : "100%",
1096
+ height: height > 0 ? height : void 0,
1097
+ transform: containerWidth > 0 ? `translate3d(${left}px, ${top}px, 0)` : void 0,
1098
+ opacity: containerWidth > 0 ? 1 : 0,
1099
+ ...semanticStyles.item
1100
+ },
1101
+ onLoad: measureItems,
1102
+ children: content
1103
+ },
1104
+ item.key
1105
+ );
1106
+ })
1107
+ }
1108
+ );
1109
+ };
1110
+ var Masonry_default = Masonry;
1111
+
1112
+ // BorderBeam/index.tsx
1113
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1114
+ var formatSizeUnit = (value, defaultValue) => {
1115
+ if (value === void 0 || value === null) {
1116
+ return defaultValue;
1117
+ }
1118
+ if (typeof value === "number") {
1119
+ return `${value}px`;
1120
+ }
1121
+ return value;
1122
+ };
1123
+ var buildGradientString = (color) => {
1124
+ if (!color) {
1125
+ return "linear-gradient(to left, #1677ff 0%, transparent 100%)";
1126
+ }
1127
+ if (typeof color === "string") {
1128
+ return `linear-gradient(to left, ${color} 0%, transparent 100%)`;
1129
+ }
1130
+ if (Array.isArray(color)) {
1131
+ if (color.length === 0) {
1132
+ return "linear-gradient(to left, #1677ff 0%, transparent 100%)";
1133
+ }
1134
+ const sorted = [...color].sort((a, b) => a.percent - b.percent);
1135
+ const mappedStops = sorted.map((stop) => {
1136
+ const mappedPercent = Math.min(Math.max(stop.percent / 100 * 80, 0), 80);
1137
+ return `${stop.color} ${mappedPercent.toFixed(2)}%`;
1138
+ });
1139
+ return `linear-gradient(to left, ${mappedStops.join(", ")}, transparent 100%)`;
1140
+ }
1141
+ return "linear-gradient(to left, #1677ff 0%, transparent 100%)";
1142
+ };
1143
+ var BorderBeam = (props) => {
1144
+ const {
1145
+ children,
1146
+ color,
1147
+ count = 1,
1148
+ duration = 6,
1149
+ lineWidth = "1px",
1150
+ outset = "0px",
1151
+ size = 100,
1152
+ className,
1153
+ style,
1154
+ ...restProps
1155
+ } = props;
1156
+ const formattedLineWidth = formatSizeUnit(lineWidth, "1px");
1157
+ const formattedOutset = formatSizeUnit(outset, "0px");
1158
+ const formattedSize = formatSizeUnit(size, "100px");
1159
+ const gradientCss = buildGradientString(color);
1160
+ const maskStyle = {
1161
+ "--border-beam-line-width": formattedLineWidth,
1162
+ "--border-beam-outset": formattedOutset,
1163
+ "--border-beam-size": formattedSize,
1164
+ "--border-beam-duration": `${duration}s`,
1165
+ "--border-beam-gradient": gradientCss
1166
+ };
1167
+ const beamCount = Math.max(1, count);
1168
+ const beams = Array.from({ length: beamCount }).map((_, index) => {
1169
+ const delaySeconds = -(duration * index / beamCount);
1170
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1171
+ "div",
1172
+ {
1173
+ className: "react-public-border-beam-item",
1174
+ style: {
1175
+ animationDelay: `${delaySeconds}s`
1176
+ }
1177
+ },
1178
+ index
1179
+ );
1180
+ });
1181
+ if (children !== void 0) {
1182
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1183
+ "div",
1184
+ {
1185
+ className: `react-public-border-beam-container ${className || ""}`.trim(),
1186
+ style,
1187
+ ...restProps,
1188
+ children: [
1189
+ children,
1190
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "react-public-border-beam-mask", style: maskStyle, children: beams })
1191
+ ]
1192
+ }
1193
+ );
1194
+ }
1195
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1196
+ "div",
1197
+ {
1198
+ className: `react-public-border-beam-mask ${className || ""}`.trim(),
1199
+ style: {
1200
+ ...maskStyle,
1201
+ ...style
1202
+ },
1203
+ ...restProps,
1204
+ children: beams
1205
+ }
1206
+ );
1207
+ };
1208
+ var BorderBeam_default = BorderBeam;
868
1209
  // Annotate the CommonJS export names for ESM import in node:
869
1210
  0 && (module.exports = {
1211
+ BorderBeam,
870
1212
  CollapseBox,
871
1213
  DisabledBox,
1214
+ Masonry,
872
1215
  Splitter
873
1216
  });
874
1217
  //# sourceMappingURL=index.cjs.map