@inc2734/unitone-css 1.4.0 → 1.4.2

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 (35) hide show
  1. package/dist/app.css +1 -1
  2. package/dist/app.js +1 -1
  3. package/dist/behaviors/dividers.js +1 -1
  4. package/dist/behaviors/index.js +1 -1
  5. package/dist/behaviors/stairs.js +1 -1
  6. package/dist/layout-primitives/cluster/react.js +1 -1
  7. package/dist/layout-primitives/index.js +1 -1
  8. package/dist/layout-primitives/marquee/behavior.js +1 -1
  9. package/dist/layout-primitives/marquee/react.js +1 -1
  10. package/dist/layout-primitives/responsive-grid/react.js +1 -1
  11. package/dist/layout-primitives/stack/react.js +1 -1
  12. package/dist/layout-primitives/switcher/react.js +1 -1
  13. package/dist/layout-primitives/vertical-writing/behavior.js +1 -1
  14. package/dist/layout-primitives/vertical-writing/react.js +1 -1
  15. package/dist/layout-primitives/with-sidebar/react.js +1 -1
  16. package/dist/library.js +1 -1
  17. package/package.json +2 -2
  18. package/src/behaviors/dividers.js +3 -1
  19. package/src/behaviors/stairs.js +3 -1
  20. package/src/layout-behavior-state.js +41 -0
  21. package/src/layout-primitives/cluster/index.jsx +25 -16
  22. package/src/layout-primitives/marquee/_index.scss +15 -8
  23. package/src/layout-primitives/marquee/behavior.js +1 -1
  24. package/src/layout-primitives/marquee/index.jsx +69 -7
  25. package/src/layout-primitives/marquee/layout.js +163 -0
  26. package/src/layout-primitives/responsive-grid/index.jsx +29 -17
  27. package/src/layout-primitives/stack/index.jsx +25 -16
  28. package/src/layout-primitives/switcher/index.jsx +26 -17
  29. package/src/layout-primitives/vertical-writing/behavior.js +3 -1
  30. package/src/layout-primitives/vertical-writing/index.jsx +11 -11
  31. package/src/layout-primitives/with-sidebar/index.jsx +27 -18
  32. package/src/library.js +379 -442
  33. package/src/observer-scope.js +53 -0
  34. package/src/register-layout-initializer.js +64 -22
  35. package/src/use-layout-behaviors.js +60 -0
package/src/library.js CHANGED
@@ -1,3 +1,15 @@
1
+ import { requestReactLayoutRefresh } from './layout-behavior-state';
2
+ import { createObserverScope } from './observer-scope';
3
+
4
+ import {
5
+ getMarqueeParts,
6
+ isReactMarquee,
7
+ markMarqueeCopy,
8
+ measureMarquee,
9
+ observeMarquee,
10
+ requestMarqueeRefresh,
11
+ } from './layout-primitives/marquee/layout';
12
+
1
13
  const layoutAttributeName = 'data-unitone-layout';
2
14
  const layoutIntersectionMargin = 200;
3
15
  const layoutIntersectionRootMargin = `${layoutIntersectionMargin}px 0px`;
