@botiverse/raft-ui 0.95.0 → 0.95.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 CHANGED
@@ -1,3 +1,23 @@
1
+ ## [0.95.1](https://github.com/botiverse/raft-design/compare/v0.95.0...v0.95.1) (2026-11-12)
2
+
3
+ ### Added
4
+
5
+ - **`SelectTrigger fit`: a select as wide as its longest option** ([Select › As wide as its longest option](https://raft-design.botiverse.build/components/select/#as-wide-as-its-longest-option)) ([#1206](https://github.com/botiverse/raft-design/pull/1206), [#1207](https://github.com/botiverse/raft-design/pull/1207)). With `fit`, the trigger is as wide as the longest label in the root's `items` or its placeholder, at least 11rem and at most 20rem; past 20rem the value ends in an ellipsis. `SelectValue` lays every label out in its own place, invisible, 0px high and hidden from screen readers, so the width needs no script and stays the same whichever value is chosen.
6
+ - Why: a settings row's select sits at the row's end, as wide as its longest option ([Forms › A setting's row](https://raft-design.botiverse.build/patterns/forms/#a-settings-row)); a trigger that only lays out its value would shrink to a short one and move the row's end with each choice.
7
+ - Affects: nothing until a trigger sets `fit`. It needs `items` on `Select` and a `SelectValue` in the trigger; without `items` it stays at 11rem. A trigger without `fit` renders as before.
8
+ - Docs: https://raft-design.botiverse.build/components/select/#as-wide-as-its-longest-option
9
+
10
+ ### Changed
11
+
12
+ - **An attached row under a row that is a target keeps its hairline and top inset** ([ListRow › A row that belongs to the setting above](https://raft-design.botiverse.build/components/list-row/#a-row-that-belongs-to-the-setting-above)) ([#1201](https://github.com/botiverse/raft-design/pull/1201)). A display-only `attached` row (`interactive={false}`) right under an interactive row now draws the hairline above it and, as a `plain` row, keeps its top inset. `ListRow` decides this from the row above (it now sets `data-interactive`), so callers change nothing.
13
+ - Why: under a target, dropping them left the target's hover and press fill ending on an edge you couldn't see, right against the attached row's own buttons (a pinned item with Remove and Replace under it).
14
+ - Affects: only that pair. An attached row under a display-only setting (quiet hours' times), and attached rows that are targets themselves (a level's per-channel rows), look as before.
15
+ - Docs: https://raft-design.botiverse.build/components/list-row/#a-row-that-belongs-to-the-setting-above
16
+ - **`ArtifactIcon` (`@botiverse/ricons` 0.1.1): its page is drawn 2 wider** ([#1210](https://github.com/botiverse/raft-design/pull/1210)). raft-ui 0.95.1 depends on ricons 0.1.1.
17
+ - Why: Lucide's portrait page is 16 wide where the icons beside it in a rail or a menu are 20, so at the same size it read one size smaller.
18
+ - Affects: the page spans x 3 to 21 of the 24 grid (was 4 to 20), same height; `FollowArtifactIcon` and `UnfollowArtifactIcon` use the same page. The `< >` is unchanged.
19
+ - Docs: https://raft-design.botiverse.build/components/icons/
20
+
1
21
  ## [0.95.0](https://github.com/botiverse/raft-design/compare/v0.94.0...v0.95.0) (2026-11-11)
2
22
 
3
23
  ### Breaking
@@ -36,6 +36,7 @@ function ListRow({ variant = "default", tone = "default", selected = false, inte
36
36
  "data-tone": tone === "default" ? void 0 : tone,
37
37
  "data-selected": selected ? "" : void 0,
38
38
  "data-attached": attached ? "" : void 0,
39
+ "data-interactive": interactive ? "" : void 0,
39
40
  className: root({ className }),
40
41
  ...interactive && !render ? { type: "button" } : {}
41
42
  };
@@ -1,5 +1,9 @@
1
1
  import { listCell, listCellFocus, listCellStates } from "../../lib/list-cell.mjs";
2
2
  import { tv } from "../../lib/tv.mjs";
3
+ // src/components/list-row/list-row.recipe.ts
4
+ const hairlineLine = ["before:pointer-events-none before:absolute before:top-0 before:right-0 before:hidden", "before:left-(--list-row-rule-start) before:border-t before:border-line-hairline"];
5
+ const hairline = [...hairlineLine, "[[data-slot=list-row]+&]:before:block [li+li>&]:before:block"];
6
+ const attachedAfterTarget = [...hairlineLine, "[[data-slot=list-row][data-interactive]:not([data-attached])+&]:before:block"];
3
7
  const listRow = tv({
4
8
  slots: {
5
9
  group: ["flex min-w-0 flex-col overflow-hidden", "rounded-container border border-line-hairline bg-layer-card text-foreground"],
@@ -70,17 +74,26 @@ const listRow = tv({
70
74
  cell: false,
71
75
  attached: false,
72
76
  variant: ["default", "plain"],
73
- class: { root: [
74
- "before:pointer-events-none before:absolute before:top-0 before:right-0 before:hidden",
75
- "before:left-(--list-row-rule-start) before:border-t before:border-line-hairline",
76
- "[[data-slot=list-row]+&]:before:block [li+li>&]:before:block"
77
- ] }
77
+ class: { root: hairline }
78
78
  },
79
79
  {
80
80
  variant: "plain",
81
81
  attached: true,
82
82
  class: { root: "pt-0" }
83
83
  },
84
+ {
85
+ cell: false,
86
+ attached: true,
87
+ interactive: false,
88
+ variant: ["default", "plain"],
89
+ class: { root: attachedAfterTarget }
90
+ },
91
+ {
92
+ variant: "plain",
93
+ attached: true,
94
+ interactive: false,
95
+ class: { root: "[[data-slot=list-row][data-interactive]:not([data-attached])+&]:pt-(--settings-row-inset)" }
96
+ },
84
97
  {
85
98
  variant: "plain",
86
99
  attached: true,
@@ -11,9 +11,19 @@ export type SelectProps<Value, Multiple extends boolean | undefined = false> = S
11
11
  */
12
12
  chrome?: SelectChrome;
13
13
  };
14
- declare function Select$1<Value, Multiple extends boolean | undefined = false>({ chrome, onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, ...props }: SelectProps<Value, Multiple>): import("react").JSX.Element;
15
- export type SelectTriggerProps = StyledProps<Select.Trigger.Props> & DataAttributes;
16
- export declare function SelectTrigger({ className, children, render, "data-invalid": dataInvalid, ...props }: SelectTriggerProps): import("react").JSX.Element;
14
+ declare function Select$1<Value, Multiple extends boolean | undefined = false>({ chrome, onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, items, ...props }: SelectProps<Value, Multiple>): import("react").JSX.Element;
15
+ export type SelectTriggerProps = StyledProps<Select.Trigger.Props> & DataAttributes & {
16
+ /**
17
+ * Makes the trigger as wide as its longest option and its placeholder, at
18
+ * least 11rem and at most 20rem, past which the value ends in an ellipsis.
19
+ * The width comes from every label in the root's `items`, so it stays the
20
+ * same whichever value is chosen. For a select at the end of a settings
21
+ * row; needs `items` on `Select` and a `SelectValue` in the trigger.
22
+ * @default false
23
+ */
24
+ fit?: boolean;
25
+ };
26
+ export declare function SelectTrigger({ className, children, render, fit, "data-invalid": dataInvalid, ...props }: SelectTriggerProps): import("react").JSX.Element;
17
27
  export type SelectValueProps = StyledProps<Select.Value.Props>;
18
28
  export declare function SelectValue({ className, ref, title, ...props }: SelectValueProps): import("react").JSX.Element;
19
29
  export type SelectIconProps = StyledProps<Select.Icon.Props>;
@@ -39,6 +39,10 @@ const select = tv({
39
39
  "[&>svg]:size-4 [&>svg]:stroke-2 [&>svg_*]:stroke-2"
40
40
  ],
41
41
  value: ["flex min-w-0 flex-1 truncate text-left"],
42
+ fitFrame: ["grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)]"],
43
+ sizer: ["col-start-1 row-start-1 flex h-0 flex-col overflow-hidden", "pointer-events-none invisible select-none"],
44
+ sizerLabel: ["whitespace-nowrap font-semibold"],
45
+ sizerPlaceholder: ["whitespace-nowrap font-medium"],
42
46
  positioner: ["isolate z-popup outline-none"],
43
47
  content: [
44
48
  "relative max-h-(--available-height) min-w-[max(var(--anchor-width),220px)] overflow-hidden",
@@ -100,19 +104,24 @@ const select = tv({
100
104
  ],
101
105
  empty: ["px-2.5 py-2 text-left text-callout text-foreground-hint"]
102
106
  },
103
- variants: { chrome: {
104
- default: {},
105
- field: {
106
- trigger: [
107
- "px-3 text-field font-field",
108
- "**:data-[slot='button-content']:font-field **:data-[slot='select-value']:font-field",
109
- "h-control rounded-(--control-radius) py-0",
110
- "data-placeholder:font-field"
111
- ],
112
- item: ["text-field font-field", "**:data-[slot='select-item-text']:font-field"],
113
- actionItem: ["text-field font-field"]
114
- }
115
- } },
107
+ variants: {
108
+ chrome: {
109
+ default: {},
110
+ field: {
111
+ trigger: [
112
+ "px-3 text-field font-field",
113
+ "**:data-[slot='button-content']:font-field **:data-[slot='select-value']:font-field",
114
+ "h-control rounded-(--control-radius) py-0",
115
+ "data-placeholder:font-field"
116
+ ],
117
+ item: ["text-field font-field", "**:data-[slot='select-item-text']:font-field"],
118
+ actionItem: ["text-field font-field"],
119
+ sizerLabel: ["font-field"],
120
+ sizerPlaceholder: ["font-field"]
121
+ }
122
+ },
123
+ fit: { true: { trigger: ["w-auto min-w-44 max-w-80"] } }
124
+ },
116
125
  defaultVariants: { chrome: "default" }
117
126
  });
118
127
  /**
@@ -133,6 +142,19 @@ function useSelectChrome() {
133
142
  return useContext(SelectChromeContext);
134
143
  }
135
144
  /**
145
+ * The root's `items`, for a `fit` trigger's sizer: Base UI keeps them in a
146
+ * store of its own that the trigger can't read.
147
+ */
148
+ const SelectItemsContext = createContext(void 0);
149
+ /** Whether the trigger around a `SelectValue` sizes to the longest option. */
150
+ const SelectFitContext = createContext(false);
151
+ /** Every label in `items`, groups flattened: what a `fit` trigger may show. */
152
+ function itemLabels(items) {
153
+ if (items == null) return [];
154
+ if (!Array.isArray(items)) return Object.values(items);
155
+ return items.flatMap((entry) => Array.isArray(entry.items) ? entry.items : [entry]).map((item) => item.label);
156
+ }
157
+ /**
136
158
  * The value a `SelectActionItem` gives Base UI, so the row joins the list's
137
159
  * navigation and typeahead like any item. The root recognises it and never lets
138
160
  * it become the value: a list action changes what the list shows, never what is
@@ -174,7 +196,7 @@ function keepActionHighlighted(event) {
174
196
  if (lent) list.removeAttribute("tabindex");
175
197
  });
176
198
  }
177
- function Select$1({ chrome = "default", onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, ...props }) {
199
+ function Select$1({ chrome = "default", onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, items, ...props }) {
178
200
  const pendingActionClose = useRef(void 0);
179
201
  const handleValueChange = useCallback((next, details) => {
180
202
  const action = findActionValue(next);
@@ -203,21 +225,28 @@ function Select$1({ chrome = "default", onValueChange, onOpenChange, isItemEqual
203
225
  const handleStringValue = useMemo(() => itemToStringValue && ((item) => item instanceof SelectActionValue ? "" : itemToStringValue(item)), [itemToStringValue]);
204
226
  return /* @__PURE__ */ jsx(SelectChromeContext, {
205
227
  value: chrome,
206
- children: /* @__PURE__ */ jsx(Select.Root, {
207
- "data-slot": "select",
208
- "data-chrome": chrome,
209
- onValueChange: handleValueChange,
210
- onOpenChange: handleOpenChange,
211
- isItemEqualToValue: handleItemEquality,
212
- itemToStringLabel: handleStringLabel,
213
- itemToStringValue: handleStringValue,
214
- ...props
228
+ children: /* @__PURE__ */ jsx(SelectItemsContext, {
229
+ value: items,
230
+ children: /* @__PURE__ */ jsx(Select.Root, {
231
+ "data-slot": "select",
232
+ "data-chrome": chrome,
233
+ onValueChange: handleValueChange,
234
+ onOpenChange: handleOpenChange,
235
+ isItemEqualToValue: handleItemEquality,
236
+ itemToStringLabel: handleStringLabel,
237
+ itemToStringValue: handleStringValue,
238
+ items,
239
+ ...props
240
+ })
215
241
  })
216
242
  });
217
243
  }
218
- function SelectTrigger({ className, children, render, "data-invalid": dataInvalid, ...props }) {
244
+ function SelectTrigger({ className, children, render, fit = false, "data-invalid": dataInvalid, ...props }) {
219
245
  const chrome = useSelectChrome();
220
- const { trigger } = select({ chrome });
246
+ const { trigger } = select({
247
+ chrome,
248
+ fit
249
+ });
221
250
  return /* @__PURE__ */ jsx(Select.Trigger, {
222
251
  "data-slot": "select-trigger",
223
252
  render: render ?? /* @__PURE__ */ jsx(Button, {
@@ -232,20 +261,59 @@ function SelectTrigger({ className, children, render, "data-invalid": dataInvali
232
261
  "data-slot": "select-trigger-content",
233
262
  "data-picker-trigger-content": "select",
234
263
  className: "flex min-w-0 flex-1 items-center gap-1.5",
235
- children
264
+ children: /* @__PURE__ */ jsx(SelectFitContext, {
265
+ value: fit,
266
+ children
267
+ })
236
268
  })
237
269
  });
238
270
  }
239
271
  function SelectValue({ className, ref, title, ...props }) {
240
- const { value } = select();
272
+ const fit = useContext(SelectFitContext);
273
+ const { value, fitFrame } = select();
241
274
  const overflowRef = useOverflowTitle(ref, title);
242
- return /* @__PURE__ */ jsx(Select.Value, {
275
+ const node = /* @__PURE__ */ jsx(Select.Value, {
243
276
  ref: overflowRef,
244
277
  title,
245
278
  "data-slot": "select-value",
246
- className: value({ className }),
279
+ className: value({ className: cn(fit && "col-start-1 row-start-1", className) }),
247
280
  ...props
248
281
  });
282
+ if (!fit) return node;
283
+ return /* @__PURE__ */ jsxs("span", {
284
+ "data-slot": "select-value-fit",
285
+ className: fitFrame(),
286
+ children: [node, /* @__PURE__ */ jsx(SelectValueSizer, { placeholder: props.placeholder })]
287
+ });
288
+ }
289
+ /**
290
+ * Every option's label and the placeholder, stacked in the value's grid cell,
291
+ * invisible and 0px high: the cell is as wide as the widest of them, so the
292
+ * trigger is too, whichever value shows. Each takes the weight it shows at, so
293
+ * the width doesn't change when the placeholder gives way to a value.
294
+ */
295
+ function SelectValueSizer({ placeholder }) {
296
+ const chrome = useSelectChrome();
297
+ const items = useContext(SelectItemsContext);
298
+ const { sizer, sizerLabel, sizerPlaceholder } = select({ chrome });
299
+ return /* @__PURE__ */ jsxs("span", {
300
+ "aria-hidden": true,
301
+ "data-slot": "select-value-sizer",
302
+ className: sizer(),
303
+ children: [typeof placeholder === "string" ? /* @__PURE__ */ jsx("span", {
304
+ "data-label": placeholder,
305
+ className: sizerPlaceholder({ className: "after:content-[attr(data-label)]" })
306
+ }) : placeholder != null && /* @__PURE__ */ jsx("span", {
307
+ className: sizerPlaceholder(),
308
+ children: placeholder
309
+ }), itemLabels(items).map((label, index) => typeof label === "string" || typeof label === "number" ? /* @__PURE__ */ jsx("span", {
310
+ "data-label": label,
311
+ className: sizerLabel({ className: "after:content-[attr(data-label)]" })
312
+ }, index) : /* @__PURE__ */ jsx("span", {
313
+ className: sizerLabel(),
314
+ children: label
315
+ }, index))]
316
+ });
249
317
  }
250
318
  function SelectIcon({ className, children, ...props }) {
251
319
  const { icon } = select();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.95.0",
3
+ "version": "0.95.1",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@base-ui/react": "^1.9.0",
66
- "@botiverse/ricons": "^0.1.0",
66
+ "@botiverse/ricons": "^0.1.1",
67
67
  "@dnd-kit/core": "^6.3.1",
68
68
  "@dnd-kit/sortable": "^10.0.0",
69
69
  "@dnd-kit/utilities": "^3.2.2",