@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, a ResizeObserver or MutationObserver
111
- * notification, `measure()`) reads the container's client and scroll sizes,
112
- * the gutters' lengths and the writing direction, caches them and draws.
113
- * The ResizeObserver watches the container (the viewport), the content
114
- * (`contentRef`, or each element child of the container) and each gutter.
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 is declared before it. */
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
- /** At most one frame at a time: however many scroll events come in, the thumb is drawn once a frame. */
272
- const scheduleDraw = useCallback(() => {
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
- if (typeof requestAnimationFrame !== "function") {
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
- scheduleDraw();
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, scheduleDraw]);
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(() => measure(true));
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 Set();
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(child);
420
+ observer.unobserve(target);
396
421
  }
397
422
  for (const child of container.children) {
398
423
  if (observed.has(child)) continue;
399
- observed.add(child);
400
- observer.observe(child);
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
- measure(true);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",