@botiverse/raft-ui 0.60.0 → 0.60.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.
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [0.60.2](https://github.com/botiverse/raft-design/compare/v0.60.1...v0.60.2) (2026-11-04)
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **ScrollArea**: no "ResizeObserver loop completed with undelivered notifications" error over a VirtualList. ([#980](https://github.com/botiverse/raft-design/pull/980))
|
|
6
|
+
- Why: ScrollArea watched every child of its container, including VirtualList's items element, whose min-height VirtualList sets inside its own ResizeObserver callback. When a row in view grew (an image loaded, a reaction was added), that element resized inside the frame's deepest callback and Chromium logged the loop error.
|
|
7
|
+
- Affects: over a VirtualList, ScrollArea now watches the rows' box inside the items element. A resize or child change is measured on the next animation frame, coalesced with a pending scroll frame; mounting and `measure()` / `actionsRef.measure()` still measure at once.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/scroll-area/
|
|
9
|
+
|
|
10
|
+
## [0.60.1](https://github.com/botiverse/raft-design/compare/v0.60.0...v0.60.1) (2026-11-04)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **DropdownMenuShortcut** and **ContextMenuShortcut**: a menu shortcut draws no cap line. ([#976](https://github.com/botiverse/raft-design/pull/976))
|
|
15
|
+
- Why: Kbd draws its border with an inset ring, and the menu shortcuts reset `ring`, which left a 1px inset box around every menu shortcut, light and dark.
|
|
16
|
+
- Affects: only the two menu shortcut parts; Kbd, KbdShortcut, tooltips and the search trigger keep their caps.
|
|
17
|
+
- Docs: https://raft-design.botiverse.build/components/kbd/
|
|
18
|
+
|
|
1
19
|
## [0.60.0](https://github.com/botiverse/raft-design/compare/v0.59.2...v0.60.0) (2026-11-03)
|
|
2
20
|
|
|
3
21
|
### Breaking Changes
|
|
@@ -51,7 +51,7 @@ const contextMenu = tv({ slots: {
|
|
|
51
51
|
label: ["px-2.5 pt-2 pb-1", "font-sans text-caption font-bold text-foreground-hint"],
|
|
52
52
|
shortcut: [
|
|
53
53
|
"ml-auto",
|
|
54
|
-
"h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none ring-0",
|
|
54
|
+
"h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none inset-ring-0",
|
|
55
55
|
"font-mono text-mono-meta font-normal text-foreground-hint"
|
|
56
56
|
],
|
|
57
57
|
separator: ["-mx-1 my-1 h-px border-0 bg-line-hairline"],
|
|
@@ -53,7 +53,7 @@ const dropdownMenu = tv({ slots: {
|
|
|
53
53
|
label: ["px-2.5 pt-2 pb-1", "font-sans text-caption font-bold text-foreground-hint"],
|
|
54
54
|
shortcut: [
|
|
55
55
|
"ml-auto",
|
|
56
|
-
"h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none ring-0",
|
|
56
|
+
"h-auto min-w-0 rounded-none bg-transparent p-0 shadow-none inset-ring-0",
|
|
57
57
|
"font-mono text-mono-meta font-normal text-foreground-hint"
|
|
58
58
|
],
|
|
59
59
|
itemCount: ["ml-auto inline-flex min-w-5 justify-center gap-0 px-1 tabular-nums"],
|
|
@@ -90,6 +90,18 @@ function warnStickyLayout(container, style) {
|
|
|
90
90
|
warnedContainers.add(container);
|
|
91
91
|
console.warn(`raft-ui: a ScrollArea container with sticky gutters ${problem}. The gutters layer is the container's first child, in flow: lay the content out in one wrapper inside it (ScrollArea's contentClassName), or bind the container with useScrollAreaContainer({ guttersPosition: "absolute" }).`, container);
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* What is watched for a child of the container. VirtualList's items element (the list as the
|
|
95
|
+
* container, as in a message timeline) is sized by the list itself, from its own ResizeObserver's
|
|
96
|
+
* notification for a row: watching it there would report a size change the browser can't deliver
|
|
97
|
+
* in that frame ("ResizeObserver loop completed with undelivered notifications"). Its rows' box
|
|
98
|
+
* inside it, which only the rows size, is watched instead; the size the list adds below the rows
|
|
99
|
+
* is the scroll size, which the next scrolled frame reads.
|
|
100
|
+
*/
|
|
101
|
+
function observedFor(child) {
|
|
102
|
+
if (child.getAttribute("data-slot") !== "virtual-list-items") return child;
|
|
103
|
+
return child.querySelector(":scope > [data-slot=\"virtual-list-visible-items\"]") ?? child;
|
|
104
|
+
}
|
|
93
105
|
/** `--r-scroll-area-thumb-min-length` when the CSS isn't loaded (or isn't a px length). */
|
|
94
106
|
const defaultMinThumbLength = 24;
|
|
95
107
|
/** With `trigger="scroll"` the scrollbars show only while scrolling, so they stay at least this long (ms) to be seen and grabbed. */
|
|
@@ -107,11 +119,12 @@ function effectiveHideDelay(trigger, hideDelay) {
|
|
|
107
119
|
* container needs no handler wired to the scrollbars, and binds again when
|
|
108
120
|
* a render finds another element there.
|
|
109
121
|
*
|
|
110
|
-
* Two paths. The layout path (mounting,
|
|
111
|
-
*
|
|
112
|
-
* the gutters' lengths and the writing
|
|
113
|
-
* The ResizeObserver watches the container
|
|
114
|
-
* (`contentRef`, or each element child of the
|
|
122
|
+
* Two paths. The layout path (mounting, `measure()`, and the animation frame
|
|
123
|
+
* after a ResizeObserver or MutationObserver notification) reads the
|
|
124
|
+
* container's client and scroll sizes, the gutters' lengths and the writing
|
|
125
|
+
* direction, caches them and draws. The ResizeObserver watches the container
|
|
126
|
+
* (the viewport), the content (`contentRef`, or each element child of the
|
|
127
|
+
* container, see `observedFor`) and each gutter.
|
|
115
128
|
* The scroll path only asks for one animation frame, however many scroll
|
|
116
129
|
* events come; that frame reads the offset and the scroll size, right after
|
|
117
130
|
* the offset forced the layout (so it costs nothing more), and draws. The
|
|
@@ -160,6 +173,8 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
160
173
|
edges: noEdges,
|
|
161
174
|
/** The pending animation frame, 0 when none. */
|
|
162
175
|
frame: 0,
|
|
176
|
+
/** The pending frame measures the layout (an observer reported a change), not only draws. */
|
|
177
|
+
frameMeasures: false,
|
|
163
178
|
idleTimer: void 0,
|
|
164
179
|
layout: {
|
|
165
180
|
clientHeight: 0,
|
|
@@ -197,7 +212,7 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
197
212
|
gutters?.toggle("r-scroll-area__gutters--scrolling", scrolling);
|
|
198
213
|
gutters?.toggle("r-scroll-area__gutters--dragging", dragging);
|
|
199
214
|
}, [guttersRef, rootRef]);
|
|
200
|
-
/** `measure`, for `draw`, which
|
|
215
|
+
/** `measure(true)`, for `draw` and `scheduleFrame`, which are declared before it. */
|
|
201
216
|
const remeasure = useRef(() => {});
|
|
202
217
|
/** Reads the offset once and draws everything that follows from it and the cached sizes. */
|
|
203
218
|
const draw = useCallback(() => {
|
|
@@ -268,20 +283,30 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
268
283
|
fadeRef,
|
|
269
284
|
rootRef
|
|
270
285
|
]);
|
|
271
|
-
/**
|
|
272
|
-
|
|
286
|
+
/**
|
|
287
|
+
* At most one frame at a time: however many scroll events and observer notifications come in,
|
|
288
|
+
* the thumb is drawn once a frame. `layout` (an observer's notification) makes the frame measure
|
|
289
|
+
* the layout first; a frame already asked for by a scroll is upgraded to it, never dropped.
|
|
290
|
+
*/
|
|
291
|
+
const scheduleFrame = useCallback((layout) => {
|
|
273
292
|
const current = state.current;
|
|
293
|
+
if (layout) current.frameMeasures = true;
|
|
274
294
|
if (current.frame !== 0) return;
|
|
275
|
-
|
|
295
|
+
const run = () => {
|
|
296
|
+
current.frame = 0;
|
|
297
|
+
if (current.frameMeasures) {
|
|
298
|
+
current.frameMeasures = false;
|
|
299
|
+
remeasure.current();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
276
302
|
applyClasses();
|
|
277
303
|
draw();
|
|
304
|
+
};
|
|
305
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
306
|
+
run();
|
|
278
307
|
return;
|
|
279
308
|
}
|
|
280
|
-
current.frame = requestAnimationFrame(
|
|
281
|
-
current.frame = 0;
|
|
282
|
-
applyClasses();
|
|
283
|
-
draw();
|
|
284
|
-
});
|
|
309
|
+
current.frame = requestAnimationFrame(run);
|
|
285
310
|
}, [applyClasses, draw]);
|
|
286
311
|
/**
|
|
287
312
|
* The layout path: reads the container's sizes and the gutters' lengths into the cache, then draws.
|
|
@@ -348,13 +373,13 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
348
373
|
const handleScroll = useCallback(() => {
|
|
349
374
|
const current = state.current;
|
|
350
375
|
current.scrolling = true;
|
|
351
|
-
|
|
376
|
+
scheduleFrame(false);
|
|
352
377
|
clearTimeout(current.idleTimer);
|
|
353
378
|
current.idleTimer = setTimeout(() => {
|
|
354
379
|
current.scrolling = false;
|
|
355
380
|
applyClasses();
|
|
356
381
|
}, latest.current.hideDelay);
|
|
357
|
-
}, [applyClasses,
|
|
382
|
+
}, [applyClasses, scheduleFrame]);
|
|
358
383
|
const setDragging = useCallback((dragging) => {
|
|
359
384
|
state.current.dragging = dragging;
|
|
360
385
|
applyClasses();
|
|
@@ -373,7 +398,7 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
373
398
|
classes?.disconnect();
|
|
374
399
|
};
|
|
375
400
|
if (typeof ResizeObserver === "undefined") return stopListening;
|
|
376
|
-
const observer = new ResizeObserver(() =>
|
|
401
|
+
const observer = new ResizeObserver(() => scheduleFrame(true));
|
|
377
402
|
state.current.observer = observer;
|
|
378
403
|
for (const element of [
|
|
379
404
|
container,
|
|
@@ -387,23 +412,24 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
387
412
|
if (state.current.observer === observer) state.current.observer = null;
|
|
388
413
|
};
|
|
389
414
|
if (content) return disconnect;
|
|
390
|
-
const observed = /* @__PURE__ */ new
|
|
415
|
+
const observed = /* @__PURE__ */ new Map();
|
|
391
416
|
const observeChildElements = () => {
|
|
392
|
-
for (const child of observed) {
|
|
393
|
-
if (child.parentNode === container) continue;
|
|
417
|
+
for (const [child, target] of observed) {
|
|
418
|
+
if (child.parentNode === container && observedFor(child) === target) continue;
|
|
394
419
|
observed.delete(child);
|
|
395
|
-
observer.unobserve(
|
|
420
|
+
observer.unobserve(target);
|
|
396
421
|
}
|
|
397
422
|
for (const child of container.children) {
|
|
398
423
|
if (observed.has(child)) continue;
|
|
399
|
-
|
|
400
|
-
|
|
424
|
+
const target = observedFor(child);
|
|
425
|
+
observed.set(child, target);
|
|
426
|
+
observer.observe(target);
|
|
401
427
|
}
|
|
402
428
|
};
|
|
403
429
|
observeChildElements();
|
|
404
430
|
const mutations = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => {
|
|
405
431
|
observeChildElements();
|
|
406
|
-
|
|
432
|
+
scheduleFrame(true);
|
|
407
433
|
});
|
|
408
434
|
mutations?.observe(container, { childList: true });
|
|
409
435
|
return () => {
|
|
@@ -413,7 +439,8 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
413
439
|
}, [
|
|
414
440
|
applyClasses,
|
|
415
441
|
handleScroll,
|
|
416
|
-
measure
|
|
442
|
+
measure,
|
|
443
|
+
scheduleFrame
|
|
417
444
|
]);
|
|
418
445
|
/** What the observers are bound to now, and how to undo it. */
|
|
419
446
|
const binding = useRef(null);
|
|
@@ -463,6 +490,7 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
|
|
|
463
490
|
clearTimeout(current.idleTimer);
|
|
464
491
|
if (current.frame !== 0) cancelAnimationFrame(current.frame);
|
|
465
492
|
current.frame = 0;
|
|
493
|
+
current.frameMeasures = false;
|
|
466
494
|
current.scrolling = false;
|
|
467
495
|
current.dragging = false;
|
|
468
496
|
};
|