@botiverse/raft-ui 0.82.0 → 0.84.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/components/app-link-card/app-link-card.mjs +19 -37
  3. package/dist/components/banner/banner.mjs +15 -2
  4. package/dist/components/dialog/dialog-sheet-context.mjs +7 -2
  5. package/dist/components/dialog/dialog-sheet.d.mts +3 -2
  6. package/dist/components/dialog/dialog-sheet.mjs +29 -43
  7. package/dist/components/dialog/dialog.d.mts +12 -1
  8. package/dist/components/dialog/dialog.mjs +50 -10
  9. package/dist/components/drag/drag.recipe.mjs +1 -1
  10. package/dist/components/fit-toolbar/fit-toolbar.d.mts +165 -0
  11. package/dist/components/fit-toolbar/fit-toolbar.mjs +736 -0
  12. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +35 -0
  13. package/dist/components/fit-toolbar/index.d.mts +2 -0
  14. package/dist/components/lightbox/index.d.mts +2 -2
  15. package/dist/components/lightbox/lightbox.d.mts +70 -4
  16. package/dist/components/lightbox/lightbox.mjs +285 -91
  17. package/dist/components/list-row/list-row.d.mts +14 -1
  18. package/dist/components/list-row/list-row.mjs +4 -2
  19. package/dist/components/list-row/list-row.recipe.d.mts +15 -3
  20. package/dist/components/list-row/list-row.recipe.mjs +18 -1
  21. package/dist/components/message-action-card/message-action-card-content.recipe.mjs +3 -3
  22. package/dist/components/message-action-card/message-action-card.mjs +7 -3
  23. package/dist/components/message-action-card/message-action-card.recipe.mjs +2 -6
  24. package/dist/components/message-embed/index.d.mts +2 -1
  25. package/dist/components/message-embed/message-embed.d.mts +8 -1
  26. package/dist/components/message-embed/message-embed.mjs +2 -2
  27. package/dist/components/message-embed/message-embed.recipe.d.mts +160 -0
  28. package/dist/components/message-embed/message-embed.recipe.mjs +49 -10
  29. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.mjs +1 -0
  30. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.recipe.mjs +1 -1
  31. package/dist/components/message-quoted-preview/message-quoted-preview.mjs +2 -0
  32. package/dist/components/message-replies/message-replies.mjs +1 -1
  33. package/dist/components/message-replies/message-replies.recipe.mjs +1 -1
  34. package/dist/components/mobile-nav/mobile-nav-server.d.mts +21 -8
  35. package/dist/components/mobile-nav/mobile-nav-server.mjs +142 -13
  36. package/dist/components/popover/popover.d.mts +11 -2
  37. package/dist/components/popover/popover.mjs +51 -38
  38. package/dist/components/preview-card/preview-card.d.mts +5 -1
  39. package/dist/components/preview-card/preview-card.mjs +24 -27
  40. package/dist/components/save-status/save-status.d.mts +16 -9
  41. package/dist/components/save-status/save-status.mjs +19 -30
  42. package/dist/components/segmented-control/segmented-control.d.mts +1 -1
  43. package/dist/components/segmented-control/segmented-control.mjs +56 -10
  44. package/dist/components/selection-toolbar/index.d.mts +2 -0
  45. package/dist/components/selection-toolbar/selection-toolbar.d.mts +100 -0
  46. package/dist/components/selection-toolbar/selection-toolbar.mjs +217 -0
  47. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +42 -0
  48. package/dist/components/task-board/task-board.d.mts +5 -0
  49. package/dist/components/task-board/task-board.mjs +8 -1
  50. package/dist/components/task-dnd/task-dnd.d.mts +10 -1
  51. package/dist/components/task-dnd/task-dnd.mjs +2 -1
  52. package/dist/components/toast/toast.mjs +2 -2
  53. package/dist/index.d.mts +4 -2
  54. package/dist/index.mjs +4 -2
  55. package/dist/lib/label-value-fit.mjs +33 -6
  56. package/dist/lib/laid-out-children.mjs +32 -0
  57. package/dist/lib/stacked-actions.mjs +7 -4
  58. package/dist/lib/use-mobile-viewport.mjs +5 -2
  59. package/dist/lib/use-soft-keyboard.mjs +72 -0
  60. package/dist/wip.d.mts +8 -3
  61. package/dist/wip.mjs +5 -3
  62. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { tv } from "../../lib/tv.mjs";
2
+ import { scrollToElement } from "../scroll-area/scroll-to-element.mjs";
2
3
  import { ScrollArea } from "../scroll-area/scroll-area.mjs";
3
4
  import { MobileNavItem } from "./mobile-nav.mjs";
4
5
  import "../../lib/touch-drag.mjs";
