@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 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
- "@container/description-list w-full grid items-baseline",
25
- "[&>[data-slot=description-item]>*]:@max-sm/description-list:col-span-full",
26
- "[&>[data-slot=description-item]>[data-slot=description-details]]:@max-sm/description-list:min-h-0",
27
- "[&>[data-slot=description-item]+[data-slot=description-item]>[data-slot=description-term]]:@max-sm/description-list:mt-2",
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
- function DescriptionList({ direction = "vertical", className, render, ...props }) {
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", "@container/action-card flex flex-col gap-2 px-3 pt-0 pb-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
- "@max-sm/action-card:grid-cols-1",
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]>&]:@max-sm/action-card:mt-2"
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
- "@max-sm/action-card:min-h-0 @max-sm/action-card:py-0"
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 { Fragment, jsx, jsxs } from "react/jsx-runtime";
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
- function MessageActionCardProperties({ className, render, ...props }) {
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",