@botiverse/raft-ui 0.75.0 → 0.76.1

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,28 @@
1
+ ## [0.76.1](https://github.com/botiverse/raft-design/compare/v0.76.0...v0.76.1) (2026-11-08)
2
+
3
+ ### Fixed
4
+
5
+ - **Select and Combobox lists never scroll sideways**: a Select list reaches into the popup's 4px inset and pads it back, so a `SelectSeparator` meets the popup's edge and stays inside the list, and Select and Combobox lists set `overflow-x-hidden`. ([#1070](https://github.com/botiverse/raft-design/pull/1070))
6
+ - Why: the separator reached 4px past each side of the list, which is the scroller, so the list scrolled sideways by 8px, and a classic scrollbar (Windows) drew a horizontal bar under the runtimes list (Artea, #proj-raft-ui:20a421bd). The list also clipped the line 4px short of the popup's edge.
7
+ - Affects: every Select with a separator, such as the runtime picker's Show installed only row: no sideways scroll, and the line now runs to the popup's border. Options keep their inset. A list action keeps its highlight after it adds items above it, as before.
8
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
9
+
10
+ ## [0.76.0](https://github.com/botiverse/raft-design/compare/v0.75.0...v0.76.0) (2026-11-08)
11
+
12
+ ### Changed
13
+
14
+ - **VirtualList writes no `--virtual-list-width`**: the list no longer writes its width as a CSS variable on its scroller, and `refreshListWidth()` on its imperative API does nothing (deprecated; it will be removed). ([#1068](https://github.com/botiverse/raft-design/pull/1068))
15
+ - Why: the variable inherits, so a write on the scroller restyled every row under it, on every frame of a window resize, and nothing read it (InNOut's trace of a window drag in the app, #proj-frontend:57f32c96).
16
+ - Affects: CSS that read `var(--virtual-list-width)` gets nothing; read the width with the list's `onResize` or a ResizeObserver. Neither raft-ui nor the Raft app read it.
17
+ - Docs: https://raft-design.botiverse.build/components/virtual-list/
18
+
19
+ ### Fixed
20
+
21
+ - **Resizing a window no longer restyles everything inside a ScrollArea**: the sizes ScrollArea measures are no longer written on the root. With sticky gutters, the viewport's height (`--r-scroll-area-viewport-height`) and the container's padding (`--r-scroll-area-padding-top`, `-left` and `-right`) go on the gutters layer, and with `fade` the viewport's height and width (`--r-scroll-area-viewport-width`) on the content its mask is on; with absolute gutters, which read none of them, nothing is written. They are registered not to inherit (`@property`). A message timeline (`useScrollAreaContainer` on a VirtualList) restyled every message on every frame of a window drag. ([#1068](https://github.com/botiverse/raft-design/pull/1068))
22
+ - Why: in the app, dragging the window's edge took 61-73ms a frame, 75-85% of it restyling, because the viewport's size changed on the root of each column's message list (InNOut, #proj-frontend:57f32c96).
23
+ - Affects: dragging the window's edge in the Raft app (a channel with a thread open, 1600 to 900 to 1600px wide): style recalculation 2.7s to 1.5s and the median step 52 to 27ms with this release, 0.3s and 17ms together with the app's own fix. CSS that read those variables inside an area now sees their initial values (100% and 0px); they were internal, and neither raft-ui nor the Raft app read them. How the scrollbars and `fade` look is unchanged.
24
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
25
+
1
26
  ## [0.75.0](https://github.com/botiverse/raft-design/compare/v0.74.0...v0.75.0) (2026-11-08)
2
27
 
3
28
  ### Changed
@@ -67,7 +67,7 @@ const combobox = tv({
67
67
  ],
68
68
  header: ["flex items-center justify-between px-3", "gap-2 py-1.5 pr-1.5 pl-4"],
69
69
  separator: ["h-px shrink-0 border-0", "bg-line-hairline"],
70
- list: ["max-h-64 overflow-y-auto overscroll-contain", "flex flex-col p-1 scroll-py-1"],
70
+ list: ["max-h-64 overflow-x-hidden overflow-y-auto overscroll-contain", "flex flex-col p-1 scroll-py-1"],
71
71
  item: [
72
72
  "flex w-full shrink-0 cursor-default select-none items-center justify-between overflow-hidden",
73
73
  "text-left outline-none transition-colors",
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * sticky the root is the container; the gutters are its first child, a
9
9
  * zero-height layer stuck to the top of the viewport and sized
10
- * by --r-scroll-area-viewport-{height,width}.
10
+ * by --r-scroll-area-viewport-height.
11
11
  * absolute the root is a positioned frame holding the container and,
12
12
  * after it, the gutters, laid over the frame (`inset: 0`). For a
13
13
  * container that can't hold children: a textarea.
@@ -49,10 +49,15 @@
49
49
  * --r-scroll-area-timeline-{y,x} the names of the container's scroll
50
50
  * timelines, unique to the ScrollArea
51
51
  * What the script writes as inline styles (React never renders them):
52
- * --r-scroll-area-viewport-{height,width} on the root: the container's
52
+ * --r-scroll-area-viewport-height with sticky gutters, on the
53
+ * gutters layer: the container's
54
+ * client height (layout path only)
55
+ * --r-scroll-area-viewport-{height,width} with sticky gutters and `fade`,
56
+ * on the content: the container's
53
57
  * client size (layout path only)
54
- * --r-scroll-area-padding-{top,left,right} on a sticky root: its padding,
55
- * which the gutters layer
58
+ * --r-scroll-area-padding-{top,left,right} with sticky gutters, on the
59
+ * gutters layer: the container's
60
+ * padding, which the layer
56
61
  * reaches over (layout path only)
57
62
  * --r-scroll-area-thumb-length on each thumb: its length, px
58
63
  * --r-scroll-area-thumb-travel on each thumb, with scroll-driven
@@ -67,8 +72,9 @@
67
72
  * (sticky) and see it, or the root hoists it (`timeline-scope`, absolute).
68
73
  * The script then writes only when a size changes. Elsewhere a scrolled
69
74
  * frame changes the thumbs' transforms, and the length with them when the
70
- * content's size changed. Never the root while scrolling, whose variables
71
- * everything inside would inherit.
75
+ * content's size changed. Never the root, whose variables everything inside
76
+ * would inherit: a write there, on every frame of a window resize, restyled
77
+ * every row of a long list under it.
72
78
  *
73
79
  * The thumb is the label colour (`foreground`) made translucent, dark on
74
80
  * light pages and light on dark ones, so it sits on any content; there is no
@@ -103,6 +109,41 @@
103
109
  initial-value: 0px;
104
110
  }
105
111
 
112
+ /*
113
+ * The container's client size and padding, written by the script on each
114
+ * layout (every frame of a window resize) on the elements that read them,
115
+ * with sticky gutters only: the height and the padding on the gutters layer,
116
+ * whose gutters take the height with `inherit`, and with `fade` the size on
117
+ * the content, whose mask is one viewport large. They don't inherit:
118
+ * a change restyles those few elements, not the content under them. 100%
119
+ * until the first measurement, as the gutters and the mask were before it.
120
+ */
121
+ @property --r-scroll-area-viewport-height {
122
+ syntax: "<length-percentage>";
123
+ inherits: false;
124
+ initial-value: 100%;
125
+ }
126
+ @property --r-scroll-area-viewport-width {
127
+ syntax: "<length-percentage>";
128
+ inherits: false;
129
+ initial-value: 100%;
130
+ }
131
+ @property --r-scroll-area-padding-top {
132
+ syntax: "<length>";
133
+ inherits: false;
134
+ initial-value: 0px;
135
+ }
136
+ @property --r-scroll-area-padding-left {
137
+ syntax: "<length>";
138
+ inherits: false;
139
+ initial-value: 0px;
140
+ }
141
+ @property --r-scroll-area-padding-right {
142
+ syntax: "<length>";
143
+ inherits: false;
144
+ initial-value: 0px;
145
+ }
146
+
106
147
  /*
107
148
  * Room kept clear at each end of the scrollbars, such as for a sticky header
108
149
  * or footer inside the area (`railInset`). Set on the root itself; they don't
@@ -170,15 +211,6 @@
170
211
 
171
212
  --r-scroll-area-thumb-size: var(--r-scroll-area-thumb-size-medium);
172
213
  --r-scroll-area-thumb-size-hover: var(--r-scroll-area-thumb-size-hover-medium);
173
- /*
174
- * Until the first measurement, and again for a nested scroll area, which
175
- * would otherwise inherit its parent's.
176
- */
177
- --r-scroll-area-viewport-height: 100%;
178
- --r-scroll-area-viewport-width: 100%;
179
- --r-scroll-area-padding-top: 0px;
180
- --r-scroll-area-padding-left: 0px;
181
- --r-scroll-area-padding-right: 0px;
182
214
  /* The gutter is as thick as the hovered thumb plus the gap on both sides. */
183
215
  --r-scroll-area-rail-size: calc(
184
216
  var(--r-scroll-area-thumb-size-hover) + 2 * var(--r-scroll-area-rail-gap)
@@ -492,6 +524,10 @@
492
524
  position: absolute;
493
525
  transition: opacity var(--r-scroll-area-duration-fade-out) var(--r-scroll-area-easing);
494
526
  }
527
+ /* The viewport's height is written on the layer and doesn't inherit: each sticky gutter takes it. */
528
+ .r-scroll-area.r-scroll-area--sticky-gutters > .r-scroll-area__gutters > .r-scroll-area__gutter {
529
+ --r-scroll-area-viewport-height: inherit;
530
+ }
495
531
  .r-scroll-area > .r-scroll-area__gutters > .r-scroll-area__gutter--vertical {
496
532
  inline-size: var(--r-scroll-area-rail-size);
497
533
  inset-block-start: var(--r-scroll-area-rail-top);
@@ -319,15 +319,19 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
319
319
  if (!container || !root) return;
320
320
  const current = state.current;
321
321
  const axes = latest.current.orientation;
322
+ const sticky = latest.current.guttersPosition === "sticky";
323
+ const gutters = guttersRef.current;
322
324
  if (layout) {
323
325
  const position = latest.current.guttersPosition;
324
- current.timeline = timelineDrives(container, root, guttersRef.current, position);
326
+ current.timeline = timelineDrives(container, root, gutters, position);
325
327
  const style = getComputedStyle(container);
326
328
  current.rtl = axes !== "vertical" && style.direction === "rtl";
327
- if (position === "sticky") {
328
- writeVar(root, "--r-scroll-area-padding-top", style.paddingTop || "0px");
329
- writeVar(root, "--r-scroll-area-padding-left", style.paddingLeft || "0px");
330
- writeVar(root, "--r-scroll-area-padding-right", style.paddingRight || "0px");
329
+ if (sticky) {
330
+ if (gutters) {
331
+ writeVar(gutters, "--r-scroll-area-padding-top", style.paddingTop || "0px");
332
+ writeVar(gutters, "--r-scroll-area-padding-left", style.paddingLeft || "0px");
333
+ writeVar(gutters, "--r-scroll-area-padding-right", style.paddingRight || "0px");
334
+ }
331
335
  warnStickyLayout(container, style);
332
336
  }
333
337
  const min = Number.parseFloat(getComputedStyle(root).getPropertyValue("--r-scroll-area-thumb-min-length"));
@@ -338,8 +342,15 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
338
342
  cache.clientWidth = container.clientWidth;
339
343
  cache.scrollHeight = container.scrollHeight;
340
344
  cache.scrollWidth = container.scrollWidth;
341
- writeVar(root, "--r-scroll-area-viewport-height", px(cache.clientHeight));
342
- writeVar(root, "--r-scroll-area-viewport-width", px(cache.clientWidth));
345
+ if (sticky) {
346
+ const height = px(cache.clientHeight);
347
+ if (gutters) writeVar(gutters, "--r-scroll-area-viewport-height", height);
348
+ const masked = latest.current.fade ? fadeRef?.current ?? null : null;
349
+ if (masked) {
350
+ writeVar(masked, "--r-scroll-area-viewport-height", height);
351
+ writeVar(masked, "--r-scroll-area-viewport-width", px(cache.clientWidth));
352
+ }
353
+ }
343
354
  const sizes = {
344
355
  ...cache,
345
356
  scrollLeft: 0,
@@ -363,6 +374,7 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
363
374
  applyClasses,
364
375
  containerRef,
365
376
  draw,
377
+ fadeRef,
366
378
  guttersRef,
367
379
  rootRef
368
380
  ]);
@@ -452,7 +464,8 @@ function useScrollbars({ containerRef, contentRef, fadeRef, guttersPosition, roo
452
464
  bind,
453
465
  orientation,
454
466
  fade,
455
- guttersPosition
467
+ guttersPosition,
468
+ guttersRef.current
456
469
  ];
457
470
  const current = binding.current;
458
471
  if (current && current.container === container && current.content === content && current.root === root && current.key.every((value, index) => value === key[index])) return;
@@ -44,7 +44,7 @@ const select = tv({
44
44
  "rounded-container bg-layer-popover p-1 text-foreground",
45
45
  "shadow-float ring-1 ring-line-seam"
46
46
  ],
47
- list: ["max-h-64 overflow-y-auto overscroll-contain scroll-py-6", "flex flex-col"],
47
+ list: ["max-h-64 overflow-x-hidden overflow-y-auto overscroll-contain scroll-py-6", "-mx-1 flex flex-col px-1"],
48
48
  item: [
49
49
  "flex w-full shrink-0 cursor-default select-none items-center justify-between overflow-hidden",
50
50
  "text-left outline-none transition-colors",
@@ -151,7 +151,9 @@ function findActionValue(value) {
151
151
  * row, the same position then holds another item while focus stays on the row,
152
152
  * and the next arrow key starts from the wrong place. Once the list has
153
153
  * re-rendered, focus the list and then the row again: the row's focus handler
154
- * re-reads its position and moves the highlight back to it.
154
+ * re-reads its position and moves the highlight back to it. The list takes
155
+ * focus only for this: it has no tabindex of its own, and Chrome focuses a
156
+ * scroller without one only while its content overflows it.
155
157
  */
156
158
  function keepActionHighlighted(event) {
157
159
  const target = event.target;
@@ -160,8 +162,12 @@ function keepActionHighlighted(event) {
160
162
  if (!row) return;
161
163
  requestAnimationFrame(() => {
162
164
  if (!row.isConnected || row.hasAttribute("data-highlighted")) return;
163
- row.closest("[role=\"listbox\"]")?.focus({ preventScroll: true });
165
+ const list = row.closest("[role=\"listbox\"]");
166
+ const lent = list !== null && !list.hasAttribute("tabindex");
167
+ if (lent) list.tabIndex = -1;
168
+ list?.focus({ preventScroll: true });
164
169
  row.focus({ preventScroll: true });
170
+ if (lent) list.removeAttribute("tabindex");
165
171
  });
166
172
  }
167
173
  function Select$1({ chrome = "default", onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, ...props }) {
@@ -913,10 +913,7 @@ function VirtualList(props) {
913
913
  const index = keyToIndexRef.current.get(key);
914
914
  if (index !== void 0) return fenwickTree.sum(index) + getRowsTop();
915
915
  });
916
- const refreshListWidth = useCallback(() => {
917
- const listEl = listElRef.current;
918
- if (listEl) listEl.style.setProperty("--virtual-list-width", `${listEl.offsetWidth}px`);
919
- }, [listElRef]);
916
+ const refreshListWidth = useCallback(() => {}, []);
920
917
  const imperativeApi = useMemo(() => ({
921
918
  listElRef,
922
919
  itemsElRef,
@@ -1054,15 +1051,15 @@ function VirtualList(props) {
1054
1051
  compensationYRef
1055
1052
  });
1056
1053
  /**
1057
- * The list's own resize: stores its height and width, sets `--virtual-list-width` and updates
1058
- * the range. Skipped while the list is hidden with `display: none`.
1054
+ * The list's own resize: stores its height and width and updates the range. Skipped while the
1055
+ * list is hidden with `display: none`. It writes no CSS variable on the list: one would be
1056
+ * inherited by every row, and restyle them all on every frame of a window resize.
1059
1057
  */
1060
1058
  const handleListResize = useEventCallback((listEl, deferRender) => {
1061
1059
  if (isHiddenByDisplayNone(listEl)) return;
1062
1060
  const height = listEl.offsetHeight;
1063
1061
  const width = listEl.offsetWidth;
1064
1062
  const viewport = viewportRef.current;
1065
- listEl.style.setProperty("--virtual-list-width", `${width}px`);
1066
1063
  if (renderCol == null && renderItemWithCols == null) {
1067
1064
  if (height === viewport.listHeight) return;
1068
1065
  } else if (height === viewport.listHeight && width === viewport.listWidth) return;
@@ -103,7 +103,11 @@ export interface VirtualListApi {
103
103
  * scroll position of your own that must hold while the rows it depends on are measured.
104
104
  */
105
105
  submitScrollAdjustingTask: (task: () => void) => void;
106
- /** Writes the scroller's width to `--virtual-list-width` again. */
106
+ /**
107
+ * Does nothing. It wrote the scroller's width to `--virtual-list-width`, which the list no longer
108
+ * writes: read the width with `onResize` or a ResizeObserver.
109
+ * @deprecated The list no longer writes `--virtual-list-width`; this will be removed.
110
+ */
107
111
  refreshListWidth: () => void;
108
112
  }
109
113
  export type RenderReason = "sessionChanged" | "itemsChanged" | "listScroll" | "batchItemResize" | "listResize";
@@ -12,10 +12,9 @@
12
12
  * In the `components` layer, so a utility in the consumer's `className` (`h-*`, `max-h-*`, `overflow-*`)
13
13
  * overrides these rules.
14
14
  *
15
- * CSS variable the list writes as an inline style, for sticky children to read:
16
- * --virtual-list-width on `.r-virtual-list`: the list's offsetWidth
17
- * The rows' element carries no CSS variable: custom properties inherit, so one written there on every
18
- * scroll would restyle every row. Content that must follow the rows' offset uses `onTransformChange`.
15
+ * The list writes no CSS variable: custom properties inherit, so one written on the scroller or the rows'
16
+ * element on every scroll, or every frame of a window resize, would restyle every row. Content that must
17
+ * follow the rows' offset uses `onTransformChange`.
19
18
  */
20
19
  @layer components {
21
20
  /* VirtualList renders its own scroller (scroll-area.md#native-scrolling): it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.75.0",
3
+ "version": "0.76.1",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",