@botiverse/raft-ui 0.74.0 → 0.75.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 +16 -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/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,19 @@
|
|
|
1
|
+
## [0.75.0](https://github.com/botiverse/raft-design/compare/v0.74.0...v0.75.0) (2026-11-08)
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **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))
|
|
6
|
+
- 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).
|
|
7
|
+
- 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.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/description-list/#labels-go-above-only-when-a-value-doesnt-fit
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **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))
|
|
13
|
+
- 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).
|
|
14
|
+
- Affects: Linux desktops and CI screenshots; Mac and Windows already matched PingFang SC or Microsoft YaHei first.
|
|
15
|
+
- Docs: https://raft-design.botiverse.build/foundations/typography/
|
|
16
|
+
|
|
1
17
|
## [0.74.0](https://github.com/botiverse/raft-design/compare/v0.73.0...v0.74.0) (2026-11-08)
|
|
2
18
|
|
|
3
19
|
### 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
|
});
|
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 };
|