@botiverse/raft-ui 0.72.0 → 0.74.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/components/activity-inbox/activity-inbox-shell.d.mts +6 -3
  3. package/dist/components/activity-inbox/activity-inbox-shell.mjs +33 -14
  4. package/dist/components/activity-inbox/activity-inbox-shell.recipe.mjs +6 -1
  5. package/dist/components/announcement-dialog/announcement-dialog.d.mts +2 -1
  6. package/dist/components/announcement-dialog/announcement-dialog.mjs +5 -4
  7. package/dist/components/choice-list/choice-list.mjs +4 -1
  8. package/dist/components/choice-list/choice-list.recipe.mjs +2 -2
  9. package/dist/components/combobox-select/combobox-select.css +2 -0
  10. package/dist/components/composer/composer-suggestion-list.d.mts +5 -2
  11. package/dist/components/composer/composer-suggestion-list.mjs +10 -8
  12. package/dist/components/composer/composer.recipe.mjs +1 -1
  13. package/dist/components/dialog/dialog.d.mts +5 -3
  14. package/dist/components/dialog/dialog.mjs +16 -10
  15. package/dist/components/files/files.d.mts +5 -2
  16. package/dist/components/files/files.mjs +11 -2
  17. package/dist/components/files/files.recipe.mjs +1 -1
  18. package/dist/components/filter-chip/filter-chip.mjs +2 -1
  19. package/dist/components/markdown/markdown-parts.d.mts +1 -1
  20. package/dist/components/markdown/markdown-parts.mjs +1 -1
  21. package/dist/components/message-action-card/message-action-card-content.recipe.mjs +2 -1
  22. package/dist/components/message-action-card/message-action-card.d.mts +6 -4
  23. package/dist/components/message-action-card/message-action-card.mjs +14 -27
  24. package/dist/components/message-list/message-list.mjs +3 -3
  25. package/dist/components/mobile-nav/mobile-nav-server.d.mts +4 -2
  26. package/dist/components/mobile-nav/mobile-nav-server.mjs +9 -5
  27. package/dist/components/notification-center/notification-center.d.mts +5 -2
  28. package/dist/components/notification-center/notification-center.mjs +10 -8
  29. package/dist/components/panel/panel-action.mjs +1 -1
  30. package/dist/components/panel/panel.d.mts +10 -2
  31. package/dist/components/panel/panel.mjs +26 -3
  32. package/dist/components/panel/panel.recipe.mjs +2 -1
  33. package/dist/components/scroll-area/scroll-area.css +27 -10
  34. package/dist/components/scroll-area/types.d.mts +3 -1
  35. package/dist/components/scroll-area/use-scroll-area-container.d.mts +2 -1
  36. package/dist/components/search/search.d.mts +5 -3
  37. package/dist/components/search/search.mjs +9 -10
  38. package/dist/components/search/search.recipe.mjs +1 -1
  39. package/dist/components/segmented-control/segmented-control.mjs +1 -1
  40. package/dist/components/sidebar/sidebar.mjs +1 -1
  41. package/dist/components/tabs/tabs.mjs +1 -1
  42. package/dist/components/tasks/tasks-panel.d.mts +6 -2
  43. package/dist/components/tasks/tasks-panel.mjs +10 -7
  44. package/dist/components/tasks/tasks-panel.recipe.mjs +1 -1
  45. package/dist/components/toggle-group/toggle-group.mjs +1 -1
  46. package/dist/components/virtual-list/styles/virtual-list.css +3 -0
  47. package/dist/index.d.mts +1 -1
  48. package/dist/index.mjs +4 -4
  49. package/dist/lib/use-scrollbar-size.d.mts +3 -0
  50. package/dist/lib/use-scrollbar-size.mjs +3 -0
  51. package/dist/lint/prefer-scroll-area.d.mts +75 -0
  52. package/dist/lint/prefer-scroll-area.mjs +157 -0
  53. package/dist/lint/recommended.json +6 -0
  54. package/dist/lint.d.mts +22 -0
  55. package/dist/lint.mjs +29 -0
  56. package/dist/styles.css +3 -2
  57. package/package.json +6 -1
  58. package/skills/raft-ui-guide/SKILL.md +1 -0
  59. package/skills/raft-ui-guide/rules/technical-overview.md +1 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## [0.74.0](https://github.com/botiverse/raft-design/compare/v0.73.0...v0.74.0) (2026-11-08)
