@botiverse/raft-ui 0.59.2 → 0.60.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 (56) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/components/composer/composer-attachment.recipe.mjs +2 -3
  3. package/dist/components/composer/composer-attachments-scroll-area.d.mts +6 -0
  4. package/dist/components/composer/composer-attachments-scroll-area.mjs +20 -31
  5. package/dist/components/composer/composer.d.mts +8 -2
  6. package/dist/components/composer/composer.mjs +7 -9
  7. package/dist/components/composer/composer.recipe.mjs +1 -0
  8. package/dist/components/context-menu/context-menu.d.mts +1 -1
  9. package/dist/components/dropdown-menu/dropdown-menu.d.mts +1 -1
  10. package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
  11. package/dist/components/kbd/index.d.mts +5 -2
  12. package/dist/components/kbd/kbd-keys.d.mts +12 -0
  13. package/dist/components/kbd/kbd-keys.mjs +297 -0
  14. package/dist/components/kbd/kbd-shortcut.d.mts +34 -0
  15. package/dist/components/kbd/kbd-shortcut.mjs +79 -0
  16. package/dist/components/kbd/kbd.d.mts +55 -2
  17. package/dist/components/kbd/kbd.mjs +39 -15
  18. package/dist/components/markdown/markdown-parts.d.mts +7 -2
  19. package/dist/components/markdown/markdown-parts.mjs +8 -3
  20. package/dist/components/markdown/markdown.recipe.mjs +1 -5
  21. package/dist/components/message-list/message-list.mjs +9 -15
  22. package/dist/components/message-list/message-list.recipe.mjs +1 -1
  23. package/dist/components/scroll-area/class-names.mjs +19 -0
  24. package/dist/components/scroll-area/gutters-binding.mjs +4 -0
  25. package/dist/components/scroll-area/index.d.mts +6 -2
  26. package/dist/components/scroll-area/scroll-area-gutters.d.mts +15 -0
  27. package/dist/components/scroll-area/scroll-area-gutters.mjs +60 -0
  28. package/dist/components/scroll-area/scroll-area-textarea.d.mts +14 -0
  29. package/dist/components/scroll-area/scroll-area-textarea.mjs +50 -0
  30. package/dist/components/scroll-area/scroll-area.css +720 -0
  31. package/dist/components/scroll-area/scroll-area.d.mts +24 -15
  32. package/dist/components/scroll-area/scroll-area.mjs +67 -45
  33. package/dist/components/scroll-area/scroll-geometry.mjs +58 -0
  34. package/dist/components/scroll-area/scroll-to-element.mjs +67 -0
  35. package/dist/components/scroll-area/types.d.mts +224 -0
  36. package/dist/components/scroll-area/use-rail-pointer.mjs +93 -0
  37. package/dist/components/scroll-area/use-scroll-area-container.d.mts +26 -0
  38. package/dist/components/scroll-area/use-scroll-area-container.mjs +150 -0
  39. package/dist/components/scroll-area/use-scrollbars.mjs +480 -0
  40. package/dist/components/search/search-trigger.d.mts +1 -1
  41. package/dist/components/sidebar/sidebar.d.mts +1 -1
  42. package/dist/components/sidebar/sidebar.mjs +24 -20
  43. package/dist/components/sidebar/sidebar.recipe.mjs +1 -6
  44. package/dist/components/tooltip/tooltip.mjs +8 -5
  45. package/dist/components/virtual-list/VirtualList.mjs +1 -1
  46. package/dist/index.d.mts +7 -3
  47. package/dist/index.mjs +5 -2
  48. package/dist/lib/inline-part.mjs +13 -0
  49. package/dist/lib/use-merged-ref.mjs +73 -47
  50. package/dist/lib/use-platform.d.mts +13 -0
  51. package/dist/lib/use-platform.mjs +29 -0
  52. package/dist/styles.css +1 -0
  53. package/dist/wip.d.mts +5 -1
  54. package/dist/wip.mjs +3 -1
  55. package/package.json +1 -1
  56. package/dist/components/scroll-area/scroll-area.recipe.mjs +0 -23
@@ -1,17 +1,26 @@
1
- import { StyledProps } from "../../lib/styled-props.mjs";
2
- import { ScrollArea } from "@base-ui/react/scroll-area";
1
+ import { ScrollAreaContentProps, ScrollAreaProps } from "./types.mjs";
3
2
  //#region src/components/scroll-area/scroll-area.d.ts
