@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.
Files changed (201) hide show
  1. package/AGENTS.md +4 -20
  2. package/README.md +3 -23
  3. package/dist/client.cjs +128 -62
  4. package/dist/client.css +537 -537
  5. package/dist/client.d.cts +11 -1
  6. package/dist/client.d.ts +11 -1
  7. package/dist/client.js +128 -62
  8. package/dist/index.cjs +128 -62
  9. package/dist/index.css +537 -537
  10. package/dist/index.d.cts +11 -1
  11. package/dist/index.d.ts +11 -1
  12. package/dist/index.js +128 -62
  13. package/dist/web-components/index.js +36 -4
  14. package/package.json +2 -10
  15. package/src/components/layout/Grid/Grid.mdx +20 -2
  16. package/src/components/layout/Grid/Grid.spec.tsx +206 -4
  17. package/src/components/layout/Grid/Grid.tsx +15 -1
  18. package/src/components/layout/Grid/masonry.ts +148 -10
  19. package/dist/native/index.cjs +0 -4337
  20. package/dist/native/index.d.cts +0 -574
  21. package/dist/native/index.d.ts +0 -574
  22. package/dist/native/index.js +0 -4421
  23. package/src/components/interaction/Button/Button.native.mdx +0 -91
  24. package/src/components/interaction/Button/Button.native.spec.tsx +0 -55
  25. package/src/components/interaction/Button/Button.native.stories.tsx +0 -141
  26. package/src/components/interaction/Button/Button.native.tsx +0 -105
  27. package/src/components/interaction/Dialog/Dialog.native.mdx +0 -61
  28. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +0 -73
  29. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +0 -53
  30. package/src/components/interaction/Dialog/Dialog.native.tsx +0 -128
  31. package/src/components/interaction/Drawer/Drawer.native.mdx +0 -58
  32. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +0 -81
  33. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +0 -33
  34. package/src/components/interaction/Drawer/Drawer.native.tsx +0 -175
  35. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +0 -74
  36. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +0 -78
  37. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +0 -51
  38. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +0 -254
  39. package/src/components/interaction/Popover/Popover.native.mdx +0 -61
  40. package/src/components/interaction/Popover/Popover.native.spec.tsx +0 -73
  41. package/src/components/interaction/Popover/Popover.native.stories.tsx +0 -44
  42. package/src/components/interaction/Popover/Popover.native.tsx +0 -87
  43. package/src/components/interaction/Rating/Rating.native.mdx +0 -55
  44. package/src/components/interaction/Rating/Rating.native.spec.tsx +0 -38
  45. package/src/components/interaction/Rating/Rating.native.stories.tsx +0 -37
  46. package/src/components/interaction/Rating/Rating.native.tsx +0 -50
  47. package/src/components/interaction/Toast/Toast.native.mdx +0 -81
  48. package/src/components/interaction/Toast/Toast.native.spec.tsx +0 -80
  49. package/src/components/interaction/Toast/Toast.native.stories.tsx +0 -47
  50. package/src/components/interaction/Toast/Toast.native.tsx +0 -202
  51. package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +0 -74
  52. package/src/components/interaction/form/Checkbox/Checkbox.native.spec.tsx +0 -42
  53. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +0 -34
  54. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +0 -80
  55. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +0 -78
  56. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +0 -57
  57. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +0 -96
  58. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +0 -61
  59. package/src/components/interaction/form/Combobox/Combobox.native.mdx +0 -86
  60. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +0 -60
  61. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +0 -99
  62. package/src/components/interaction/form/Combobox/Combobox.native.tsx +0 -211
  63. package/src/components/interaction/form/FileInput/FileInput.native.mdx +0 -86
  64. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +0 -73
  65. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +0 -69
  66. package/src/components/interaction/form/FileInput/FileInput.native.tsx +0 -129
  67. package/src/components/interaction/form/FormInput/FormInput.native.mdx +0 -81
  68. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +0 -51
  69. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +0 -54
  70. package/src/components/interaction/form/FormInput/FormInput.native.tsx +0 -122
  71. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +0 -87
  72. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +0 -63
  73. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +0 -63
  74. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +0 -198
  75. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +0 -79
  76. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +0 -44
  77. package/src/components/interaction/form/Radio/Radio.native.tsx +0 -79
  78. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +0 -79
  79. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +0 -66
  80. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +0 -96
  81. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +0 -61
  82. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +0 -78
  83. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +0 -82
  84. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +0 -99
  85. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +0 -121
  86. package/src/components/interaction/form/Select/Select.native.mdx +0 -85
  87. package/src/components/interaction/form/Select/Select.native.spec.tsx +0 -64
  88. package/src/components/interaction/form/Select/Select.native.stories.tsx +0 -75
  89. package/src/components/interaction/form/Select/Select.native.tsx +0 -252
  90. package/src/components/interaction/form/Slider/Slider.native.mdx +0 -83
  91. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +0 -51
  92. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +0 -58
  93. package/src/components/interaction/form/Slider/Slider.native.tsx +0 -188
  94. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +0 -60
  95. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +0 -33
  96. package/src/components/interaction/form/Switch/Switch.native.tsx +0 -59
  97. package/src/components/interaction/form/Textarea/Textarea.native.mdx +0 -76
  98. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +0 -50
  99. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +0 -50
  100. package/src/components/interaction/form/Textarea/Textarea.native.tsx +0 -108
  101. package/src/components/interaction/form/atoms/Label.native.spec.tsx +0 -39
  102. package/src/components/interaction/form/atoms/Label.native.stories.tsx +0 -18
  103. package/src/components/interaction/form/atoms/Label.native.tsx +0 -29
  104. package/src/components/interaction/form/atoms/Message.native.spec.tsx +0 -30
  105. package/src/components/interaction/form/atoms/Message.native.stories.tsx +0 -22
  106. package/src/components/interaction/form/atoms/Message.native.tsx +0 -34
  107. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +0 -83
  108. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +0 -44
  109. package/src/components/layout/ButtonGroup/ButtonGroup.native.tsx +0 -44
  110. package/src/components/layout/Card/Card.native.spec.tsx +0 -102
  111. package/src/components/layout/Card/Card.native.stories.tsx +0 -51
  112. package/src/components/layout/Card/Card.native.tsx +0 -89
  113. package/src/components/layout/IconWrapper/IconWrapper.native.spec.tsx +0 -39
  114. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +0 -37
  115. package/src/components/layout/IconWrapper/IconWrapper.native.tsx +0 -20
  116. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +0 -51
  117. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +0 -36
  118. package/src/components/layout/SectionHeader/SectionHeader.native.tsx +0 -79
  119. package/src/components/ui/Accordion/Accordion.native.mdx +0 -65
  120. package/src/components/ui/Accordion/Accordion.native.spec.tsx +0 -69
  121. package/src/components/ui/Accordion/Accordion.native.stories.tsx +0 -52
  122. package/src/components/ui/Accordion/Accordion.native.tsx +0 -141
  123. package/src/components/ui/Alert/Alert.native.mdx +0 -62
  124. package/src/components/ui/Alert/Alert.native.spec.tsx +0 -76
  125. package/src/components/ui/Alert/Alert.native.stories.tsx +0 -41
  126. package/src/components/ui/Alert/Alert.native.tsx +0 -94
  127. package/src/components/ui/Avatar/Avatar.native.mdx +0 -50
  128. package/src/components/ui/Avatar/Avatar.native.spec.tsx +0 -47
  129. package/src/components/ui/Avatar/Avatar.native.stories.tsx +0 -32
  130. package/src/components/ui/Avatar/Avatar.native.tsx +0 -85
  131. package/src/components/ui/Badge/Badge.native.mdx +0 -54
  132. package/src/components/ui/Badge/Badge.native.spec.tsx +0 -45
  133. package/src/components/ui/Badge/Badge.native.stories.tsx +0 -34
  134. package/src/components/ui/Badge/Badge.native.tsx +0 -50
  135. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +0 -52
  136. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +0 -49
  137. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +0 -46
  138. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +0 -83
  139. package/src/components/ui/Calendar/Calendar.native.mdx +0 -58
  140. package/src/components/ui/Calendar/Calendar.native.spec.tsx +0 -92
  141. package/src/components/ui/Calendar/Calendar.native.stories.tsx +0 -45
  142. package/src/components/ui/Calendar/Calendar.native.tsx +0 -294
  143. package/src/components/ui/Carousel/Carousel.native.mdx +0 -55
  144. package/src/components/ui/Carousel/Carousel.native.spec.tsx +0 -55
  145. package/src/components/ui/Carousel/Carousel.native.stories.tsx +0 -59
  146. package/src/components/ui/Carousel/Carousel.native.tsx +0 -184
  147. package/src/components/ui/Collapsible/Collapsible.native.mdx +0 -53
  148. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +0 -68
  149. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +0 -48
  150. package/src/components/ui/Collapsible/Collapsible.native.tsx +0 -139
  151. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +0 -50
  152. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +0 -37
  153. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +0 -48
  154. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +0 -54
  155. package/src/components/ui/Link/Link.native.mdx +0 -53
  156. package/src/components/ui/Link/Link.native.spec.tsx +0 -35
  157. package/src/components/ui/Link/Link.native.stories.tsx +0 -46
  158. package/src/components/ui/Link/Link.native.tsx +0 -48
  159. package/src/components/ui/Loading/Loading.native.spec.tsx +0 -24
  160. package/src/components/ui/Loading/Loading.native.stories.tsx +0 -31
  161. package/src/components/ui/Loading/Loading.native.tsx +0 -29
  162. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +0 -85
  163. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +0 -37
  164. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +0 -76
  165. package/src/components/ui/Pagination/Pagination.native.mdx +0 -62
  166. package/src/components/ui/Pagination/Pagination.native.spec.tsx +0 -86
  167. package/src/components/ui/Pagination/Pagination.native.stories.tsx +0 -47
  168. package/src/components/ui/Pagination/Pagination.native.tsx +0 -157
  169. package/src/components/ui/Progress/Progress.native.spec.tsx +0 -56
  170. package/src/components/ui/Progress/Progress.native.stories.tsx +0 -34
  171. package/src/components/ui/Progress/Progress.native.tsx +0 -84
  172. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +0 -63
  173. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +0 -39
  174. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +0 -36
  175. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +0 -176
  176. package/src/components/ui/Separator/Separator.native.spec.tsx +0 -39
  177. package/src/components/ui/Separator/Separator.native.stories.tsx +0 -38
  178. package/src/components/ui/Separator/Separator.native.tsx +0 -32
  179. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +0 -39
  180. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +0 -31
  181. package/src/components/ui/Skeleton/Skeleton.native.tsx +0 -42
  182. package/src/components/ui/Table/Table.native.mdx +0 -72
  183. package/src/components/ui/Table/Table.native.spec.tsx +0 -83
  184. package/src/components/ui/Table/Table.native.stories.tsx +0 -55
  185. package/src/components/ui/Table/Table.native.tsx +0 -196
  186. package/src/components/ui/Tabs/Tabs.native.mdx +0 -65
  187. package/src/components/ui/Tabs/Tabs.native.spec.tsx +0 -65
  188. package/src/components/ui/Tabs/Tabs.native.stories.tsx +0 -57
  189. package/src/components/ui/Tabs/Tabs.native.tsx +0 -133
  190. package/src/components/ui/Tooltip/Tooltip.native.mdx +0 -47
  191. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +0 -37
  192. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +0 -38
  193. package/src/components/ui/Tooltip/Tooltip.native.tsx +0 -57
  194. package/src/components/ui/Typography/Typography.native.mdx +0 -56
  195. package/src/components/ui/Typography/Typography.native.spec.tsx +0 -49
  196. package/src/components/ui/Typography/Typography.native.stories.tsx +0 -36
  197. package/src/components/ui/Typography/Typography.native.tsx +0 -65
  198. package/src/native/ThemeContext.tsx +0 -28
  199. package/src/native/tokens.ts +0 -13
  200. package/src/native.ts +0 -76
  201. package/src/stories/ReactNative.mdx +0 -156