@@ -16,9 +17,10 @@ const mobileNavServer = tv({ slots: {
16
17
  content: [
17
18
  "flex flex-col",
18
19
  "w-[calc(var(--anchor-width)-16px)] min-w-0",
19
- "max-h-[min(60dvh,calc(var(--available-height)-env(safe-area-inset-top,0px)))]"
20
+ "max-h-[min(max(60dvh,var(--mobile-nav-server-popover-floor,0px)),calc(var(--available-height)-env(safe-area-inset-top,0px)))]"
20
21
  ],
21
- list: ["flex min-h-0 flex-1 flex-col p-1"],
22
+ scroll: ["flex min-h-0 flex-col"],
23
+ list: ["flex min-h-[calc(var(--spacing-option)+var(--text-mono-meta--line-height)+(--spacing(2)))] flex-1 flex-col p-1"],
22
24
  group: ["flex shrink-0 flex-col p-1"],
23
25
  row: [
24
26
  "flex w-full shrink-0 cursor-default select-none items-center",
@@ -26,7 +28,7 @@ const mobileNavServer = tv({ slots: {
26
28
  "min-h-option gap-2.5 rounded-chip px-2.5 py-option-inset",
27
29
  "font-sans text-callout font-medium text-foreground",
28
30
  "hover:bg-brand-soft active:bg-brand-soft focus-visible:bg-brand-soft",
29
- "aria-[current=true]:font-bold",
31
+ "aria-current:font-bold",
30
32
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg:not([class*='size-'])]:size-4",
31
33
  "[&>svg]:text-foreground-muted",
32
34
  "*:data-[slot=avatar]:size-5 *:data-[slot=avatar]:shrink-0"
@@ -162,21 +164,49 @@ function MobileNavServerItem({ className, id, ref, onClick, onContextMenu, onPoi
162
164
  }
163
165
  /**
164
166
  * The popover above the tab bar, 8px from each side of it and 8px above it,
165
- * as tall as its content and at most 60% of the screen. It stops below the
166
- * top safe area (a notch in a standalone web app): Base UI's collision padding
167
- * takes only pixels, so the height cap subtracts `env(safe-area-inset-top)`.
168
- * The bottom needs none: the tab bar under the popover already pads the
169
- * bottom safe area. A column:
167
+ * as tall as its content and at most 60% of the screen. The one exception:
168
+ * the list always keeps one whole server row, so where the parts after it
169
+ * leave less, the popover grows past 60% as far as that row needs (the
170
+ * floor, measured from its children into `--mobile-nav-server-popover-floor`).
171
+ * Where even that doesn't fit, the whole popover scrolls, the footer and the
172
+ * pages with it: the children sit in a ScrollArea
173
+ * (`data-slot="mobile-nav-server-popover-scroll"`) inside the popup. It stops below the top safe area (a notch in a standalone
174
+ * web app): Base UI's collision padding takes only pixels, so the height cap
175
+ * subtracts `env(safe-area-inset-top)`. The bottom needs none: the tab bar
176
+ * under the popover already pads the bottom safe area. A column:
170
177
  * `MobileNavServerPopoverList` takes the room left and scrolls, and the
171
178
  * destinations and the footer after it stay in view.
179
+ *
180
+ * Focus on open goes to the current server's row (`aria-current="true"` in
181
+ * the list), which the list has already scrolled into view, so focusing it
182
+ * never scrolls the list back to the top. A touch opens it with focus on the
183
+ * popover itself, without scrolling, so no keyboard comes up. `initialFocus`
184
+ * overrides this.
172
185
  */
173
- function MobileNavServerPopoverContent({ anchor, side = "top", align = "center", sideOffset = 8, collisionPadding = 8, className, ...props }) {
186
+ function MobileNavServerPopoverContent({ anchor, side = "top", align = "center", sideOffset = 8, collisionPadding = 8, initialFocus, className, children, ref, ...props }) {
174
187
  const { triggerRef } = useMobileNavServerContext("MobileNavServerPopoverContent");
175
- const { content } = mobileNavServer();
188
+ const { content, scroll } = mobileNavServer();
189
+ const popupRef = useRef(null);
176
190
  const tabBar = useCallback(() => {
177
191
  const trigger = triggerRef.current;
178
192
  return trigger?.closest("[data-slot=mobile-nav-root]") ?? trigger;
179
193
  }, [triggerRef]);
194
+ const setRef = useCallback((node) => {
195
+ popupRef.current = node;
196
+ assignRef(ref, node);
197
+ if (!node) return;
198
+ return () => {
199
+ popupRef.current = null;
200
+ assignRef(ref, null);
201
+ };
202
+ }, [ref]);
203
+ const setScrollRef = useCallback((node) => {
204
+ if (node) return keepOneRow(node);
205
+ }, []);
206
+ const focusCurrent = useCallback((openType) => {
207
+ if (openType === "touch") return true;
208
+ return popupRef.current?.querySelector(CURRENT_ROW) ?? true;
209
+ }, []);
180
210
  return /* @__PURE__ */ jsx(PopoverContent, {
181
211
  "data-slot": "mobile-nav-server-popover-content",
182
212
  anchor: anchor ?? tabBar,
@@ -184,16 +214,115 @@ function MobileNavServerPopoverContent({ anchor, side = "top", align = "center",
184
214
  align,
185
215
  sideOffset,
186
216
  collisionPadding,
217
+ initialFocus: initialFocus ?? focusCurrent,
187
218
  className: content({ className }),
188
- ...props
219
+ ref: setRef,
220
+ ...props,
221
+ children: /* @__PURE__ */ jsx(ScrollArea, {
222
+ "data-slot": "mobile-nav-server-popover-scroll",
223
+ overscroll: "contain",
224
+ tabIndex: -1,
225
+ ref: setScrollRef,
226
+ className: scroll(),
227
+ children
228
+ })
189
229
  });
190
230
  }
231
+ const LIST_SLOT = "mobile-nav-server-popover-list";
232
+ const CURRENT_ROW = `[data-slot=${LIST_SLOT}] [aria-current="true"]`;
233
+ const GUTTERS_SLOT = "scroll-area-gutters";
234
+ /** Padding and border along the block axis. */
235
+ function blockFrame(element) {
236
+ const style = getComputedStyle(element);
237
+ return parseFloat(style.paddingTop) + parseFloat(style.paddingBottom) + parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
238
+ }
239
+ /**
240
+ * Writes the popover's floor onto the popup, its height with the list at its
241
+ * one-row minimum: the list's `min-height` plus the height of every other
242
+ * part in the scroll area, and the popup's and the area's own padding and
243
+ * border. Watched as the parts change.
244
+ */
245
+ function keepOneRow(area) {
246
+ const popup = area.parentElement;
247
+ const measure = () => {
248
+ if (!popup) return;
249
+ let floor = blockFrame(popup) + blockFrame(area);
250
+ for (const child of area.children) {
251
+ if (!(child instanceof HTMLElement) || child.dataset.slot === GUTTERS_SLOT) continue;
252
+ const style = getComputedStyle(child);
253
+ if (style.display === "none" || style.position === "absolute" || style.position === "fixed") continue;
254
+ floor += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
255
+ floor += child.dataset.slot === LIST_SLOT ? parseFloat(style.minHeight) || 0 : child.getBoundingClientRect().height;
256
+ }
257
+ popup.style.setProperty("--mobile-nav-server-popover-floor", `${floor}px`);
258
+ };
259
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
260
+ const watch = () => {
261
+ resize?.disconnect();
262
+ for (const child of area.children) if (child instanceof HTMLElement && child.dataset.slot !== LIST_SLOT) resize?.observe(child);
263
+ measure();
264
+ };
265
+ const mutation = new MutationObserver(watch);
266
+ mutation.observe(area, { childList: true });
267
+ watch();
268
+ return () => {
269
+ resize?.disconnect();
270
+ mutation.disconnect();
271
+ };
272
+ }
273
+ /**
274
+ * Opens the list on the current server: scrolls it, and only it, to the row
275
+ * with `aria-current="true"` at once, with no animation, and again as the
276
+ * list takes its final size, until people scroll or press in it themselves.
277
+ */
278
+ function openOnCurrent(list) {
279
+ let pinned = true;
280
+ const show = () => {
281
+ if (!pinned) return;
282
+ const current = list.querySelector("[aria-current=\"true\"]");
283
+ if (!current) return;
284
+ const offset = parseFloat(getComputedStyle(list).paddingBottom) || 0;
285
+ scrollToElement(list, current, "vertical", {
286
+ block: "nearest",
287
+ offset
288
+ });
289
+ };
290
+ const release = () => {
291
+ pinned = false;
292
+ };
293
+ const events = [
294
+ "pointerdown",
295
+ "touchstart",
296
+ "wheel",
297
+ "keydown"
298
+ ];
299
+ for (const type of events) list.addEventListener(type, release, { passive: true });
300
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(show);
301
+ resize?.observe(list);
302
+ show();
303
+ return () => {
304
+ resize?.disconnect();
305
+ for (const type of events) list.removeEventListener(type, release);
306
+ };
307
+ }
191
308
  /** The server list: takes the room left in the popover and scrolls inside it,
192
- * a ScrollArea (a column with no gap, so its gutters move nothing). */
193
- function MobileNavServerPopoverList({ className, ...props }) {
309
+ * a ScrollArea (a column with no gap, so its gutters move nothing). It is
310
+ * never shorter than one whole two-line row, and it opens scrolled to the
311
+ * current server's row (`aria-current="true"`), with no animation. */
312
+ function MobileNavServerPopoverList({ className, ref, ...props }) {
194
313
  const { list } = mobileNavServer();
314
+ const setRef = useCallback((node) => {
315
+ assignRef(ref, node);
316
+ if (!node) return;
317
+ const stop = openOnCurrent(node);
318
+ return () => {
319
+ stop();
320
+ assignRef(ref, null);
321
+ };
322
+ }, [ref]);
195
323
  return /* @__PURE__ */ jsx(ScrollArea, {
196
324
  "data-slot": "mobile-nav-server-popover-list",
325
+ ref: setRef,
197
326
  overscroll: "contain",
198
327
  tabIndex: -1,
199
328
  ...props,
@@ -13,8 +13,17 @@ export declare function PopoverPopup({ className, ...props }: PopoverPopupProps)
13
13
  export type PopoverContentProps = StyledProps<Popover.Popup.Props> & Pick<Popover.Positioner.Props, "align" | "alignOffset" | "anchor" | "arrowPadding" | "collisionAvoidance" | "collisionBoundary" | "collisionPadding" | "disableAnchorTracking" | "positionMethod" | "side" | "sideOffset" | "sticky"> & {
14
14
  container?: PopoverPortalProps["container"];
15
15
  portalProps?: PopoverPortalProps;
16
+ /**
17
+ * `item` for a popover about an app's item, such as the question before
18
+ * its link previews are shown: 400px, and `PopoverBody`, `PopoverTitle`
19
+ * and `PopoverActions` take a card's parts and 12px inset, like the item
20
+ * card (PreviewCard `size="item"`). Choose it by what the popover is
21
+ * about, not by its parts.
22
+ * @default "default"
23
+ */
24
+ size?: "default" | "item";
16
25
  };
17
- export declare function PopoverContent({ align, alignOffset, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod, side, sideOffset, sticky, container, portalProps, className, ...props }: PopoverContentProps): import("react").JSX.Element;
26
+ export declare function PopoverContent({ align, alignOffset, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod, side, sideOffset, sticky, container, portalProps, size, className, ...props }: PopoverContentProps): import("react").JSX.Element;
18
27
  export type PopoverViewportProps = StyledProps<Popover.Viewport.Props>;
19
28
  export declare function PopoverViewport({ className, ...props }: PopoverViewportProps): import("react").JSX.Element;
20
29
  export type PopoverHeaderProps = ComponentPropsWithRef<"div">;
@@ -32,7 +41,7 @@ export declare function PopoverArrow({ className, ...props }: PopoverArrowProps)
32
41
  export type PopoverBackdropProps = StyledProps<Popover.Backdrop.Props>;
33
42
  export declare function PopoverBackdrop({ className, ...props }: PopoverBackdropProps): import("react").JSX.Element;
34
43
  export type PopoverBodyProps = ComponentPropsWithRef<"div">;
35
- /** The panel's text: a `PopoverTitle` and a `PopoverDescription`, 16px in from its edges. */
44
+ /** The panel's text: a `PopoverTitle` and a `PopoverDescription`, 16px in from its edges (12px in a `size="item"` popover). */
36
45
  export declare function PopoverBody({ className, ...props }: PopoverBodyProps): import("react").JSX.Element;
37
46
  export type PopoverActionsProps = ComponentPropsWithRef<"div">;
38
47
  /** The panel's buttons, at its end, under `PopoverBody`: the way out first, then the confirm button. */
@@ -1,37 +1,46 @@
1
1
  import { tv } from "../../lib/tv.mjs";
2
2
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
3
3
  import { usePopupCollision } from "../../lib/frame-insets.mjs";
4
+ import { messageCard } from "../message-embed/message-embed.recipe.mjs";
5
+ import { createContext, useContext } from "react";
4
6
  import { jsx } from "react/jsx-runtime";
5
7
  import { Popover } from "@base-ui/react/popover";
6
8
  //#region src/components/popover/popover.tsx
7
- const popover = tv({ slots: {
8
- trigger: ["select-none"],
9
- positioner: ["isolate z-50 outline-none"],
10
- content: [
11
- "max-h-(--available-height) min-w-[220px] overflow-hidden",
12
- "outline-none",
13
- "rounded-container bg-layer-popover text-foreground",
14
- "shadow-float ring-1 ring-line-seam"
15
- ],
16
- header: ["flex items-center justify-between px-3", "min-h-11 gap-2 py-1.5 pr-1.5 pl-4"],
17
- body: ["flex min-w-0 flex-col gap-1.5", "px-4 pt-4 pb-3 last:pb-4"],
18
- actions: ["flex flex-wrap items-center justify-end gap-2", "px-4 pb-3 last:pb-4"],
19
- separator: ["border-t", "border-line-hairline"],
20
- title: ["m-0", "font-sans text-headline font-bold text-foreground"],
21
- description: ["m-0", "font-sans text-footnote text-foreground-muted"],
22
- close: [
23
- "inline-flex items-center justify-center",
24
- "outline-none transition-colors",
25
- "h-8 min-w-8 rounded-chip px-1.5",
26
- "font-sans text-footnote font-bold text-foreground",
27
- "hover:bg-fill-medium active:bg-fill-medium",
28
- "focus-visible:shadow-focus",
29
- "[&_svg]:size-4.5"
30
- ],
31
- arrow: ["size-2 rotate-45", "border-l border-t border-line-seam bg-layer-popover"],
32
- backdrop: ["fixed inset-0", "bg-transparent"],
33
- viewport: ["block"]
34
- } });
9
+ const popover = tv({
10
+ slots: {
11
+ trigger: ["select-none"],
12
+ positioner: ["isolate z-50 outline-none"],
13
+ content: [
14
+ "max-h-(--available-height) min-w-[220px] overflow-hidden",
15
+ "outline-none",
16
+ "rounded-container bg-layer-popover text-foreground",
17
+ "shadow-float ring-1 ring-line-seam"
18
+ ],
19
+ header: ["flex items-center justify-between px-3", "min-h-11 gap-2 py-1.5 pr-1.5 pl-4"],
20
+ body: ["flex min-w-0 flex-col gap-1.5", "px-4 pt-4 pb-3 last:pb-4"],
21
+ actions: ["flex flex-wrap items-center justify-end gap-2", "px-4 pb-3 last:pb-4"],
22
+ separator: ["border-t", "border-line-hairline"],
23
+ title: ["m-0", "font-sans text-headline font-bold text-foreground"],
24
+ description: ["m-0", "font-sans text-footnote text-foreground-muted"],
25
+ close: [
26
+ "inline-flex items-center justify-center",
27
+ "outline-none transition-colors",
28
+ "h-8 min-w-8 rounded-chip px-1.5",
29
+ "font-sans text-footnote font-bold text-foreground",
30
+ "hover:bg-fill-medium active:bg-fill-medium",
31
+ "focus-visible:shadow-focus",
32
+ "[&_svg]:size-4.5"
33
+ ],
34
+ arrow: ["size-2 rotate-45", "border-l border-t border-line-seam bg-layer-popover"],
35
+ backdrop: ["fixed inset-0", "bg-transparent"],
36
+ viewport: ["block"]
37
+ },
38
+ variants: { size: {
39
+ default: {},
40
+ item: { content: messageCard().popover() }
41
+ } }
42
+ });
43
+ const PopoverSizeContext = createContext("default");
35
44
  function Popover$1(props) {
36
45
  return /* @__PURE__ */ jsx(Popover.Root, {
37
46
  "data-slot": "popover",
@@ -67,8 +76,8 @@ function PopoverFramePositioner({ collisionBoundary, collisionPadding, container
67
76
  ...props
68
77
  });
69
78
  }
70
- function PopoverContent({ align = "start", alignOffset = 0, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod = "fixed", side = "bottom", sideOffset = 4, sticky, container, portalProps, className, ...props }) {
71
- const { positioner, content } = popover();
79
+ function PopoverContent({ align = "start", alignOffset = 0, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod = "fixed", side = "bottom", sideOffset = 4, sticky, container, portalProps, size = "default", className, ...props }) {
80
+ const { positioner, content } = popover({ size });
72
81
  const portalContainer = container !== void 0 ? container : portalProps?.container;
73
82
  return /* @__PURE__ */ jsx(PopoverPortal, {
74
83
  ...portalProps,
@@ -88,10 +97,14 @@ function PopoverContent({ align = "start", alignOffset = 0, anchor, arrowPadding
88
97
  sideOffset,
89
98
  sticky,
90
99
  className: positioner(),
91
- children: /* @__PURE__ */ jsx(Popover.Popup, {
92
- "data-slot": "popover-content",
93
- className: content({ className }),
94
- ...props
100
+ children: /* @__PURE__ */ jsx(PopoverSizeContext.Provider, {
101
+ value: size,
102
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
103
+ "data-slot": "popover-content",
104
+ "data-size": size,
105
+ className: content({ className }),
106
+ ...props
107
+ })
95
108
  })
96
109
  })
97
110
  });
@@ -123,7 +136,7 @@ function PopoverSeparator({ className, ...props }) {
123
136
  });
124
137
  }
125
138
  function PopoverTitle({ className, ...props }) {
126
- const { title } = popover();
139
+ const title = useContext(PopoverSizeContext) === "item" ? messageCard().title : popover().title;
127
140
  return /* @__PURE__ */ jsx(Popover.Title, {
128
141
  "data-slot": "popover-title",
129
142
  className: title({ className }),
@@ -162,9 +175,9 @@ function PopoverBackdrop({ className, ...props }) {
162
175
  ...props
163
176
  });
164
177
  }
165
- /** The panel's text: a `PopoverTitle` and a `PopoverDescription`, 16px in from its edges. */
178
+ /** The panel's text: a `PopoverTitle` and a `PopoverDescription`, 16px in from its edges (12px in a `size="item"` popover). */
166
179
  function PopoverBody({ className, ...props }) {
167
- const { body } = popover();
180
+ const body = useContext(PopoverSizeContext) === "item" ? messageCard().body : popover().body;
168
181
  return /* @__PURE__ */ jsx("div", {
169
182
  "data-slot": "popover-body",
170
183
  className: body({ className }),
@@ -173,7 +186,7 @@ function PopoverBody({ className, ...props }) {
173
186
  }
174
187
  /** The panel's buttons, at its end, under `PopoverBody`: the way out first, then the confirm button. */
175
188
  function PopoverActions({ className, ...props }) {
176
- const { actions } = popover();
189
+ const actions = useContext(PopoverSizeContext) === "item" ? messageCard().actions : popover().actions;
177
190
  return /* @__PURE__ */ jsx("div", {
178
191
  "data-slot": "popover-actions",
179
192
  className: actions({ className }),
@@ -28,7 +28,11 @@ export declare function PreviewCardDescription({ className, ...props }: PreviewC
28
28
  export type PreviewCardArrowProps = StyledProps<PreviewCard.Arrow.Props>;
29
29
  export declare function PreviewCardArrow({ className, ...props }: PreviewCardArrowProps): import("react").JSX.Element;
30
30
  export type PreviewCardBodyProps = ComponentPropsWithRef<"div">;
31
- /** The item card's block: the id line, the title and the details row, in that order. */
31
+ /**
32
+ * The item card's block: the id line, the title and the details row, in that
33
+ * order. Its parts are a card's in a message (AppLinkCard), so the two read
34
+ * the same.
35
+ */
32
36
  export declare function PreviewCardBody({ className, ...props }: PreviewCardBodyProps): import("react").JSX.Element;
33
37
  export type PreviewCardEyebrowProps = ComponentPropsWithRef<"p">;
34
38
  /** The muted line above the title that identifies the item, such as its id. One line, truncated. */
@@ -2,6 +2,8 @@ import { tv } from "../../lib/tv.mjs";
2
2
  import { useTheme } from "../../lib/theme/use-theme.mjs";
3
3
  import { resolveThemeScopeProps, useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
4
4
  import { usePopupCollision } from "../../lib/frame-insets.mjs";
5
+ import { messageCard } from "../message-embed/message-embed.recipe.mjs";
6
+ import { createContext, useContext } from "react";
5
7
  import { jsx } from "react/jsx-runtime";
6
8
  import { PreviewCard } from "@base-ui/react/preview-card";
7
9
  //#region src/components/preview-card/preview-card.tsx
@@ -24,24 +26,11 @@ const previewCard = tv({
24
26
  separator: ["border-t", "border-line-hairline"],
25
27
  title: ["m-0 truncate", "font-sans text-headline font-bold text-foreground"],
26
28
  description: ["m-0 truncate", "px-4 py-3 last:pb-4 font-sans text-callout text-foreground-muted"],
27
- arrow: ["size-2 rotate-45", "border-l border-t border-line-seam bg-layer-popover"],
28
- body: [
29
- "flex min-w-0 flex-col gap-1.5",
30
- "px-4 pt-4 pb-3 last:pb-4",
31
- "*:data-[slot=preview-card-title]:line-clamp-2 *:data-[slot=preview-card-title]:whitespace-normal"
32
- ],
33
- eyebrow: ["m-0 truncate", "font-mono text-mono-meta text-foreground-muted"],
34
- meta: ["flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", "m-0 font-sans text-callout text-foreground-muted"],
35
- actions: ["flex flex-wrap items-center justify-end gap-2", "px-4 pb-3 last:pb-4"],
36
- footer: [
37
- "m-0 truncate",
38
- "border-t border-line-hairline px-4 py-2",
39
- "font-sans text-caption text-foreground-muted"
40
- ]
29
+ arrow: ["size-2 rotate-45", "border-l border-t border-line-seam bg-layer-popover"]
41
30
  },
42
31
  variants: { size: {
43
32
  profile: { popup: "w-[300px]" },
44
- item: { popup: "w-[400px]" }
33
+ item: { popup: messageCard().popover() }
45
34
  } },
46
35
  defaultVariants: { size: "profile" }
47
36
  });
@@ -132,8 +121,9 @@ function PreviewCardSeparator({ className, ...props }) {
132
121
  ...props
133
122
  });
134
123
  }
124
+ const InItemBody = createContext(false);
135
125
  function PreviewCardTitle({ className, ...props }) {
136
- const { title } = previewCard();
126
+ const title = useContext(InItemBody) ? messageCard().title : previewCard().title;
137
127
  return /* @__PURE__ */ jsx("p", {
138
128
  "data-slot": "preview-card-title",
139
129
  className: title({ className }),
@@ -156,18 +146,25 @@ function PreviewCardArrow({ className, ...props }) {
156
146
  ...props
157
147
  });
158
148
  }
159
- /** The item card's block: the id line, the title and the details row, in that order. */
149
+ /**
150
+ * The item card's block: the id line, the title and the details row, in that
151
+ * order. Its parts are a card's in a message (AppLinkCard), so the two read
152
+ * the same.
153
+ */
160
154
  function PreviewCardBody({ className, ...props }) {
161
- const { body } = previewCard();
162
- return /* @__PURE__ */ jsx("div", {
163
- "data-slot": "preview-card-body",
164
- className: body({ className }),
165
- ...props
155
+ const { body } = messageCard();
156
+ return /* @__PURE__ */ jsx(InItemBody.Provider, {
157
+ value: true,
158
+ children: /* @__PURE__ */ jsx("div", {
159
+ "data-slot": "preview-card-body",
160
+ className: body({ className }),
161
+ ...props
162
+ })
166
163
  });
167
164
  }
168
165
  /** The muted line above the title that identifies the item, such as its id. One line, truncated. */
169
166
  function PreviewCardEyebrow({ className, ...props }) {
170
- const { eyebrow } = previewCard();
167
+ const { eyebrow } = messageCard();
171
168
  return /* @__PURE__ */ jsx("p", {
172
169
  "data-slot": "preview-card-eyebrow",
173
170
  className: eyebrow({ className }),
@@ -176,7 +173,7 @@ function PreviewCardEyebrow({ className, ...props }) {
176
173
  }
177
174
  /** The row under the title: one state chip, then muted details. Wraps when it runs out of room. */
178
175
  function PreviewCardMeta({ className, ...props }) {
179
- const { meta } = previewCard();
176
+ const { meta } = messageCard();
180
177
  return /* @__PURE__ */ jsx("div", {
181
178
  "data-slot": "preview-card-meta",
182
179
  className: meta({ className }),
@@ -185,7 +182,7 @@ function PreviewCardMeta({ className, ...props }) {
185
182
  }
186
183
  /** The row of at most two buttons. */
187
184
  function PreviewCardActions({ className, ...props }) {
188
- const { actions } = previewCard();
185
+ const { actions } = messageCard();
189
186
  return /* @__PURE__ */ jsx("div", {
190
187
  "data-slot": "preview-card-actions",
191
188
  className: actions({ className }),
@@ -194,10 +191,10 @@ function PreviewCardActions({ className, ...props }) {
194
191
  }
195
192
  /** The caption under a hairline at the bottom of the card: where the item comes from. */
196
193
  function PreviewCardFooter({ className, ...props }) {
197
- const { footer } = previewCard();
194
+ const { sourceFooter } = messageCard();
198
195
  return /* @__PURE__ */ jsx("p", {
199
196
  "data-slot": "preview-card-footer",
200
- className: footer({ className }),
197
+ className: sourceFooter({ className }),
201
198
  ...props
202
199
  });
203
200
  }
@@ -2,22 +2,29 @@ import { ComponentPropsWithRef } from "react";
2
2
  //#region src/components/save-status/save-status.d.ts
3
3
  export type SaveStatusProps = ComponentPropsWithRef<"span">;
4
4
  /**
5
- * The reserved line itself. Render it whatever the setting's state, and put
6
- * `SaveStatusSaved` or `SaveStatusError` inside. A `span` (shown as a block),
7
- * like every ListRow part, so it stays valid inside a row's body.
5
+ * The line under a setting that applies at once. Render it whatever the
6
+ * setting's state, with `SaveStatusError` inside when the change failed and
7
+ * nothing inside otherwise: empty, it is hidden and takes no height. A `span`
8
+ * (shown as a block), like every ListRow part, so it stays valid inside a
9
+ * row's body; give it an `id` for the control's `aria-describedby`.
8
10
  */
9
11
  export declare function SaveStatus({ className, ...props }: SaveStatusProps): import("react").JSX.Element;
10
12
  export type SaveStatusSavedProps = ComponentPropsWithRef<"span">;
11
13
  /**
12
- * The polite live region that says the change is stored. Keep it mounted,
13
- * empty, while nothing has happened, so a screen reader announces the words
14
- * when they arrive; the check shows only with them.
14
+ * @deprecated A setting that applies at once no longer says "Saved": the
15
+ * control showing its new value is the confirmation (Forms, settings that
16
+ * apply at once). Renders nothing, whatever its children, so its
17
+ * `SaveStatus` stays empty and takes no height. Leave it out: render
18
+ * `SaveStatusError` when the change failed and nothing otherwise. It drops
19
+ * every prop, its `id` too: point a control's `aria-describedby` at the
20
+ * `SaveStatus` instead.
15
21
  */
16
- export declare function SaveStatusSaved({ className, children, ...props }: SaveStatusSavedProps): import("react").JSX.Element;
22
+ export declare function SaveStatusSaved(_props: SaveStatusSavedProps): null;
17
23
  export type SaveStatusErrorProps = ComponentPropsWithRef<"span">;
18
24
  /**
19
- * What failed, in place of `SaveStatusSaved`. It is an alert, announced when
20
- * it appears, and stays until the next change.
25
+ * What failed. It is an alert, announced when it appears, and stays until
26
+ * the next change. Its line appears with it, so the setting grows by one
27
+ * line while it shows.
21
28
  */
22
29
  export declare function SaveStatusError({ className, role, ...props }: SaveStatusErrorProps): import("react").JSX.Element;
23
30
  //#endregion
@@ -1,24 +1,20 @@
1
1
  import { tv } from "../../lib/tv.mjs";
2
- import { CircleCheck } from "lucide-react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx } from "react/jsx-runtime";
4
3
  //#region src/components/save-status/save-status.tsx
5
4
  const saveStatus = tv({ slots: {
6
5
  root: [
7
- "block min-h-lh min-w-0",
6
+ "block min-w-0 empty:hidden",
8
7
  "mt-1 [[data-slot=field]>&]:-mt-0.5 [[data-slot=list-row-body]>&]:mt-0.5",
9
8
  "text-footnote"
10
9
  ],
11
- saved: [
12
- "flex min-w-0 items-center gap-1",
13
- "text-foreground-muted",
14
- "[&>svg]:size-3 [&>svg]:shrink-0"
15
- ],
16
10
  error: ["block wrap-anywhere", "text-danger-strong"]
17
11
  } });
18
12
  /**
19
- * The reserved line itself. Render it whatever the setting's state, and put
20
- * `SaveStatusSaved` or `SaveStatusError` inside. A `span` (shown as a block),
21
- * like every ListRow part, so it stays valid inside a row's body.
13
+ * The line under a setting that applies at once. Render it whatever the
14
+ * setting's state, with `SaveStatusError` inside when the change failed and
15
+ * nothing inside otherwise: empty, it is hidden and takes no height. A `span`
16
+ * (shown as a block), like every ListRow part, so it stays valid inside a
17
+ * row's body; give it an `id` for the control's `aria-describedby`.
22
18
  */
23
19
  function SaveStatus({ className, ...props }) {
24
20
  const { root } = saveStatus();
@@ -29,28 +25,21 @@ function SaveStatus({ className, ...props }) {
29
25
  });
30
26
  }
31
27
  /**
32
- * The polite live region that says the change is stored. Keep it mounted,
33
- * empty, while nothing has happened, so a screen reader announces the words
34
- * when they arrive; the check shows only with them.
28
+ * @deprecated A setting that applies at once no longer says "Saved": the
29
+ * control showing its new value is the confirmation (Forms, settings that
30
+ * apply at once). Renders nothing, whatever its children, so its
31
+ * `SaveStatus` stays empty and takes no height. Leave it out: render
32
+ * `SaveStatusError` when the change failed and nothing otherwise. It drops
33
+ * every prop, its `id` too: point a control's `aria-describedby` at the
34
+ * `SaveStatus` instead.
35
35
  */
36
- function SaveStatusSaved({ className, children, ...props }) {
37
- const { saved } = saveStatus();
38
- const hasContent = children !== null && children !== void 0 && children !== false && children !== "";
39
- return /* @__PURE__ */ jsxs("span", {
40
- "data-slot": "save-status-saved",
41
- role: "status",
42
- "aria-live": "polite",
43
- className: saved({ className }),
44
- ...props,
45
- children: [hasContent ? /* @__PURE__ */ jsx(CircleCheck, {
46
- "aria-hidden": "true",
47
- "data-slot": "save-status-icon"
48
- }) : null, children]
49
- });
36
+ function SaveStatusSaved(_props) {
37
+ return null;
50
38
  }
51
39
  /**
52
- * What failed, in place of `SaveStatusSaved`. It is an alert, announced when
53
- * it appears, and stays until the next change.
40
+ * What failed. It is an alert, announced when it appears, and stays until
41
+ * the next change. Its line appears with it, so the setting grows by one
42
+ * line while it shows.
54
43
  */
55
44
  function SaveStatusError({ className, role = "alert", ...props }) {
56
45
  const { error } = saveStatus();
@@ -28,7 +28,7 @@ export type SegmentedControlProps<T extends string = string> = StyledProps<Radio
28
28
  */
29
29
  variant?: "default" | "on-frame";
30
30
  };
31
- export declare function SegmentedControl<T extends string = string>({ disabled, iconsOnlyBelow, variant, className, ref, ...props }: SegmentedControlProps<T>): import("react").JSX.Element;
31
+ export declare function SegmentedControl<T extends string = string>({ disabled, iconsOnlyBelow: iconsOnlyBelowProp, variant, className, ref, ...props }: SegmentedControlProps<T>): import("react").JSX.Element;
32
32
  export type SegmentedControlItemProps<T extends string = string> = StyledProps<Radio.Root.Props<T>>;
33
33
  export declare function SegmentedControlItem<T extends string = string>({ className, ref, ...props }: SegmentedControlItemProps<T>): import("react").JSX.Element;
34
34
  export type SegmentedControlLabelProps = ComponentPropsWithRef<"span">;