@boostdev/design-system-components 2.4.1 → 2.5.0
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/AGENTS.md +4 -20
- package/README.md +3 -23
- package/dist/client.cjs +128 -62
- package/dist/client.css +537 -537
- package/dist/client.d.cts +11 -1
- package/dist/client.d.ts +11 -1
- package/dist/client.js +128 -62
- package/dist/index.cjs +128 -62
- package/dist/index.css +537 -537
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +128 -62
- package/dist/web-components/index.js +36 -4
- package/package.json +2 -10
- package/src/components/layout/Grid/Grid.mdx +20 -2
- package/src/components/layout/Grid/Grid.spec.tsx +206 -4
- package/src/components/layout/Grid/Grid.tsx +15 -1
- package/src/components/layout/Grid/masonry.ts +148 -10
- package/dist/native/index.cjs +0 -4337
- package/dist/native/index.d.cts +0 -574
- package/dist/native/index.d.ts +0 -574
- package/dist/native/index.js +0 -4421
- package/src/components/interaction/Button/Button.native.mdx +0 -91
- package/src/components/interaction/Button/Button.native.spec.tsx +0 -55
- package/src/components/interaction/Button/Button.native.stories.tsx +0 -141
- package/src/components/interaction/Button/Button.native.tsx +0 -105
- package/src/components/interaction/Dialog/Dialog.native.mdx +0 -61
- package/src/components/interaction/Dialog/Dialog.native.spec.tsx +0 -73
- package/src/components/interaction/Dialog/Dialog.native.stories.tsx +0 -53
- package/src/components/interaction/Dialog/Dialog.native.tsx +0 -128
- package/src/components/interaction/Drawer/Drawer.native.mdx +0 -58
- package/src/components/interaction/Drawer/Drawer.native.spec.tsx +0 -81
- package/src/components/interaction/Drawer/Drawer.native.stories.tsx +0 -33
- package/src/components/interaction/Drawer/Drawer.native.tsx +0 -175
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +0 -74
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +0 -78
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +0 -51
- package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +0 -254
- package/src/components/interaction/Popover/Popover.native.mdx +0 -61
- package/src/components/interaction/Popover/Popover.native.spec.tsx +0 -73
- package/src/components/interaction/Popover/Popover.native.stories.tsx +0 -44
- package/src/components/interaction/Popover/Popover.native.tsx +0 -87
- package/src/components/interaction/Rating/Rating.native.mdx +0 -55
- package/src/components/interaction/Rating/Rating.native.spec.tsx +0 -38
- package/src/components/interaction/Rating/Rating.native.stories.tsx +0 -37
- package/src/components/interaction/Rating/Rating.native.tsx +0 -50
- package/src/components/interaction/Toast/Toast.native.mdx +0 -81
- package/src/components/interaction/Toast/Toast.native.spec.tsx +0 -80
- package/src/components/interaction/Toast/Toast.native.stories.tsx +0 -47
- package/src/components/interaction/Toast/Toast.native.tsx +0 -202
- package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +0 -74
- package/src/components/interaction/form/Checkbox/Checkbox.native.spec.tsx +0 -42
- package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +0 -34
- package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +0 -80
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +0 -78
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +0 -57
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +0 -96
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +0 -61
- package/src/components/interaction/form/Combobox/Combobox.native.mdx +0 -86
- package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +0 -60
- package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +0 -99
- package/src/components/interaction/form/Combobox/Combobox.native.tsx +0 -211
- package/src/components/interaction/form/FileInput/FileInput.native.mdx +0 -86
- package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +0 -73
- package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +0 -69
- package/src/components/interaction/form/FileInput/FileInput.native.tsx +0 -129
- package/src/components/interaction/form/FormInput/FormInput.native.mdx +0 -81
- package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +0 -51
- package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +0 -54
- package/src/components/interaction/form/FormInput/FormInput.native.tsx +0 -122
- package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +0 -87
- package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +0 -63
- package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +0 -63
- package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +0 -198
- package/src/components/interaction/form/Radio/Radio.native.spec.tsx +0 -79
- package/src/components/interaction/form/Radio/Radio.native.stories.tsx +0 -44
- package/src/components/interaction/form/Radio/Radio.native.tsx +0 -79
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +0 -79
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +0 -66
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +0 -96
- package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +0 -61
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +0 -78
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +0 -82
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +0 -99
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +0 -121
- package/src/components/interaction/form/Select/Select.native.mdx +0 -85
- package/src/components/interaction/form/Select/Select.native.spec.tsx +0 -64
- package/src/components/interaction/form/Select/Select.native.stories.tsx +0 -75
- package/src/components/interaction/form/Select/Select.native.tsx +0 -252
- package/src/components/interaction/form/Slider/Slider.native.mdx +0 -83
- package/src/components/interaction/form/Slider/Slider.native.spec.tsx +0 -51
- package/src/components/interaction/form/Slider/Slider.native.stories.tsx +0 -58
- package/src/components/interaction/form/Slider/Slider.native.tsx +0 -188
- package/src/components/interaction/form/Switch/Switch.native.spec.tsx +0 -60
- package/src/components/interaction/form/Switch/Switch.native.stories.tsx +0 -33
- package/src/components/interaction/form/Switch/Switch.native.tsx +0 -59
- package/src/components/interaction/form/Textarea/Textarea.native.mdx +0 -76
- package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +0 -50
- package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +0 -50
- package/src/components/interaction/form/Textarea/Textarea.native.tsx +0 -108
- package/src/components/interaction/form/atoms/Label.native.spec.tsx +0 -39
- package/src/components/interaction/form/atoms/Label.native.stories.tsx +0 -18
- package/src/components/interaction/form/atoms/Label.native.tsx +0 -29
- package/src/components/interaction/form/atoms/Message.native.spec.tsx +0 -30
- package/src/components/interaction/form/atoms/Message.native.stories.tsx +0 -22
- package/src/components/interaction/form/atoms/Message.native.tsx +0 -34
- package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +0 -83
- package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +0 -44
- package/src/components/layout/ButtonGroup/ButtonGroup.native.tsx +0 -44
- package/src/components/layout/Card/Card.native.spec.tsx +0 -102
- package/src/components/layout/Card/Card.native.stories.tsx +0 -51
- package/src/components/layout/Card/Card.native.tsx +0 -89
- package/src/components/layout/IconWrapper/IconWrapper.native.spec.tsx +0 -39
- package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +0 -37
- package/src/components/layout/IconWrapper/IconWrapper.native.tsx +0 -20
- package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +0 -51
- package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +0 -36
- package/src/components/layout/SectionHeader/SectionHeader.native.tsx +0 -79
- package/src/components/ui/Accordion/Accordion.native.mdx +0 -65
- package/src/components/ui/Accordion/Accordion.native.spec.tsx +0 -69
- package/src/components/ui/Accordion/Accordion.native.stories.tsx +0 -52
- package/src/components/ui/Accordion/Accordion.native.tsx +0 -141
- package/src/components/ui/Alert/Alert.native.mdx +0 -62
- package/src/components/ui/Alert/Alert.native.spec.tsx +0 -76
- package/src/components/ui/Alert/Alert.native.stories.tsx +0 -41
- package/src/components/ui/Alert/Alert.native.tsx +0 -94
- package/src/components/ui/Avatar/Avatar.native.mdx +0 -50
- package/src/components/ui/Avatar/Avatar.native.spec.tsx +0 -47
- package/src/components/ui/Avatar/Avatar.native.stories.tsx +0 -32
- package/src/components/ui/Avatar/Avatar.native.tsx +0 -85
- package/src/components/ui/Badge/Badge.native.mdx +0 -54
- package/src/components/ui/Badge/Badge.native.spec.tsx +0 -45
- package/src/components/ui/Badge/Badge.native.stories.tsx +0 -34
- package/src/components/ui/Badge/Badge.native.tsx +0 -50
- package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +0 -52
- package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +0 -49
- package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +0 -46
- package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +0 -83
- package/src/components/ui/Calendar/Calendar.native.mdx +0 -58
- package/src/components/ui/Calendar/Calendar.native.spec.tsx +0 -92
- package/src/components/ui/Calendar/Calendar.native.stories.tsx +0 -45
- package/src/components/ui/Calendar/Calendar.native.tsx +0 -294
- package/src/components/ui/Carousel/Carousel.native.mdx +0 -55
- package/src/components/ui/Carousel/Carousel.native.spec.tsx +0 -55
- package/src/components/ui/Carousel/Carousel.native.stories.tsx +0 -59
- package/src/components/ui/Carousel/Carousel.native.tsx +0 -184
- package/src/components/ui/Collapsible/Collapsible.native.mdx +0 -53
- package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +0 -68
- package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +0 -48
- package/src/components/ui/Collapsible/Collapsible.native.tsx +0 -139
- package/src/components/ui/DescriptionList/DescriptionList.native.mdx +0 -50
- package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +0 -37
- package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +0 -48
- package/src/components/ui/DescriptionList/DescriptionList.native.tsx +0 -54
- package/src/components/ui/Link/Link.native.mdx +0 -53
- package/src/components/ui/Link/Link.native.spec.tsx +0 -35
- package/src/components/ui/Link/Link.native.stories.tsx +0 -46
- package/src/components/ui/Link/Link.native.tsx +0 -48
- package/src/components/ui/Loading/Loading.native.spec.tsx +0 -24
- package/src/components/ui/Loading/Loading.native.stories.tsx +0 -31
- package/src/components/ui/Loading/Loading.native.tsx +0 -29
- package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +0 -85
- package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +0 -37
- package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +0 -76
- package/src/components/ui/Pagination/Pagination.native.mdx +0 -62
- package/src/components/ui/Pagination/Pagination.native.spec.tsx +0 -86
- package/src/components/ui/Pagination/Pagination.native.stories.tsx +0 -47
- package/src/components/ui/Pagination/Pagination.native.tsx +0 -157
- package/src/components/ui/Progress/Progress.native.spec.tsx +0 -56
- package/src/components/ui/Progress/Progress.native.stories.tsx +0 -34
- package/src/components/ui/Progress/Progress.native.tsx +0 -84
- package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +0 -63
- package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +0 -39
- package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +0 -36
- package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +0 -176
- package/src/components/ui/Separator/Separator.native.spec.tsx +0 -39
- package/src/components/ui/Separator/Separator.native.stories.tsx +0 -38
- package/src/components/ui/Separator/Separator.native.tsx +0 -32
- package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +0 -39
- package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +0 -31
- package/src/components/ui/Skeleton/Skeleton.native.tsx +0 -42
- package/src/components/ui/Table/Table.native.mdx +0 -72
- package/src/components/ui/Table/Table.native.spec.tsx +0 -83
- package/src/components/ui/Table/Table.native.stories.tsx +0 -55
- package/src/components/ui/Table/Table.native.tsx +0 -196
- package/src/components/ui/Tabs/Tabs.native.mdx +0 -65
- package/src/components/ui/Tabs/Tabs.native.spec.tsx +0 -65
- package/src/components/ui/Tabs/Tabs.native.stories.tsx +0 -57
- package/src/components/ui/Tabs/Tabs.native.tsx +0 -133
- package/src/components/ui/Tooltip/Tooltip.native.mdx +0 -47
- package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +0 -37
- package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +0 -38
- package/src/components/ui/Tooltip/Tooltip.native.tsx +0 -57
- package/src/components/ui/Typography/Typography.native.mdx +0 -56
- package/src/components/ui/Typography/Typography.native.spec.tsx +0 -49
- package/src/components/ui/Typography/Typography.native.stories.tsx +0 -36
- package/src/components/ui/Typography/Typography.native.tsx +0 -65
- package/src/native/ThemeContext.tsx +0 -28
- package/src/native/tokens.ts +0 -13
- package/src/native.ts +0 -76
- package/src/stories/ReactNative.mdx +0 -156
|
@@ -151,16 +151,17 @@ function resolvePreferredColumns(container, value, totalCols) {
|
|
|
151
151
|
if (Number.isFinite(n) && n > 0) return Math.min(totalCols, n);
|
|
152
152
|
return Math.max(1, Math.round(totalCols * NAMED_SPAN_FRACTION[value]));
|
|
153
153
|
}
|
|
154
|
-
var
|
|
154
|
+
var TARGET_MAX_IMPLICIT_ROWS = 5e3;
|
|
155
155
|
function supportsNativeMasonry() {
|
|
156
156
|
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
|
|
157
157
|
return CSS.supports("display", "grid-lanes") || CSS.supports("grid-template-rows", "masonry");
|
|
158
158
|
}
|
|
159
159
|
var ORIGINAL_COL_ATTR = "data-bds-masonry-original-column";
|
|
160
|
+
var LAST_SPAN_ATTR = "data-bds-masonry-span";
|
|
161
|
+
var UNIT_ATTR = "data-bds-masonry-unit";
|
|
160
162
|
function applyMasonryLayout(container, rawChildren, options = {}) {
|
|
161
163
|
const cs = window.getComputedStyle(container);
|
|
162
164
|
const targetGap = parseFloat(cs.columnGap) || 0;
|
|
163
|
-
container.style.gridAutoRows = `${ROW_UNIT_PX}px`;
|
|
164
165
|
container.style.rowGap = "0px";
|
|
165
166
|
container.style.gridAutoFlow = "dense";
|
|
166
167
|
container.style.overflowAnchor = "none";
|
|
@@ -198,9 +199,38 @@ function applyMasonryLayout(container, rawChildren, options = {}) {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
const heights = children.map((child) => child.offsetHeight);
|
|
202
|
+
const cols = countGridColumns(container) || 1;
|
|
203
|
+
const totalRowSum = heights.reduce(
|
|
204
|
+
(s, h) => s + Math.max(1, Math.ceil(h + targetGap)),
|
|
205
|
+
0
|
|
206
|
+
);
|
|
207
|
+
const projectedRows = totalRowSum / cols;
|
|
208
|
+
const autoUnit = Math.max(
|
|
209
|
+
1,
|
|
210
|
+
Math.ceil(projectedRows / TARGET_MAX_IMPLICIT_ROWS)
|
|
211
|
+
);
|
|
212
|
+
const rowUnit = options.rowUnitPx !== void 0 && options.rowUnitPx > 0 ? Math.floor(options.rowUnitPx) : autoUnit;
|
|
213
|
+
container.style.gridAutoRows = `${rowUnit}px`;
|
|
214
|
+
const prevUnit = container.getAttribute(UNIT_ATTR);
|
|
215
|
+
if (prevUnit !== String(rowUnit)) {
|
|
216
|
+
for (const child of children) child.removeAttribute(LAST_SPAN_ATTR);
|
|
217
|
+
container.setAttribute(UNIT_ATTR, String(rowUnit));
|
|
218
|
+
}
|
|
219
|
+
const spans = heights.map(
|
|
220
|
+
(h) => Math.max(1, Math.ceil((h + targetGap) / rowUnit))
|
|
221
|
+
);
|
|
222
|
+
let changed = false;
|
|
223
|
+
for (let i3 = 0; i3 < children.length; i3++) {
|
|
224
|
+
const prev = children[i3].getAttribute(LAST_SPAN_ATTR);
|
|
225
|
+
if (prev !== String(spans[i3])) {
|
|
226
|
+
changed = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!changed) return;
|
|
201
231
|
for (let i3 = 0; i3 < children.length; i3++) {
|
|
202
|
-
|
|
203
|
-
children[i3].
|
|
232
|
+
children[i3].style.gridRowEnd = `span ${spans[i3]}`;
|
|
233
|
+
children[i3].setAttribute(LAST_SPAN_ATTR, String(spans[i3]));
|
|
204
234
|
}
|
|
205
235
|
}
|
|
206
236
|
function countGridColumns(container) {
|
|
@@ -227,6 +257,7 @@ function clearMasonryLayout(container, rawChildren) {
|
|
|
227
257
|
container.style.gridAutoFlow = "";
|
|
228
258
|
container.style.rowGap = "";
|
|
229
259
|
container.style.overflowAnchor = "";
|
|
260
|
+
container.removeAttribute(UNIT_ATTR);
|
|
230
261
|
for (const child of rawChildren) {
|
|
231
262
|
if (child.nodeType !== 1) continue;
|
|
232
263
|
child.style.gridRow = "";
|
|
@@ -234,6 +265,7 @@ function clearMasonryLayout(container, rawChildren) {
|
|
|
234
265
|
child.style.gridRowEnd = "";
|
|
235
266
|
child.style.alignSelf = "";
|
|
236
267
|
child.style.overflowAnchor = "";
|
|
268
|
+
child.removeAttribute(LAST_SPAN_ATTR);
|
|
237
269
|
restoreOriginalColumn(child);
|
|
238
270
|
}
|
|
239
271
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "BoostDev React component library: accessible, token-driven components built on @boostdev/design-system-foundation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"React",
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
-
"react-native": "./dist/native/index.js",
|
|
30
29
|
"types": "./dist/index.d.ts",
|
|
31
30
|
"import": "./dist/index.js",
|
|
32
31
|
"require": "./dist/index.cjs"
|
|
@@ -262,7 +261,6 @@
|
|
|
262
261
|
"./css/section-header": "./src/components/layout/SectionHeader/SectionHeader.module.css"
|
|
263
262
|
},
|
|
264
263
|
"main": "./dist/index.js",
|
|
265
|
-
"react-native": "./dist/native/index.js",
|
|
266
264
|
"types": "./dist/index.d.ts",
|
|
267
265
|
"files": [
|
|
268
266
|
"dist",
|
|
@@ -297,14 +295,12 @@
|
|
|
297
295
|
"@types/node": "^25.5.0",
|
|
298
296
|
"@types/react": "^19",
|
|
299
297
|
"@types/react-dom": "^19",
|
|
300
|
-
"@types/react-native": "^0.73.0",
|
|
301
298
|
"@vitejs/plugin-react": "^5.1.4",
|
|
302
299
|
"eslint": "^9",
|
|
303
300
|
"git-cliff": "^2.12.0",
|
|
304
301
|
"identity-obj-proxy": "^3.0.0",
|
|
305
302
|
"jsdom": "^28.1.0",
|
|
306
303
|
"lit": "^3.3.2",
|
|
307
|
-
"react-native-web": "^0.21.2",
|
|
308
304
|
"remark-gfm": "^4.0.1",
|
|
309
305
|
"storybook": "^8.6.18",
|
|
310
306
|
"storybook-addon-tag-badges": "^1.4.0",
|
|
@@ -321,15 +317,11 @@
|
|
|
321
317
|
"peerDependencies": {
|
|
322
318
|
"@boostdev/design-system-foundation": ">=2.0.0",
|
|
323
319
|
"react": ">=18",
|
|
324
|
-
"react-dom": ">=18"
|
|
325
|
-
"react-native": ">=0.70"
|
|
320
|
+
"react-dom": ">=18"
|
|
326
321
|
},
|
|
327
322
|
"peerDependenciesMeta": {
|
|
328
323
|
"react-dom": {
|
|
329
324
|
"optional": true
|
|
330
|
-
},
|
|
331
|
-
"react-native": {
|
|
332
|
-
"optional": true
|
|
333
325
|
}
|
|
334
326
|
},
|
|
335
327
|
"publishConfig": {
|
|
@@ -131,7 +131,7 @@ Set `isMasonry` on any `Grid` to switch to a masonry (a.k.a. Pinterest-style) la
|
|
|
131
131
|
Masonry targets [CSS Grid Layout Module Level 3](https://www.w3.org/TR/css-grid-3/) ("grid lanes"):
|
|
132
132
|
|
|
133
133
|
- **Native path** — when the browser supports `display: grid-lanes` or `grid-template-rows: masonry`, CSS handles everything and the JS polyfill short-circuits. Currently Safari Technology Preview only; the polyfill runs in all other browsers.
|
|
134
|
-
- **Polyfill path** — row-span packing. The container's row tracks are collapsed to a
|
|
134
|
+
- **Polyfill path** — row-span packing. The container's row tracks are collapsed to a small auto-track with `grid-auto-flow: dense`, and each item is given a `grid-row-end: span ceil((height + gap) / rowUnit)`. Items stay in **normal document flow** (no absolute positioning), so surrounding content — footers, scroll anchors, page margins — lays out correctly around the grid. The row-unit defaults to 1 px; for very long galleries the polyfill auto-grows it — see [Row-unit auto-tuner](#row-unit-auto-tuner).
|
|
135
135
|
|
|
136
136
|
The polyfill re-runs on child size changes (`ResizeObserver` on each child) and child addition/removal (`MutationObserver` on the container). The container itself is not observed — setting row-spans changes the container block-size, which would loop back through the observer.
|
|
137
137
|
|
|
@@ -145,7 +145,7 @@ The polyfill re-runs on child size changes (`ResizeObserver` on each child) and
|
|
|
145
145
|
|
|
146
146
|
- **Initial layout** — three passes (clear, batch-read all heights, batch-write all spans). Height reads and writes are each grouped to avoid O(N) forced reflows; the measured cost is one layout pass regardless of item count.
|
|
147
147
|
- **Re-layout** — on child resize or mutation the polyfill is scheduled via `requestAnimationFrame`, so bursts of observer callbacks coalesce into a single re-run per frame.
|
|
148
|
-
- **Ceil overshoot** — the 1 px row track
|
|
148
|
+
- **Ceil overshoot** — at the default 1 px row track each item's span can overshoot by up to 1 px. Not visible in practice (row-gap is 0; the gap is baked into each item's span). The auto-tuner may grow the row-unit for very long galleries — overshoot is then bounded by the chosen unit (still imperceptible at the unit sizes the auto-tuner picks).
|
|
149
149
|
- **Long lists** — for galleries with hundreds of items, virtualisation is still the correct answer; masonry is orthogonal.
|
|
150
150
|
|
|
151
151
|
### SSR / hydration
|
|
@@ -237,6 +237,24 @@ Rules:
|
|
|
237
237
|
|
|
238
238
|
Column detection runs inside the masonry polyfill's `useLayoutEffect`, so it only executes on the client after mount. The initial server render emits each auto item at its default `full` span until hydration resolves the first polyfill pass — which matches the existing masonry SSR behavior.
|
|
239
239
|
|
|
240
|
+
## Row-unit auto-tuner
|
|
241
|
+
|
|
242
|
+
Browsers cap the implicit grid track count for resource protection — historically Chromium at 1000 and Firefox at ~10000 tracks. Past that cap the engine clamps or wraps items, which the user sees as overlapping thumbnails toward the bottom of a long masonry stack. The polyfill expresses each item's height as a row-span (`ceil((height + gap) / rowUnit)`), so a long enough gallery at the default 1 px row-unit will hit the cap.
|
|
243
|
+
|
|
244
|
+
To stay safely below it, the polyfill auto-tunes the row-unit on every layout pass:
|
|
245
|
+
|
|
246
|
+
1. Project the implicit row count for the current children using the live column count (`sum(spans) / columns`).
|
|
247
|
+
2. If the projection stays under the safe threshold (~5000 rows — 2× headroom under the strictest reasonable cap), keep the row-unit at 1 px and get pixel-accurate row gaps.
|
|
248
|
+
3. If it would exceed the threshold, grow the row-unit just enough to bring the projection back under (`ceil(projectedRows / threshold)`). Each item's span is recomputed against the new unit; the per-child span cache is invalidated automatically when the unit changes between runs.
|
|
249
|
+
|
|
250
|
+
This is automatic — most galleries never trigger it. For the rare case where you've measured the auto-tuner picking the wrong unit, escape with `masonryRowUnitPx`:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<Grid isMasonry masonryRowUnitPx={4}>…</Grid>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Trade-off: smaller unit = more pixel-accurate row gaps; larger unit = more headroom under browser caps. Set this **only** when you've confirmed the auto-tuner is wrong for your case — the auto-pick is the right answer for almost every gallery.
|
|
257
|
+
|
|
240
258
|
## Migration from `@greenchoice/design-system`
|
|
241
259
|
|
|
242
260
|
| Greenchoice | BoostDev equivalent |
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { act } from 'react';
|
|
3
|
+
import { afterEach, beforeEach, vi } from 'vitest';
|
|
3
4
|
import { Grid } from './Grid';
|
|
4
5
|
import { GridItem, GridItemSpan } from './GridItem';
|
|
5
6
|
import { applyMasonryLayout, clearMasonryLayout } from './masonry';
|
|
@@ -93,9 +94,12 @@ describe('applyMasonryLayout', () => {
|
|
|
93
94
|
return el;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
it('overrides container to grid-auto-
|
|
97
|
+
it('overrides container to grid-auto-flow: dense + row-gap: 0 and picks a row-track unit', () => {
|
|
97
98
|
const container = makeContainer(10);
|
|
98
99
|
applyMasonryLayout(container, []);
|
|
100
|
+
// Auto-tuner picks 1px when the projected row count is well under
|
|
101
|
+
// the implicit-grid cap; long galleries get a larger unit (covered
|
|
102
|
+
// in dedicated tests below).
|
|
99
103
|
expect(container.style.gridAutoRows).toBe('1px');
|
|
100
104
|
expect(container.style.gridAutoFlow).toBe('dense');
|
|
101
105
|
expect(container.style.rowGap).toBe('0px');
|
|
@@ -141,16 +145,15 @@ describe('applyMasonryLayout', () => {
|
|
|
141
145
|
expect(a.style.overflowAnchor).toBe('');
|
|
142
146
|
});
|
|
143
147
|
|
|
144
|
-
it('assigns grid-row-end: span ceil(height + targetGap) to each item', () => {
|
|
148
|
+
it('assigns grid-row-end: span ceil((height + targetGap) / rowUnit) to each item', () => {
|
|
145
149
|
const container = makeContainer(10);
|
|
146
150
|
const a = makeItem(100);
|
|
147
151
|
const b = makeItem(201);
|
|
148
152
|
container.append(a, b);
|
|
149
153
|
applyMasonryLayout(container, [a, b]);
|
|
150
154
|
|
|
151
|
-
//
|
|
155
|
+
// Auto-tuner stays at unit=1 here — projected rows is tiny.
|
|
152
156
|
expect(a.style.gridRowEnd).toBe('span 110');
|
|
153
|
-
// ceil(201 + 10) = 211
|
|
154
157
|
expect(b.style.gridRowEnd).toBe('span 211');
|
|
155
158
|
});
|
|
156
159
|
|
|
@@ -196,6 +199,37 @@ describe('applyMasonryLayout', () => {
|
|
|
196
199
|
expect(b.style.alignSelf).toBe('start');
|
|
197
200
|
});
|
|
198
201
|
|
|
202
|
+
it('skips re-writing row-spans when measured heights match the cached value', () => {
|
|
203
|
+
// ResizeObserver fires freely on `<img>` decode in Firefox even when the
|
|
204
|
+
// parent's reserved aspect-ratio box hasn't actually changed. Without this
|
|
205
|
+
// short-circuit, every fire triggers a `grid-auto-flow: dense` re-pack —
|
|
206
|
+
// visible as image overlap during fast scroll because Firefox's compositor
|
|
207
|
+
// hasn't caught up with the newly shuffled item positions.
|
|
208
|
+
const container = makeContainer(10);
|
|
209
|
+
const item = makeItem(50);
|
|
210
|
+
container.append(item);
|
|
211
|
+
applyMasonryLayout(container, [item]);
|
|
212
|
+
expect(item.style.gridRowEnd).toBe('span 60');
|
|
213
|
+
|
|
214
|
+
// Simulate something else writing to gridRowEnd — if the polyfill re-runs
|
|
215
|
+
// unnecessarily, this would be overwritten.
|
|
216
|
+
item.style.gridRowEnd = 'span 999';
|
|
217
|
+
applyMasonryLayout(container, [item]);
|
|
218
|
+
expect(item.style.gridRowEnd).toBe('span 999');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('re-writes row-spans when measured heights change', () => {
|
|
222
|
+
const container = makeContainer(10);
|
|
223
|
+
const item = makeItem(50);
|
|
224
|
+
container.append(item);
|
|
225
|
+
applyMasonryLayout(container, [item]);
|
|
226
|
+
expect(item.style.gridRowEnd).toBe('span 60');
|
|
227
|
+
|
|
228
|
+
Object.defineProperty(item, 'offsetHeight', { configurable: true, value: 80 });
|
|
229
|
+
applyMasonryLayout(container, [item]);
|
|
230
|
+
expect(item.style.gridRowEnd).toBe('span 90');
|
|
231
|
+
});
|
|
232
|
+
|
|
199
233
|
it('clearMasonryLayout undoes container and child styles', () => {
|
|
200
234
|
const container = makeContainer(10);
|
|
201
235
|
const item = makeItem(50);
|
|
@@ -211,6 +245,54 @@ describe('applyMasonryLayout', () => {
|
|
|
211
245
|
expect(item.style.alignSelf).toBe('');
|
|
212
246
|
});
|
|
213
247
|
|
|
248
|
+
describe('row-unit auto-tuner', () => {
|
|
249
|
+
it('grows the row-unit when projected implicit row count would exceed the safe threshold', () => {
|
|
250
|
+
// Build a worst-case scenario: many tall items in a single column so the
|
|
251
|
+
// projection equals sum(spans). 50 items × 600px each = 30,000 row-units
|
|
252
|
+
// at unit=1, well over the 5,000 threshold → auto picks unit=ceil(30000/5000)=6.
|
|
253
|
+
const container = makeContainer(0);
|
|
254
|
+
container.style.gridTemplateColumns = '100px';
|
|
255
|
+
const items = Array.from({ length: 50 }, () => makeItem(600));
|
|
256
|
+
container.append(...items);
|
|
257
|
+
applyMasonryLayout(container, items);
|
|
258
|
+
expect(container.style.gridAutoRows).toBe('6px');
|
|
259
|
+
// span = ceil(600 / 6) = 100
|
|
260
|
+
expect(items[0].style.gridRowEnd).toBe('span 100');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('honors an explicit rowUnitPx override', () => {
|
|
264
|
+
const container = makeContainer(10);
|
|
265
|
+
const item = makeItem(100);
|
|
266
|
+
container.append(item);
|
|
267
|
+
applyMasonryLayout(container, [item], { rowUnitPx: 8 });
|
|
268
|
+
expect(container.style.gridAutoRows).toBe('8px');
|
|
269
|
+
// ceil((100 + 10) / 8) = 14
|
|
270
|
+
expect(item.style.gridRowEnd).toBe('span 14');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('invalidates the per-child span cache when the row-unit changes between runs', () => {
|
|
274
|
+
const container = makeContainer(10);
|
|
275
|
+
const item = makeItem(100);
|
|
276
|
+
container.append(item);
|
|
277
|
+
applyMasonryLayout(container, [item], { rowUnitPx: 1 });
|
|
278
|
+
expect(item.style.gridRowEnd).toBe('span 110');
|
|
279
|
+
// Switching units must rewrite — without cache invalidation the diff
|
|
280
|
+
// check would short-circuit because the cached "110" still matches the
|
|
281
|
+
// newly-computed value (which would be 14 under unit=8).
|
|
282
|
+
applyMasonryLayout(container, [item], { rowUnitPx: 8 });
|
|
283
|
+
expect(item.style.gridRowEnd).toBe('span 14');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('clearMasonryLayout removes the unit attribute so the next mount re-tunes', () => {
|
|
287
|
+
const container = makeContainer(10);
|
|
288
|
+
const item = makeItem(100);
|
|
289
|
+
container.append(item);
|
|
290
|
+
applyMasonryLayout(container, [item], { rowUnitPx: 8 });
|
|
291
|
+
clearMasonryLayout(container, [item]);
|
|
292
|
+
expect(container.hasAttribute('data-bds-masonry-unit')).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
214
296
|
describe('preferredColumnSpan', () => {
|
|
215
297
|
function makeAutoItem(height: number) {
|
|
216
298
|
const el = makeItem(height);
|
|
@@ -317,6 +399,126 @@ describe('applyMasonryLayout', () => {
|
|
|
317
399
|
});
|
|
318
400
|
});
|
|
319
401
|
|
|
402
|
+
describe('useMasonry scroll debounce', () => {
|
|
403
|
+
// jsdom has no ResizeObserver — install a mock that captures the callback so
|
|
404
|
+
// tests can simulate an image decode firing during/outside of a scroll.
|
|
405
|
+
let roCallback: ResizeObserverCallback | null = null;
|
|
406
|
+
const originalRO = globalThis.ResizeObserver;
|
|
407
|
+
|
|
408
|
+
beforeEach(() => {
|
|
409
|
+
roCallback = null;
|
|
410
|
+
class MockResizeObserver {
|
|
411
|
+
constructor(cb: ResizeObserverCallback) {
|
|
412
|
+
roCallback = cb;
|
|
413
|
+
}
|
|
414
|
+
observe() {}
|
|
415
|
+
unobserve() {}
|
|
416
|
+
disconnect() {}
|
|
417
|
+
}
|
|
418
|
+
(globalThis as unknown as { ResizeObserver: typeof ResizeObserver }).ResizeObserver =
|
|
419
|
+
MockResizeObserver as unknown as typeof ResizeObserver;
|
|
420
|
+
vi.useFakeTimers();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
afterEach(() => {
|
|
424
|
+
vi.useRealTimers();
|
|
425
|
+
if (originalRO) {
|
|
426
|
+
(globalThis as unknown as { ResizeObserver: typeof ResizeObserver }).ResizeObserver = originalRO;
|
|
427
|
+
} else {
|
|
428
|
+
delete (globalThis as unknown as Record<string, unknown>).ResizeObserver;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
function stubHeight(el: Element, height: number) {
|
|
433
|
+
Object.defineProperty(el, 'offsetHeight', { configurable: true, value: height });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
it('defers re-runs while the user is actively scrolling and applies them once scroll idles', () => {
|
|
437
|
+
// Regression: during fast scroll in Firefox, an image decode grows its GridItem,
|
|
438
|
+
// ResizeObserver fires, and the synchronous `grid-auto-flow: dense` re-pack
|
|
439
|
+
// races the compositor — items appear to overlap because cached tiles from the
|
|
440
|
+
// pre-shuffle layout are painted over the new layout for a frame or two.
|
|
441
|
+
const { container } = render(
|
|
442
|
+
<Grid isMasonry>
|
|
443
|
+
<GridItem data-testid="a">a</GridItem>
|
|
444
|
+
</Grid>,
|
|
445
|
+
);
|
|
446
|
+
const item = container.querySelector('[data-testid="a"]') as HTMLElement;
|
|
447
|
+
stubHeight(item, 100);
|
|
448
|
+
|
|
449
|
+
// Flush the initial rAF-scheduled layout pass.
|
|
450
|
+
act(() => {
|
|
451
|
+
vi.advanceTimersByTime(0);
|
|
452
|
+
// rAF in jsdom is backed by setTimeout(..., 0)
|
|
453
|
+
});
|
|
454
|
+
act(() => {
|
|
455
|
+
vi.runAllTimers();
|
|
456
|
+
});
|
|
457
|
+
const initialSpan = item.style.gridRowEnd;
|
|
458
|
+
expect(initialSpan).not.toBe('');
|
|
459
|
+
|
|
460
|
+
// User starts scrolling.
|
|
461
|
+
window.dispatchEvent(new Event('scroll'));
|
|
462
|
+
|
|
463
|
+
// Image decode grows the item; RO fires.
|
|
464
|
+
stubHeight(item, 300);
|
|
465
|
+
act(() => {
|
|
466
|
+
roCallback?.([] as unknown as ResizeObserverEntry[], {} as ResizeObserver);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Mid-scroll: the new span is NOT applied — deferred until scroll idle.
|
|
470
|
+
expect(item.style.gridRowEnd).toBe(initialSpan);
|
|
471
|
+
|
|
472
|
+
// Scroll stops; after the idle window, the deferred layout pass runs.
|
|
473
|
+
act(() => {
|
|
474
|
+
vi.advanceTimersByTime(200);
|
|
475
|
+
});
|
|
476
|
+
expect(item.style.gridRowEnd).not.toBe(initialSpan);
|
|
477
|
+
expect(item.style.gridRowEnd).toBe('span 300');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('still lays out newly-mounted children during active scroll', async () => {
|
|
481
|
+
// Regression: deferring the whole polyfill during scroll left skeletons
|
|
482
|
+
// invisible — a child mounted mid-scroll would keep `grid-auto-rows: 1px`
|
|
483
|
+
// and never receive a row-span write, collapsing to 1px tall until scroll
|
|
484
|
+
// stopped. Only RO-triggered resizes of already-laid-out items should
|
|
485
|
+
// defer; initial layout and MutationObserver-driven runs must not.
|
|
486
|
+
const stub = (h: number) => (el: HTMLElement | null) => {
|
|
487
|
+
if (el) stubHeight(el, h);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
function Harness({ extra = false }: { extra?: boolean }) {
|
|
491
|
+
return (
|
|
492
|
+
<Grid isMasonry>
|
|
493
|
+
<GridItem data-testid="a" ref={stub(100)}>a</GridItem>
|
|
494
|
+
{extra ? <GridItem data-testid="b" ref={stub(80)}>b</GridItem> : null}
|
|
495
|
+
</Grid>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const { container, rerender } = render(<Harness />);
|
|
500
|
+
await act(async () => {
|
|
501
|
+
await vi.runAllTimersAsync();
|
|
502
|
+
});
|
|
503
|
+
const a = container.querySelector('[data-testid="a"]') as HTMLElement;
|
|
504
|
+
expect(a.style.gridRowEnd).not.toBe('');
|
|
505
|
+
|
|
506
|
+
// User is actively scrolling when the new child mounts.
|
|
507
|
+
window.dispatchEvent(new Event('scroll'));
|
|
508
|
+
|
|
509
|
+
await act(async () => {
|
|
510
|
+
rerender(<Harness extra />);
|
|
511
|
+
});
|
|
512
|
+
await act(async () => {
|
|
513
|
+
await vi.runAllTimersAsync();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const b = container.querySelector('[data-testid="b"]') as HTMLElement;
|
|
517
|
+
// The new child must have received a span even though we're mid-scroll.
|
|
518
|
+
expect(b.style.gridRowEnd).toBe('span 80');
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
320
522
|
describe('GridItem', () => {
|
|
321
523
|
it('defaults to full span', () => {
|
|
322
524
|
const { container } = render(<GridItem>x</GridItem>);
|
|
@@ -58,6 +58,16 @@ type GridOwnProps = WithClassName & {
|
|
|
58
58
|
* keep their span.
|
|
59
59
|
*/
|
|
60
60
|
masonryPreferredColumnSpan?: Exclude<GridItemSpanValue, 'auto'>;
|
|
61
|
+
/**
|
|
62
|
+
* Masonry only. Manual override for the row-track unit (in pixels) the
|
|
63
|
+
* polyfill uses for `grid-auto-rows` and as the ceil() granularity of each
|
|
64
|
+
* item's row span. Omit to let the polyfill auto-pick the smallest unit
|
|
65
|
+
* that keeps the implicit row count under any browser cap — that's the
|
|
66
|
+
* right answer for almost every gallery and yields pixel-perfect row gaps
|
|
67
|
+
* when the gallery is short enough to not need rounding. Set this only if
|
|
68
|
+
* you've confirmed the auto-tuner is wrong for your case.
|
|
69
|
+
*/
|
|
70
|
+
masonryRowUnitPx?: number;
|
|
61
71
|
as?: ElementType;
|
|
62
72
|
};
|
|
63
73
|
|
|
@@ -73,12 +83,16 @@ export function Grid<C extends ElementType = 'div'>({
|
|
|
73
83
|
isMasonry = false,
|
|
74
84
|
autoSpanMedia = false,
|
|
75
85
|
masonryPreferredColumnSpan,
|
|
86
|
+
masonryRowUnitPx,
|
|
76
87
|
as,
|
|
77
88
|
style,
|
|
78
89
|
...rest
|
|
79
90
|
}: GridProps<C>) {
|
|
80
91
|
const ref = useRef<HTMLElement>(null);
|
|
81
|
-
useMasonry(ref, isMasonry, {
|
|
92
|
+
useMasonry(ref, isMasonry, {
|
|
93
|
+
preferredColumnSpan: masonryPreferredColumnSpan,
|
|
94
|
+
rowUnitPx: masonryRowUnitPx,
|
|
95
|
+
});
|
|
82
96
|
|
|
83
97
|
const classNames = cn(
|
|
84
98
|
css.grid,
|