@@ -65,7 +65,15 @@ function resolvePreferredColumns(
65
65
  * `grid-template-rows: masonry` value, the polyfill yields to CSS.
66
66
  */
67
67
 
68
- const ROW_UNIT_PX = 1;
68
+ // Target ceiling on the implicit grid row count produced by the polyfill.
69
+ // Browsers cap implicit grid tracks for resource-protection — historically
70
+ // Chromium at 1000 and Firefox at ~10000; past the cap, the layout engine
71
+ // clamps/wraps items and they overlap visually (e.g. bottom thumbnails of a
72
+ // long masonry stack on top of each other in Firefox). The auto-tuner picks
73
+ // a `rowUnitPx` so the projected row count stays under this number. Lower
74
+ // is safer; higher gives finer (more pixel-accurate) row gaps. 5000 leaves
75
+ // 2× headroom under the strictest reasonable cap.
76
+ const TARGET_MAX_IMPLICIT_ROWS = 5000;
69
77
 
70
78
  export function supportsNativeMasonry(): boolean {
71
79
  if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return false;
@@ -87,6 +95,17 @@ export type MasonryOptions = {
87
95
  * budget for the demotion calculation.
88
96
  */
89
97
  preferredColumnSpan?: Exclude<GridItemSpanValue, 'auto'>;
98
+ /**
99
+ * Manual override for the row-track unit (in pixels) the polyfill uses for
100
+ * `grid-auto-rows` and as the ceil() granularity of each item's row span.
101
+ * When omitted (recommended), the polyfill auto-picks the smallest unit that
102
+ * keeps projected implicit row count under `TARGET_MAX_IMPLICIT_ROWS`.
103
+ *
104
+ * Trade-off: smaller = more pixel-accurate row gaps; larger = more headroom
105
+ * under browser implicit-grid-track caps for very long galleries. Set this
106
+ * only if you've confirmed the auto-tuner is wrong for your case.
107
+ */
108
+ rowUnitPx?: number;
90
109
  };
91
110
 
92
111
  // Data attribute used to remember an auto item's inline `grid-column` before the
@@ -95,6 +114,24 @@ export type MasonryOptions = {
95
114
  // items pinned to their polyfill-assigned span.
96
115
  const ORIGINAL_COL_ATTR = 'data-bds-masonry-original-column';
97
116
 
117
+ // Data attribute caching the last applied row-span. Used to short-circuit no-op
118
+ // re-runs: when every child's newly-measured span matches the cached value, the
119
+ // polyfill skips all writes. This prevents `grid-auto-flow: dense` from
120
+ // re-packing on spurious ResizeObserver fires (subpixel fluctuations, `<img>`
121
+ // decode in Firefox where offsetHeight briefly wobbles). Dense re-packing
122
+ // during fast scroll in Firefox is visible as images overlapping, because
123
+ // Firefox's compositor paints cached tiles from the pre-shuffle layout on top
124
+ // of the newly laid out items for a frame or two.
125
+ const LAST_SPAN_ATTR = 'data-bds-masonry-span';
126
+
127
+ // Container attribute caching the last applied row-unit (px). When the
128
+ // auto-tuner picks a different unit on a re-run (e.g. children added pushed
129
+ // the projected row count past the threshold), every per-child cached span
130
+ // becomes stale because they were computed against the old unit — we clear
131
+ // them so the next diff check forces a full rewrite. Cleared in
132
+ // clearMasonryLayout.
133
+ const UNIT_ATTR = 'data-bds-masonry-unit';
134
+
98
135
  export function applyMasonryLayout(
99
136
  container: HTMLElement,
100
137
  rawChildren: HTMLElement[],
@@ -107,7 +144,6 @@ export function applyMasonryLayout(
107
144
  const cs = window.getComputedStyle(container);
108
145
  const targetGap = parseFloat(cs.columnGap) || 0;
109
146
 
110
- container.style.gridAutoRows = `${ROW_UNIT_PX}px`;
111
147
  container.style.rowGap = '0px';
112
148
  container.style.gridAutoFlow = 'dense';
113
149
  // `dense` + per-child `ResizeObserver` can fight the browser's scroll anchoring:
@@ -180,11 +216,66 @@ export function applyMasonryLayout(
180
216
  // O(N) forced-reflow cost of a read-then-write loop.
181
217
  const heights = children.map((child) => child.offsetHeight);
182
218
 
219
+ // Pick the row-track unit. Manual override wins; otherwise auto-tune.
220
+ //
221
+ // Auto-tune uses the projected implicit row count (assuming ideal `dense`
222
+ // packing across the current grid columns) and picks the smallest unit such
223
+ // that the projection stays under TARGET_MAX_IMPLICIT_ROWS. Galleries that
224
+ // don't risk hitting any browser cap stay at unit=1 and get pixel-perfect
225
+ // gaps; only galleries large enough to threaten the cap pay the rounding
226
+ // cost. The unit always grows monotonically with content, so once a gallery
227
+ // is above the threshold its layout is stable across runs.
228
+ const cols = countGridColumns(container) || 1;
229
+ const totalRowSum = heights.reduce(
230
+ (s, h) => s + Math.max(1, Math.ceil(h + targetGap)),
231
+ 0,
232
+ );
233
+ const projectedRows = totalRowSum / cols;
234
+ const autoUnit = Math.max(
235
+ 1,
236
+ Math.ceil(projectedRows / TARGET_MAX_IMPLICIT_ROWS),
237
+ );
238
+ const rowUnit =
239
+ options.rowUnitPx !== undefined && options.rowUnitPx > 0
240
+ ? Math.floor(options.rowUnitPx)
241
+ : autoUnit;
242
+
243
+ // Apply the chosen unit. Setting `gridAutoRows` here (not before measurement)
244
+ // is safe because `align-self: start` makes offsetHeight return the content
245
+ // height regardless of the cell size — heights are independent of the unit.
246
+ container.style.gridAutoRows = `${rowUnit}px`;
247
+
248
+ // If the unit changed since the last run, every cached span is stale.
249
+ // Force a re-write by clearing the cache attributes before the diff check.
250
+ const prevUnit = container.getAttribute(UNIT_ATTR);
251
+ if (prevUnit !== String(rowUnit)) {
252
+ for (const child of children) child.removeAttribute(LAST_SPAN_ATTR);
253
+ container.setAttribute(UNIT_ATTR, String(rowUnit));
254
+ }
255
+
256
+ // Compute spans; skip the entire write pass if every span matches the
257
+ // previously-applied value. ResizeObserver fires freely in Firefox during
258
+ // image decode and scroll — without this guard, each fire triggers a dense
259
+ // re-pack that's visible as overlap on fast scroll (the compositor hasn't
260
+ // caught up with the newly shuffled positions).
261
+ const spans = heights.map((h) =>
262
+ Math.max(1, Math.ceil((h + targetGap) / rowUnit)),
263
+ );
264
+ let changed = false;
265
+ for (let i = 0; i < children.length; i++) {
266
+ const prev = children[i].getAttribute(LAST_SPAN_ATTR);
267
+ if (prev !== String(spans[i])) {
268
+ changed = true;
269
+ break;
270
+ }
271
+ }
272
+ if (!changed) return;
273
+
183
274
  // Pass 3 — batch all row-span writes. No reads in between, so the browser can
184
275
  // coalesce style invalidation and do layout once for the whole batch.
185
276
  for (let i = 0; i < children.length; i++) {
186
- const span = Math.max(1, Math.ceil(heights[i] + targetGap));
187
- children[i].style.gridRowEnd = `span ${span}`;
277
+ children[i].style.gridRowEnd = `span ${spans[i]}`;
278
+ children[i].setAttribute(LAST_SPAN_ATTR, String(spans[i]));
188
279
  }
189
280
  }
190
281
 
@@ -223,6 +314,7 @@ export function clearMasonryLayout(container: HTMLElement, rawChildren: HTMLElem
223
314
  container.style.gridAutoFlow = '';
224
315
  container.style.rowGap = '';
225
316
  container.style.overflowAnchor = '';
317
+ container.removeAttribute(UNIT_ATTR);
226
318
  for (const child of rawChildren) {
227
319
  if (child.nodeType !== 1) continue;
228
320
  child.style.gridRow = '';
@@ -230,6 +322,7 @@ export function clearMasonryLayout(container: HTMLElement, rawChildren: HTMLElem
230
322
  child.style.gridRowEnd = '';
231
323
  child.style.alignSelf = '';
232
324
  child.style.overflowAnchor = '';
325
+ child.removeAttribute(LAST_SPAN_ATTR);
233
326
  // Restore any `gridColumn` value the polyfill overwrote. Items the
234
327
  // polyfill never touched have no ORIGINAL_COL_ATTR and are left alone.
235
328
  restoreOriginalColumn(child);
@@ -252,24 +345,66 @@ export function useMasonry(
252
345
  enabled: boolean,
253
346
  options: MasonryOptions = {},
254
347
  ): void {
255
- const { preferredColumnSpan } = options;
348
+ const { preferredColumnSpan, rowUnitPx } = options;
256
349
  useLayoutEffect(() => {
257
350
  const container = ref.current;
258
351
  if (!container || !enabled) return;
259
352
  if (supportsNativeMasonry()) return;
260
353
 
261
354
  let frame = 0;
262
- const runNow = () => {
263
- cancelAnimationFrame(frame);
355
+ let scrollTimer: ReturnType<typeof setTimeout> | null = null;
356
+ let scrolling = false;
357
+ let pending = false;
358
+
359
+ const apply = () => {
264
360
  applyMasonryLayout(container, Array.from(container.children) as HTMLElement[], {
265
361
  preferredColumnSpan,
362
+ rowUnitPx,
266
363
  });
267
364
  };
365
+ const runNow = () => {
366
+ cancelAnimationFrame(frame);
367
+ apply();
368
+ };
268
369
  const schedule = () => {
269
370
  cancelAnimationFrame(frame);
270
371
  frame = requestAnimationFrame(runNow);
271
372
  };
272
373
 
374
+ // RO-triggered re-runs (resizes of existing items — image decode, font swap) are
375
+ // the ones that cause the Firefox compositor race: `grid-auto-flow: dense` re-packs
376
+ // already-laid-out items, and cached tiles from the pre-shuffle layout paint over
377
+ // the new positions for a frame or two, looking like overlap. Deferring *those*
378
+ // writes until scroll stops hides the race. Initial mount and MutationObserver-
379
+ // triggered runs (new children added) must NOT defer — a skeleton mounting during
380
+ // active scroll would otherwise stay at `grid-auto-rows: 1px` height, invisibly
381
+ // collapsed, until scroll idles.
382
+ const onResize = () => {
383
+ if (scrolling) {
384
+ pending = true;
385
+ return;
386
+ }
387
+ apply();
388
+ };
389
+
390
+ const SCROLL_IDLE_MS = 150;
391
+ const onScroll = () => {
392
+ scrolling = true;
393
+ if (scrollTimer) clearTimeout(scrollTimer);
394
+ scrollTimer = setTimeout(() => {
395
+ scrolling = false;
396
+ scrollTimer = null;
397
+ if (pending) {
398
+ pending = false;
399
+ apply();
400
+ }
401
+ }, SCROLL_IDLE_MS);
402
+ };
403
+ // `capture: true` catches scroll events from any nested scroll container, since
404
+ // scroll doesn't bubble. `passive: true` tells the browser we won't preventDefault —
405
+ // critical for not blocking scroll performance.
406
+ window.addEventListener('scroll', onScroll, { passive: true, capture: true });
407
+
273
408
  schedule();
274
409
 
275
410
  // ResizeObserver fires during the "update the rendering" step, AFTER layout and
@@ -278,9 +413,10 @@ export function useMasonry(
278
413
  // defer to rAF instead, there's a visible frame where a grown item (e.g. a
279
414
  // lazy-loaded image whose dimensions just resolved) overflows its stale cell
280
415
  // and visually overlaps the next item in its column — easy to see on fast
281
- // scroll through a long gallery.
416
+ // scroll through a long gallery. (onResize itself still defers writes when the
417
+ // user is actively scrolling — see its `scrolling` guard above.)
282
418
  const ro =
283
- typeof ResizeObserver !== 'undefined' ? new ResizeObserver(runNow) : null;
419
+ typeof ResizeObserver !== 'undefined' ? new ResizeObserver(onResize) : null;
284
420
  const observeDescendants = (child: Element) => {
285
421
  if (!ro) return;
286
422
  ro.observe(child);
@@ -315,9 +451,11 @@ export function useMasonry(
315
451
 
316
452
  return () => {
317
453
  cancelAnimationFrame(frame);
454
+ if (scrollTimer) clearTimeout(scrollTimer);
455
+ window.removeEventListener('scroll', onScroll, { capture: true } as EventListenerOptions);
318
456
  ro?.disconnect();
319
457
  mo?.disconnect();
320
458
  clearMasonryLayout(container, Array.from(container.children) as HTMLElement[]);
321
459
  };
322
- }, [ref, enabled, preferredColumnSpan]);
460
+ }, [ref, enabled, preferredColumnSpan, rowUnitPx]);
323
461
  }