2
+
3
+ ### Added
4
+
5
+ - **Lint rules for apps**: `@botiverse/raft-ui/lint` is a lint plugin with the design system's rules for app code, in the ESLint plugin format, which Oxlint loads as a JS plugin. Its first rule, `raft-ui/prefer-scroll-area`, reports a class or an inline style that makes an element scroll natively (`overflow-auto`, `overflow-scroll` and their `-x-` and `-y-` forms, with any variant) where an app writes it, unless a `// raft-ui: native-scroll <reason>` comment on the line above says which exception it is. The comment covers the string on the next line only, not the rest of a multi-line `cn(...)`, so a class added to the list later is still checked. Turn on every rule at once by extending `./node_modules/@botiverse/raft-ui/dist/lint/recommended.json` from `.oxlintrc.json` (or `configs.recommended` in an ESLint flat config); a rule added later reaches the app with the upgrade. ([#1058](https://github.com/botiverse/raft-design/pull/1058), [#1059](https://github.com/botiverse/raft-design/pull/1059))
6
+ - Why: the design system's rules should reach the apps, and the agents working in them, with raft-ui itself rather than through each repository's instructions; every region that scrolls is a ScrollArea, and nothing outside raft-ui checked it (Artea, #proj-raft-ui:#209).
7
+ - Affects: nothing until an app turns the plugin on. Then each native scroller in its code is reported: make it a ScrollArea (or `PanelScrollViewport` in a Panel), or add the comment with its reason.
8
+ - Docs: https://raft-design.botiverse.build/about/use-raft-ui/#lint-rules
9
+
10
+ ### Changed
11
+
12
+ - **useScrollAreaContainer gives its container the overflow**: the classes in `containerProps.className` make the element scroll in the `orientation`, as ScrollArea's own container does (vertical: `overflow-y: auto` and `overflow-x: hidden`; horizontal: the other way round; both: `auto`), so the element needs no overflow class of its own and nothing for `raft-ui/prefer-scroll-area` to report. `ComposerInput` and `ActivityInboxViewport` no longer carry `overflow-y-auto`. ([#1059](https://github.com/botiverse/raft-design/pull/1059))
13
+ - Why: the hook asked the app to add a native overflow class, which the new lint rule reports (Jasper's review of the lint rule).
14
+ - Affects: apps can drop the overflow class from a `useScrollAreaContainer` element. One they keep still wins (the hook's rule has no specificity), so nothing breaks. A vertical container that had only `overflow-y-auto` now clips sideways (`overflow-x: hidden`) instead of scrolling sideways with no scrollbar; pass `orientation="both"` for content that scrolls both ways.
15
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/#your-own-container
16
+
17
+ ### Design System
18
+
19
+ - **A thread's timeline**: conversation.md lists what a thread's timeline shows (the message it started from and every message a person wrote), when it appears, the rail for a mouse, the Timeline entry in the thread's More menu on every device, a half-height sheet on a phone and a popover wider, and how its items are marked. No raft-ui component changed. ([#1057](https://github.com/botiverse/raft-design/pull/1057))
20
+ - Why: a long thread buries the few messages people wrote under agents' work (Artea, #proj-raft-ui:#206).
21
+ - Docs: https://raft-design.botiverse.build/patterns/conversation/#a-threads-timeline
22
+
23
+ ## [0.73.0](https://github.com/botiverse/raft-design/compare/v0.72.0...v0.73.0) (2026-11-08)
24
+
25
+ ### Changed
26
+
27
+ - **Every region that scrolls is a ScrollArea**: `PanelScrollViewport`, `ActivityInboxViewport`, `SearchShellViewport`, `NotificationCenterScroller`, `FilesViewport`, `TasksPanelViewport`, ChoiceList's scroller, `MobileNavServerPopoverList`, `ComposerSuggestionList`, `AnnouncementDialogBody`, `MessageActionCardScopeListViewport` and `DialogNav` show ScrollArea's overlay scrollbar instead of the platform's. Native scrolling stays only in the places scroll-area.md lists, each with its reason, and a test fails on any other. ([#1055](https://github.com/botiverse/raft-design/pull/1055))
28
+ - Why: one look for scrollbars on every platform and theme, and a scrollbar that takes no room from a list's 8px inset (Artea, #proj-raft-ui:#209).
29
+ - Affects: **keyboard order**: a `PanelScrollViewport` whose content overflows is now a Tab stop of its own, before the controls inside it (a scrolling region must be reachable by keyboard). Check tests that depend on Tab order when upgrading. `PanelScrollViewport`'s `data-slot` (`panel-scroll-viewport`) replaces ScrollArea's own `scroll-area` on that element, so a selector for `[data-slot=scroll-area]` doesn't match it. On a phone MessageList keeps its native scroller. ChoiceList, DialogNav, the mobile server list and the composer suggestions keep their scroller out of the Tab order (`tabIndex={-1}`), since their rows take focus. `scrollbar-list` and `useScrollbarSize` are now only for a native scroller that can't be a ScrollArea.
30
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
31
+
32
+ ### Fixed
33
+
34
+ - **FilterChipRemove**: on a touch screen its 44px tap area no longer reaches over the chip's label. It grows 10px up and down, 6px toward the label (exactly to its edge) and 14px outward, so a tap on the label's end no longer clears the filter. ([#1054](https://github.com/botiverse/raft-design/pull/1054))
35
+ - Why: the tap area grew 10px on every side, across the 6px gap and 4px into the label (Remy, #proj-workspace:#221).
36
+ - Affects: touch screens. Keep the next control 16px from the chip (was 12), since the tap area now reaches 10px past the chip's end.
37
+ - Docs: https://raft-design.botiverse.build/components/filter-chip/
38
+ - **ArtifactIcon** is pinned to Lucide's `file-code` by a test, so a Lucide upgrade that changes that icon fails instead of letting the rail drift from the Artifact chip. ([#1054](https://github.com/botiverse/raft-design/pull/1054))
39
+
1
40
  ## [0.72.0](https://github.com/botiverse/raft-design/compare/v0.71.0...v0.72.0) (2026-11-07)
2
41
 
3
42
  ### Changed
@@ -4,9 +4,12 @@ import { ComponentPropsWithRef } from "react";
4
4
  export type ActivityInboxPanelProps = ComponentPropsWithRef<typeof Panel>;
5
5
  export declare function ActivityInboxPanel({ className, ...props }: ActivityInboxPanelProps): import("react").JSX.Element;
6
6
  export type ActivityInboxViewportProps = ComponentPropsWithRef<"div">;
7
- /** The list's scroller. It publishes its scrollbar's width for the item
8
- * cells (useScrollbarSize), so their fills sit 8px from both edges. */
9
- export declare function ActivityInboxViewport({ className, ref, ...props }: ActivityInboxViewportProps): import("react").JSX.Element;
7
+ /** The list's scroller, a ScrollArea container with absolute gutters: the
8
+ * overlay scrollbar takes no room, so the item cells' fills sit 8px from both
9
+ * edges with or without it (design/patterns/lists.md), and the scroller lays
10
+ * out its own column of items. `ref`, `className` and the native props are
11
+ * the scrolling element's; a frame around it holds the scrollbar. */
12
+ export declare function ActivityInboxViewport({ children, className, ref, style, ...props }: ActivityInboxViewportProps): import("react").JSX.Element;
10
13
  export type ActivityInboxLoadingMoreProps = ComponentPropsWithRef<"div">;
11
14
  export declare function ActivityInboxLoadingMore({ className, ...props }: ActivityInboxLoadingMoreProps): import("react").JSX.Element;
12
15
  //#endregion
@@ -1,8 +1,10 @@
1
- import { useMergedRef } from "../../lib/use-merged-ref.mjs";
1
+ import { cn } from "../../cn.mjs";
2
+ import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
3
+ import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
2
4
  import { Panel } from "../panel/panel.mjs";
3
- import { useScrollbarSize } from "../../lib/use-scrollbar-size.mjs";
4
5
  import { activityInboxShell } from "./activity-inbox-shell.recipe.mjs";
5
- import { jsx } from "react/jsx-runtime";
6
+ import { useMemo } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
6
8
  //#region src/components/activity-inbox/activity-inbox-shell.tsx
7
9
  function ActivityInboxPanel({ className, ...props }) {
8
10
  const { panel } = activityInboxShell();
@@ -14,17 +16,34 @@ function ActivityInboxPanel({ className, ...props }) {
14
16
  ...props
15
17
  });
16
18
  }
17
- /** The list's scroller. It publishes its scrollbar's width for the item
18
- * cells (useScrollbarSize), so their fills sit 8px from both edges. */
19
- function ActivityInboxViewport({ className, ref, ...props }) {
20
- const { viewport } = activityInboxShell();
21
- const scrollbarRef = useScrollbarSize();
22
- const [setRef] = useMergedRef(ref, scrollbarRef);
23
- return /* @__PURE__ */ jsx("div", {
24
- "data-slot": "activity-inbox-viewport",
25
- className: viewport({ className }),
26
- ...props,
27
- ref: setRef
19
+ /** The list's scroller, a ScrollArea container with absolute gutters: the
20
+ * overlay scrollbar takes no room, so the item cells' fills sit 8px from both
21
+ * edges with or without it (design/patterns/lists.md), and the scroller lays
22
+ * out its own column of items. `ref`, `className` and the native props are
23
+ * the scrolling element's; a frame around it holds the scrollbar. */
24
+ function ActivityInboxViewport({ children, className, ref, style, ...props }) {
25
+ const { frame, viewport } = activityInboxShell();
26
+ const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
27
+ containerRef: ref,
28
+ guttersPosition: "absolute",
29
+ overscroll: "contain"
30
+ });
31
+ const ownStyle = useMemo(() => ({
32
+ ...containerProps.style,
33
+ ...style
34
+ }), [containerProps.style, style]);
35
+ return /* @__PURE__ */ jsxs("div", {
36
+ ...frameProps,
37
+ "data-slot": "activity-inbox-viewport-frame",
38
+ className: frame({ className: frameProps.className }),
39
+ children: [/* @__PURE__ */ jsx("div", {
40
+ "data-slot": "activity-inbox-viewport",
41
+ ...containerProps,
42
+ ...props,
43
+ style: ownStyle,
44
+ className: viewport({ className: cn(containerProps.className, className) }),
45
+ children
46
+ }), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
28
47
  });
29
48
  }
30
49
  function ActivityInboxLoadingMore({ className, ...props }) {
@@ -7,7 +7,12 @@ const activityInboxShell = tv({ slots: {
7
7
  "bg-layer-canvas font-sans text-foreground",
8
8
  listColumnMargin
9
9
  ],
10
- viewport: ["relative flex flex-1 flex-col gap-0.5 overflow-y-auto scrollbar-list *:shrink-0", "bg-layer-canvas px-0 pt-2 pb-[calc(8px+env(safe-area-inset-bottom,0px))]"],
10
+ frame: ["min-h-0 flex-1"],
11
+ viewport: [
12
+ "relative flex min-h-0 flex-1 flex-col gap-0.5 *:shrink-0",
13
+ "outline-none focus-visible:shadow-focus",
14
+ "bg-layer-canvas px-0 pt-2 pb-[calc(8px+env(safe-area-inset-bottom,0px))]"
15
+ ],
11
16
  loadingMore: ["py-3 text-center", "text-caption text-foreground-hint"]
12
17
  } });
13
18
  //#endregion
@@ -1,11 +1,12 @@
1
1
  import { ButtonProps } from "../button/button.mjs";
2
2
  import "../button/index.mjs";
3
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
3
4
  import { Dialog, DialogClose, DialogContentProps, DialogTitleProps, DialogTrigger } from "../dialog/dialog.mjs";
4
5
  import "../dialog/index.mjs";
5
6
  import { ComponentPropsWithRef } from "react";
6
7
  //#region src/components/announcement-dialog/announcement-dialog.d.ts
7
8
  export declare function AnnouncementDialogContent({ className, initialFocus, ref, ...props }: DialogContentProps): import("react").JSX.Element;
8
- export declare function AnnouncementDialogBody({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
9
+ export declare function AnnouncementDialogBody({ className, ...props }: ScrollAreaProps): import("react").JSX.Element;
9
10
  export declare function AnnouncementDialogHeader({ className, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
10
11
  export declare function AnnouncementDialogLabel({ className, children, ...props }: ComponentPropsWithRef<"div">): import("react").JSX.Element;
11
12
  export declare function AnnouncementDialogTitle({ className, ...props }: DialogTitleProps): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { tv } from "../../lib/tv.mjs";
2
2
  import { Button } from "../button/button.mjs";
3
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
3
4
  import { Dialog, DialogClose, DialogContent, DialogTitle, DialogTrigger } from "../dialog/dialog.mjs";
4
5
  import { useCallback, useRef } from "react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -7,7 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
7
8
  const announcement = tv({ slots: {
8
9
  content: ["@container/announcement max-w-xl gap-0 overflow-clip p-0"],
9
10
  body: [
10
- "min-h-0 max-h-[70dvh] overflow-y-auto pb-5",
11
+ "min-h-0 max-h-[70dvh] pb-5",
11
12
  "[&_[data-slot=announcement-picture]+[data-slot=announcement-text]]:mt-5",
12
13
  "px-(--surface-inset) pt-(--surface-inset)"
13
14
  ],
@@ -43,10 +44,10 @@ function AnnouncementDialogContent({ className, initialFocus, ref, ...props }) {
43
44
  }
44
45
  function AnnouncementDialogBody({ className, ...props }) {
45
46
  const { body } = announcement();
46
- return /* @__PURE__ */ jsx("div", {
47
+ return /* @__PURE__ */ jsx(ScrollArea, {
47
48
  "data-slot": "announcement-body",
48
- className: body({ className }),
49
- ...props
49
+ ...props,
50
+ className: body({ className })
50
51
  });
51
52
  }
52
53
  function AnnouncementDialogHeader({ className, ...props }) {
@@ -1,4 +1,5 @@
1
1
  import { Button } from "../button/button.mjs";
2
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
2
3
  import { Checkbox } from "../checkbox/checkbox.mjs";
3
4
  import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
4
5
  import { InputGroup, InputGroupAddon, InputGroupInput } from "../input-group/input-group.mjs";
@@ -255,8 +256,10 @@ function ChoiceList(props) {
255
256
  }) : null, /* @__PURE__ */ jsxs("div", {
256
257
  "data-slot": "choice-list-frame",
257
258
  className: slots.frame(),
258
- children: [/* @__PURE__ */ jsx("div", {
259
+ children: [/* @__PURE__ */ jsx(ScrollArea, {
259
260
  "data-slot": "choice-list-scroller",
261
+ overscroll: "contain",
262
+ tabIndex: -1,
260
263
  className: slots.scroller(),
261
264
  children: items.length === 0 ? /* @__PURE__ */ jsx("p", {
262
265
  "data-slot": "choice-list-note",
@@ -31,11 +31,11 @@ const choiceList = tv({
31
31
  footer: ["shrink-0 border-t border-line-seam p-1"]
32
32
  },
33
33
  variants: {
34
- scroll: { true: { scroller: ["max-h-72 overflow-y-auto overscroll-contain"] } },
34
+ scroll: { true: { scroller: ["max-h-72"] } },
35
35
  fill: { true: {
36
36
  root: ["flex min-h-0 flex-1 flex-col"],
37
37
  frame: ["min-h-0 flex-1"],
38
- scroller: ["flex-1 overflow-y-auto overscroll-contain"]
38
+ scroller: ["flex-1"]
39
39
  } },
40
40
  hasAction: { true: { item: ["pr-11"] } }
41
41
  }
@@ -181,6 +181,8 @@
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
186
  .r-combobox-select__list {
185
187
  box-sizing: border-box;
186
188
  max-height: var(--combobox-select-list-max-height, 16rem);
@@ -1,4 +1,5 @@
1
1
  import { VariantProps } from "../../lib/tv.mjs";
2
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
2
3
  import { useRender } from "@base-ui/react/use-render";
3
4
  //#region src/components/composer/composer-suggestion-list.d.ts
4
5
  declare const composerSuggestionList: import("tailwind-variants/lite").TVReturnType<{
@@ -113,8 +114,10 @@ declare const composerSuggestionList: import("tailwind-variants/lite").TVReturnT
113
114
  aside: string[];
114
115
  meta: string[];
115
116
  }>>;
116
- export type ComposerSuggestionListProps = useRender.ComponentProps<"div">;
117
- export declare function ComposerSuggestionList({ className, render, ...props }: ComposerSuggestionListProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
117
+ export type ComposerSuggestionListProps = ScrollAreaProps;
118
+ /** The suggestions' popup, a ScrollArea: `ref`, `className`, `render` and
119
+ * the native props (the listbox's role and id) are the scrolling element's. */
120
+ export declare function ComposerSuggestionList({ className, ...props }: ComposerSuggestionListProps): import("react").JSX.Element;
118
121
  type ComposerSuggestionListVariantProps = VariantProps<typeof composerSuggestionList>;
119
122
  export type ComposerSuggestionGroupProps = useRender.ComponentProps<"div"> & Pick<ComposerSuggestionListVariantProps, "separated">;
120
123
  export declare function ComposerSuggestionGroup({ separated, className, render, ...props }: ComposerSuggestionGroupProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -1,11 +1,13 @@
1
1
  import { tv } from "../../lib/tv.mjs";
2
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
3
+ import { jsx } from "react/jsx-runtime";
2
4
  import { mergeProps } from "@base-ui/react/merge-props";
3
5
  import { useRender } from "@base-ui/react/use-render";
4
6
  //#region src/components/composer/composer-suggestion-list.tsx
5
7
  const composerSuggestionList = tv({
6
8
  slots: {
7
9
  root: [
8
- "absolute inset-x-0 bottom-full w-auto overflow-y-auto",
10
+ "absolute inset-x-0 bottom-full w-auto",
9
11
  "mb-2 max-h-60 overflow-x-hidden rounded-container p-1",
10
12
  "bg-layer-popover text-foreground ring-1 ring-line-seam shadow-float"
11
13
  ],
@@ -47,16 +49,16 @@ const composerSuggestionList = tv({
47
49
  metaVariant: "text"
48
50
  }
49
51
  });
50
- function ComposerSuggestionList({ className, render, ...props }) {
52
+ /** The suggestions' popup, a ScrollArea: `ref`, `className`, `render` and
53
+ * the native props (the listbox's role and id) are the scrolling element's. */
54
+ function ComposerSuggestionList({ className, ...props }) {
51
55
  const { root } = composerSuggestionList();
52
- const defaultProps = {
56
+ return /* @__PURE__ */ jsx(ScrollArea, {
53
57
  "data-slot": "composer-suggestion-list",
58
+ overscroll: "contain",
59
+ tabIndex: -1,
60
+ ...props,
54
61
  className: root({ className })
55
- };
56
- return useRender({
57
- defaultTagName: "div",
58
- render,
59
- props: mergeProps(defaultProps, props)
60
62
  });
61
63
  }
62
64
  function ComposerSuggestionGroup({ separated, className, render, ...props }) {
@@ -12,7 +12,7 @@ const composer = tv({
12
12
  inputFrame: ["w-full min-w-0", "[--r-scroll-area-resize-grip-size:0px]"],
13
13
  input: [
14
14
  "max-h-32 w-full resize-none bg-transparent outline-none",
15
- "field-sizing-content overflow-y-auto",
15
+ "field-sizing-content",
16
16
  "min-h-6 px-0.5",
17
17
  "font-sans text-body font-normal text-foreground",
18
18
  "placeholder:text-foreground-hint"
@@ -1,6 +1,7 @@
1
1
  import { StyledProps } from "../../lib/styled-props.mjs";
2
2
  import { ButtonProps } from "../button/button.mjs";
3
3
  import "../button/index.mjs";
4
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
4
5
  import { PanelActionProps } from "../panel/panel-action.mjs";
5
6
  import { Dialog } from "@base-ui/react/dialog";
6
7
  import { ComponentPropsWithRef, ReactNode } from "react";
@@ -78,8 +79,9 @@ export type DialogSurfaceProps = useRender.ComponentProps<"div">;
78
79
  * DialogContent draws the same frame for a Base UI dialog.
79
80
  */
80
81
  export declare function DialogSurface({ className, render, children, ...props }: DialogSurfaceProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
81
- export type DialogNavProps = useRender.ComponentProps<"nav">;
82
- /** The section list of a wide (`size="wide"`) dialog, inside a `bar` body. */
83
- export declare function DialogNav({ className, render, ...props }: DialogNavProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
82
+ export type DialogNavProps = Omit<ScrollAreaProps, "contentClassName">;
83
+ /** The section list of a wide (`size="wide"`) dialog, inside a `bar` body: a
84
+ * `<nav>` that is a ScrollArea, its rows in the area's content element. */
85
+ export declare function DialogNav({ className, render, ...props }: DialogNavProps): import("react").JSX.Element;
84
86
  //#endregion
85
87
  export { Dialog$1 as Dialog };
@@ -7,6 +7,7 @@ import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
7
7
  import { useParentOverlayKind, withOverlayLayer } from "../../lib/overlay-depth.mjs";
8
8
  import { Button } from "../button/button.mjs";
9
9
  import { DialogSheetHeadContext, DialogSheetReset, dialogSheet, useDialogSheet, useInDialogSheetHead } from "./dialog-sheet-context.mjs";
10
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
10
11
  import { TextLink } from "../text-link/text-link.mjs";
11
12
  import { PanelAction } from "../panel/panel-action.mjs";
12
13
  import { Dialog } from "@base-ui/react/dialog";
@@ -354,17 +355,22 @@ function DialogSurface({ className, render, children, ...props }) {
354
355
  }, props, { children: /* @__PURE__ */ jsx(DialogSheetReset, { children }) })
355
356
  });
356
357
  }
357
- const dialogNav = tv({ slots: { root: ["flex shrink-0 flex-col overflow-y-auto", "w-52 gap-0.5 border-r border-line-seam py-2 max-md:hidden"] } });
358
- /** The section list of a wide (`size="wide"`) dialog, inside a `bar` body. */
358
+ const dialogNav = tv({ slots: {
359
+ root: ["shrink-0", "w-52 border-r border-line-seam py-2 max-md:hidden"],
360
+ content: ["flex flex-col gap-0.5"]
361
+ } });
362
+ /** The section list of a wide (`size="wide"`) dialog, inside a `bar` body: a
363
+ * `<nav>` that is a ScrollArea, its rows in the area's content element. */
359
364
  function DialogNav({ className, render, ...props }) {
360
- const { root } = dialogNav();
361
- return useRender({
362
- defaultTagName: "nav",
363
- render,
364
- props: mergeProps({
365
- "data-slot": "dialog-nav",
366
- className: root({ className })
367
- }, props)
365
+ const { root, content } = dialogNav();
366
+ return /* @__PURE__ */ jsx(ScrollArea, {
367
+ "data-slot": "dialog-nav",
368
+ overscroll: "contain",
369
+ tabIndex: -1,
370
+ ...props,
371
+ render: render ?? /* @__PURE__ */ jsx("nav", {}),
372
+ className: root({ className }),
373
+ contentClassName: content()
368
374
  });
369
375
  }
370
376
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { ButtonProps } from "../button/button.mjs";
2
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
2
3
  import { useRender } from "@base-ui/react/use-render";
3
4
  //#region src/components/files/files.d.ts
4
5
  type FileTag = "button" | "div" | "img" | "section" | "span";
@@ -6,8 +7,10 @@ type DataAttributes = Record<`data-${string}`, string | undefined>;
6
7
  type FileSlotProps<Tag extends FileTag> = useRender.ComponentProps<Tag> & DataAttributes;
7
8
  export type FilesPanelProps = FileSlotProps<"section">;
8
9
  export declare function FilesPanel(props: FilesPanelProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
9
- export type FilesViewportProps = FileSlotProps<"div">;
10
- export declare function FilesViewport(props: FilesViewportProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
10
+ export type FilesViewportProps = ScrollAreaProps & DataAttributes;
11
+ /** The files' scroller, a ScrollArea: `ref`, `className`, `render`, `onScroll`
12
+ * and the native props are the scrolling element's. */
13
+ export declare function FilesViewport({ className, ...props }: FilesViewportProps): import("react").JSX.Element;
11
14
  export type FilesListProps = FileSlotProps<"div">;
12
15
  export declare function FilesList(props: FilesListProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
13
16
  export type FileRowProps = FileSlotProps<"div">;
@@ -1,4 +1,5 @@
1
1
  import { Button } from "../button/button.mjs";
2
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
2
3
  import { files } from "./files.recipe.mjs";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  import { useRender } from "@base-ui/react/use-render";
@@ -19,8 +20,16 @@ function useFileSlot(slotName, dataSlot, defaultTagName, props) {
19
20
  function FilesPanel(props) {
20
21
  return useFileSlot("panel", "files-panel", "section", props);
21
22
  }
22
- function FilesViewport(props) {
23
- return useFileSlot("viewport", "files-viewport", "div", props);
23
+ /** The files' scroller, a ScrollArea: `ref`, `className`, `render`, `onScroll`
24
+ * and the native props are the scrolling element's. */
25
+ function FilesViewport({ className, ...props }) {
26
+ const { viewport } = files();
27
+ return /* @__PURE__ */ jsx(ScrollArea, {
28
+ "data-slot": "files-viewport",
29
+ overscroll: "contain",
30
+ ...props,
31
+ className: viewport({ className })
32
+ });
24
33
  }
25
34
  function FilesList(props) {
26
35
  return useFileSlot("list", "files-list", "div", props);
@@ -2,7 +2,7 @@ import { tv } from "../../lib/tv.mjs";
2
2
  //#region src/components/files/files.recipe.ts
3
3
  const files = tv({ slots: {
4
4
  panel: ["flex min-h-0 flex-1 flex-col", "bg-layer-canvas text-foreground"],
5
- viewport: ["min-h-0 flex-1 overflow-y-auto"],
5
+ viewport: ["min-h-0 flex-1"],
6
6
  list: ["flex flex-col gap-2 px-(--surface-inset) py-3", "max-md:px-4"],
7
7
  row: [
8
8
  "group/file-row flex w-full min-w-0 gap-3 px-3",
@@ -17,7 +17,8 @@ const filterChip = tv({ slots: {
17
17
  remove: [
18
18
  ...chipRemoveBase,
19
19
  "relative size-6",
20
- "after:absolute after:content-[''] pointer-coarse:after:-inset-2.5"
20
+ "after:absolute after:content-['']",
21
+ "pointer-coarse:after:-inset-y-2.5 pointer-coarse:after:-left-1.5 pointer-coarse:after:-right-3.5"
21
22
  ]
22
23
  } });
23
24
  /**
@@ -1,8 +1,8 @@
1
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
1
2
  import { CheckboxProps } from "../checkbox/checkbox.mjs";
2
3
  import "../checkbox/index.mjs";
3
4
  import { InlineCodeProps } from "../inline-code/inline-code.mjs";
4
5
  import "../inline-code/index.mjs";
5
- import { ScrollAreaProps } from "../scroll-area/types.mjs";
6
6
  import { ComponentPropsWithRef } from "react";
7
7
  //#region src/components/markdown/markdown-parts.d.ts
8
8
  declare const HEADING_TAGS: {
@@ -1,5 +1,5 @@
1
- import { Checkbox } from "../checkbox/checkbox.mjs";
2
1
  import { ScrollArea } from "../scroll-area/scroll-area.mjs";
2
+ import { Checkbox } from "../checkbox/checkbox.mjs";
3
3
  import { InlineCode } from "../inline-code/inline-code.mjs";
4
4
  import { markdown } from "./markdown.recipe.mjs";
5
5
  import { jsx } from "react/jsx-runtime";
@@ -24,7 +24,8 @@ const messageActionCardContent = tv({ slots: {
24
24
  "@max-sm/action-card:min-h-0 @max-sm/action-card:py-0"
25
25
  ],
26
26
  scopeList: ["mt-0 stack-y-1.5"],
27
- scopeListViewport: ["overflow-y-auto overscroll-contain", "max-h-[136px] stack-y-1.5 pr-1"],
27
+ scopeListViewport: ["max-h-[136px]"],
28
+ scopeListViewportContent: ["stack-y-1.5 pr-1"],
28
29
  scopeListLabel: ["text-footnote font-medium text-foreground-hint"],
29
30
  scope: ["min-w-0 border p-1.5", "rounded-control border-line-hairline bg-layer-card px-2.5 py-2"],
30
31
  scopeName: ["break-all font-mono", "text-mono-meta font-normal text-foreground"],
@@ -1,3 +1,4 @@
1
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
1
2
  import { BadgeProps } from "../badge/badge.mjs";
2
3
  import "../badge/index.mjs";
3
4
  import { ComponentProps, ReactNode } from "react";
@@ -38,10 +39,11 @@ export declare function MessageActionCardPropertyValue({ className, render, ...p
38
39
  export declare function MessageActionCardScopeList({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
39
40
  export declare function MessageActionCardScopeListLabel({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
40
41
  /**
41
- * Scrolls a long permission list. It is a Tab stop only while it overflows, so
42
- * keyboard users can scroll it; a short list adds no empty stop.
42
+ * Scrolls a long permission list: a ScrollArea, so it is a Tab stop only while
43
+ * it overflows (keyboard users can scroll it; a short list adds no empty stop),
44
+ * and its overlay scrollbar takes no room from the rows.
43
45
  */
44
- export declare function MessageActionCardScopeListViewport({ className, render, ...props }: MessageActionCardScopeListViewportProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
46
+ export declare function MessageActionCardScopeListViewport({ className, ...props }: MessageActionCardScopeListViewportProps): import("react").JSX.Element;
45
47
  export declare function MessageActionCardScope({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
46
48
  export declare function MessageActionCardScopeName({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
47
49
  export declare function MessageActionCardScopeDescription({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -67,7 +69,7 @@ export type MessageActionCardCollapseTriggerProps = Omit<ComponentProps<typeof C
67
69
  className?: string;
68
70
  "aria-label": string;
69
71
  };
70
- export type MessageActionCardScopeListViewportProps = useRender.ComponentProps<"div"> & {
72
+ export type MessageActionCardScopeListViewportProps = Omit<ScrollAreaProps, "contentClassName" | "aria-label"> & {
71
73
  /** Names the scroll area for screen readers, e.g. "Permissions". */
72
74
  "aria-label": string;
73
75
  };
@@ -1,10 +1,10 @@
1
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
1
2
  import { Badge } from "../badge/badge.mjs";
2
3
  import { MessageEmbed, MessageEmbedContent, MessageEmbedHeader } from "../message-embed/message-embed.mjs";
3
4
  import { messageActionCard } from "./message-action-card.recipe.mjs";
4
5
  import { messageActionCardContent } from "./message-action-card-content.recipe.mjs";
5
6
  import { ChevronDown, Info, Zap } from "lucide-react";
6
- import { useEffect, useState } from "react";
7
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  import { mergeProps } from "@base-ui/react/merge-props";
9
9
  import { useRender } from "@base-ui/react/use-render";
10
10
  import { Collapsible } from "@base-ui/react/collapsible";
@@ -182,31 +182,18 @@ function MessageActionCardScopeListLabel({ className, render, ...props }) {
182
182
  });
183
183
  }
184
184
  /**
185
- * Scrolls a long permission list. It is a Tab stop only while it overflows, so
186
- * keyboard users can scroll it; a short list adds no empty stop.
185
+ * Scrolls a long permission list: a ScrollArea, so it is a Tab stop only while
186
+ * it overflows (keyboard users can scroll it; a short list adds no empty stop),
187
+ * and its overlay scrollbar takes no room from the rows.
187
188
  */
188
- function MessageActionCardScopeListViewport({ className, render, ...props }) {
189
- const { scopeListViewport } = messageActionCardContent();
190
- const [element, setElement] = useState(null);
191
- const [overflows, setOverflows] = useState(false);
192
- useEffect(() => {
193
- if (element === null) return;
194
- const measure = () => setOverflows(element.scrollHeight > element.clientHeight);
195
- measure();
196
- const observer = new ResizeObserver(measure);
197
- observer.observe(element);
198
- for (const child of element.children) observer.observe(child);
199
- return () => observer.disconnect();
200
- }, [element]);
201
- return useRender({
202
- defaultTagName: "div",
203
- render,
204
- ref: setElement,
205
- props: mergeProps({
206
- className: scopeListViewport({ className }),
207
- tabIndex: overflows ? 0 : void 0
208
- }, props),
209
- state: { slot: "message-action-card-scope-list-viewport" }
189
+ function MessageActionCardScopeListViewport({ className, ...props }) {
190
+ const { scopeListViewport, scopeListViewportContent } = messageActionCardContent();
191
+ return /* @__PURE__ */ jsx(ScrollArea, {
192
+ "data-slot": "message-action-card-scope-list-viewport",
193
+ overscroll: "contain",
194
+ ...props,
195
+ className: scopeListViewport({ className }),
196
+ contentClassName: scopeListViewportContent()
210
197
  });
211
198
  }
212
199
  function MessageActionCardScope({ className, render, ...props }) {
@@ -262,7 +249,7 @@ function MessageActionCardNotice({ className, render, icon, children, ...props }
262
249
  render,
263
250
  props: mergeProps({
264
251
  className: notice({ className }),
265
- children: /* @__PURE__ */ jsxs(Fragment$1, { children: [icon ?? /* @__PURE__ */ jsx(Info, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", { children })] })
252
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [icon ?? /* @__PURE__ */ jsx(Info, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", { children })] })
266
253
  }, props),
267
254
  state: { slot: "message-action-card-notice" }
268
255
  });
@@ -1,6 +1,6 @@
1
- import { useMobileViewport } from "../../lib/use-mobile-viewport.mjs";
2
1
  import { ScrollArea } from "../scroll-area/scroll-area.mjs";
3
- import { PanelScrollViewport } from "../panel/panel.mjs";
2
+ import { useMobileViewport } from "../../lib/use-mobile-viewport.mjs";
3
+ import { PanelNativeScrollViewport } from "../panel/panel.mjs";
4
4
  import { messageList } from "./message-list.recipe.mjs";
5
5
  import { useMemo } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
@@ -15,7 +15,7 @@ function MessageList({ children, className, render, rootClassName, style, ...pro
15
15
  if (isMobile) return /* @__PURE__ */ jsx("div", {
16
16
  "data-slot": "message-list",
17
17
  className: root({ className: rootClassName }),
18
- children: /* @__PURE__ */ jsx(PanelScrollViewport, {
18
+ children: /* @__PURE__ */ jsx(PanelNativeScrollViewport, {
19
19
  ...props,
20
20
  render,
21
21
  "data-slot": "message-list-viewport",
@@ -1,3 +1,4 @@
1
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
1
2
  import { MobileNavItemProps } from "./mobile-nav.mjs";
2
3
  import { PopoverContentProps, PopoverProps } from "../popover/popover.mjs";
3
4
  import { ComponentPropsWithRef } from "react";
@@ -27,8 +28,9 @@ export type MobileNavServerPopoverContentProps = PopoverContentProps;
27
28
  * destinations and the footer after it stay in view.
28
29
  */
29
30
  export declare function MobileNavServerPopoverContent({ anchor, side, align, sideOffset, collisionPadding, className, ...props }: MobileNavServerPopoverContentProps): import("react").JSX.Element;
30
- export type MobileNavServerPopoverListProps = ComponentPropsWithRef<"div">;
31
- /** The server list: takes the room left in the popover and scrolls inside it. */
31
+ export type MobileNavServerPopoverListProps = ScrollAreaProps;
32
+ /** The server list: takes the room left in the popover and scrolls inside it,
33
+ * a ScrollArea (a column with no gap, so its gutters move nothing). */
32
34
  export declare function MobileNavServerPopoverList({ className, ...props }: MobileNavServerPopoverListProps): import("react").JSX.Element;
33
35
  export type MobileNavServerPopoverGroupProps = ComponentPropsWithRef<"div">;
34
36
  /** Rows that always stay in view, such as the footer actions. */