@botiverse/raft-ui 0.76.0 → 0.77.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [0.77.0](https://github.com/botiverse/raft-design/compare/v0.76.1...v0.77.0) (2026-11-08)
2
+
3
+ ### Changed
4
+
5
+ - **Select, Combobox and ComboboxSelect lists scroll in a ScrollArea**: each list is a `useScrollAreaContainer` container with absolute gutters, inside a frame (`data-slot="select-list-frame"`, `combobox-list-frame`) that holds the overlay scrollbar. The list is never a Tab stop: focus stays in the input or on the options. ([#1074](https://github.com/botiverse/raft-design/pull/1074))
6
+ - Why: they were the last native-scrolling exception for Base UI lists; on Windows they drew a classic scrollbar with arrows that took width from the options (Artea's runtimes list, #proj-raft-ui:20a421bd).
7
+ - Affects: the scrollbar is the thin overlay thumb on every platform. `SelectList` / `ComboboxList` render a frame `<div>` around the list element; `ref`, `className`, `style` and the other props still go to the list. CSS or tests that expected the list to be the popup's direct child, or waited on `document.getAnimations()` to finish (the thumb runs on a scroll timeline), need updating.
8
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
9
+
10
+ ## [0.76.1](https://github.com/botiverse/raft-design/compare/v0.76.0...v0.76.1) (2026-11-08)
11
+
12
+ ### Fixed
13
+
14
+ - **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))
15
+ - 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.
16
+ - 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.
17
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
18
+
1
19
  ## [0.76.0](https://github.com/botiverse/raft-design/compare/v0.75.0...v0.76.0) (2026-11-08)
2
20
 
3
21
  ### Changed
@@ -42,7 +42,13 @@ export type ComboboxContentProps = StyledProps<Combobox.Popup.Props> & Pick<Comb
42
42
  };
43
43
  export declare function ComboboxContent({ align, alignOffset, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod, side, sideOffset, sticky, portalProps, className, initialFocus, finalFocus, ...props }: ComboboxContentProps): import("react").JSX.Element;
44
44
  export type ComboboxListProps = StyledProps<Combobox.List.Props>;
45
- export declare function ComboboxList({ className, ...props }: ComboboxListProps): import("react").JSX.Element;
45
+ /**
46
+ * The scrolling list, a ScrollArea container with absolute gutters: the
47
+ * overlay scrollbar lies on a frame around it, since Base UI owns the list's
48
+ * children (items can be a function of each item). The list is never a Tab
49
+ * stop: focus stays in the input, as Base UI manages it.
50
+ */
51
+ export declare function ComboboxList({ className, ref, style, ...props }: ComboboxListProps): import("react").JSX.Element;
46
52
  export type ComboboxItemProps = StyledProps<Combobox.Item.Props>;
47
53
  export declare function ComboboxItem({ className, ...props }: ComboboxItemProps): import("react").JSX.Element;
48
54
  export type ComboboxItemIndicatorProps = StyledProps<Combobox.ItemIndicator.Props>;
@@ -1,12 +1,15 @@
1
+ import { cn } from "../../cn.mjs";
1
2
  import { tv } from "../../lib/tv.mjs";
2
3
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
3
4
  import { Button } from "../button/button.mjs";
5
+ import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
6
+ import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
4
7
  import { usePopupCollision } from "../../lib/frame-insets.mjs";
5
8
  import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
6
9
  import { chipRemoveBase } from "../../lib/chip-remove.mjs";
7
10
  import { Check, X } from "lucide-react";
8
11
  import { createContext, use, useCallback, useMemo, useState } from "react";
9
- import { jsx } from "react/jsx-runtime";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
10
13
  import { Combobox } from "@base-ui/react/combobox";
11
14
  //#region src/components/combobox/combobox.tsx
12
15
  const combobox = tv({
@@ -67,7 +70,8 @@ const combobox = tv({
67
70
  ],
68
71
  header: ["flex items-center justify-between px-3", "gap-2 py-1.5 pr-1.5 pl-4"],
69
72
  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"],
73
+ listFrame: ["min-h-0"],
74
+ list: ["max-h-64 min-h-0", "flex flex-col p-1 scroll-py-1"],
71
75
  item: [
72
76
  "flex w-full shrink-0 cursor-default select-none items-center justify-between overflow-hidden",
73
77
  "text-left outline-none transition-colors",
@@ -319,12 +323,35 @@ function ComboboxContent({ align = "start", alignOffset = 0, anchor, arrowPaddin
319
323
  })
320
324
  });
321
325
  }
322
- function ComboboxList({ className, ...props }) {
323
- const { list } = combobox();
324
- return /* @__PURE__ */ jsx(Combobox.List, {
325
- "data-slot": "combobox-list",
326
- className: list({ className }),
327
- ...props
326
+ /**
327
+ * The scrolling list, a ScrollArea container with absolute gutters: the
328
+ * overlay scrollbar lies on a frame around it, since Base UI owns the list's
329
+ * children (items can be a function of each item). The list is never a Tab
330
+ * stop: focus stays in the input, as Base UI manages it.
331
+ */
332
+ function ComboboxList({ className, ref, style, ...props }) {
333
+ const { list, listFrame } = combobox();
334
+ const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
335
+ containerRef: ref,
336
+ guttersPosition: "absolute",
337
+ overscroll: "contain"
338
+ });
339
+ const ownStyle = useMemo(() => ({
340
+ ...containerProps.style,
341
+ ...style
342
+ }), [containerProps.style, style]);
343
+ return /* @__PURE__ */ jsxs("div", {
344
+ ...frameProps,
345
+ "data-slot": "combobox-list-frame",
346
+ className: listFrame({ className: frameProps.className }),
347
+ children: [/* @__PURE__ */ jsx(Combobox.List, {
348
+ "data-slot": "combobox-list",
349
+ ...containerProps,
350
+ tabIndex: -1,
351
+ ...props,
352
+ style: ownStyle,
353
+ className: list({ className: cn(containerProps.className, className) })
354
+ }), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
328
355
  });
329
356
  }
330
357
  function ComboboxItem({ className, ...props }) {
@@ -181,13 +181,14 @@
181
181
  var(--elevation-float);
182
182
  outline: 0;
183
183
  }
184
- /* Native scroll, an allowed exception (scroll-area.md#native-scrolling): Base UI
185
- owns the list element and its children (items can be a function of each item). */
184
+ .r-combobox-select__list-frame {
185
+ min-height: 0;
186
+ }
187
+ /* A ScrollArea container (absolute gutters): useScrollAreaContainer gives it its overflow. */
186
188
  .r-combobox-select__list {
187
189
  box-sizing: border-box;
188
190
  max-height: var(--combobox-select-list-max-height, 16rem);
189
- overflow-y: auto;
190
- overscroll-behavior: contain;
191
+ min-height: 0;
191
192
  scroll-padding-block: 4px;
192
193
  padding: 4px;
193
194
  }
@@ -1,5 +1,7 @@
1
1
  import { useTheme } from "../../lib/theme/use-theme.mjs";
2
2
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
3
+ import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
4
+ import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
3
5
  import { usePopupCollision } from "../../lib/frame-insets.mjs";
4
6
  import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
5
7
  import { usePhoneMedia } from "../../lib/phone-media.mjs";
@@ -457,9 +459,8 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
457
459
  className: popupClassName,
458
460
  children: [
459
461
  /* @__PURE__ */ jsx(Combobox.Status, { children: loadingMessage }),
460
- /* @__PURE__ */ jsx(Combobox.List, {
461
- className: "r-combobox-select__list",
462
- "aria-busy": loading,
462
+ /* @__PURE__ */ jsx(ComboboxSelectList, {
463
+ loading,
463
464
  children: renderGroup
464
465
  }),
465
466
  /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyMessage })
@@ -469,5 +470,28 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
469
470
  })]
470
471
  });
