@botiverse/raft-ui 0.74.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/components/description-list/description-list.d.mts +1 -1
- package/dist/components/description-list/description-list.mjs +15 -5
- package/dist/components/message-action-card/message-action-card-content.recipe.mjs +4 -4
- package/dist/components/message-action-card/message-action-card.d.mts +1 -1
- package/dist/components/message-action-card/message-action-card.mjs +13 -3
- package/dist/components/scroll-area/scroll-area.css +51 -15
- package/dist/components/scroll-area/use-scrollbars.mjs +21 -8
- 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/dist/fonts.css +2 -2
- package/dist/lib/label-value-fit.mjs +117 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## [0.76.0](https://github.com/botiverse/raft-design/compare/v0.75.0...v0.76.0) (2026-11-08)
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **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))
|
|
6
|
+
- 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).
|
|
7
|
+
- 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.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/virtual-list/
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **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))
|
|
13
|
+
- 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).
|
|
14
|
+
- 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.
|
|
15
|
+
- Docs: https://raft-design.botiverse.build/components/scroll-area/
|
|
16
|
+
|
|
17
|
+
## [0.75.0](https://github.com/botiverse/raft-design/compare/v0.74.0...v0.75.0) (2026-11-08)
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **A horizontal DescriptionList puts its labels above their values only when a value doesn't fit**: the list measures its labels and values and keeps each label beside its value while every value fits on one line there; once one doesn't (but would on a line of its own), or the value column would be narrower than 160px, the whole list stacks. It no longer stacks below a fixed 24rem (384px), and is no longer a size container. MessageActionCard's properties follow the same rule. ([#1065](https://github.com/botiverse/raft-design/pull/1065))
|
|
22
|
+
- Why: an agent's Role, Computer and Model stacked in a 370px profile though they fit beside their labels; the switch should follow the content, not a width picked for the longest label (Artea, #triage:428e3ce6).
|
|
23
|
+
- Affects: short facts in panels from about 300px to 384px, and on a phone, now stand beside their labels. A list whose values are wide still stacks, at the width they stop fitting.
|
|
24
|
+
- Docs: https://raft-design.botiverse.build/components/description-list/#labels-go-above-only-when-a-value-doesnt-fit
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Chinese text on Linux uses Noto Sans CJK**: the Simplified Chinese fallback lists `Noto Sans CJK SC`, the name Linux distributions install it under, after `Noto Sans SC`, as the Traditional Chinese list already did with `Noto Sans CJK TC`. ([#1063](https://github.com/botiverse/raft-design/pull/1063))
|
|
29
|
+
- Why: on a Linux machine with only the distribution's CJK font, Chinese and its full-width punctuation fell to the system's last-resort face, whose colon sits centered with space around it (Remy, #proj-raft-ui:0c3f4e06; seen in the read line's screenshots from CI).
|
|
30
|
+
- Affects: Linux desktops and CI screenshots; Mac and Windows already matched PingFang SC or Microsoft YaHei first.
|
|
31
|
+
- Docs: https://raft-design.botiverse.build/foundations/typography/
|
|
32
|
+
|
|
1
33
|
## [0.74.0](https://github.com/botiverse/raft-design/compare/v0.73.0...v0.74.0) (2026-11-08)
|
|
2
34
|
|
|
3
35
|
### Added
|
|
@@ -49,7 +49,7 @@ declare const descriptionList: import("tailwind-variants/lite").TVReturnType<{
|
|
|
49
49
|
}>>;
|
|
50
50
|
export type DescriptionListDirection = NonNullable<VariantProps<typeof descriptionList>["direction"]>;
|
|
51
51
|
export type DescriptionListProps = useRender.ComponentProps<"dl"> & Pick<VariantProps<typeof descriptionList>, "direction">;
|
|
52
|
-
export declare function DescriptionList({ direction, className, render, ...props }: DescriptionListProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
52
|
+
export declare function DescriptionList({ direction, className, render, ref, ...props }: DescriptionListProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
53
53
|
export type DescriptionItemProps = useRender.ComponentProps<"div">;
|
|
54
54
|
export declare function DescriptionItem({ className, render, ...props }: DescriptionItemProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
55
55
|
export type DescriptionTermProps = useRender.ComponentProps<"dt">;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
|
+
import { useLabelValueFit } from "../../lib/label-value-fit.mjs";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
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/description-list/description-list.tsx
|
|
@@ -21,10 +23,10 @@ const descriptionList = tv({
|
|
|
21
23
|
variants: { direction: {
|
|
22
24
|
vertical: {},
|
|
23
25
|
horizontal: { root: [
|
|
24
|
-
"
|
|
25
|
-
"[
|
|
26
|
-
"[
|
|
27
|
-
"[
|
|
26
|
+
"w-full grid items-baseline",
|
|
27
|
+
"[&[data-stacked]>[data-slot=description-item]>*]:col-span-full",
|
|
28
|
+
"[&[data-stacked]>[data-slot=description-item]>[data-slot=description-details]]:min-h-0",
|
|
29
|
+
"[&[data-stacked]>[data-slot=description-item]+[data-slot=description-item]>[data-slot=description-term]]:mt-2",
|
|
28
30
|
"*:data-[slot=description-item]:contents",
|
|
29
31
|
"[&>[data-slot=description-item]>[data-slot=description-term]]:mb-0",
|
|
30
32
|
"[&>[data-slot=description-item]>[data-slot=description-term]]:max-w-40",
|
|
@@ -52,12 +54,20 @@ const descriptionList = tv({
|
|
|
52
54
|
} },
|
|
53
55
|
defaultVariants: { direction: "vertical" }
|
|
54
56
|
});
|
|
55
|
-
|
|
57
|
+
const listParts = {
|
|
58
|
+
label: ":scope > [data-slot=description-item] > [data-slot=description-term]",
|
|
59
|
+
value: ":scope > [data-slot=description-item] > [data-slot=description-details]"
|
|
60
|
+
};
|
|
61
|
+
function DescriptionList({ direction = "vertical", className, render, ref, ...props }) {
|
|
62
|
+
const [list, setList] = useState(null);
|
|
63
|
+
const listRef = useCallback((element) => setList(element), []);
|
|
64
|
+
useLabelValueFit(list, direction === "horizontal", listParts);
|
|
56
65
|
const { root } = descriptionList({ direction });
|
|
57
66
|
const defaultProps = { className: root({ className }) };
|
|
58
67
|
return useRender({
|
|
59
68
|
defaultTagName: "dl",
|
|
60
69
|
render,
|
|
70
|
+
ref: ref ? [listRef, ref] : listRef,
|
|
61
71
|
props: mergeProps(defaultProps, props),
|
|
62
72
|
state: { slot: "description-list" }
|
|
63
73
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
//#region src/components/message-action-card/message-action-card-content.recipe.ts
|
|
3
3
|
const messageActionCardContent = tv({ slots: {
|
|
4
|
-
root: ["p-3", "
|
|
4
|
+
root: ["p-3", "flex flex-col gap-2 px-3 pt-0 pb-3"],
|
|
5
5
|
panel: [
|
|
6
6
|
"h-(--collapsible-panel-height) overflow-hidden",
|
|
7
7
|
"transition-[height] duration-200 ease-out motion-reduce:transition-none",
|
|
@@ -9,19 +9,19 @@ const messageActionCardContent = tv({ slots: {
|
|
|
9
9
|
],
|
|
10
10
|
properties: [
|
|
11
11
|
"m-0 grid",
|
|
12
|
-
"
|
|
12
|
+
"data-stacked:grid-cols-1",
|
|
13
13
|
"grid-cols-[minmax(88px,max-content)_minmax(0,1fr)] items-baseline gap-x-4 gap-y-0.5"
|
|
14
14
|
],
|
|
15
15
|
property: ["contents"],
|
|
16
16
|
propertyLabel: [
|
|
17
17
|
"max-w-40 min-w-0 wrap-break-word",
|
|
18
18
|
"text-footnote font-medium text-foreground-hint",
|
|
19
|
-
"[[data-slot=message-action-card-property]+[data-slot=message-action-card-property]>&]
|
|
19
|
+
"[[data-stacked]>[data-slot=message-action-card-property]+[data-slot=message-action-card-property]>&]:mt-2"
|
|
20
20
|
],
|
|
21
21
|
propertyValue: [
|
|
22
22
|
"m-0 min-w-0 wrap-break-word",
|
|
23
23
|
"min-h-8 py-1.5 text-callout text-foreground",
|
|
24
|
-
"
|
|
24
|
+
"[[data-stacked]>[data-slot=message-action-card-property]>&]:min-h-0 [[data-stacked]>[data-slot=message-action-card-property]>&]:py-0"
|
|
25
25
|
],
|
|
26
26
|
scopeList: ["mt-0 stack-y-1.5"],
|
|
27
27
|
scopeListViewport: ["max-h-[136px]"],
|
|
@@ -32,7 +32,7 @@ export declare function MessageActionCardStatus({ className, appearance, ...prop
|
|
|
32
32
|
export declare function MessageActionCardCollapseTrigger({ className, ...props }: MessageActionCardCollapseTriggerProps): import("react").JSX.Element;
|
|
33
33
|
export declare function MessageActionCardHeader({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").JSX.Element;
|
|
34
34
|
export declare function MessageActionCardHeaderStatus({ className, render, ...props }: useRender.ComponentProps<"span">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
35
|
-
export declare function MessageActionCardProperties({ className, render, ...props }: useRender.ComponentProps<"dl">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
35
|
+
export declare function MessageActionCardProperties({ className, render, ref, ...props }: useRender.ComponentProps<"dl">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
36
36
|
export declare function MessageActionCardProperty({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
37
37
|
export declare function MessageActionCardPropertyLabel({ className, render, ...props }: useRender.ComponentProps<"dt">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
38
38
|
export declare function MessageActionCardPropertyValue({ className, render, ...props }: useRender.ComponentProps<"dd">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ScrollArea } from "../scroll-area/scroll-area.mjs";
|
|
2
2
|
import { Badge } from "../badge/badge.mjs";
|
|
3
|
+
import { useLabelValueFit } from "../../lib/label-value-fit.mjs";
|
|
3
4
|
import { MessageEmbed, MessageEmbedContent, MessageEmbedHeader } from "../message-embed/message-embed.mjs";
|
|
4
5
|
import { messageActionCard } from "./message-action-card.recipe.mjs";
|
|
5
6
|
import { messageActionCardContent } from "./message-action-card-content.recipe.mjs";
|
|
6
7
|
import { ChevronDown, Info, Zap } from "lucide-react";
|
|
7
|
-
import {
|
|
8
|
+
import { useCallback, useState } from "react";
|
|
9
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
10
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
9
11
|
import { useRender } from "@base-ui/react/use-render";
|
|
10
12
|
import { Collapsible } from "@base-ui/react/collapsible";
|
|
@@ -127,11 +129,19 @@ function MessageActionCardHeaderStatus({ className, render, ...props }) {
|
|
|
127
129
|
state: { slot: "message-action-card-header-status" }
|
|
128
130
|
});
|
|
129
131
|
}
|
|
130
|
-
|
|
132
|
+
const propertyParts = {
|
|
133
|
+
label: ":scope > [data-slot=message-action-card-property] > [data-slot=message-action-card-property-label]",
|
|
134
|
+
value: ":scope > [data-slot=message-action-card-property] > [data-slot=message-action-card-property-value]"
|
|
135
|
+
};
|
|
136
|
+
function MessageActionCardProperties({ className, render, ref, ...props }) {
|
|
137
|
+
const [list, setList] = useState(null);
|
|
138
|
+
const listRef = useCallback((element) => setList(element), []);
|
|
139
|
+
useLabelValueFit(list, true, propertyParts);
|
|
131
140
|
const { properties } = messageActionCardContent();
|
|
132
141
|
return useRender({
|
|
133
142
|
defaultTagName: "dl",
|
|
134
143
|
render,
|
|
144
|
+
ref: ref ? [listRef, ref] : listRef,
|
|
135
145
|
props: mergeProps({ className: properties({ className }) }, props),
|
|
136
146
|
state: { slot: "message-action-card-properties" }
|
|
137
147
|
});
|
|
@@ -249,7 +259,7 @@ function MessageActionCardNotice({ className, render, icon, children, ...props }
|
|
|
249
259
|
render,
|
|
250
260
|
props: mergeProps({
|
|
251
261
|
className: notice({ className }),
|
|
252
|
-
children: /* @__PURE__ */ jsxs(Fragment, { children: [icon ?? /* @__PURE__ */ jsx(Info, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", { children })] })
|
|
262
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [icon ?? /* @__PURE__ */ jsx(Info, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", { children })] })
|
|
253
263
|
}, props),
|
|
254
264
|
state: { slot: "message-action-card-notice" }
|
|
255
265
|
});
|
|
@@ -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;
|
|
@@ -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
|
package/dist/fonts.css
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
:where(:root) {
|
|
12
12
|
--heading-font:
|
|
13
13
|
"Space Grotesk", Arial, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
14
|
-
"Noto Sans SC", system-ui, sans-serif;
|
|
14
|
+
"Noto Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;
|
|
15
15
|
--sans-font:
|
|
16
16
|
"Space Grotesk", Arial, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
17
|
-
"Noto Sans SC", system-ui, sans-serif;
|
|
17
|
+
"Noto Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;
|
|
18
18
|
--mono-font: "Space Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useLayoutEffect } from "react";
|
|
2
|
+
//#region src/lib/label-value-fit.ts
|
|
3
|
+
/** The narrowest the label column gets: `minmax(88px, max-content)` in both lists. */
|
|
4
|
+
const minLabelColumn = 88;
|
|
5
|
+
/**
|
|
6
|
+
* The narrowest the value column may be beside its labels: as wide as a label
|
|
7
|
+
* may grow (`max-w-40`). A value column narrower than its label column reads
|
|
8
|
+
* as the label's footnote, not its answer.
|
|
9
|
+
*/
|
|
10
|
+
const minValueColumn = 160;
|
|
11
|
+
/**
|
|
12
|
+
* Reads every element's width with nothing wrapping inside it: all of them
|
|
13
|
+
* go to one-line layout, are read in one pass and get their inline style
|
|
14
|
+
* back, so the list is laid out once for the read, not once per element.
|
|
15
|
+
*/
|
|
16
|
+
function oneLineWidths(els) {
|
|
17
|
+
const saved = els.map(({ style }) => [
|
|
18
|
+
style.width,
|
|
19
|
+
style.maxWidth,
|
|
20
|
+
style.whiteSpace,
|
|
21
|
+
style.flexWrap
|
|
22
|
+
]);
|
|
23
|
+
for (const { style } of els) {
|
|
24
|
+
style.width = "max-content";
|
|
25
|
+
style.maxWidth = "none";
|
|
26
|
+
style.whiteSpace = "nowrap";
|
|
27
|
+
style.flexWrap = "nowrap";
|
|
28
|
+
}
|
|
29
|
+
const widths = els.map((el) => el.getBoundingClientRect().width);
|
|
30
|
+
els.forEach(({ style }, i) => {
|
|
31
|
+
[style.width, style.maxWidth, style.whiteSpace, style.flexWrap] = saved[i];
|
|
32
|
+
});
|
|
33
|
+
return widths;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a two-column label/value list has to put its labels above their
|
|
37
|
+
* values: some value doesn't fit on one line beside the label column, but
|
|
38
|
+
* would on a line of its own, or the value column would be narrower than
|
|
39
|
+
* `minValueColumn`. A value too long for a line of its own (a paragraph)
|
|
40
|
+
* wraps either way and doesn't decide. Measured in one-line layout whatever
|
|
41
|
+
* the list shows now, so the answer depends only on the list's width and its
|
|
42
|
+
* content and can't flap at the boundary.
|
|
43
|
+
*/
|
|
44
|
+
function labelsGoAbove(list, parts) {
|
|
45
|
+
const style = getComputedStyle(list);
|
|
46
|
+
const width = list.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
47
|
+
const gap = parseFloat(style.columnGap) || 0;
|
|
48
|
+
const labels = Array.from(list.querySelectorAll(parts.label));
|
|
49
|
+
const values = Array.from(list.querySelectorAll(parts.value));
|
|
50
|
+
const caps = labels.map((label) => parseFloat(getComputedStyle(label).maxWidth));
|
|
51
|
+
const widths = oneLineWidths([...labels, ...values]);
|
|
52
|
+
let labelColumn = minLabelColumn;
|
|
53
|
+
labels.forEach((_, i) => {
|
|
54
|
+
const natural = widths[i];
|
|
55
|
+
const cap = caps[i];
|
|
56
|
+
labelColumn = Math.max(labelColumn, Number.isNaN(cap) ? natural : Math.min(natural, cap));
|
|
57
|
+
});
|
|
58
|
+
const valueColumn = width - labelColumn - gap;
|
|
59
|
+
if (valueColumn < minValueColumn) return true;
|
|
60
|
+
for (const natural of widths.slice(labels.length)) if (natural - valueColumn > .5 && natural - width <= .5) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sets `data-stacked` on a two-column label/value list while its labels have
|
|
65
|
+
* to go above their values (`labelsGoAbove`), and takes it off when they fit
|
|
66
|
+
* beside them again: the list's CSS does the rest. The decision is taken
|
|
67
|
+
* synchronously, in the layout effect and the observers' callbacks, so the
|
|
68
|
+
* first frame and every resize paint the right layout; again when the list
|
|
69
|
+
* or a value resizes, a label or value changes, or a web font arrives
|
|
70
|
+
* (design/patterns/responsive.md#when-a-row-runs-out-of-room).
|
|
71
|
+
*/
|
|
72
|
+
function useLabelValueFit(list, enabled, parts) {
|
|
73
|
+
const { label, value } = parts;
|
|
74
|
+
useLayoutEffect(() => {
|
|
75
|
+
if (!list || !enabled || typeof ResizeObserver === "undefined") return void 0;
|
|
76
|
+
const selectors = {
|
|
77
|
+
label,
|
|
78
|
+
value
|
|
79
|
+
};
|
|
80
|
+
const fit = () => {
|
|
81
|
+
const stacked = labelsGoAbove(list, selectors);
|
|
82
|
+
if (stacked !== list.hasAttribute("data-stacked")) list.toggleAttribute("data-stacked", stacked);
|
|
83
|
+
};
|
|
84
|
+
const resize = new ResizeObserver(fit);
|
|
85
|
+
const observeAll = () => {
|
|
86
|
+
resize.disconnect();
|
|
87
|
+
resize.observe(list);
|
|
88
|
+
for (const el of Array.from(list.querySelectorAll(`${label}, ${value}`))) for (const child of Array.from(el.children)) resize.observe(child);
|
|
89
|
+
};
|
|
90
|
+
observeAll();
|
|
91
|
+
const mutation = new MutationObserver(() => {
|
|
92
|
+
observeAll();
|
|
93
|
+
fit();
|
|
94
|
+
});
|
|
95
|
+
mutation.observe(list, {
|
|
96
|
+
childList: true,
|
|
97
|
+
subtree: true,
|
|
98
|
+
characterData: true
|
|
99
|
+
});
|
|
100
|
+
const fonts = typeof document === "undefined" ? void 0 : document.fonts;
|
|
101
|
+
fonts?.addEventListener("loadingdone", fit);
|
|
102
|
+
fit();
|
|
103
|
+
return () => {
|
|
104
|
+
resize.disconnect();
|
|
105
|
+
mutation.disconnect();
|
|
106
|
+
fonts?.removeEventListener("loadingdone", fit);
|
|
107
|
+
list.removeAttribute("data-stacked");
|
|
108
|
+
};
|
|
109
|
+
}, [
|
|
110
|
+
list,
|
|
111
|
+
enabled,
|
|
112
|
+
label,
|
|
113
|
+
value
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { labelsGoAbove, useLabelValueFit };
|