4
- export type ScrollAreaProps = StyledProps<ScrollArea.Root.Props>;
5
- declare function ScrollArea$1({ className, ...props }: ScrollAreaProps): import("react").JSX.Element;
6
- export type ScrollAreaViewportProps = StyledProps<ScrollArea.Viewport.Props>;
7
- export declare function ScrollAreaViewport({ className, ...props }: ScrollAreaViewportProps): import("react").JSX.Element;
8
- export type ScrollAreaContentProps = StyledProps<ScrollArea.Content.Props>;
3
+ /**
4
+ * An optional box around the content, inside the container (the demo's
5
+ * `ScrollAreaResizableContent`): for padding, so the scrollbars stay at the
6
+ * edges, or a layout of the content's own. In a horizontal (or `both`) area
7
+ * it is as wide as its widest child. It is watched for size changes like any
8
+ * child of the container.
9
+ */
9
10
  export declare function ScrollAreaContent({ className, ...props }: ScrollAreaContentProps): import("react").JSX.Element;
10
- export type ScrollAreaScrollbarProps = StyledProps<ScrollArea.Scrollbar.Props>;
11
- export declare function ScrollAreaScrollbar({ className, orientation, ...props }: ScrollAreaScrollbarProps): import("react").JSX.Element;
12
- export type ScrollAreaThumbProps = StyledProps<ScrollArea.Thumb.Props>;
13
- export declare function ScrollAreaThumb({ className, ...props }: ScrollAreaThumbProps): import("react").JSX.Element;
14
- export type ScrollAreaCornerProps = StyledProps<ScrollArea.Corner.Props>;
15
- export declare function ScrollAreaCorner({ className, ...props }: ScrollAreaCornerProps): import("react").JSX.Element;
16
- //#endregion
17
- export { ScrollArea$1 as ScrollArea };
11
+ /**
12
+ * A scrolling box with thin overlay scrollbars that look the same on every
13
+ * platform and theme. Scrolling itself stays native (wheel, touch,
14
+ * keyboard); the scrollbars can also be dragged, and a press on a gutter
15
+ * pages.
16
+ *
17
+ * Modelled on 07akioni/react-scroll-area-demo: one element, the container
18
+ * (the element that scrolls), with the gutters (the scrollbars) as its first
19
+ * child, a zero-height sticky layer. `ref`, `className`, `style` and the
20
+ * native props are the container's; `render` puts another element in its
21
+ * place; `actionsRef` adds `scrollToElement` and `measure`. For an element of
22
+ * your own that can't take children through ScrollArea (a textarea,
23
+ * VirtualList's scroller), see `useScrollAreaContainer`.
24
+ */
25
+ export declare function ScrollArea({ actionsRef, children, className, contentClassName, contentRef, contentStyle, fade, hideDelay, onEdgeChange, onScroll, orientation, overscroll, railInset, ref, render, size, style, tabIndex, trigger, ...props }: ScrollAreaProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
26
+ //#endregion
@@ -1,55 +1,77 @@
1
- import { scrollArea } from "./scroll-area.recipe.mjs";
2
- import { jsx } from "react/jsx-runtime";
3
- import { ScrollArea } from "@base-ui/react/scroll-area";
1
+ import { cn } from "../../cn.mjs";
2
+ import { ScrollAreaGutters } from "./scroll-area-gutters.mjs";
3
+ import { useScrollAreaContainer } from "./use-scroll-area-container.mjs";
4
+ import { useMemo, useRef } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { mergeProps } from "@base-ui/react/merge-props";
7
+ import { useRender } from "@base-ui/react/use-render";
4
8
  //#region src/components/scroll-area/scroll-area.tsx
5
- function ScrollArea$1({ className, ...props }) {
6
- const { root } = scrollArea();
7
- return /* @__PURE__ */ jsx(ScrollArea.Root, {
8
- "data-slot": "scroll-area",
9
- className: root({ className }),
10
- ...props
11
- });
12
- }
13
- function ScrollAreaViewport({ className, ...props }) {
14
- const { viewport } = scrollArea();
15
- return /* @__PURE__ */ jsx(ScrollArea.Viewport, {
16
- "data-slot": "scroll-area-viewport",
17
- className: viewport({ className }),
18
- ...props
19
- });
20
- }
9
+ /**
10
+ * An optional box around the content, inside the container (the demo's
11
+ * `ScrollAreaResizableContent`): for padding, so the scrollbars stay at the
12
+ * edges, or a layout of the content's own. In a horizontal (or `both`) area
13
+ * it is as wide as its widest child. It is watched for size changes like any
14
+ * child of the container.
15
+ */
21
16
  function ScrollAreaContent({ className, ...props }) {
22
- const { content } = scrollArea();
23
- return /* @__PURE__ */ jsx(ScrollArea.Content, {
17
+ return /* @__PURE__ */ jsx("div", {
24
18
  "data-slot": "scroll-area-content",
25
- className: content({ className }),
26
- ...props
19
+ ...props,
20
+ className: cn("r-scroll-area__content", className)
27
21
  });
28
22
  }
29
- function ScrollAreaScrollbar({ className, orientation = "vertical", ...props }) {
30
- const { scrollbar } = scrollArea();
31
- return /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
32
- "data-slot": "scroll-area-scrollbar",
23
+ /**
24
+ * A scrolling box with thin overlay scrollbars that look the same on every
25
+ * platform and theme. Scrolling itself stays native (wheel, touch,
26
+ * keyboard); the scrollbars can also be dragged, and a press on a gutter
27
+ * pages.
28
+ *
29
+ * Modelled on 07akioni/react-scroll-area-demo: one element, the container
30
+ * (the element that scrolls), with the gutters (the scrollbars) as its first
31
+ * child, a zero-height sticky layer. `ref`, `className`, `style` and the
32
+ * native props are the container's; `render` puts another element in its
33
+ * place; `actionsRef` adds `scrollToElement` and `measure`. For an element of
34
+ * your own that can't take children through ScrollArea (a textarea,
35
+ * VirtualList's scroller), see `useScrollAreaContainer`.
36
+ */
37
+ function ScrollArea({ actionsRef, children, className, contentClassName, contentRef, contentStyle, fade = false, hideDelay, onEdgeChange, onScroll, orientation, overscroll, railInset, ref, render, size, style, tabIndex, trigger, ...props }) {
38
+ const fadeRef = useRef(null);
39
+ const { containerProps, guttersProps } = useScrollAreaContainer({
40
+ actionsRef,
41
+ containerRef: ref,
42
+ contentRef,
43
+ fade,
44
+ fadeRef,
45
+ hideDelay,
46
+ onEdgeChange,
33
47
  orientation,
34
- className: scrollbar({ className }),
35
- ...props
48
+ overscroll,
49
+ railInset,
50
+ size,
51
+ trigger
36
52
  });
37
- }
38
- function ScrollAreaThumb({ className, ...props }) {
39
- const { thumb } = scrollArea();
40
- return /* @__PURE__ */ jsx(ScrollArea.Thumb, {
41
- "data-slot": "scroll-area-thumb",
42
- className: thumb({ className }),
43
- ...props
44
- });
45
- }
46
- function ScrollAreaCorner({ className, ...props }) {
47
- const { corner } = scrollArea();
48
- return /* @__PURE__ */ jsx(ScrollArea.Corner, {
49
- "data-slot": "scroll-area-corner",
50
- className: corner({ className }),
51
- ...props
53
+ const ownStyle = useMemo(() => ({
54
+ ...containerProps.style,
55
+ ...style
56
+ }), [containerProps.style, style]);
57
+ const content = contentClassName !== void 0 || contentStyle !== void 0 || fade ? /* @__PURE__ */ jsx(ScrollAreaContent, {
58
+ className: contentClassName,
59
+ ref: fadeRef,
60
+ style: contentStyle,
61
+ children
62
+ }) : children;
63
+ return useRender({
64
+ defaultTagName: "div",
65
+ render,
66
+ props: mergeProps({ "data-slot": "scroll-area" }, props, {
67
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps }), content] }),
68
+ className: cn(containerProps.className, "r-scroll-area__viewport", className),
69
+ onScroll,
70
+ ref: containerProps.ref,
71
+ style: ownStyle,
72
+ tabIndex: tabIndex ?? containerProps.tabIndex
73
+ })
52
74
  });
53
75
  }
54
76
  //#endregion
55
- export { ScrollArea$1 as ScrollArea, ScrollAreaContent, ScrollAreaCorner, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport };
77
+ export { ScrollArea, ScrollAreaContent };
@@ -0,0 +1,58 @@
1
+ //#region src/components/scroll-area/scroll-geometry.ts
2
+ /**
3
+ * Where the thumb of one axis goes, or `null` when that axis has nothing to
4
+ * scroll. One pixel of overflow is ignored: it is usually rounding.
5
+ */
6
+ function thumbPlacement(metrics, axis) {
7
+ const client = axis === "y" ? metrics.clientHeight : metrics.clientWidth;
8
+ const scroll = axis === "y" ? metrics.scrollHeight : metrics.scrollWidth;
9
+ const offset = axis === "y" ? metrics.scrollTop : metrics.scrollLeft;
10
+ const overflow = scroll - client;
11
+ if (client <= 0 || overflow <= 1) return null;
12
+ return {
13
+ progress: Math.min(1, Math.max(0, offset / overflow)),
14
+ ratio: client / scroll
15
+ };
16
+ }
17
+ /** The thumb's length in px: its share of the rail, at least `min`, and never longer than the rail. */
18
+ function thumbLength(ratio, rail, min) {
19
+ return Math.max(0, Math.min(rail, Math.max(min, ratio * rail)));
20
+ }
21
+ /** How far down (or along) its rail a thumb of `length` px starts, in px. */
22
+ function thumbOffset(progress, rail, length) {
23
+ return Math.max(0, rail - length) * progress;
24
+ }
25
+ /** The px left past each edge of the axes that have a thumb (`thumbPlacement` isn't `null`). */
26
+ function scrollOverflow(metrics, axes) {
27
+ const [yStart, yEnd] = axes.y ? remaining(metrics.scrollTop, metrics.scrollHeight - metrics.clientHeight) : [0, 0];
28
+ const [xStart, xEnd] = axes.x ? remaining(metrics.scrollLeft, metrics.scrollWidth - metrics.clientWidth) : [0, 0];
29
+ return {
30
+ xEnd,
31
+ xStart,
32
+ yEnd,
33
+ yStart
34
+ };
35
+ }
36
+ function remaining(offset, overflow) {
37
+ const start = Math.min(overflow, Math.max(0, offset));
38
+ return [start, overflow - start];
39
+ }
40
+ /** An edge has more content past it once at least a pixel is left; less is rounding. */
41
+ function overflows(remainingPx) {
42
+ return remainingPx >= 1;
43
+ }
44
+ /**
45
+ * How far a click on the rail pages: most of a viewport, keeping a strip of
46
+ * the previous page in view, as browsers do for Page Down.
47
+ */
48
+ function pageDistance(client) {
49
+ return Math.round(client * .875);
50
+ }
51
+ /** -1 when the pointer is before the thumb, 1 after it, 0 on it. */
52
+ function pageDirection(pointer, thumbStart, thumbEnd) {
53
+ if (pointer < thumbStart) return -1;
54
+ if (pointer > thumbEnd) return 1;
55
+ return 0;
56
+ }
57
+ //#endregion
58
+ export { overflows, pageDirection, pageDistance, scrollOverflow, thumbLength, thumbOffset, thumbPlacement };
@@ -0,0 +1,67 @@
1
+ //#region src/components/scroll-area/scroll-to-element.ts
2
+ /**
3
+ * The scroll offset on one axis that brings an element into view, clamped to
4
+ * what the container can scroll. `nearest` doesn't move an element that is
5
+ * already in view (inside the `offset` margins), and otherwise lines it up
6
+ * with the closer edge. Works on physical coordinates, so it holds for a
7
+ * negative `scrollLeft` in right-to-left layouts too.
8
+ */
9
+ function alignedScrollOffset({ align, itemSize, itemStart, max, min, offset, scroll, viewSize, viewStart }) {
10
+ const position = scroll + itemStart - viewStart;
11
+ const atStart = position - offset;
12
+ const atEnd = position + itemSize - viewSize + offset;
13
+ let target;
14
+ if (align === "start") target = atStart;
15
+ else if (align === "end") target = atEnd;
16
+ else if (align === "center") target = position - (viewSize - itemSize) / 2;
17
+ else if (atStart < scroll) target = atStart;
18
+ else if (atEnd > scroll) target = Math.min(atEnd, atStart);
19
+ else target = scroll;
20
+ return Math.min(max, Math.max(min, target));
21
+ }
22
+ /**
23
+ * Scrolls `container`, and nothing around it, so that `element` is in view.
24
+ * `block` and `offset` apply to the axis the area scrolls in: vertical,
25
+ * unless the area is horizontal only (where `start` is the inline start, so
26
+ * the right edge in right-to-left layouts). With `orientation="both"` the
27
+ * horizontal axis just brings the element into view.
28
+ */
29
+ function scrollToElement(container, element, orientation, { behavior, block = "start", offset = 0 } = {}) {
30
+ const view = container.getBoundingClientRect();
31
+ const item = element.getBoundingClientRect();
32
+ const target = {};
33
+ if (behavior) target.behavior = behavior;
34
+ if (orientation !== "horizontal") target.top = alignedScrollOffset({
35
+ align: block,
36
+ itemSize: item.height,
37
+ itemStart: item.top,
38
+ max: container.scrollHeight - container.clientHeight,
39
+ min: 0,
40
+ offset,
41
+ scroll: container.scrollTop,
42
+ viewSize: container.clientHeight,
43
+ viewStart: view.top + container.clientTop
44
+ });
45
+ if (orientation !== "vertical") {
46
+ const rtl = getComputedStyle(container).direction === "rtl";
47
+ const max = container.scrollWidth - container.clientWidth;
48
+ const inline = orientation === "horizontal";
49
+ let align = inline ? block : "nearest";
50
+ if (rtl && align === "start") align = "end";
51
+ else if (rtl && align === "end") align = "start";
52
+ target.left = alignedScrollOffset({
53
+ align,
54
+ itemSize: item.width,
55
+ itemStart: item.left,
56
+ max: rtl ? 0 : max,
57
+ min: rtl ? -max : 0,
58
+ offset: inline ? offset : 0,
59
+ scroll: container.scrollLeft,
60
+ viewSize: container.clientWidth,
61
+ viewStart: view.left + container.clientLeft
62
+ });
63
+ }
64
+ container.scrollTo(target);
65
+ }
66
+ //#endregion
67
+ export { alignedScrollOffset, scrollToElement };
@@ -0,0 +1,224 @@
1
+ import { CSSProperties, ComponentPropsWithRef, ReactNode, Ref, RefCallback, RefObject, UIEventHandler } from "react";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ //#region src/components/scroll-area/types.d.ts
4
+ /**
5
+ * `hover` shows the scrollbars while the pointer is over the area or it
6
+ * scrolls; `scroll` only while it scrolls; `none` always shows them.
7
+ */
8
+ export type ScrollAreaTrigger = "hover" | "scroll" | "none";
9
+ export type ScrollAreaSize = "small" | "medium";
10
+ /** The directions the area scrolls in, each with its own scrollbar. */
11
+ export type ScrollAreaOrientation = "vertical" | "horizontal" | "both";
12
+ /**
13
+ * What a scroll does at the end of the area: `none` stops there, with no bounce; `contain` stops
14
+ * there but lets Mac trackpads and iOS bounce the area; `auto` carries on to the page behind.
15
+ */
16
+ export type ScrollAreaOverscroll = "none" | "contain" | "auto";
17
+ /** Which edges the area is scrolled to: `true` when there is nothing more past that edge. */
18
+ export type ScrollAreaEdges = {
19
+ top: boolean;
20
+ bottom: boolean;
21
+ left: boolean;
22
+ right: boolean;
23
+ };
24
+ /**
25
+ * Room in px the scrollbars keep clear at each end, such as for a sticky
26
+ * header or footer inside the area. `top` and `bottom` shorten the vertical
27
+ * scrollbar, `left` and `right` the horizontal one.
28
+ */
29
+ export type ScrollAreaRailInset = {
30
+ top?: number;
31
+ bottom?: number;
32
+ left?: number;
33
+ right?: number;
34
+ };
35
+ export type ScrollAreaScrollToElementOptions = {
36
+ /** Where the element lines up, as in `scrollIntoView`. */
37
+ block?: "start" | "center" | "end" | "nearest";
38
+ /** Room in px kept between the element and that edge, such as a sticky header's height. `center` ignores it. */
39
+ offset?: number;
40
+ behavior?: ScrollBehavior;
41
+ };
42
+ /** Methods on `actionsRef`. */
43
+ export type ScrollAreaActions = {
44
+ /** Scrolls the container, and only the container (never the page), so that `element` inside it is in view. */
45
+ scrollToElement: (element: Element, options?: ScrollAreaScrollToElementOptions) => void;
46
+ /** Measures the container again now, for a size change nothing observed reports. */
47
+ measure: () => void;
48
+ };
49
+ /** How the scrollbars look and behave: set on `ScrollArea`, or passed to `useScrollAreaContainer`. */
50
+ export type ScrollAreaOptions = {
51
+ /** Methods to scroll from code that the container doesn't have natively. */
52
+ actionsRef?: Ref<ScrollAreaActions>;
53
+ /** Fade the content out toward an edge that has more past it. The scrollbars don't fade. */
54
+ fade?: boolean;
55
+ /** How long the scrollbars stay after scrolling stops, in ms. At least 100 with `trigger="scroll"`. */
56
+ hideDelay?: number;
57
+ /** The directions the area scrolls in. Content too wide for a vertical area is cut off. */
58
+ orientation?: ScrollAreaOrientation;
59
+ /**
60
+ * What a scroll that reaches the end does: stop there with no bounce (`none`, the default), stop
61
+ * there but let Mac trackpads and iOS bounce the area (`contain`), or carry on to the page behind
62
+ * (`auto`, which leaves the container's own `overscroll-behavior` alone). Only in the directions
63
+ * the area overflows right now.
64
+ */
65
+ overscroll?: ScrollAreaOverscroll;
66
+ /**
67
+ * Room in px the scrollbars keep clear at each end, such as a sticky header's height, so
68
+ * they don't run over it. Also settable in CSS, on the root:
69
+ * `--r-scroll-area-rail-inset-{top,bottom,left,right}`, which don't inherit.
70
+ */
71
+ railInset?: ScrollAreaRailInset;
72
+ /** Thumb thickness: `small` is 5px, `medium` 7px; both grow while pointed at or dragged. */
73
+ size?: ScrollAreaSize;
74
+ /** When the scrollbars show. They always show while scrolling or dragging. */
75
+ trigger?: ScrollAreaTrigger;
76
+ /** Called when the area reaches or leaves an edge, and after mounting when the content overflows. */
77
+ onEdgeChange?: (edges: ScrollAreaEdges) => void;
78
+ };
79
+ /**
80
+ * Where the gutters (the scrollbars) go. `sticky`: inside the container, as its first child, a
81
+ * zero-height layer stuck to the top of the viewport; the container is the root, with no frame
82
+ * around it. `absolute`: beside the container, a layer laid over a positioned frame (the root)
83
+ * that holds both; for a container that can't hold children (a textarea).
84
+ */
85
+ export type ScrollAreaGuttersPosition = "sticky" | "absolute";
86
+ /** What `useScrollAreaContainer` takes: ScrollArea's options, where the gutters go, and your refs. */
87
+ export type ScrollAreaContainerOptions<E extends HTMLElement = HTMLElement> = ScrollAreaOptions & {
88
+ /** Your ref to the element that scrolls: `containerProps.ref` forwards to it. */
89
+ containerRef?: Ref<E> | undefined;
90
+ /** With absolute gutters, your ref to the frame: `frameProps.ref` forwards to it. */
91
+ frameRef?: Ref<HTMLDivElement> | undefined;
92
+ /** Where the gutters go: inside the container (`sticky`, the default) or beside it in a frame (`absolute`). */
93
+ guttersPosition?: ScrollAreaGuttersPosition;
94
+ /**
95
+ * An element whose size is the content's, watched for size changes (such as a hidden mirror of a
96
+ * textarea's text). Without it each element child of the container is watched, and children
97
+ * added or removed.
98
+ */
99
+ contentRef?: RefObject<HTMLElement | null>;
100
+ /**
101
+ * With `fade` and sticky gutters, the element the fade masks: a content element inside the
102
+ * container (`ScrollAreaContent`), since the mask can't sit on a container that holds the
103
+ * gutters. With absolute gutters the container itself is masked and this isn't read.
104
+ */
105
+ fadeRef?: RefObject<HTMLElement | null>;
106
+ };
107
+ /**
108
+ * What to spread on the element that scrolls (the container), merged with your own `className`
109
+ * and `style`, so the area measures it and its gutters follow it.
110
+ */
111
+ export type ScrollAreaContainerProps<E extends HTMLElement = HTMLElement> = {
112
+ /**
113
+ * Put it on the element that scrolls (it fits any element's `ref` and VirtualList's `domRef`).
114
+ * It forwards to the `containerRef` you pass, so there is nothing to merge.
115
+ */
116
+ ref: RefCallback<E>;
117
+ /**
118
+ * Before your own classes. With sticky gutters the container is the root, so these are the
119
+ * root's classes and modifiers; with absolute gutters, `r-scroll-area__container`.
120
+ */
121
+ className: string;
122
+ /** With sticky gutters, the root's variables (`railInset`, the timelines); merge your style after it. */
123
+ style: CSSProperties | undefined;
124
+ /** `0` while there is something to scroll, so the container is a tab stop; else `undefined`. */
125
+ tabIndex: 0 | undefined;
126
+ };
127
+ /** With absolute gutters, what to spread on the positioned frame (the root) around the container and the gutters. */
128
+ export type ScrollAreaFrameProps = {
129
+ /** It forwards to the `frameRef` you pass. */
130
+ ref: RefCallback<HTMLDivElement>;
131
+ /** The root's classes and modifiers, before your own (size and frame the area there). */
132
+ className: string;
133
+ /** The root's variables (`railInset`, the timelines); merge your style after it. */
134
+ style: CSSProperties;
135
+ };
136
+ declare const guttersBinding: unique symbol;
137
+ /**
138
+ * How `<ScrollAreaGutters />` is wired to its container: opaque, made only by
139
+ * `useScrollAreaContainer`, so how the gutters are wired can change without breaking a caller.
140
+ */
141
+ export type ScrollAreaGuttersBinding = {
142
+ readonly [guttersBinding]: true;
143
+ };
144
+ /** What `<ScrollAreaGutters />` takes: spread `useScrollAreaContainer`'s `guttersProps`, never built by hand. */
145
+ export type ScrollAreaGuttersProps = {
146
+ /** The binding to its container, from `useScrollAreaContainer`. */
147
+ binding: ScrollAreaGuttersBinding;
148
+ };
149
+ /**
150
+ * What `useScrollAreaContainer` returns: props to spread on the elements you render (the
151
+ * container, the frame with absolute gutters, `<ScrollAreaGutters />`), and the area's methods.
152
+ */
153
+ export type ScrollAreaContainer<E extends HTMLElement = HTMLElement> = {
154
+ /** Spread on the element that scrolls. */
155
+ containerProps: ScrollAreaContainerProps<E>;
156
+ /** With absolute gutters, spread on the frame around the container and the gutters; `undefined` with sticky gutters. */
157
+ frameProps: ScrollAreaFrameProps | undefined;
158
+ /** Spread on `<ScrollAreaGutters />`. */
159
+ guttersProps: ScrollAreaGuttersProps;
160
+ /** Where to put the gutters: the container's first child (`sticky`) or right after it (`absolute`). */
161
+ guttersPosition: ScrollAreaGuttersPosition;
162
+ /** Measures again now, for a size change nothing observed reports (a textarea's input). */
163
+ measure: () => void;
164
+ /** Scrolls the container, and only the container, so that `element` inside it is in view. */
165
+ scrollToElement: (element: Element, options?: ScrollAreaScrollToElementOptions) => void;
166
+ /** Whether either axis has something to scroll now. */
167
+ scrollable: boolean;
168
+ };
169
+ export type ScrollAreaOwnProps = ScrollAreaOptions & {
170
+ /** The scrolling content, in ScrollArea's own container. */
171
+ children?: ReactNode;
172
+ /**
173
+ * An element whose size is the content's, watched for size changes (such as a hidden mirror of a
174
+ * textarea's text). Without it each element child of the container is watched, and children
175
+ * added or removed.
176
+ */
177
+ contentRef?: RefObject<HTMLElement | null>;
178
+ /** Class of the area, the element that scrolls: size and frame it here. */
179
+ className?: string;
180
+ /** Style of the area. */
181
+ style?: CSSProperties;
182
+ /**
183
+ * Class of a content element around `children`, inside the container: put padding and the
184
+ * content's own layout (grid, flex, `gap`, `stack-y-*`) here, so the scrollbars stay at the edges
185
+ * and the gutters layer is never one of the laid-out children. Only with it, `contentStyle` or
186
+ * `fade` is there one.
187
+ */
188
+ contentClassName?: string;
189
+ /** Style of the content element; renders it, as `contentClassName` does. */
190
+ contentStyle?: CSSProperties;
191
+ /** The container's scroll events. */
192
+ onScroll?: UIEventHandler<HTMLDivElement>;
193
+ /** The container's: by default `0` while there is something to scroll. */
194
+ tabIndex?: number;
195
+ /**
196
+ * Another element in place of ScrollArea's own `<div>`, as the container (an element or a
197
+ * function that gets the props). It must hold children: the gutters are its first child.
198
+ */
199
+ render?: useRender.RenderProp | undefined;
200
+ };
201
+ /**
202
+ * `ref`, `className`, `style` and the native props go to the area: one element, the container,
203
+ * the element that scrolls, with the scrollbars inside it (sticky gutters).
204
+ */
205
+ export type ScrollAreaProps = ScrollAreaOwnProps & Omit<ComponentPropsWithRef<"div">, keyof ScrollAreaOwnProps>;
206
+ /** An optional box around the content, inside the container: native div props and `ref`. */
207
+ export type ScrollAreaContentProps = ComponentPropsWithRef<"div">;
208
+ export type ScrollAreaTextareaOwnProps = {
209
+ /** How long the scrollbar stays after scrolling stops, as on ScrollArea. */
210
+ hideDelay?: number;
211
+ /** Whether a scroll that reaches the end carries on to the page, as on ScrollArea. */
212
+ overscroll?: ScrollAreaOverscroll;
213
+ /** Thumb thickness, as on ScrollArea. */
214
+ size?: ScrollAreaSize;
215
+ /** When the scrollbar shows, as on ScrollArea. */
216
+ trigger?: ScrollAreaTrigger;
217
+ /** Class of the root that holds the textarea and its scrollbar. */
218
+ wrapperClassName?: string;
219
+ /** Style of the root; size the textarea itself with `className` / `style`. */
220
+ wrapperStyle?: CSSProperties;
221
+ };
222
+ /** Native textarea props; `ref`, `className`, `style` and `onScroll` go to the textarea. */
223
+ export type ScrollAreaTextareaProps = ScrollAreaTextareaOwnProps & Omit<ComponentPropsWithRef<"textarea">, keyof ScrollAreaTextareaOwnProps>;
224
+ //#endregion
@@ -0,0 +1,93 @@
1
+ import { pageDirection, pageDistance } from "./scroll-geometry.mjs";
2
+ import { useLayoutEffect, useRef, useState } from "react";
3
+ //#region src/components/scroll-area/use-rail-pointer.ts
4
+ const reducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
5
+ /**
6
+ * Mouse and pen input on one scrollbar: dragging the thumb scrolls the
7
+ * container in proportion, a press on the rail beside the thumb pages
8
+ * toward it. Wheel, touch and keyboard scrolling stay native.
9
+ *
10
+ * `scrollLeft` is negative in right-to-left layouts, and both a drag delta
11
+ * and a page step are physical (left is negative), so neither needs a
12
+ * direction check.
13
+ */
14
+ function useRailPointer({ axis, containerRef, onDraggingChange }) {
15
+ const drag = useRef(null);
16
+ const [dragging, setDragging] = useState(false);
17
+ const changeDragging = (next) => {
18
+ setDragging(next);
19
+ onDraggingChange(next);
20
+ };
21
+ const notifyRef = useRef(onDraggingChange);
22
+ useLayoutEffect(() => {
23
+ notifyRef.current = onDraggingChange;
24
+ });
25
+ useLayoutEffect(() => () => {
26
+ const current = drag.current;
27
+ if (!current) return;
28
+ drag.current = null;
29
+ try {
30
+ if (current.thumb.hasPointerCapture(current.pointerId)) current.thumb.releasePointerCapture(current.pointerId);
31
+ } catch {}
32
+ notifyRef.current(false);
33
+ }, []);
34
+ const vertical = axis === "y";
35
+ const along = (event) => vertical ? event.clientY : event.clientX;
36
+ const onRailPointerDown = (event) => {
37
+ const container = containerRef.current;
38
+ const thumb = event.currentTarget.firstElementChild;
39
+ if (!container || !thumb || event.button !== 0 || event.pointerType === "touch") return;
40
+ event.preventDefault();
41
+ const rect = thumb.getBoundingClientRect();
42
+ const distance = (vertical ? pageDirection(event.clientY, rect.top, rect.bottom) : pageDirection(event.clientX, rect.left, rect.right)) * pageDistance(vertical ? container.clientHeight : container.clientWidth);
43
+ container.scrollBy({
44
+ behavior: reducedMotion() ? "instant" : "smooth",
45
+ [vertical ? "top" : "left"]: distance
46
+ });
47
+ };
48
+ const onThumbPointerDown = (event) => {
49
+ const container = containerRef.current;
50
+ const thumb = event.currentTarget;
51
+ const rail = thumb.parentElement;
52
+ if (!container || !rail || event.button !== 0) return;
53
+ event.preventDefault();
54
+ event.stopPropagation();
55
+ const travel = vertical ? rail.clientHeight - thumb.offsetHeight : rail.clientWidth - thumb.offsetWidth;
56
+ const overflow = vertical ? container.scrollHeight - container.clientHeight : container.scrollWidth - container.clientWidth;
57
+ drag.current = {
58
+ pointerId: event.pointerId,
59
+ thumb,
60
+ scale: travel > 0 ? overflow / travel : 0,
61
+ scrollStart: vertical ? container.scrollTop : container.scrollLeft,
62
+ start: along(event)
63
+ };
64
+ thumb.setPointerCapture(event.pointerId);
65
+ changeDragging(true);
66
+ };
67
+ const onThumbPointerMove = (event) => {
68
+ const container = containerRef.current;
69
+ const current = drag.current;
70
+ if (!container || current?.pointerId !== event.pointerId) return;
71
+ const scroll = current.scrollStart + (along(event) - current.start) * current.scale;
72
+ if (vertical) container.scrollTop = scroll;
73
+ else container.scrollLeft = scroll;
74
+ };
75
+ const endDrag = (event) => {
76
+ if (drag.current?.pointerId !== event.pointerId) return;
77
+ drag.current = null;
78
+ changeDragging(false);
79
+ };
80
+ return {
81
+ dragging,
82
+ railHandlers: { onPointerDown: onRailPointerDown },
83
+ thumbHandlers: {
84
+ onLostPointerCapture: endDrag,
85
+ onPointerCancel: endDrag,
86
+ onPointerDown: onThumbPointerDown,
87
+ onPointerMove: onThumbPointerMove,
88
+ onPointerUp: endDrag
89
+ }
90
+ };
91
+ }
92
+ //#endregion
93
+ export { useRailPointer };
@@ -0,0 +1,26 @@
1
+ import { ScrollAreaContainer, ScrollAreaContainerOptions, ScrollAreaFrameProps } from "./types.mjs";
2
+ //#region src/components/scroll-area/use-scroll-area-container.d.ts
3
+ /**
4
+ * ScrollArea's scrollbars for an element you render yourself: the demo's `useContainer`, as a
5
+ * hook in your own component. It measures the element that scrolls (the container), listens to
6
+ * its scroll, and keeps the state; you render the elements.
7
+ *
8
+ * Spread `containerProps` on the element that scrolls (a `<ul>`, a `<textarea>`, VirtualList's
9
+ * scroller through `domRef`), merging your own `className` and `style` with them (pass your own
10
+ * refs as `containerRef` and `frameRef`: the returned `ref`s already forward to them), and render
11
+ * `<ScrollAreaGutters {...guttersProps} />` where `guttersPosition` says:
12
+ *
13
+ * - `sticky` (the default): as the container's first child. The container is the root and
14
+ * carries its classes; `frameProps` is `undefined`.
15
+ * - `absolute`: right after the container, both inside a frame that gets `frameProps` (the
16
+ * root, positioned, sized and bordered by you).
17
+ *
18
+ * `<ScrollArea>` is this hook with a `<div>` of its own.
19
+ */
20
+ export declare function useScrollAreaContainer<E extends HTMLElement = HTMLElement>(options: ScrollAreaContainerOptions<E> & {
21
+ guttersPosition: "absolute";
22
+ }): ScrollAreaContainer<E> & {
23
+ frameProps: ScrollAreaFrameProps;
24
+ };
25
+ export declare function useScrollAreaContainer<E extends HTMLElement = HTMLElement>(options?: ScrollAreaContainerOptions<E>): ScrollAreaContainer<E>;
26
+ //#endregion