471
472
  }
473
+ /**
474
+ * The scrolling list, a ScrollArea container with absolute gutters: the
475
+ * overlay scrollbar lies on a frame around it, since Base UI owns the list's
476
+ * children (a function of each group). The list is never a Tab stop: focus
477
+ * stays in the input, as Base UI manages it.
478
+ */
479
+ function ComboboxSelectList({ children, loading }) {
480
+ const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
481
+ guttersPosition: "absolute",
482
+ overscroll: "contain"
483
+ });
484
+ return /* @__PURE__ */ jsxs("div", {
485
+ ...frameProps,
486
+ className: `${frameProps.className} r-combobox-select__list-frame`,
487
+ children: [/* @__PURE__ */ jsx(Combobox.List, {
488
+ ...containerProps,
489
+ tabIndex: -1,
490
+ className: `${containerProps.className} r-combobox-select__list`,
491
+ "aria-busy": loading,
492
+ children
493
+ }), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
494
+ });
495
+ }
472
496
  //#endregion
473
497
  export { ComboboxSelect };
@@ -35,7 +35,14 @@ export type SelectContentProps = StyledProps<Select.Popup.Props> & Pick<Select.P
35
35
  };
36
36
  export declare function SelectContent({ align, alignItemWithTrigger, alignOffset, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod, side, sideOffset, sticky, portalProps, className, ...props }: SelectContentProps): import("react").JSX.Element;
37
37
  export type SelectListProps = StyledProps<Select.List.Props>;
38
- export declare function SelectList({ className, ...props }: SelectListProps): import("react").JSX.Element;
38
+ /**
39
+ * The scrolling list, a ScrollArea container with absolute gutters: its frame
40
+ * reaches into the popup's 4px inset and the list pads it back, so a
41
+ * separator meets the popup's edge, and the overlay scrollbar takes no room
42
+ * from the options, on every platform. The list is never a Tab stop: focus
43
+ * stays on the options, as Base UI manages it.
44
+ */
45
+ export declare function SelectList({ className, ref, style, ...props }: SelectListProps): import("react").JSX.Element;
39
46
  export type SelectItemProps = StyledProps<Select.Item.Props>;