@@ -40,31 +52,38 @@ const setLayoutTokens = (element, tokens) => {
40
52
  * Observes target resizes and invokes the callback when a relevant change is detected.
41
53
  *
42
54
  * @param {Element} target Target element.
43
- * @param {(target: Element, entry?: ResizeObserverEntry) => void} callback Callback to run.
44
- * @param {{ getValue?: (entry: ResizeObserverEntry) => unknown, delay?: number }} [options]
55
+ * @param {(target: Element) => void} callback Callback to run.
56
+ * @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
57
+ * @param {(entry: ResizeObserverEntry) => unknown} [getValue] Value to compare.
45
58
  * @returns {ResizeObserver} ResizeObserver instance.
46
59
  */
47
- const createResizeObserver = (target, callback, { getValue, delay = 250 } = {}) => {
48
- let prevValue;
49
- let isFirstEntry = true;
50
-
51
- const observer = new ResizeObserver(
52
- debounce((entries) => {
53
- for (const entry of entries) {
54
- const currentValue = getValue?.(entry);
55
- if (isFirstEntry) {
56
- prevValue = currentValue;
57
- isFirstEntry = false;
58
- continue;
59
- }
60
+ const createResizeObserver = (target, callback, scope, getValue) => {
61
+ const prevValues = new WeakMap();
62
+ const onResize = debounce(() => callback(target), 250);
63
+ const observer = new ResizeObserver((entries) => {
64
+ if (scope.disposed) {
65
+ return;
66
+ }
60
67
 
61
- if (undefined === currentValue || currentValue !== prevValue) {
62
- callback(entry.target, entry);
63
- prevValue = currentValue;
64
- }
68
+ let changed = false;
69
+ for (const entry of entries) {
70
+ const currentValue = getValue?.(entry);
71
+ if (
72
+ !prevValues.has(entry.target) ||
73
+ undefined === currentValue ||
74
+ currentValue !== prevValues.get(entry.target)
75
+ ) {
76
+ changed = true;
65
77
  }
66
- }, delay),
67
- );
78
+ prevValues.set(entry.target, currentValue);
79
+ }
80
+
81
+ if (changed) {
82
+ onResize();
83
+ }
84
+ });
85
+ scope.addCleanup(onResize.cancel);
86
+ scope.addCleanup(() => observer.disconnect());
68
87
 
69
88
  observer.observe(target);
70
89
 
@@ -77,22 +96,18 @@ const createResizeObserver = (target, callback, { getValue, delay = 250 } = {})
77
96
  * @param {Node} target Target node.
78
97
  * @param {MutationObserverInit} options Observer options.
79
98
  * @param {(entries: MutationRecord[]) => void} callback Callback to run.
80
- * @returns {MutationObserver} MutationObserver instance.
99
+ * @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
100
+ * @returns {void}
81
101
  */
82
- const createMutationObserver = (target, options, callback) => {
102
+ const createMutationObserver = (target, options, callback, scope) => {
83
103
  const observer = new MutationObserver((entries) => {
84
- requestAnimationFrame(() => {
85
- if (!target?.isConnected) {
86
- return;
87
- }
88
-
104
+ if (!scope.disposed && target.isConnected) {
89
105
  callback(entries);
90
- });
106
+ }
91
107
  });
92
108
 
93
109
  observer.observe(target, options);
94
-
95
- return observer;
110
+ scope.addCleanup(() => observer.disconnect());
96
111
  };
97
112
 
98
113
  /**
@@ -100,12 +115,13 @@ const createMutationObserver = (target, options, callback) => {
100
115
  *
101
116
  * @param {Element} target Target element.
102
117
  * @param {(entry: IntersectionObserverEntry) => void} callback Callback to run.
103
- * @returns {IntersectionObserver} IntersectionObserver instance.
118
+ * @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
119
+ * @returns {void}
104
120
  */
105
- const createIntersectionObserver = (target, callback) => {
121
+ const createIntersectionObserver = (target, callback, scope) => {
106
122
  const observer = new IntersectionObserver(
107
123
  ([entry]) => {
108
- if (entry) {
124
+ if (entry && !scope.disposed) {
109
125
  callback(entry);
110
126
  }
111
127
  },
@@ -113,8 +129,7 @@ const createIntersectionObserver = (target, callback) => {
113
129
  );
114
130
 
115
131
  observer.observe(target);
116
-
117
- return observer;
132
+ scope.addCleanup(() => observer.disconnect());
118
133
  };
119
134
 
120
135
  /**
@@ -122,27 +137,19 @@ const createIntersectionObserver = (target, callback) => {
122
137
  *
123
138
  * @param {Element} target Target element.
124
139
  * @param {(target: Element) => void} callback Callback to run.
140
+ * @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
125
141
  * @returns {() => void} Schedule function.
126
142
  */
127
- const createScheduledTargetCallback = (target, callback) => {
143
+ const createScheduledTargetCallback = (target, callback, scope) => {
128
144
  let rafId = 0;
129
- let defaultView;
130
145
 
131
146
  return () => {
132
- defaultView = target?.ownerDocument?.defaultView;
133
- if (!defaultView?.requestAnimationFrame) {
134
- callback(target);
147
+ if (scope.disposed || rafId) {
135
148
  return;
136
149
  }
137
150
 
138
- if (rafId) {
139
- return;
140
- }
141
-
142
- rafId = defaultView.requestAnimationFrame(() => {
151
+ rafId = scope.requestAnimationFrame(() => {
143
152
  rafId = 0;
144
- defaultView = null;
145
-
146
153
  if (target?.isConnected) {
147
154
  callback(target);
148
155
  }
@@ -151,268 +158,189 @@ const createScheduledTargetCallback = (target, callback) => {
151
158
  };
152
159
 
153
160
  /**
154
- * Observes resizes on the target and its direct children.
161
+ * Keeps resize and optional attribute observation in sync with direct children.
155
162
  *
156
163
  * @param {Element} target Target element.
164
+ * @param {ResizeObserver} resizeObserver Shared size observer.
157
165
  * @param {(target: Element) => void} callback Callback to run.
158
- * @param {{ getValue?: (entry: ResizeObserverEntry) => unknown, delay?: number, onChildList?: (entries: MutationRecord[]) => void }} [options]
159
- * @returns {{ resizeObserver: ResizeObserver, mutationObserver: MutationObserver }}
166
+ * @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
167
+ * @param {{ attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean }} [attributes]
168
+ * @param {(entries: MutationRecord[]) => boolean} [shouldApplyChildList] Filters temporary children.
169
+ * @returns {void}
160
170
  */
161
- const createDirectChildrenResizeObserver = (
171
+ const observeLayoutChildren = (
162
172
  target,
173
+ resizeObserver,
163
174
  callback,
164
- { getValue, delay = 250, onChildList } = {},
175
+ scope,
176
+ attributes,
177
+ shouldApplyChildList,
165
178
  ) => {
166
- const prevValues = new WeakMap();
167
179
  const observedChildren = new Set();
168
-
169
- const observer = new ResizeObserver(
170
- debounce((entries) => {
171
- let shouldApply = false;
172
-
173
- for (const entry of entries) {
174
- const currentValue = getValue?.(entry);
175
- if (!prevValues.has(entry.target)) {
176
- prevValues.set(entry.target, currentValue);
177
- continue;
178
- }
179
-
180
- if (undefined === currentValue || currentValue !== prevValues.get(entry.target)) {
181
- shouldApply = true;
180
+ const attributeObserver = attributes
181
+ ? new MutationObserver((entries) => {
182
+ if (
183
+ !scope.disposed &&
184
+ target.isConnected &&
185
+ hasAttributeMutation(
186
+ entries,
187
+ (entry) => entry.target.parentElement === target && attributes.shouldApply(entry),
188
+ )
189
+ ) {
190
+ callback(target);
182
191
  }
192
+ })
193
+ : null;
194
+ scope.addCleanup(() => {
195
+ observedChildren.clear();
196
+ attributeObserver?.disconnect();
197
+ });
183
198
 
184
- prevValues.set(entry.target, currentValue);
185
- }
186
-
187
- if (shouldApply) {
188
- callback(target);
189
- }
190
- }, delay),
191
- );
192
-
193
- const syncObservedChildren = () => {
194
- Array.from(observedChildren).forEach((child) => {
199
+ const syncChildren = () => {
200
+ for (const child of observedChildren) {
195
201
  if (child.parentElement !== target) {
196
- observer.unobserve(child);
202
+ resizeObserver.unobserve(child);
197
203
  observedChildren.delete(child);
198
- prevValues.delete(child);
199
204
  }
200
- });
201
-
202
- Array.from(target?.children ?? []).forEach((child) => {
203
- if (observedChildren.has(child)) {
204
- return;
205
- }
206
-
207
- observer.observe(child);
208
- observedChildren.add(child);
209
- });
210
- };
211
-
212
- observer.observe(target);
213
- syncObservedChildren();
214
-
215
- const mutationObserver = createMutationObserver(target, { childList: true }, (entries) => {
216
- if (!entries.some((entry) => 'childList' === entry.type)) {
217
- return;
218
205
  }
219
206
 
220
- syncObservedChildren();
221
- onChildList?.(entries);
222
- callback(target);
223
- });
224
-
225
- return {
226
- resizeObserver: observer,
227
- mutationObserver,
228
- };
229
- };
230
-
231
- /**
232
- * Observes attribute changes on direct children.
233
- *
234
- * @param {Element} target Target element.
235
- * @param {(target: Element) => void} callback Callback to run.
236
- * @param {{ attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean, attributeOldValue?: boolean }} options
237
- * @returns {{ observer: MutationObserver, syncObservedChildren: () => void }}
238
- */
239
- const createDirectChildrenAttributeObserver = (
240
- target,
241
- callback,
242
- { attributeFilter, shouldApply, attributeOldValue = true } = {},
243
- ) => {
244
- const observer = new MutationObserver((entries) => {
245
- requestAnimationFrame(() => {
246
- if (!target?.isConnected) {
247
- return;
248
- }
249
-
250
- if (
251
- entries.some(
252
- (entry) =>
253
- 'attributes' === entry.type &&
254
- entry.target.parentElement === target &&
255
- shouldApply(entry),
256
- )
257
- ) {
258
- callback(target);
207
+ attributeObserver?.disconnect();
208
+ for (const child of target.children) {
209
+ if (!observedChildren.has(child)) {
210
+ resizeObserver.observe(child);
211
+ observedChildren.add(child);
259
212
  }
260
- });
261
- });
262
-
263
- const syncObservedChildren = () => {
264
- observer.disconnect();
265
- Array.from(target?.children ?? []).forEach((child) => {
266
- observer.observe(child, {
213
+ attributeObserver?.observe(child, {
267
214
  attributes: true,
268
- attributeFilter,
269
- attributeOldValue,
215
+ attributeFilter: attributes.attributeFilter,
216
+ attributeOldValue: true,
270
217
  });
271
- });
218
+ }
272
219
  };
273
220
 
274
- syncObservedChildren();
275
-
276
- return {
277
- observer,
278
- syncObservedChildren,
279
- };
221
+ syncChildren();
222
+ createMutationObserver(
223
+ target,
224
+ { childList: true },
225
+ (entries) => {
226
+ syncChildren();
227
+ if (!shouldApplyChildList || shouldApplyChildList(entries)) callback(target);
228
+ },
229
+ scope,
230
+ );
280
231
  };
281
232
 
282
233
  /**
283
234
  * Creates a bundled observer setup for layout re-application.
284
235
  *
285
236
  * @param {Element} target Target element.
286
- * @param {(target: Element) => void} apply Apply function.
237
+ * @param {(target: Element, scope: ReturnType<typeof createObserverScope>) => void} apply Apply function.
287
238
  * @param {{
288
239
  * getResizeValue?: (entry: ResizeObserverEntry) => unknown,
289
- * delay?: number,
290
- * observeResize?: boolean,
291
- * observeIntersection?: boolean,
292
240
  * observeDirectChildrenResize?: boolean,
293
- * targetMutation?: { options: MutationObserverInit, shouldApply?: (entries: MutationRecord[]) => boolean },
294
- * directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean, attributeOldValue?: boolean }
241
+ * targetMutation?: { options: MutationObserverInit, shouldApply: (entries: MutationRecord[]) => boolean },
242
+ * directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean },
243
+ * shouldApplyChildList?: (entries: MutationRecord[]) => boolean
295
244
  * }} [options]
296
- * @returns {void}
245
+ * @returns {() => void} Stops observation and cancels queued work.
297
246
  */
298
247
  const createLayoutObserver = (
299
248
  target,
300
249
  apply,
301
250
  {
302
251
  getResizeValue,
303
- delay = 250,
304
- observeResize = true,
305
- observeIntersection = false,
306
252
  observeDirectChildrenResize = false,
307
253
  targetMutation,
308
254
  directChildMutation,
255
+ shouldApplyChildList,
309
256
  } = {},
310
257
  ) => {
311
- const shouldObserveIntersection =
312
- observeIntersection && 'undefined' !== typeof IntersectionObserver;
258
+ const scope = createObserverScope(target);
259
+ const shouldObserveIntersection = 'undefined' !== typeof IntersectionObserver;
313
260
  let isIntersecting = !shouldObserveIntersection || isNearViewport(target);
314
- let needsApply = shouldObserveIntersection && !isIntersecting;
261
+ let needsApply = !isIntersecting;
315
262
 
316
- const runApply = (element = target) => {
317
- if (!element?.isConnected) {
263
+ const runApply = () => {
264
+ if (scope.disposed || !target.isConnected) {
318
265
  return;
319
266
  }
320
267
 
321
- if (shouldObserveIntersection && !isIntersecting) {
268
+ if (!isIntersecting) {
322
269
  needsApply = true;
323
270
  return;
324
271
  }
325
272
 
326
273
  needsApply = false;
327
- apply(element);
274
+ apply(target, scope);
328
275
  };
329
276
 
330
- const schedule = createScheduledTargetCallback(target, runApply);
277
+ const schedule = createScheduledTargetCallback(target, runApply, scope);
331
278
  const scheduleApply = () => {
332
- if (!target?.isConnected) {
333
- return;
334
- }
335
-
336
- if (shouldObserveIntersection && !isIntersecting) {
337
- needsApply = true;
279
+ if (scope.disposed || !target?.isConnected) {
338
280
  return;
339
281
  }
340
282
 
341
283
  needsApply = true;
342
- schedule();
284
+ if (isIntersecting) {
285
+ schedule();
286
+ }
343
287
  };
344
288
 
345
- let syncDirectChildAttributes = () => {};
346
-
347
- const resizeBundle = !observeResize
348
- ? {
349
- resizeObserver: null,
350
- mutationObserver: null,
351
- }
352
- : observeDirectChildrenResize
353
- ? createDirectChildrenResizeObserver(target, scheduleApply, {
354
- getValue: getResizeValue,
355
- delay,
356
- onChildList: () => {
357
- syncDirectChildAttributes();
358
- },
359
- })
360
- : {
361
- resizeObserver: createResizeObserver(target, scheduleApply, {
362
- getValue: getResizeValue,
363
- delay,
364
- }),
365
- };
366
-
367
- if (shouldObserveIntersection) {
368
- createIntersectionObserver(target, (entry) => {
369
- isIntersecting = entry.isIntersecting;
370
- if (isIntersecting && needsApply) {
371
- scheduleApply();
372
- }
373
- });
289
+ const resizeObserver = createResizeObserver(target, scheduleApply, scope, getResizeValue);
290
+ if (observeDirectChildrenResize) {
291
+ observeLayoutChildren(
292
+ target,
293
+ resizeObserver,
294
+ scheduleApply,
295
+ scope,
296
+ directChildMutation,
297
+ shouldApplyChildList,
298
+ );
374
299
  }
375
300
 
376
- if (targetMutation?.options) {
377
- createMutationObserver(target, targetMutation.options, (entries) => {
378
- if (targetMutation.shouldApply?.(entries) ?? 0 < entries.length) {
379
- scheduleApply();
380
- }
381
- });
301
+ if (shouldObserveIntersection) {
302
+ createIntersectionObserver(
303
+ target,
304
+ (entry) => {
305
+ isIntersecting = entry.isIntersecting;
306
+ if (isIntersecting && needsApply) {
307
+ scheduleApply();
308
+ }
309
+ },
310
+ scope,
311
+ );
382
312
  }
383
313
 
384
- const directChildAttributeBundle =
385
- directChildMutation?.attributeFilter && directChildMutation.shouldApply
386
- ? createDirectChildrenAttributeObserver(target, scheduleApply, {
387
- attributeFilter: directChildMutation.attributeFilter,
388
- shouldApply: directChildMutation.shouldApply,
389
- attributeOldValue: directChildMutation.attributeOldValue,
390
- })
391
- : null;
392
-
393
- if (directChildAttributeBundle) {
394
- syncDirectChildAttributes = directChildAttributeBundle.syncObservedChildren;
314
+ if (targetMutation) {
315
+ createMutationObserver(
316
+ target,
317
+ targetMutation.options,
318
+ (entries) => {
319
+ if (targetMutation.shouldApply(entries)) {
320
+ scheduleApply();
321
+ }
322
+ },
323
+ scope,
324
+ );
395
325
  }
396
326
 
397
- if (!resizeBundle.mutationObserver && directChildAttributeBundle) {
398
- createMutationObserver(target, { childList: true }, (entries) => {
399
- if (!entries.some((entry) => 'childList' === entry.type)) {
400
- return;
401
- }
402
-
403
- syncDirectChildAttributes();
404
- scheduleApply();
405
- });
327
+ if (isIntersecting) {
328
+ runApply();
406
329
  }
407
330
 
408
- if (!shouldObserveIntersection || isIntersecting) {
409
- runApply(target);
410
- }
331
+ // Keep the public callable disposer; React also uses its internal refresh operation.
332
+ return Object.assign(scope.dispose, { refresh: runApply });
411
333
  };
412
334
 
413
335
  const getBorderBoxInlineSize = (entry) => entry.borderBoxSize?.[0].inlineSize;
414
336
 
415
- const getContentRectWidth = (entry) => parseInt(entry.contentRect?.width);
337
+ const getElementStyle = (element) =>
338
+ (element.ownerDocument.defaultView ?? window).getComputedStyle(element);
339
+
340
+ const isInFlow = (element) => {
341
+ const { position, display } = getElementStyle(element);
342
+ return !['absolute', 'fixed'].includes(position) && 'none' !== display;
343
+ };
416
344
 
417
345
  const hasLayoutBox = (element) => !!element?.isConnected && 0 < element.getClientRects().length;
418
346
 
@@ -468,32 +396,51 @@ const getNormalizedInlineRect = (
468
396
  };
469
397
 
470
398
  /**
471
- * Returns whether the mutation list contains a matching attributes record.
399
+ * Compares each attribute before the batch with its final value, ignoring intermediate writes.
472
400
  *
473
401
  * @param {MutationRecord[]} entries Mutation records.
474
402
  * @param {(entry: MutationRecord) => boolean} predicate Match predicate.
475
403
  * @returns {boolean} Whether a matching attributes mutation exists.
476
404
  */
477
- const hasAttributeMutation = (entries, predicate) =>
478
- entries.some((entry) => 'attributes' === entry.type && predicate(entry));
405
+ const hasAttributeMutation = (entries, predicate) => {
406
+ const seen = new Map();
407
+ return entries.some((entry) => {
408
+ if ('attributes' !== entry.type) {
409
+ return false;
410
+ }
411
+ const attributes = seen.get(entry.target) ?? new Set();
412
+ if (attributes.has(entry.attributeName)) {
413
+ return false;
414
+ }
415
+ attributes.add(entry.attributeName);
416
+ seen.set(entry.target, attributes);
417
+ return predicate(entry);
418
+ });
419
+ };
479
420
 
480
421
  /**
481
422
  * Coalesces repeated calls into the final call within the delay window.
482
423
  *
483
424
  * @param {Function} fn Function to wrap.
484
425
  * @param {number} delay Delay in milliseconds.
485
- * @returns {Function} Debounced function.
426
+ * @returns {Function & { cancel: () => void }} Debounced function with cancellation.
486
427
  */
487
428
  export function debounce(fn, delay) {
488
429
  let timer;
489
430
 
490
- return function (...args) {
431
+ const debounced = function (...args) {
491
432
  const context = this;
492
433
  clearTimeout(timer);
493
434
  timer = setTimeout(() => {
435
+ timer = undefined;
494
436
  fn.apply(context, args);
495
437
  }, delay);
496
438
  };
439
+ debounced.cancel = () => {
440
+ clearTimeout(timer);
441
+ timer = undefined;
442
+ };
443
+ return debounced;
497
444
  }
498
445
 
499
446
  /**
@@ -502,7 +449,7 @@ export function debounce(fn, delay) {
502
449
  * @param {Element} target Target element.
503
450
  * @returns {void}
504
451
  */
505
- export const setDividerLinewrap = (target) => {
452
+ const updateDividerLinewrap = (target) => {
506
453
  const children = Array.from(target?.children ?? []);
507
454
  const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), [
508
455
  'divider:initialized',
@@ -515,14 +462,14 @@ export const setDividerLinewrap = (target) => {
515
462
  layoutTokens: withoutLayoutTokens(getLayoutTokens(child), ['-bol', '-linewrap']),
516
463
  }));
517
464
 
518
- const resetChildLayouts = () => {
465
+ const applyChildLayouts = () => {
519
466
  childLayouts.forEach(({ child, layoutTokens }) => {
520
467
  setLayoutTokens(child, layoutTokens);
521
468
  });
522
469
  };
523
470
 
524
471
  if (!currentLayoutArray.some((value) => value.startsWith('-divider:'))) {
525
- resetChildLayouts();
472
+ applyChildLayouts();
526
473
  return;
527
474
  }
528
475
 
@@ -532,41 +479,34 @@ export const setDividerLinewrap = (target) => {
532
479
  }
533
480
 
534
481
  if (!hasLayoutBox(target)) {
535
- resetChildLayouts();
482
+ applyChildLayouts();
536
483
  return;
537
484
  }
538
485
 
539
486
  const defaultView =
540
487
  target?.ownerDocument?.defaultView ?? ('undefined' !== typeof window ? window : undefined);
541
488
  if (!defaultView?.getComputedStyle) {
542
- resetChildLayouts();
489
+ applyChildLayouts();
543
490
  return;
544
491
  }
545
492
 
546
- const targetStyle = defaultView.getComputedStyle(target);
493
+ const targetStyle = getElementStyle(target);
547
494
  const flow = {
548
495
  direction: targetStyle.getPropertyValue('direction'),
549
496
  flexDirection: targetStyle.getPropertyValue('flex-direction'),
550
497
  writingMode: targetStyle.getPropertyValue('writing-mode'),
551
498
  };
552
499
 
553
- const targetChildren = childLayouts.reduce((accumulator, { child, layoutTokens }) => {
554
- const style = defaultView.getComputedStyle(child);
555
- const position = style.getPropertyValue('position');
556
- const display = style.getPropertyValue('display');
557
- if ('absolute' !== position && 'fixed' !== position && 'none' !== display) {
558
- const rect = child.getBoundingClientRect();
559
- accumulator.push({
560
- child,
561
- layoutTokens,
562
- inlineRect: getNormalizedInlineRect(rect, flow),
563
- });
500
+ const targetChildren = [];
501
+ for (const entry of childLayouts) {
502
+ if (isInFlow(entry.child)) {
503
+ entry.inlineRect = getNormalizedInlineRect(entry.child.getBoundingClientRect(), flow);
504
+ targetChildren.push(entry);
564
505
  }
565
- return accumulator;
566
- }, []);
506
+ }
567
507
 
568
508
  if (0 === targetChildren.length) {
569
- resetChildLayouts();
509
+ applyChildLayouts();
570
510
  setLayoutTokens(target, [...currentLayoutArray, 'divider:initialized']);
571
511
  return;
572
512
  }
@@ -574,15 +514,14 @@ export const setDividerLinewrap = (target) => {
574
514
  let prevInlineRect;
575
515
  let hasWrapped = false;
576
516
  let isStack = true;
577
- const nextChildLayouts = targetChildren.map(({ child, layoutTokens, inlineRect }, index) => {
578
- const nextLayoutTokens = [...layoutTokens];
517
+ targetChildren.forEach(({ layoutTokens, inlineRect }, index) => {
579
518
  const isBeginningOfLine =
580
519
  0 === index ||
581
520
  inlineRect.start < prevInlineRect.end - layoutPositionTolerance ||
582
521
  inlineRect.start <= prevInlineRect.start + layoutPositionTolerance;
583
522
 
584
523
  if (isBeginningOfLine) {
585
- nextLayoutTokens.push('-bol');
524
+ layoutTokens.push('-bol');
586
525
  if (0 < index) {
587
526
  hasWrapped = true;
588
527
  }
@@ -591,19 +530,12 @@ export const setDividerLinewrap = (target) => {
591
530
  }
592
531
 
593
532
  if (hasWrapped) {
594
- nextLayoutTokens.push('-linewrap');
533
+ layoutTokens.push('-linewrap');
595
534
  }
596
535
 
597
536
  prevInlineRect = inlineRect;
598
- return { child, layoutTokens: nextLayoutTokens };
599
- });
600
-
601
- const nextChildLayoutMap = new Map(
602
- nextChildLayouts.map(({ child, layoutTokens }) => [child, layoutTokens]),
603
- );
604
- childLayouts.forEach(({ child, layoutTokens }) => {
605
- setLayoutTokens(child, nextChildLayoutMap.get(child) ?? layoutTokens);
606
537
  });
538
+ applyChildLayouts();
607
539
 
608
540
  const nextTargetLayout = [...currentLayoutArray];
609
541
  if (isStack) {
@@ -614,56 +546,56 @@ export const setDividerLinewrap = (target) => {
614
546
  setLayoutTokens(target, nextTargetLayout);
615
547
  };
616
548
 
549
+ const withoutAttributeTokens = (value, ignoredTokens) =>
550
+ withoutLayoutTokens((value ?? '').split(/\s+/).filter(Boolean), ignoredTokens)
551
+ .sort()
552
+ .join(' ');
553
+
554
+ const hasChangedAttribute = (entry) => {
555
+ const current = entry.target.getAttribute(entry.attributeName);
556
+ // Behaviors may append their tokens in different orders on the same element.
557
+ if (['data-unitone-layout', 'class'].includes(entry.attributeName)) {
558
+ return withoutAttributeTokens(current, []) !== withoutAttributeTokens(entry.oldValue, []);
559
+ }
560
+ return (current ?? '') !== (entry.oldValue ?? '');
561
+ };
562
+
563
+ /** Refreshes the layout, deferring React-managed targets to their component. */
564
+ export const setDividerLinewrap = (target) => {
565
+ if (!requestReactLayoutRefresh(target)) updateDividerLinewrap(target);
566
+ };
567
+
617
568
  /**
618
569
  * Creates the observer bundle for divider layouts.
619
570
  *
620
571
  * @param {Element} target Target element.
621
572
  * @param {{ ignore?: { layout?: string[], className?: string[] } }} [args]
622
- * @returns {void}
573
+ * @returns {() => void} Stops observation and cancels queued work.
623
574
  */
624
575
  export const dividersResizeObserver = (target, args = {}) => {
576
+ if (!args.reactOwned && requestReactLayoutRefresh(target)) return () => {};
625
577
  const shouldRecalculateByAttributeMutation = (entry) => {
626
- if ('data-unitone-layout' === entry.attributeName) {
627
- const ignoreUnitoneLayouts = [
628
- ...(args?.ignore?.layout ?? []),
629
- ...['divider:initialized', '-bol', '-linewrap', '-stack'],
630
- ];
631
-
632
- const current = (entry.target.getAttribute(entry.attributeName) ?? '')
633
- .split(' ')
634
- .filter((v) => !ignoreUnitoneLayouts.includes(v))
635
- .join(' ');
636
-
637
- const old = (entry.oldValue ?? '')
638
- .split(' ')
639
- .filter((v) => !ignoreUnitoneLayouts.includes(v))
640
- .join(' ');
641
-
642
- return current !== old;
578
+ const { attributeName, oldValue } = entry;
579
+ const currentValue = entry.target.getAttribute(attributeName);
580
+ const ignoredTokens =
581
+ layoutAttributeName === attributeName
582
+ ? [...(args?.ignore?.layout ?? [])]
583
+ : 'class' === attributeName
584
+ ? [...(args?.ignore?.className ?? [])]
585
+ : null;
586
+
587
+ if (ignoredTokens) {
588
+ return (
589
+ withoutAttributeTokens(currentValue, ignoredTokens) !==
590
+ withoutAttributeTokens(oldValue, ignoredTokens)
591
+ );
643
592
  }
644
593
 
645
- if ('class' === entry.attributeName) {
646
- const ignoreClassNames = [...(args?.ignore?.className ?? [])];
647
-
648
- const current = (entry.target.getAttribute(entry.attributeName) ?? '')
649
- .split(' ')
650
- .filter((v) => !ignoreClassNames.includes(v))
651
- .join(' ');
652
-
653
- const old = (entry.oldValue ?? '')
654
- .split(' ')
655
- .filter((v) => !ignoreClassNames.includes(v))
656
- .join(' ');
657
-
658
- return current !== old;
659
- }
660
-
661
- return ['style', 'dir'].includes(entry.attributeName);
594
+ return ['style', 'dir'].includes(attributeName) && (currentValue ?? '') !== (oldValue ?? '');
662
595
  };
663
596
 
664
- createLayoutObserver(target, setDividerLinewrap, {
597
+ return createLayoutObserver(target, updateDividerLinewrap, {
665
598
  getResizeValue: getBorderBoxInlineSize,
666
- observeIntersection: true,
667
599
  observeDirectChildrenResize: true,
668
600
  targetMutation: {
669
601
  options: {
@@ -679,7 +611,6 @@ export const dividersResizeObserver = (target, args = {}) => {
679
611
  },
680
612
  directChildMutation: {
681
613
  attributeFilter: ['style', 'data-unitone-layout', 'class'],
682
- attributeOldValue: true,
683
614
  shouldApply: shouldRecalculateByAttributeMutation,
684
615
  },
685
616
  });
@@ -691,18 +622,17 @@ export const dividersResizeObserver = (target, args = {}) => {
691
622
  * @param {Element} target Target element.
692
623
  * @returns {void}
693
624
  */
694
- export const setStairsStep = (target) => {
625
+ const updateStairsStep = (target) => {
695
626
  const children = Array.from(target.children);
696
627
  const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), ['stairs:initialized']);
697
628
  setLayoutTokens(target, currentLayoutArray);
698
629
 
699
- const firstChild = children[0];
700
- if (!firstChild) {
630
+ if (0 === children.length) {
701
631
  setLayoutTokens(target, [...currentLayoutArray, 'stairs:initialized']);
702
632
  return;
703
633
  }
704
634
 
705
- // Reset
635
+ // Measure the unshifted layout before applying steps and measuring their overflow.
706
636
  target.style.removeProperty('--unitone--stairs-step-overflow-volume');
707
637
  target.style.removeProperty('--unitone--max-stairs-step');
708
638
  children.forEach((child) => {
@@ -720,12 +650,10 @@ export const setStairsStep = (target) => {
720
650
 
721
651
  const isAlternatingStairs = ['up-down', 'down-up'].includes(stairsUp);
722
652
 
723
- const direction = window.getComputedStyle(target).getPropertyValue('flex-direction');
653
+ const direction = getElementStyle(target).getPropertyValue('flex-direction');
724
654
  const targetBottom = target.getBoundingClientRect().bottom;
725
655
  const filteredChildren = children.reduce((accumulator, child) => {
726
- const position = window.getComputedStyle(child).getPropertyValue('position');
727
- const display = window.getComputedStyle(child).getPropertyValue('display');
728
- if ('absolute' === position || 'fixed' === position || 'none' === display) {
656
+ if (!isInFlow(child)) {
729
657
  return accumulator;
730
658
  }
731
659
 
@@ -744,7 +672,7 @@ export const setStairsStep = (target) => {
744
672
  const isBol =
745
673
  'row-reverse' === direction ? prevRect?.left <= rect.left : prevRect?.left >= rect.left;
746
674
 
747
- if (0 === index || (firstChild === child && !prevRect) || isBol) {
675
+ if (0 === index || isBol) {
748
676
  stairsStep = 0;
749
677
  } else if (isAlternatingStairs) {
750
678
  stairsStep = 0 === stairsStep ? 1 : 0;
@@ -779,16 +707,33 @@ export const setStairsStep = (target) => {
779
707
  setLayoutTokens(target, [...currentLayoutArray, 'stairs:initialized']);
780
708
  };
781
709
 
710
+ /** Refreshes the layout, deferring React-managed targets to their component. */
711
+ export const setStairsStep = (target) => {
712
+ if (!requestReactLayoutRefresh(target)) updateStairsStep(target);
713
+ };
714
+
782
715
  /**
783
716
  * Creates the observer bundle for stairs layouts.
784
717
  *
785
718
  * @param {Element} target Target element.
786
- * @returns {void}
719
+ * @returns {() => void} Stops observation and cancels queued work.
787
720
  */
788
- export const stairsResizeObserver = (target) => {
789
- createLayoutObserver(target, setStairsStep, {
790
- observeIntersection: true,
721
+ export const stairsResizeObserver = (target, args = {}) => {
722
+ if (!args.reactOwned && requestReactLayoutRefresh(target)) return () => {};
723
+ return createLayoutObserver(target, updateStairsStep, {
791
724
  observeDirectChildrenResize: true,
725
+ targetMutation: {
726
+ options: {
727
+ attributes: true,
728
+ attributeFilter: ['style', 'class', 'dir', 'data-unitone-layout'],
729
+ attributeOldValue: true,
730
+ },
731
+ shouldApply: (entries) => hasAttributeMutation(entries, hasChangedAttribute),
732
+ },
733
+ directChildMutation: {
734
+ attributeFilter: ['style', 'class', 'dir', 'data-unitone-layout'],
735
+ shouldApply: hasChangedAttribute,
736
+ },
792
737
  });
793
738
  };
794
739
 
@@ -800,11 +745,7 @@ export const stairsResizeObserver = (target) => {
800
745
  */
801
746
  const isIgnoredVerticalWritingMutationNode = (node) =>
802
747
  node?.nodeType === Node.ELEMENT_NODE &&
803
- [
804
- 'vertical-writing__thresholder',
805
- 'vertical-writing:initialized',
806
- 'vertical-writing:safari',
807
- ].includes(node.getAttribute('data-unitone-layout'));
748
+ 'vertical-writing__thresholder' === node.getAttribute(layoutAttributeName);
808
749
 
809
750
  /**
810
751
  * Returns whether vertical-writing mutations require re-application.
@@ -813,10 +754,9 @@ const isIgnoredVerticalWritingMutationNode = (node) =>
813
754
  * @returns {boolean} Whether re-application is required.
814
755
  */
815
756
  const shouldApplyVerticalWritingMutation = (entries) =>
757
+ hasAttributeMutation(entries, hasChangedAttribute) ||
816
758
  entries.some((entry) => {
817
- if ('attributes' === entry.type) {
818
- return true;
819
- }
759
+ if ('characterData' === entry.type) return true;
820
760
 
821
761
  if ('childList' !== entry.type) {
822
762
  return false;
@@ -831,9 +771,10 @@ const shouldApplyVerticalWritingMutation = (entries) =>
831
771
  * Recalculates column count and height for vertical-writing layouts.
832
772
  *
833
773
  * @param {Element} target Target element.
774
+ * @param {ReturnType<typeof createObserverScope>} [scope] Observer lifetime.
834
775
  * @returns {void}
835
776
  */
836
- export const setColumnCountForVertical = (target) => {
777
+ const updateColumnCountForVertical = (target, scope) => {
837
778
  if (!target) {
838
779
  return;
839
780
  }
@@ -854,10 +795,7 @@ export const setColumnCountForVertical = (target) => {
854
795
  Array.from(target.children)
855
796
  .reverse()
856
797
  .some((child) => {
857
- if (
858
- !['absolute', 'fixed'].includes(getComputedStyle(child).position) &&
859
- 'none' !== getComputedStyle(child).display
860
- ) {
798
+ if (isInFlow(child)) {
861
799
  lastChild = child;
862
800
  return true;
863
801
  }
@@ -868,11 +806,11 @@ export const setColumnCountForVertical = (target) => {
868
806
  return;
869
807
  }
870
808
 
871
- const computedStyle = getComputedStyle(target);
809
+ const computedStyle = getElementStyle(target);
872
810
  const threshold = String(computedStyle.getPropertyValue('--unitone--threshold')).trim();
873
811
  let forceSwitch = false;
874
812
 
875
- if (threshold) {
813
+ if (threshold && !/^[+-]?(?:0+\.?0*|\.0+)(?:[a-z]+|%)?$/i.test(threshold)) {
876
814
  const thresholder = target.ownerDocument.createElement('div');
877
815
  thresholder.setAttribute(layoutAttributeName, 'vertical-writing__thresholder');
878
816
  target.appendChild(thresholder);
@@ -892,7 +830,10 @@ export const setColumnCountForVertical = (target) => {
892
830
 
893
831
  setLayoutTokens(target, [...nextLayoutTokens, 'vertical-writing:initialized']);
894
832
 
895
- requestAnimationFrame(() => {
833
+ const schedule = scope
834
+ ? (callback) => scope.requestAnimationFrame(callback)
835
+ : (callback) => target.ownerDocument.defaultView.requestAnimationFrame(callback);
836
+ schedule(() => {
896
837
  if (!target?.isConnected) {
897
838
  return;
898
839
  }
@@ -915,27 +856,40 @@ export const setColumnCountForVertical = (target) => {
915
856
  };
916
857
 
917
858
  /**
918
- * Creates the observer bundle for vertical-writing layouts.
859
+ * Recalculates vertical-writing columns without creating persistent observers.
919
860
  *
920
861
  * @param {Element} target Target element.
921
862
  * @returns {void}
922
863
  */
923
- export const verticalsResizeObserver = (target) => {
924
- const applyVerticalColumns = (element) => {
864
+ export const setColumnCountForVertical = (target) => {
865
+ if (!requestReactLayoutRefresh(target)) updateColumnCountForVertical(target);
866
+ };
867
+
868
+ /**
869
+ * Creates the observer bundle for vertical-writing layouts.
870
+ *
871
+ * @param {Element} target Target element.
872
+ * @returns {() => void} Stops observation and cancels queued work.
873
+ */
874
+ export const verticalsResizeObserver = (target, args = {}) => {
875
+ if (!args.reactOwned && requestReactLayoutRefresh(target)) return () => {};
876
+ const applyVerticalColumns = (element, scope) => {
925
877
  if (element.parentNode?.style) {
926
878
  element.parentNode.style.height = '';
927
879
  }
928
880
 
929
- setColumnCountForVertical(element);
881
+ updateColumnCountForVertical(element, scope);
930
882
  };
931
883
 
932
- createLayoutObserver(target, applyVerticalColumns, {
933
- getResizeValue: getContentRectWidth,
934
- observeIntersection: true,
884
+ return createLayoutObserver(target, applyVerticalColumns, {
885
+ observeDirectChildrenResize: true,
886
+ shouldApplyChildList: shouldApplyVerticalWritingMutation,
935
887
  targetMutation: {
936
888
  options: {
937
889
  attributes: true,
938
- attributeFilter: ['style'],
890
+ attributeFilter: ['style', 'class', 'dir', 'data-unitone-layout'],
891
+ attributeOldValue: true,
892
+ characterData: true,
939
893
  childList: true,
940
894
  subtree: true,
941
895
  },
@@ -944,111 +898,94 @@ export const verticalsResizeObserver = (target) => {
944
898
  });
945
899
  };
946
900
 
947
- /**
948
- * Creates duplicated marquee content and refreshes initialization markers.
949
- *
950
- * @param {Element} target Target element.
951
- * @returns {Element | undefined} The duplicated marquee element, if created.
952
- */
953
- export const setMarquee = (target) => {
954
- let clonedMarquee;
901
+ const marqueeStates = new WeakMap();
902
+
903
+ /** Updates copied items while keeping the original nodes and animation intact. */
904
+ const updateMarquee = (target) => {
905
+ let parts = getMarqueeParts(target);
906
+ const { marquee, originals } = parts;
907
+ if (!marquee) return;
908
+ const state = marqueeStates.get(target);
909
+ const source = originals.map((item) => item.outerHTML).join('');
910
+ // Snapshots exclude generated nodes, so author additions after copies are detected too.
911
+ if (
912
+ state?.marquee !== marquee ||
913
+ state?.source !== source ||
914
+ originals.some(
915
+ (item) =>
916
+ item.matches('input, textarea, select') || item.querySelector('input, textarea, select'),
917
+ )
918
+ ) {
919
+ parts.copies.forEach((copy) => copy.remove());
920
+ parts.copies = [];
921
+ }
955
922
 
956
- const addInitializedToken = (element) => {
957
- const layout = element.getAttribute('data-unitone-layout') ?? '';
958
- if (layout.split(/\s+/).includes('marquee:initialized')) {
959
- return;
923
+ let firstClone;
924
+ const renderCopies = (groups) => {
925
+ const count = groups * originals.length;
926
+ parts.copies.splice(count).forEach((copy) => copy.remove());
927
+ const fragment = target.ownerDocument.createDocumentFragment();
928
+ while (parts.copies.length < count) {
929
+ const copy = originals[parts.copies.length % originals.length].cloneNode(true);
930
+ markMarqueeCopy(copy);
931
+ parts.copies.push(copy);
932
+ fragment.append(copy);
933
+ firstClone ??= copy;
960
934
  }
961
- element.setAttribute('data-unitone-layout', `${layout} marquee:initialized`.trim());
962
- };
963
-
964
- const removeInitializedToken = (element) => {
965
- const layout = element.getAttribute('data-unitone-layout') ?? '';
966
- const next = layout
967
- .split(/\s+/)
968
- .filter((value) => value && 'marquee:initialized' !== value)
969
- .join(' ');
970
- element.setAttribute('data-unitone-layout', next);
935
+ if (fragment.childNodes.length) marquee.append(fragment);
971
936
  };
972
937
 
973
- const getMarquees = () => target.querySelectorAll(':scope > [data-unitone-layout~="marquee"]');
974
- let marquees = getMarquees();
975
- if (0 === marquees.length) {
976
- return;
938
+ let layout = measureMarquee(target, parts);
939
+ if (layout.groups && !parts.copies.length) {
940
+ renderCopies(1);
941
+ layout = measureMarquee(target, parts);
977
942
  }
978
-
979
- const shouldRestartAnimation = Array.from(marquees).some((marquee) =>
980
- (marquee.getAttribute(layoutAttributeName) ?? '').split(/\s+/).includes('marquee:initialized'),
981
- );
982
-
983
- if (1 === target.childElementCount && 1 === marquees.length) {
984
- const marquee = marquees[0];
985
- clonedMarquee = marquee.cloneNode(true);
986
- clonedMarquee.setAttribute('aria-hidden', 'true');
987
- marquee.after(clonedMarquee);
943
+ renderCopies(layout.groups);
944
+ if (marquee) {
945
+ const travel = `${layout.rtl ? layout.distance : -layout.distance}px`;
946
+ if (marquee.style.getPropertyValue('--unitone--marquee-travel') !== travel) {
947
+ marquee.style.setProperty('--unitone--marquee-travel', travel);
948
+ }
949
+ if (layout.distance > 0) {
950
+ const tokens = getLayoutTokens(marquee);
951
+ if (!tokens.includes('marquee:initialized'))
952
+ setLayoutTokens(marquee, [...tokens, 'marquee:initialized']);
953
+ }
988
954
  }
955
+ marqueeStates.set(target, { marquee, source });
956
+ return firstClone?.isConnected ? firstClone : undefined;
957
+ };
989
958
 
990
- marquees = getMarquees();
991
-
992
- if (!shouldRestartAnimation) {
993
- marquees.forEach((marquee) => {
994
- addInitializedToken(marquee);
995
- });
996
- return clonedMarquee;
959
+ /**
960
+ * Refreshes copied items without restarting the animation.
961
+ * React-owned marquees receive a refresh request and render copies through React.
962
+ *
963
+ * @param {Element} target Marquee wrapper.
964
+ * @returns {Element | undefined} The first newly created HTML item copy, if any.
965
+ */
966
+ export const setMarquee = (target) => {
967
+ if (isReactMarquee(target)) {
968
+ requestMarqueeRefresh(target);
969
+ return;
997
970
  }
998
-
999
- marquees.forEach((marquee) => {
1000
- removeInitializedToken(marquee);
1001
- });
1002
-
1003
- requestAnimationFrame(() => {
1004
- if (!target?.isConnected) {
1005
- return;
1006
- }
1007
- getMarquees().forEach((marquee) => {
1008
- addInitializedToken(marquee);
1009
- });
1010
- });
1011
-
1012
- return clonedMarquee;
971
+ return updateMarquee(target);
1013
972
  };
1014
973
 
1015
974
  /**
1016
- * Creates the observer bundle for marquee layouts.
975
+ * Observes marquee and original item sizes and content changes.
976
+ * React components own their observer lifetime and generated children.
1017
977
  *
1018
- * @param {Element} target Target element.
1019
- * @returns {void}
978
+ * @param {Element} target Marquee wrapper.
979
+ * @returns {() => void} Stops observation and cancels queued work.
1020
980
  */
1021
981
  export const marqueeResizeObserver = (target) => {
1022
- let clonedMarquee;
1023
-
1024
- const applyMarquee = (element) => {
1025
- clonedMarquee = setMarquee(element);
982
+ if (isReactMarquee(target)) {
983
+ return observeMarquee(target, () => requestMarqueeRefresh(target), { listenForRefresh: false })
984
+ .dispose;
985
+ }
986
+ const controller = observeMarquee(target, () => updateMarquee(target));
987
+ return () => {
988
+ controller.dispose();
989
+ marqueeStates.delete(target);
1026
990
  };
1027
-
1028
- createLayoutObserver(target, applyMarquee, {
1029
- observeResize: false,
1030
- observeIntersection: true,
1031
- targetMutation: {
1032
- options: {
1033
- childList: true,
1034
- },
1035
- shouldApply: (entries) => {
1036
- const addedNodes = entries.flatMap((entry) => Array.from(entry.addedNodes ?? []));
1037
- const removedNodes = entries.flatMap((entry) => Array.from(entry.removedNodes ?? []));
1038
-
1039
- if (
1040
- clonedMarquee?.isConnected &&
1041
- 1 === addedNodes.length &&
1042
- 0 === removedNodes.length &&
1043
- addedNodes[0] === clonedMarquee
1044
- ) {
1045
- clonedMarquee = null;
1046
- return false;
1047
- }
1048
-
1049
- clonedMarquee = null;
1050
- return true;
1051
- },
1052
- },
1053
- });
1054
991
  };