@botiverse/raft-ui 0.79.0 → 0.81.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,40 @@
1
+ ## [0.81.0](https://github.com/botiverse/raft-design/compare/v0.80.0...v0.81.0) (2026-11-09)
2
+
3
+ ### Fixed
4
+
5
+ - **ScrollArea's scrollbars paint over everything inside the area**: the gutters layer is at the highest `z-index`, and the area's root is a stacking context of its own (`isolation: isolate`), so a sticky heading or any other positioned content inside the area, whatever its `z-index`, no longer covers the thumb, with sticky and absolute gutters alike, and the scrollbars still never paint over anything outside the area.
6
+ - Why: in the Raft app's activity list each day's sticky heading (an opaque background at z-15) painted over the thumb as it slid by, cutting it into pieces, while the gutters were at z-1 (Artea, #proj-raft-ui:21bae128). Starting the scrollbar below the stuck heading (`railInset`) fixed only the heading at the top, not the ones scrolling past.
7
+ - Affects: content inside a ScrollArea can no longer cover anything outside it, whatever its `z-index`: a popup, a menu or a dragged copy rendered in place inside an area stacks with the area. Render those through a portal (raft-ui's overlays do, and now `DragLift` by default, below). The scrollbars' strip at the edge now takes the pointer over positioned content there too, as it already did over the rest. How the scrollbars look is unchanged.
8
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/
9
+ - **`DragLift` renders the copy in `document.body` by default**: the lifted copy goes through a portal to the body unless `container` names another element, or `null` keeps it in place; the portal keeps the theme scope it was rendered in. `TaskDragOverlay` follows, as `SortableTabsList` and `SortableComposerAttachments` already did.
10
+ - Why: in place inside a ScrollArea, now a stacking context, the copy couldn't rise above anything outside the area. On the Raft app's Tasks board a dragged card was cut off by the left rail and covered by the top bar (`TaskDragOverlay` with no `container`).
11
+ - Affects: a `DragLift` or `TaskDragOverlay` without `container` now renders its copy in the body: CSS that styles the copy through an ancestor of the item (`.board [data-slot=drag-lift]`) no longer reaches it, and neither do CSS variables set on that ancestor; style the copy through its own children. The writing direction (`dir`) is carried over, below. Pass `container={null}` for the old in-place copy. How the copy looks is unchanged.
12
+ - Docs: https://raft-design.botiverse.build/components/drag/#anatomy
13
+ - **In a right-to-left layout the dragged copy keeps its direction**: `DragLift`'s portal carries the item's computed writing direction (`dir`) to the body, as it carries the theme scope.
14
+ - Why: in the body, the copy took its direction from `<html>`, so an item dragged out of a right-to-left container came out left-to-right (Jasper's review of #1094).
15
+ - Affects: a copy of an item in a `dir="rtl"` container now lays out right-to-left. Nothing else around the item reaches the copy (font, colour, a container's custom properties); the docs now say so.
16
+ - Docs: https://raft-design.botiverse.build/components/drag/#anatomy
17
+ - **ChoiceList's row actions keep clear of the scrollbar**: in a list that scrolls (search on, or `fill`), a row's action sits 12px from the end instead of 4px, and rows with an action keep their text 52px from the end instead of 44px.
18
+ - Why: the scrollbar's 16px strip at the end of a ScrollArea now paints over and takes the pointer from everything in the area; the last 8px of each 32px action lay under it, so a click there paged the list instead.
19
+ - Affects: in a scrolling ChoiceList, each action moves 8px toward the start, and a long name or description cuts off 8px sooner. A list that doesn't scroll is unchanged.
20
+ - Docs: https://raft-design.botiverse.build/components/choice-list/#behavior
21
+
22
+ ## [0.80.0](https://github.com/botiverse/raft-design/compare/v0.79.0...v0.80.0) (2026-11-09)
23
+
24
+ ### Changed
25
+
26
+ - **A panel head is one line from 768px by touch too**: `PanelHeaderContent` puts the `PanelMeta` after the title on its line from 768px at any pointer, where it used to do so only with a mouse; a phone still puts it under the title.
27
+ - Why: a touch tablet stacked the title and its count while a laptop showed them on one line, so the same page looked different on the two, and the Raft app had started overriding the layout page by page (xxchan, Kirby, #proj-workspace:dc3c9fc9).
28
+ - Affects: on a touch screen 768px or wider, every panel head (Tasks, Artifacts, a thread, a profile, a channel) shows its subtitle on the title's line, and the subtitle truncates first. Head heights don't change (56px by touch). An app that stacked them itself can drop its override.
29
+ - Docs: https://raft-design.botiverse.build/components/panel/#anatomy
30
+
31
+ ### Fixed
32
+
33
+ - **A gallery row of three images sits three across**: `MessageImageGalleryRow` with three images (`count: 3`) lays them out three across at every width, on the shorter rows already sized for three, where it used to put two over one.
34
+ - Why: six images showed as rows of 2, 1, 2, 1 and read as an F (xxchan, #triage:c3c22ffb).
35
+ - Affects: three, six and nine images in a message now show three to a row; each image is narrower (about 117px instead of 178px in a 360px gallery), so a wide screenshot is cropped more in the thumbnail. Opening an image still shows all of it.
36
+ - Docs: https://raft-design.botiverse.build/components/message-attachment/#rules
37
+
1
38
  ## [0.79.0](https://github.com/botiverse/raft-design/compare/v0.78.0...v0.79.0) (2026-11-09)
2
39
 
3
40
  ### Changed
@@ -31,14 +31,27 @@ const choiceList = tv({
31
31
  footer: ["shrink-0 border-t border-line-seam p-1"]
32
32
  },
33
33
  variants: {
34
- scroll: { true: { scroller: ["max-h-72"] } },
34
+ scroll: { true: {
35
+ scroller: ["max-h-72"],
36
+ action: ["pr-3"]
37
+ } },
35
38
  fill: { true: {
36
39
  root: ["flex min-h-0 flex-1 flex-col"],
37
40
  frame: ["min-h-0 flex-1"],
38
- scroller: ["flex-1"]
41
+ scroller: ["flex-1"],
42
+ action: ["pr-3"]
39
43
  } },
40
44
  hasAction: { true: { item: ["pr-11"] } }
41
- }
45
+ },
46
+ compoundVariants: [{
47
+ scroll: true,
48
+ hasAction: true,
49
+ class: { item: ["pr-13"] }
50
+ }, {
51
+ fill: true,
52
+ hasAction: true,
53
+ class: { item: ["pr-13"] }
54
+ }]
42
55
  });
43
56
  //#endregion
44
57
  export { choiceList };
@@ -1,6 +1,6 @@
1
1
  import { ComposerAttachmentsScrollArea } from "./composer-attachments-scroll-area.mjs";
2
2
  import { createDragAnnouncements, dragInstructions } from "../drag/drag-announcements.mjs";
3
- import { DragLift, useDragSiblingTransition, useLiftContainer } from "../drag/drag-lift.mjs";
3
+ import { DragLift, useDragSiblingTransition } from "../drag/drag-lift.mjs";
4
4
  import { useDragSensors } from "../drag/drag-sensors.mjs";
5
5
  import { DropPlaceholder } from "../drag/drop-placeholder.mjs";
6
6
  import { Children, createContext, isValidElement, use, useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
@@ -87,7 +87,6 @@ function SortableComposerAttachmentsRoot({ items, onReorder, children, messages,
87
87
  const draftIdsRef = useRef([]);
88
88
  const lastReorderPointerRef = useRef(null);
89
89
  const sensors = useDragSensors();
90
- const liftContainer = useLiftContainer(container);
91
90
  const attachments = useMemo(() => attachmentsById(children), [children]);
92
91
  const itemIds = useMemo(() => items.map(String), [items]);
93
92
  const [announced] = useState(() => ({
@@ -186,7 +185,7 @@ function SortableComposerAttachmentsRoot({ items, onReorder, children, messages,
186
185
  })
187
186
  }), /* @__PURE__ */ jsx(DragLift, {
188
187
  className: "rounded-control",
189
- container: liftContainer,
188
+ container,
190
189
  children: activeId && overlay ? overlay : null
191
190
  })]
192
191
  });
@@ -50,8 +50,8 @@ export declare function keepGrabInside(proxyWidth: number, inset?: number): Modi
50
50
  export declare function cancelDrag(win?: Window): void;
51
51
  /**
52
52
  * Above every raft-ui layer (popovers 50, a stacked dialog 60, toasts and the
53
- * lightbox 70) and an app's own top layers: the copy in the hand is always on
54
- * top. dnd-kit's own default.
53
+ * lightbox 70) and an app's own top layers, among the layers rendered in
54
+ * `document.body`, where the copy goes by default. dnd-kit's own default.
55
55
  */
56
56
  export declare const DRAG_LIFT_Z_INDEX = 999;
57
57
  export type DragLiftProps = {
@@ -74,10 +74,14 @@ export type DragLiftProps = {
74
74
  */
75
75
  autoScroll?: boolean;
76
76
  /**
77
- * Render the copy in another element, such as `document.body` when an
78
- * ancestor has a transform (a drawer, a dialog), which would offset a
79
- * `position: fixed` copy. It keeps the theme scope it was rendered in
80
- * (`light` / `dark`), as raft-ui's portals do. By default it renders in place.
77
+ * Where the copy renders: `document.body` by default, so no ancestor holds
78
+ * it in, neither a stacking context (a ScrollArea, an `isolate`) it couldn't
79
+ * rise out of nor a transform (a drawer, a dialog) that would offset a
80
+ * `position: fixed` copy. Another element renders it there, and `null` in
81
+ * place. It keeps the theme scope it was rendered in (`light` / `dark`), as
82
+ * raft-ui's portals do, and the item's writing direction (`dir`). Nothing
83
+ * else is inherited from where it was picked up (font, colour, custom
84
+ * properties a container sets): give the copy its own styles.
81
85
  */
82
86
  container?: Element | DocumentFragment | null;
83
87
  /** @default DRAG_LIFT_Z_INDEX (999) */
@@ -110,17 +110,16 @@ function cancelDrag(win = window) {
110
110
  }
111
111
  /**
112
112
  * Above every raft-ui layer (popovers 50, a stacked dialog 60, toasts and the
113
- * lightbox 70) and an app's own top layers: the copy in the hand is always on
114
- * top. dnd-kit's own default.
113
+ * lightbox 70) and an app's own top layers, among the layers rendered in
114
+ * `document.body`, where the copy goes by default. dnd-kit's own default.
115
115
  */
116
116
  const DRAG_LIFT_Z_INDEX = 999;
117
117
  const noSubscribe = () => () => {};
118
118
  const clientBody = () => document.body;
119
119
  const serverBody = () => null;
120
120
  /**
121
- * For a component whose copy goes to `document.body` unless it is told
122
- * otherwise: `undefined` is the body (once on the client, so hydration
123
- * matches), `null` is in place, and an element is that element.
121
+ * Where the copy goes: `undefined` is the body (once on the client, so
122
+ * hydration matches), `null` is in place, and an element is that element.
124
123
  */
125
124
  function useLiftContainer(container) {
126
125
  const body = useSyncExternalStore(noSubscribe, clientBody, serverBody);
@@ -141,8 +140,10 @@ function useLiftContainer(container) {
141
140
  function DragLift({ children, className, style, modifiers, autoScroll = true, container, zIndex = 999 }) {
142
141
  const { lift } = drag();
143
142
  const { resolvedMode } = useTheme();
143
+ const target = useLiftContainer(container);
144
144
  const reduced = useSyncExternalStore(subscribeReducedMotion, reducedMotion, () => false);
145
- const { activatorEvent } = useDndContext();
145
+ const { activatorEvent, activeNode } = useDndContext();
146
+ const dir = useMemo(() => activeNode ? getComputedStyle(activeNode).direction : void 0, [activeNode]);
146
147
  const liftStyle = useMemo(() => isKeyboardEvent(activatorEvent) ? {
147
148
  translate: KEYBOARD_OFFSET,
148
149
  ...style
@@ -162,11 +163,12 @@ function DragLift({ children, className, style, modifiers, autoScroll = true, co
162
163
  })
163
164
  })
164
165
  });
165
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(DragSession, { autoScroll }), container ? createPortal(/* @__PURE__ */ jsx("div", {
166
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(DragSession, { autoScroll }), target ? createPortal(/* @__PURE__ */ jsx("div", {
166
167
  "data-slot": "drag-lift-portal",
167
168
  className: cn("contents", resolvedMode),
169
+ dir,
168
170
  children: overlay
169
- }), container) : overlay] });
171
+ }), target) : overlay] });
170
172
  }
171
173
  /** Where the pointer is during a drag: auto-scroll and the refusal hint read it. */
172
174
  const pointer = {
@@ -273,4 +275,4 @@ function autoScrollAt(x, y) {
273
275
  if (dy) window.scrollBy(0, dy);
274
276
  }
275
277
  //#endregion
276
- export { DRAG_AUTO_SCROLL_ZONE_PX, DRAG_KEYBOARD_OFFSET_PX, DRAG_LIFT_Z_INDEX, DRAG_SETTLE, DRAG_SIBLING_TRANSITION, DROP_GLIDE, DragLift, LIFT_CLASS, ORIGIN_OPACITY, autoScrollAt, cancelDrag, dragPointer, keepGrabInside, useDragSiblingTransition, useLiftContainer };
278
+ export { DRAG_AUTO_SCROLL_ZONE_PX, DRAG_KEYBOARD_OFFSET_PX, DRAG_LIFT_Z_INDEX, DRAG_SETTLE, DRAG_SIBLING_TRANSITION, DROP_GLIDE, DragLift, LIFT_CLASS, ORIGIN_OPACITY, autoScrollAt, cancelDrag, dragPointer, keepGrabInside, useDragSiblingTransition };
@@ -44,7 +44,7 @@ const messageImageGallery = tv({
44
44
  count: {
45
45
  1: { row: "grid-cols-1" },
46
46
  2: { row: "grid-cols-2" },
47
- 3: { row: "grid-cols-2" }
47
+ 3: { row: "grid-cols-3" }
48
48
  },
49
49
  /** A single-image gallery keeps the image's proportions instead of a row height. */
50
50
  single: { true: { item: ["inline-block w-fit max-w-[26rem] justify-self-start", "rounded-container"] } },
@@ -72,9 +72,9 @@ const messageForwardedBundleGallery = tv({
72
72
  tile: ["[&_img]:block [&_img]:size-full [&_img]:object-cover"]
73
73
  },
74
74
  variants: { count: {
75
- 1: { row: "grid-cols-1 auto-rows-[8rem] @min-[360px]/bundle:auto-rows-[9rem]" },
76
- 2: { row: "grid-cols-2 auto-rows-[8rem] @min-[360px]/bundle:auto-rows-[9rem]" },
77
- 3: { row: "grid-cols-2 auto-rows-[7rem] @min-[360px]/bundle:auto-rows-[8rem] @min-[360px]/bundle:grid-cols-3" }
75
+ 1: { row: "grid-cols-1 auto-rows-32 @min-[360px]/bundle:auto-rows-36" },
76
+ 2: { row: "grid-cols-2 auto-rows-32 @min-[360px]/bundle:auto-rows-36" },
77
+ 3: { row: "grid-cols-3 auto-rows-28 @min-[360px]/bundle:auto-rows-32" }
78
78
  } }
79
79
  });
80
80
  //#endregion
@@ -34,10 +34,10 @@ const panelHeader = tv({
34
34
  "not-data-[icon-kind=avatar]:[&_svg]:size-3.5 not-data-[icon-kind=avatar]:[&_svg]:stroke-2",
35
35
  "data-[icon-kind=avatar]:size-auto data-[icon-kind=avatar]:border-transparent data-[icon-kind=avatar]:bg-transparent"
36
36
  ],
37
- headerContent: ["flex min-w-0 flex-1 flex-col justify-center", "md:pointer-fine:flex-row md:pointer-fine:items-baseline md:pointer-fine:justify-start md:pointer-fine:gap-2"],
37
+ headerContent: ["flex min-w-0 flex-1 flex-col justify-center", "md:flex-row md:items-baseline md:justify-start md:gap-2"],
38
38
  heading: "flex min-w-0 items-center gap-2",
39
39
  title: "truncate font-sans text-headline font-bold text-foreground",
40
- meta: ["min-w-0 truncate font-sans text-caption text-foreground-hint", "md:pointer-fine:flex-1 md:pointer-fine:shrink-1000"],
40
+ meta: ["min-w-0 truncate font-sans text-caption text-foreground-hint", "md:flex-1 md:shrink-1000"],
41
41
  status: [
42
42
  "inline-flex shrink-0 items-center gap-1 border-0 bg-transparent p-0",
43
43
  "font-sans text-caption font-medium normal-case text-foreground-hint",
@@ -67,7 +67,7 @@ const panelHeader = tv({
67
67
  slots: ["sectionHeader"],
68
68
  class: [
69
69
  "max-md:bg-layer-frame max-md:text-foreground-on-frame",
70
- "max-md:[&>[data-slot=panel-header-icon]]:hidden",
70
+ "max-md:*:data-[slot=panel-header-icon]:hidden",
71
71
  "max-md:**:data-[slot=panel-title]:text-foreground-on-frame",
72
72
  "max-md:**:data-[slot=panel-meta]:text-foreground-on-frame-muted",
73
73
  "max-md:**:data-[slot=panel-action]:text-foreground-on-frame",
@@ -452,11 +452,24 @@
452
452
  resize: vertical;
453
453
  }
454
454
 
455
- /* The gutters layer: out of the way of the content, but for each gutter itself. */
455
+ /*
456
+ * The scrollbars paint over everything in the area, as native ones do: a
457
+ * sticky day heading with its own background and `z-index` (z-15 in an
458
+ * activity list) covered the thumb as it slid by, cutting it into pieces.
459
+ * The root is a stacking context of its own (`isolation: isolate`), so the
460
+ * layer's z-index ranks it only among the area's own content and never
461
+ * lifts it over anything outside the area: a fixed bar, a popup. What
462
+ * must cover things outside the area portals out of it, as raft-ui's
463
+ * overlays do.
464
+ */
465
+ .r-scroll-area {
466
+ isolation: isolate;
467
+ }
468
+ /* The gutters layer: out of the way of the content, but for each gutter itself; above all of it. */
456
469
  .r-scroll-area > .r-scroll-area__gutters {
457
470
  pointer-events: none;
458
471
  user-select: none;
459
- z-index: 1;
472
+ z-index: 2147483647;
460
473
  }
461
474
  /*
462
475
  * Sticky: a zero-height layer at the top of the viewport, the container's
@@ -2,7 +2,7 @@ import { tv } from "../../lib/tv.mjs";
2
2
  import { useMergedRef } from "../../lib/use-merged-ref.mjs";
3
3
  import { useTapActivation } from "../../lib/tap-activation.mjs";
4
4
  import { createDragAnnouncements, dragInstructions } from "../drag/drag-announcements.mjs";
5
- import { DragLift, useDragSiblingTransition, useLiftContainer } from "../drag/drag-lift.mjs";
5
+ import { DragLift, useDragSiblingTransition } from "../drag/drag-lift.mjs";
6
6
  import { useDragSensors } from "../drag/drag-sensors.mjs";
7
7
  import { DropPlaceholder } from "../drag/drop-placeholder.mjs";
8
8
  import { createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
@@ -275,7 +275,6 @@ function SortableTabsList({ value, onReorder, onDragStart, messages, container,
275
275
  const sensors = useDragSensors();
276
276
  const [copies] = useState(() => /* @__PURE__ */ new Map());
277
277
  const [nodes] = useState(() => /* @__PURE__ */ new Map());
278
- const liftContainer = useLiftContainer(container);
279
278
  const selectedValue = use(TabsValueContext);
280
279
  const { tab: tabClassName, content: contentClassName } = tabs({
281
280
  variant,
@@ -353,7 +352,7 @@ function SortableTabsList({ value, onReorder, onDragStart, messages, container,
353
352
  })
354
353
  }), /* @__PURE__ */ jsx(DragLift, {
355
354
  className: "rounded-control",
356
- container: liftContainer,
355
+ container,
357
356
  children: activeId === void 0 ? null : /* @__PURE__ */ jsx("span", {
358
357
  "data-slot": "tabs-tab",
359
358
  "data-active": selectedValue === activeId ? "" : void 0,
@@ -62,7 +62,11 @@ export declare function TaskDropPlaceholder({ id, placement, className, render,
62
62
  export type TaskDragOverlayProps = {
63
63
  children?: ReactNode;
64
64
  className?: string;
65
- /** Render the copy in another element, such as `document.body` when an ancestor has a transform. */
65
+ /**
66
+ * Where the copy renders: `document.body` by default, so it rises above
67
+ * everything outside a ScrollArea the board is in. `null` renders it in
68
+ * place (`DragLift`'s `container`).
69
+ */
66
70
  container?: Element | DocumentFragment | null;
67
71
  };
68
72
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.79.0",
3
+ "version": "0.81.0",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",