@a-type/ui 1.0.14 → 1.1.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.
- package/dist/cjs/components/card/Card.d.ts +7 -6
- package/dist/cjs/components/card/Card.js +10 -2
- package/dist/cjs/components/card/Card.js.map +1 -1
- package/dist/cjs/components/card/Card.stories.d.ts +2 -0
- package/dist/cjs/components/card/Card.stories.js +19 -1
- package/dist/cjs/components/card/Card.stories.js.map +1 -1
- package/dist/cjs/components/masonry/masonry.d.ts +8 -0
- package/dist/cjs/components/masonry/masonry.js +137 -0
- package/dist/cjs/components/masonry/masonry.js.map +1 -0
- package/dist/cjs/components/masonry/masonry.stories.d.ts +15 -0
- package/dist/cjs/components/masonry/masonry.stories.js +25 -0
- package/dist/cjs/components/masonry/masonry.stories.js.map +1 -0
- package/dist/cjs/components/masonry.d.ts +1 -0
- package/dist/cjs/components/masonry.js +19 -0
- package/dist/cjs/components/masonry.js.map +1 -0
- package/dist/css/main.css +2 -2
- package/dist/esm/components/card/Card.d.ts +7 -6
- package/dist/esm/components/card/Card.js +8 -1
- package/dist/esm/components/card/Card.js.map +1 -1
- package/dist/esm/components/card/Card.stories.d.ts +2 -0
- package/dist/esm/components/card/Card.stories.js +19 -1
- package/dist/esm/components/card/Card.stories.js.map +1 -1
- package/dist/esm/components/masonry/masonry.d.ts +8 -0
- package/dist/esm/components/masonry/masonry.js +133 -0
- package/dist/esm/components/masonry/masonry.js.map +1 -0
- package/dist/esm/components/masonry/masonry.stories.d.ts +15 -0
- package/dist/esm/components/masonry/masonry.stories.js +22 -0
- package/dist/esm/components/masonry/masonry.stories.js.map +1 -0
- package/dist/esm/components/masonry.d.ts +1 -0
- package/dist/esm/components/masonry.js +3 -0
- package/dist/esm/components/masonry.js.map +1 -0
- package/package.json +1 -1
- package/src/components/card/Card.stories.tsx +58 -0
- package/src/components/card/Card.tsx +20 -7
- package/src/components/masonry/masonry.stories.tsx +30 -0
- package/src/components/masonry/masonry.tsx +180 -0
- package/src/components/masonry.ts +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @unocss-include
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
class MasonryLayout {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.containerResizeObserver = null;
|
|
8
|
+
this.containerMutationObserver = null;
|
|
9
|
+
this.container = null;
|
|
10
|
+
this.columns = 0;
|
|
11
|
+
this.attach = (container) => {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
(_a = this.containerResizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
14
|
+
(_b = this.containerMutationObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
|
|
15
|
+
this.container = container;
|
|
16
|
+
this.containerResizeObserver = new ResizeObserver(this.handleContainerResize);
|
|
17
|
+
this.containerMutationObserver = new MutationObserver(this.handleContainerMutation);
|
|
18
|
+
this.containerResizeObserver.observe(container);
|
|
19
|
+
this.containerMutationObserver.observe(container, { childList: true });
|
|
20
|
+
container.style.setProperty('position', 'relative');
|
|
21
|
+
container.style.setProperty('overflow', 'hidden');
|
|
22
|
+
container.style.setProperty('visibility', 'visible');
|
|
23
|
+
container.childNodes.forEach((node) => {
|
|
24
|
+
if (node instanceof HTMLElement) {
|
|
25
|
+
node.style.setProperty('position', 'absolute');
|
|
26
|
+
// hide until laid out
|
|
27
|
+
node.style.setProperty('visibility', 'hidden');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
this.updateFromContainerSize(container.offsetWidth);
|
|
31
|
+
this.relayout();
|
|
32
|
+
return () => {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
(_a = this.containerResizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
35
|
+
(_b = this.containerMutationObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
|
|
36
|
+
container.style.removeProperty('position');
|
|
37
|
+
container.style.removeProperty('overflow');
|
|
38
|
+
this.container = null;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
this.updateConfig = (config) => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
const gapChanged = config.gap !== this.config.gap;
|
|
44
|
+
this.config = config;
|
|
45
|
+
// hacky way to avoid updating twice...
|
|
46
|
+
if (!this.updateFromContainerSize((_b = (_a = this.container) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : 0) &&
|
|
47
|
+
gapChanged) {
|
|
48
|
+
this.relayout();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
this.handleContainerResize = (entries) => {
|
|
52
|
+
const containerWidth = entries[0].contentRect.width;
|
|
53
|
+
this.updateFromContainerSize(containerWidth);
|
|
54
|
+
};
|
|
55
|
+
this.updateFromContainerSize = (containerWidth) => {
|
|
56
|
+
if (typeof this.config.columns === 'function') {
|
|
57
|
+
const newValue = this.config.columns(containerWidth);
|
|
58
|
+
if (newValue !== this.columns) {
|
|
59
|
+
this.columns = newValue;
|
|
60
|
+
this.relayout();
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
this.handleContainerMutation = (entries) => {
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
entry.addedNodes.forEach((node) => {
|
|
69
|
+
if (node instanceof HTMLElement) {
|
|
70
|
+
node.style.setProperty('position', 'absolute');
|
|
71
|
+
// hide until laid out
|
|
72
|
+
node.style.setProperty('visibility', 'hidden');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// TODO: why is this timeout necessary?
|
|
77
|
+
setTimeout(this.relayout, 100);
|
|
78
|
+
};
|
|
79
|
+
this.relayout = () => {
|
|
80
|
+
if (!this.container) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log('relayout');
|
|
84
|
+
const tracks = new Array(this.columns).fill(0);
|
|
85
|
+
const gap = this.config.gap;
|
|
86
|
+
// percentage-based width and x position so that items automatically
|
|
87
|
+
// layout correctly when the container is resized (as long as columns
|
|
88
|
+
// are the same)
|
|
89
|
+
const pixelColumnWidthMinusGap = (this.container.offsetWidth - gap * (this.columns - 1)) / this.columns;
|
|
90
|
+
const columnPercentageWidth = (pixelColumnWidthMinusGap / this.container.offsetWidth) * 100;
|
|
91
|
+
const gapPercentageWidth = (gap / this.container.offsetWidth) * 100;
|
|
92
|
+
const children = Array.from(this.container.children);
|
|
93
|
+
children.forEach((child) => {
|
|
94
|
+
const trackIndex = tracks.indexOf(Math.min(...tracks));
|
|
95
|
+
const x = (columnPercentageWidth + gapPercentageWidth) * trackIndex;
|
|
96
|
+
const y = tracks[trackIndex];
|
|
97
|
+
const width = columnPercentageWidth;
|
|
98
|
+
child.style.setProperty('position', 'absolute');
|
|
99
|
+
child.style.setProperty('visibility', 'visible');
|
|
100
|
+
child.style.setProperty('width', `${width}%`);
|
|
101
|
+
child.style.setProperty('left', `${x}%`);
|
|
102
|
+
child.style.setProperty('top', `${y}px`);
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
child.classList.add('transition-all');
|
|
105
|
+
});
|
|
106
|
+
tracks[trackIndex] += child.offsetHeight + gap;
|
|
107
|
+
});
|
|
108
|
+
this.container.style.setProperty('height', `${Math.max(...tracks) - gap}px`);
|
|
109
|
+
};
|
|
110
|
+
this.columns =
|
|
111
|
+
typeof config.columns === 'function' ? config.columns(0) : config.columns;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const initialStyle = {
|
|
115
|
+
height: 0,
|
|
116
|
+
position: 'relative',
|
|
117
|
+
overflow: 'hidden',
|
|
118
|
+
visibility: 'hidden',
|
|
119
|
+
};
|
|
120
|
+
export function Masonry({ className, children, columns = 3, gap = 16, }) {
|
|
121
|
+
const [layout] = useState(() => new MasonryLayout({ columns, gap }));
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
layout.updateConfig({ columns, gap });
|
|
124
|
+
}, [layout, columns, gap]);
|
|
125
|
+
const ref = useRef(null);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (ref.current) {
|
|
128
|
+
return layout.attach(ref.current);
|
|
129
|
+
}
|
|
130
|
+
}, [layout, ref]);
|
|
131
|
+
return (_jsx("div", Object.assign({ ref: ref, style: initialStyle, className: className }, { children: children })));
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=masonry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"masonry.js","sourceRoot":"","sources":["../../../../src/components/masonry/masonry.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react';
|
|
2
|
+
import { Masonry } from './masonry.js';
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof Masonry;
|
|
6
|
+
argTypes: {};
|
|
7
|
+
parameters: {
|
|
8
|
+
controls: {
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof Masonry>;
|
|
15
|
+
export declare const Default: Story;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @unocss-include
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Masonry } from './masonry.js';
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Masonry',
|
|
6
|
+
component: Masonry,
|
|
7
|
+
argTypes: {},
|
|
8
|
+
parameters: {
|
|
9
|
+
controls: { expanded: true },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export default meta;
|
|
13
|
+
const sizes = Array.from({ length: 100 }, (_, i) => {
|
|
14
|
+
const size = 100 + Math.floor(Math.random() * 100);
|
|
15
|
+
return (_jsx("div", Object.assign({ className: "bg-gray-5 rounded-lg", style: { height: size } }, { children: size }), i));
|
|
16
|
+
});
|
|
17
|
+
export const Default = {
|
|
18
|
+
render(props) {
|
|
19
|
+
return _jsx(Masonry, Object.assign({}, props, { children: sizes }));
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=masonry.stories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"masonry.stories.js","sourceRoot":"","sources":["../../../../src/components/masonry/masonry.stories.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,IAAI,GAAG;IACZ,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,OAAO;IAClB,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;KAC5B;CAC8B,CAAC;AAEjC,eAAe,IAAI,CAAC;AAIpB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;IAClD,MAAM,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IACnD,OAAO,CACN,4BAAa,SAAS,EAAC,sBAAsB,EAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,gBACnE,IAAI,KADI,CAAC,CAEL,CACN,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,OAAO,GAAU;IAC7B,MAAM,CAAC,KAAK;QACX,OAAO,KAAC,OAAO,oBAAK,KAAK,cAAG,KAAK,IAAW,CAAC;IAC9C,CAAC;CACD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './masonry/masonry.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"masonry.js","sourceRoot":"","sources":["../../../src/components/masonry.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import {
|
|
3
|
+
Card,
|
|
3
4
|
CardActions,
|
|
4
5
|
CardContent,
|
|
5
6
|
CardFooter,
|
|
7
|
+
cardGridColumns,
|
|
6
8
|
CardImage,
|
|
7
9
|
CardMain,
|
|
8
10
|
CardMenu,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
} from './Card.js';
|
|
12
14
|
import { Button } from '../button.js';
|
|
13
15
|
import { Icon } from '../icon.js';
|
|
16
|
+
import { useState } from 'react';
|
|
14
17
|
|
|
15
18
|
const meta = {
|
|
16
19
|
title: 'Card',
|
|
@@ -153,3 +156,58 @@ export const AsChildNonInteractive: Story = {
|
|
|
153
156
|
</CardRoot>
|
|
154
157
|
),
|
|
155
158
|
};
|
|
159
|
+
|
|
160
|
+
export const Grid: Story = {
|
|
161
|
+
render: () => {
|
|
162
|
+
const [sizes, setSizes] = useState(() =>
|
|
163
|
+
Array.from(
|
|
164
|
+
{ length: 40 },
|
|
165
|
+
(_, i) => 50 + Math.floor(Math.random() * 300),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const remove = (index: number) =>
|
|
169
|
+
setSizes((v) => v.filter((_, i) => i !== index));
|
|
170
|
+
return (
|
|
171
|
+
<Card.Grid>
|
|
172
|
+
{sizes.map((size, i) => (
|
|
173
|
+
<GridCard key={i} size={size} remove={() => remove(i)} />
|
|
174
|
+
))}
|
|
175
|
+
</Card.Grid>
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const GridCompact: Story = {
|
|
181
|
+
render: () => {
|
|
182
|
+
const [sizes, setSizes] = useState(() =>
|
|
183
|
+
Array.from(
|
|
184
|
+
{ length: 40 },
|
|
185
|
+
(_, i) => 50 + Math.floor(Math.random() * 300),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
const remove = (index: number) =>
|
|
189
|
+
setSizes((v) => v.filter((_, i) => i !== index));
|
|
190
|
+
return (
|
|
191
|
+
<Card.Grid columns={cardGridColumns.small}>
|
|
192
|
+
{sizes.map((size, i) => (
|
|
193
|
+
<GridCard key={i} size={size} remove={() => remove(i)} />
|
|
194
|
+
))}
|
|
195
|
+
</Card.Grid>
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
function GridCard({ size, remove }: { size: number; remove: () => void }) {
|
|
201
|
+
return (
|
|
202
|
+
<CardRoot style={{ height: size }}>
|
|
203
|
+
<CardMain>
|
|
204
|
+
<CardTitle>{size}</CardTitle>
|
|
205
|
+
</CardMain>
|
|
206
|
+
<CardActions>
|
|
207
|
+
<Button size="small" onClick={remove}>
|
|
208
|
+
Delete
|
|
209
|
+
</Button>
|
|
210
|
+
</CardActions>
|
|
211
|
+
</CardRoot>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { HTMLProps, MouseEvent, ReactNode, forwardRef } from 'react';
|
|
2
|
-
import { withClassName } from '../../hooks.js';
|
|
2
|
+
import { useStableCallback, withClassName } from '../../hooks.js';
|
|
3
3
|
import { Slot } from '@radix-ui/react-slot';
|
|
4
|
-
import classNames from 'clsx';
|
|
4
|
+
import classNames, { clsx } from 'clsx';
|
|
5
5
|
import { SlotDiv } from '../utility/SlotDiv.js';
|
|
6
|
+
import { Masonry, MasonryProps } from '../masonry/masonry.js';
|
|
6
7
|
|
|
7
8
|
export const CardRoot = withClassName(
|
|
8
9
|
'div',
|
|
@@ -74,11 +75,23 @@ export const CardMenu = withClassName(
|
|
|
74
75
|
'layer-components:(mr-0 ml-auto my-auto flex flex-row gap-1 items-center bg-overlay rounded-full p-0)',
|
|
75
76
|
);
|
|
76
77
|
|
|
77
|
-
export const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
export const cardGridColumns = {
|
|
79
|
+
default: (size: number) => (size < 480 ? 1 : size < 800 ? 2 : 3),
|
|
80
|
+
small: (size: number) =>
|
|
81
|
+
size < 320 ? 1 : size < 480 ? 2 : size < 800 ? 3 : 4,
|
|
82
|
+
};
|
|
83
|
+
export const CardGrid = ({
|
|
84
|
+
children,
|
|
85
|
+
className,
|
|
86
|
+
columns = cardGridColumns.default,
|
|
87
|
+
gap,
|
|
88
|
+
}: MasonryProps) => {
|
|
89
|
+
return (
|
|
90
|
+
<Masonry className={className} columns={columns} gap={gap}>
|
|
91
|
+
{children}
|
|
92
|
+
</Masonry>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
82
95
|
|
|
83
96
|
export const Card = Object.assign(CardRoot, {
|
|
84
97
|
Main: CardMain,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Masonry } from './masonry.js';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Masonry',
|
|
6
|
+
component: Masonry,
|
|
7
|
+
argTypes: {},
|
|
8
|
+
parameters: {
|
|
9
|
+
controls: { expanded: true },
|
|
10
|
+
},
|
|
11
|
+
} satisfies Meta<typeof Masonry>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof Masonry>;
|
|
16
|
+
|
|
17
|
+
const sizes = Array.from({ length: 100 }, (_, i) => {
|
|
18
|
+
const size = 100 + Math.floor(Math.random() * 100);
|
|
19
|
+
return (
|
|
20
|
+
<div key={i} className="bg-gray-5 rounded-lg" style={{ height: size }}>
|
|
21
|
+
{size}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
render(props) {
|
|
28
|
+
return <Masonry {...props}>{sizes}</Masonry>;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface MasonryLayoutConfig {
|
|
4
|
+
columns: number | ((containerWidth: number) => number);
|
|
5
|
+
gap: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class MasonryLayout {
|
|
9
|
+
private containerResizeObserver: ResizeObserver | null = null;
|
|
10
|
+
private containerMutationObserver: MutationObserver | null = null;
|
|
11
|
+
|
|
12
|
+
private container: HTMLElement | null = null;
|
|
13
|
+
|
|
14
|
+
private columns: number = 0;
|
|
15
|
+
|
|
16
|
+
constructor(private config: MasonryLayoutConfig) {
|
|
17
|
+
this.columns =
|
|
18
|
+
typeof config.columns === 'function' ? config.columns(0) : config.columns;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attach = (container: HTMLElement) => {
|
|
22
|
+
this.containerResizeObserver?.disconnect();
|
|
23
|
+
this.containerMutationObserver?.disconnect();
|
|
24
|
+
|
|
25
|
+
this.container = container;
|
|
26
|
+
|
|
27
|
+
this.containerResizeObserver = new ResizeObserver(
|
|
28
|
+
this.handleContainerResize,
|
|
29
|
+
);
|
|
30
|
+
this.containerMutationObserver = new MutationObserver(
|
|
31
|
+
this.handleContainerMutation,
|
|
32
|
+
);
|
|
33
|
+
this.containerResizeObserver.observe(container);
|
|
34
|
+
this.containerMutationObserver.observe(container, { childList: true });
|
|
35
|
+
|
|
36
|
+
container.style.setProperty('position', 'relative');
|
|
37
|
+
container.style.setProperty('overflow', 'hidden');
|
|
38
|
+
container.style.setProperty('visibility', 'visible');
|
|
39
|
+
container.childNodes.forEach((node) => {
|
|
40
|
+
if (node instanceof HTMLElement) {
|
|
41
|
+
node.style.setProperty('position', 'absolute');
|
|
42
|
+
// hide until laid out
|
|
43
|
+
node.style.setProperty('visibility', 'hidden');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.updateFromContainerSize(container.offsetWidth);
|
|
48
|
+
|
|
49
|
+
this.relayout();
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
this.containerResizeObserver?.disconnect();
|
|
53
|
+
this.containerMutationObserver?.disconnect();
|
|
54
|
+
container.style.removeProperty('position');
|
|
55
|
+
container.style.removeProperty('overflow');
|
|
56
|
+
this.container = null;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
updateConfig = (config: MasonryLayoutConfig) => {
|
|
61
|
+
const gapChanged = config.gap !== this.config.gap;
|
|
62
|
+
this.config = config;
|
|
63
|
+
// hacky way to avoid updating twice...
|
|
64
|
+
if (
|
|
65
|
+
!this.updateFromContainerSize(this.container?.offsetWidth ?? 0) &&
|
|
66
|
+
gapChanged
|
|
67
|
+
) {
|
|
68
|
+
this.relayout();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
private handleContainerResize = (entries: ResizeObserverEntry[]) => {
|
|
73
|
+
const containerWidth = entries[0].contentRect.width;
|
|
74
|
+
this.updateFromContainerSize(containerWidth);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
private updateFromContainerSize = (containerWidth: number) => {
|
|
78
|
+
if (typeof this.config.columns === 'function') {
|
|
79
|
+
const newValue = this.config.columns(containerWidth);
|
|
80
|
+
if (newValue !== this.columns) {
|
|
81
|
+
this.columns = newValue;
|
|
82
|
+
this.relayout();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private handleContainerMutation = (entries: MutationRecord[]) => {
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
entry.addedNodes.forEach((node) => {
|
|
92
|
+
if (node instanceof HTMLElement) {
|
|
93
|
+
node.style.setProperty('position', 'absolute');
|
|
94
|
+
// hide until laid out
|
|
95
|
+
node.style.setProperty('visibility', 'hidden');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// TODO: why is this timeout necessary?
|
|
100
|
+
setTimeout(this.relayout, 100);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
private relayout = () => {
|
|
104
|
+
if (!this.container) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('relayout');
|
|
109
|
+
|
|
110
|
+
const tracks = new Array(this.columns).fill(0);
|
|
111
|
+
const gap = this.config.gap;
|
|
112
|
+
// percentage-based width and x position so that items automatically
|
|
113
|
+
// layout correctly when the container is resized (as long as columns
|
|
114
|
+
// are the same)
|
|
115
|
+
const pixelColumnWidthMinusGap =
|
|
116
|
+
(this.container.offsetWidth - gap * (this.columns - 1)) / this.columns;
|
|
117
|
+
const columnPercentageWidth =
|
|
118
|
+
(pixelColumnWidthMinusGap / this.container.offsetWidth) * 100;
|
|
119
|
+
const gapPercentageWidth = (gap / this.container.offsetWidth) * 100;
|
|
120
|
+
|
|
121
|
+
const children = Array.from(this.container.children) as HTMLElement[];
|
|
122
|
+
children.forEach((child) => {
|
|
123
|
+
const trackIndex = tracks.indexOf(Math.min(...tracks));
|
|
124
|
+
const x = (columnPercentageWidth + gapPercentageWidth) * trackIndex;
|
|
125
|
+
const y = tracks[trackIndex];
|
|
126
|
+
const width = columnPercentageWidth;
|
|
127
|
+
child.style.setProperty('position', 'absolute');
|
|
128
|
+
child.style.setProperty('visibility', 'visible');
|
|
129
|
+
child.style.setProperty('width', `${width}%`);
|
|
130
|
+
child.style.setProperty('left', `${x}%`);
|
|
131
|
+
child.style.setProperty('top', `${y}px`);
|
|
132
|
+
requestAnimationFrame(() => {
|
|
133
|
+
child.classList.add('transition-all');
|
|
134
|
+
});
|
|
135
|
+
tracks[trackIndex] += child.offsetHeight + gap;
|
|
136
|
+
});
|
|
137
|
+
this.container.style.setProperty(
|
|
138
|
+
'height',
|
|
139
|
+
`${Math.max(...tracks) - gap}px`,
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface MasonryProps {
|
|
145
|
+
children: ReactNode;
|
|
146
|
+
className?: string;
|
|
147
|
+
columns?: number | ((containerWidth: number) => number);
|
|
148
|
+
gap?: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const initialStyle: CSSProperties = {
|
|
152
|
+
height: 0,
|
|
153
|
+
position: 'relative',
|
|
154
|
+
overflow: 'hidden',
|
|
155
|
+
visibility: 'hidden',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export function Masonry({
|
|
159
|
+
className,
|
|
160
|
+
children,
|
|
161
|
+
columns = 3,
|
|
162
|
+
gap = 16,
|
|
163
|
+
}: MasonryProps) {
|
|
164
|
+
const [layout] = useState(() => new MasonryLayout({ columns, gap }));
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
layout.updateConfig({ columns, gap });
|
|
167
|
+
}, [layout, columns, gap]);
|
|
168
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (ref.current) {
|
|
171
|
+
return layout.attach(ref.current);
|
|
172
|
+
}
|
|
173
|
+
}, [layout, ref]);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div ref={ref} style={initialStyle} className={className}>
|
|
177
|
+
{children}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './masonry/masonry.js';
|