40
47
  export declare function SelectItem({ className, ...props }: SelectItemProps): import("react").JSX.Element;
41
48
  export type SelectItemLeadingProps = StyledProps<ComponentPropsWithRef<"span">>;
@@ -1,11 +1,14 @@
1
+ import { cn } from "../../cn.mjs";
1
2
  import { tv } from "../../lib/tv.mjs";
2
3
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
3
4
  import { Button } from "../button/button.mjs";
5
+ import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
6
+ import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
4
7
  import { usePopupCollision } from "../../lib/frame-insets.mjs";
5
8
  import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
6
9
  import { Check, ChevronDown } from "lucide-react";
7
10
  import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
8
- import { jsx } from "react/jsx-runtime";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
9
12
  import { Select } from "@base-ui/react/select";
10
13
  //#region src/components/select/select.tsx
11
14
  const select = tv({
@@ -44,7 +47,8 @@ const select = tv({
44
47
  "rounded-container bg-layer-popover p-1 text-foreground",
45
48
  "shadow-float ring-1 ring-line-seam"
46
49
  ],
47
- list: ["max-h-64 overflow-y-auto overscroll-contain scroll-py-6", "flex flex-col"],
50
+ listFrame: ["-mx-1 min-h-0"],
51
+ list: ["max-h-64 min-h-0 scroll-py-6", "flex flex-col px-1"],
48
52
  item: [
49
53
  "flex w-full shrink-0 cursor-default select-none items-center justify-between overflow-hidden",
50
54
  "text-left outline-none transition-colors",
@@ -151,7 +155,9 @@ function findActionValue(value) {
151
155
  * row, the same position then holds another item while focus stays on the row,
152
156
  * and the next arrow key starts from the wrong place. Once the list has
153
157
  * 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.
158
+ * re-reads its position and moves the highlight back to it. The list takes
159
+ * focus only for this: it has no tabindex of its own, and Chrome focuses a
160
+ * scroller without one only while its content overflows it.
155
161
  */
156
162
  function keepActionHighlighted(event) {
157
163
  const target = event.target;
@@ -160,8 +166,12 @@ function keepActionHighlighted(event) {
160
166
  if (!row) return;
161
167
  requestAnimationFrame(() => {
162
168
  if (!row.isConnected || row.hasAttribute("data-highlighted")) return;
163
- row.closest("[role=\"listbox\"]")?.focus({ preventScroll: true });
169
+ const list = row.closest("[role=\"listbox\"]");
170
+ const lent = list !== null && !list.hasAttribute("tabindex");
171
+ if (lent) list.tabIndex = -1;
172
+ list?.focus({ preventScroll: true });
164
173
  row.focus({ preventScroll: true });
174
+ if (lent) list.removeAttribute("tabindex");
165
175
  });
166
176
  }
167
177
  function Select$1({ chrome = "default", onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, ...props }) {
@@ -300,12 +310,36 @@ function SelectContent({ align = "start", alignItemWithTrigger = false, alignOff
300
310
  children: positionerNode
301
311
  });
302
312
  }
303
- function SelectList({ className, ...props }) {
304
- const { list } = select();
305
- return /* @__PURE__ */ jsx(Select.List, {
306
- "data-slot": "select-list",
307
- className: list({ className }),
308
- ...props
313
+ /**
314
+ * The scrolling list, a ScrollArea container with absolute gutters: its frame
315
+ * reaches into the popup's 4px inset and the list pads it back, so a
316
+ * separator meets the popup's edge, and the overlay scrollbar takes no room
317
+ * from the options, on every platform. The list is never a Tab stop: focus
318
+ * stays on the options, as Base UI manages it.
319
+ */
320
+ function SelectList({ className, ref, style, ...props }) {
321
+ const { list, listFrame } = select();
322
+ const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
323
+ containerRef: ref,
324
+ guttersPosition: "absolute",
325
+ overscroll: "contain"
326
+ });
327
+ const ownStyle = useMemo(() => ({
328
+ ...containerProps.style,
329
+ ...style
330
+ }), [containerProps.style, style]);
331
+ return /* @__PURE__ */ jsxs("div", {
332
+ ...frameProps,
333
+ "data-slot": "select-list-frame",
334
+ className: listFrame({ className: frameProps.className }),
335
+ children: [/* @__PURE__ */ jsx(Select.List, {
336
+ "data-slot": "select-list",
337
+ ...containerProps,
338
+ tabIndex: -1,
339
+ ...props,
340
+ style: ownStyle,
341
+ className: list({ className: cn(containerProps.className, className) })
342
+ }), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
309
343
  });
310
344
  }
311
345
  const SelectItemPartsContext = createContext(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.76.0",
3
+ "version": "0.77.0",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",