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