@botiverse/raft-ui 0.76.1 → 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,12 @@
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
+
1
10
  ## [0.76.1](https://github.com/botiverse/raft-design/compare/v0.76.0...v0.76.1) (2026-11-08)
2
11
 
3
12
  ### Fixed
@@ -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-x-hidden 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-x-hidden overflow-y-auto overscroll-contain scroll-py-6", "-mx-1 flex flex-col px-1"],
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",
@@ -306,12 +310,36 @@ function SelectContent({ align = "start", alignItemWithTrigger = false, alignOff
306
310
  children: positionerNode
307
311
  });
308
312
  }
309
- function SelectList({ className, ...props }) {
310
- const { list } = select();
311
- return /* @__PURE__ */ jsx(Select.List, {
312
- "data-slot": "select-list",
313
- className: list({ className }),
314
- ...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 })]
315
343
  });
316
344
  }
317
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.1",
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",