@botiverse/raft-ui 0.95.4 → 0.96.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 (31) hide show
  1. package/CHANGELOG.md +126 -98
  2. package/dist/components/combobox/combobox.mjs +1 -1
  3. package/dist/components/combobox-select/combobox-select.mjs +1 -1
  4. package/dist/components/context-menu/context-menu.mjs +1 -1
  5. package/dist/components/dialog/dialog-sheet-context.mjs +4 -2
  6. package/dist/components/dialog/dialog-sheet.mjs +9 -1
  7. package/dist/components/dialog/dialog.mjs +22 -10
  8. package/dist/components/drawer/drawer.d.mts +1 -1
  9. package/dist/components/drawer/drawer.mjs +17 -4
  10. package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
  11. package/dist/components/lightbox/lightbox.d.mts +1 -1
  12. package/dist/components/lightbox/lightbox.mjs +11 -9
  13. package/dist/components/notification-center/notification-center.mjs +1 -1
  14. package/dist/components/popover/popover.mjs +1 -1
  15. package/dist/components/preview-card/preview-card.mjs +1 -1
  16. package/dist/components/select/select.mjs +1 -1
  17. package/dist/components/tooltip/tooltip.mjs +1 -1
  18. package/dist/lib/frame-insets.mjs +56 -12
  19. package/dist/lib/overlay-depth.d.mts +5 -0
  20. package/dist/lib/overlay-depth.mjs +30 -11
  21. package/dist/lib/popup-boundary.d.mts +24 -0
  22. package/dist/lib/popup-boundary.mjs +256 -0
  23. package/dist/lint/no-raw-z-index.mjs +4 -4
  24. package/dist/lint/prefer-scroll-area.d.mts +60 -19
  25. package/dist/lint/prefer-scroll-area.mjs +393 -38
  26. package/dist/wip-exports.d.mts +5 -4
  27. package/dist/wip-exports.mjs +4 -3
  28. package/dist/wip.d.mts +2 -1
  29. package/package.json +1 -1
  30. package/skills/raft-ui-guide/SKILL.md +3 -1
  31. package/skills/raft-ui-guide/rules/layout.md +1 -0
@@ -289,7 +289,7 @@ function ComboboxSeparator({ className, ...props }) {
289
289
  });
290
290
  }
291
291
  function ComboboxFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
292
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
292
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container, void 0, props.anchor);
293
293
  return /* @__PURE__ */ jsx(Combobox.Positioner, {
294
294
  ...collision,
295
295
  ...props
@@ -68,7 +68,7 @@ const PHONE_LIST_COLLISION = {
68
68
  fallbackAxisSide: "none"
69
69
  };
70
70
  function ComboboxSelectFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
71
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
71
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container, void 0, props.anchor);
72
72
  return /* @__PURE__ */ jsx(Combobox.Positioner, {
73
73
  ...collision,
74
74
  ...props
@@ -85,7 +85,7 @@ function ContextMenuPortal(props) {
85
85
  });
86
86
  }
87
87
  function ContextMenuFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
88
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
88
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container, void 0, props.anchor);
89
89
  return /* @__PURE__ */ jsx(ContextMenu.Positioner, {
90
90
  ...collision,
91
91
  ...props
@@ -1,3 +1,4 @@
1
+ import { PopupSheetReset } from "../../lib/popup-boundary.mjs";
1
2
  import { tv } from "../../lib/tv.mjs";
2
3
  import { createContext, createElement, useContext } from "react";
3
4
  // src/components/dialog/dialog-sheet-context.ts
@@ -25,10 +26,11 @@ function useInDialogSheetHead() {
25
26
  /**
26
27
  * Every overlay surface starts outside any sheet: a confirm, a dialog or a
27
28
  * drawer opened from inside a sheet keeps its own head, footer and commit
28
- * button instead of taking the sheet's.
29
+ * button instead of taking the sheet's, and its popups aren't kept inside the
30
+ * sheet below it (PopupSheetContext).
29
31
  */
30
32
  function DialogSheetReset({ children }) {
31
- return createElement(DialogSheetSurfaceContext.Provider, { value: false }, createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, children)));
33
+ return createElement(DialogSheetSurfaceContext.Provider, { value: false }, createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, createElement(PopupSheetReset, null, children))));
32
34
  }
33
35
  /**
34
36
  * A dialog drawn as a phone sheet (design: dialog.md "On a phone"). The
@@ -1,4 +1,5 @@
1
1
  import { cn } from "../../cn.mjs";
2
+ import { PopupSheetContext } from "../../lib/popup-boundary.mjs";
2
3
  import { useMobileViewport } from "../../lib/use-mobile-viewport.mjs";
3
4
  import { useSoftKeyboard } from "../../lib/use-soft-keyboard.mjs";
4
5
  import { Button } from "../button/button.mjs";
@@ -40,6 +41,10 @@ function DialogSheetContent({ className, overlay, placement, style, ref, childre
40
41
  setPopup(element);
41
42
  assignRef(ref, element);
42
43
  }, [ref]);
44
+ const popupSheet = useMemo(() => phone && popup ? {
45
+ head: () => popup.querySelector("[data-slot=\"dialog-header\"]"),
46
+ surface: () => popup
47
+ } : void 0, [phone, popup]);
43
48
  const sheet = dialogSheet();
44
49
  const sheetOverlay = useMemo(() => overlay === false ? false : {
45
50
  ...typeof overlay === "object" ? overlay : null,
@@ -65,7 +70,10 @@ function DialogSheetContent({ className, overlay, placement, style, ref, childre
65
70
  "data-sheet": phone ? "" : void 0,
66
71
  children: /* @__PURE__ */ jsx(DialogSheetContext.Provider, {
67
72
  value: state,
68
- children
73
+ children: /* @__PURE__ */ jsx(PopupSheetContext.Provider, {
74
+ value: popupSheet,
75
+ children
76
+ })
69
77
  })
70
78
  })
71
79
  });
@@ -1,6 +1,7 @@
1
1
  import { cn } from "../../cn.mjs";
2
2
  import { barButtonDesktop, headEdgePull } from "../../lib/head-edges.mjs";
3
3
  import { decisionWidth } from "../../lib/decision-width.mjs";
4
+ import { PopupSheetReset } from "../../lib/popup-boundary.mjs";
4
5
  import { ScrollArea } from "../scroll-area/scroll-area.mjs";
5
6
  import { stackedActions, useStackedActions } from "../../lib/stacked-actions.mjs";
6
7
  import { tv } from "../../lib/tv.mjs";
@@ -23,6 +24,7 @@ import { createPortal } from "react-dom";
23
24
  const dialog = tv({
24
25
  slots: {
25
26
  overlay: ["fixed inset-0 z-dialog bg-layer-backdrop"],
27
+ portal: [],
26
28
  content: [
27
29
  "fixed top-1/2 left-1/2 z-dialog flex w-[calc(100%-2rem)] -translate-1/2 flex-col",
28
30
  "max-h-[calc(100dvh-2rem)] outline-none",
@@ -84,9 +86,13 @@ const dialog = tv({
84
86
  },
85
87
  1: {
86
88
  overlay: ["z-dialog-layer"],
87
- content: ["z-dialog-layer"]
89
+ content: []
88
90
  }
89
91
  },
92
+ lifted: {
93
+ false: {},
94
+ true: { portal: ["relative isolate z-dialog-layer"] }
95
+ },
90
96
  nested: {
91
97
  false: {},
92
98
  true: { overlay: ["bg-layer-backdrop-nested"] }
@@ -141,19 +147,25 @@ function DialogContent({ className, children, portalProps, overlay, size, layer,
141
147
  const sheetSurface = useContext(DialogSheetSurfaceContext);
142
148
  const [keyboard, setKeyboard] = useState(null);
143
149
  const raised = !sheetSurface && keyboard != null;
144
- const { content } = dialog({
150
+ const lifted = layer === 1;
151
+ const { content, portal } = dialog({
145
152
  size,
146
- layer,
147
153
  placement,
154
+ lifted,
148
155
  keyboard: raised || void 0
149
156
  });
150
- const overlayProps = typeof overlay === "object" ? overlay : {};
157
+ const { layer: overlayLayer, ...overlayRest } = typeof overlay === "object" ? overlay : {};
158
+ const overlayProps = lifted ? overlayRest : {
159
+ layer: overlayLayer,
160
+ ...overlayRest
161
+ };
162
+ const portalClassName = portalProps?.className;
163
+ const liftedPortalClassName = typeof portalClassName === "function" ? (state) => portal({ className: portalClassName(state) }) : portal({ className: portalClassName });
151
164
  return /* @__PURE__ */ jsxs(DialogPortal, {
152
165
  ...portalProps,
153
- children: [overlay === false ? null : /* @__PURE__ */ jsx(DialogOverlay, {
154
- layer,
155
- ...overlayProps
156
- }), /* @__PURE__ */ jsxs(Dialog.Popup, {
166
+ "data-layer": lifted ? 1 : void 0,
167
+ className: lifted ? liftedPortalClassName : portalClassName,
168
+ children: [overlay === false ? null : /* @__PURE__ */ jsx(DialogOverlay, { ...overlayProps }), /* @__PURE__ */ jsxs(Dialog.Popup, {
157
169
  "data-slot": "dialog-content",
158
170
  "data-placement": top ? "top" : void 0,
159
171
  "data-soft-keyboard": raised ? "" : void 0,
@@ -255,9 +267,9 @@ function DialogHeader({ className, bar = false, children, ...props }) {
255
267
  "data-sheet": "",
256
268
  className: dialog({ bar: false }).header({ className: cn(head.header(), className) }),
257
269
  ...props,
258
- children: [/* @__PURE__ */ jsxs(DialogSheetHeadContext.Provider, {
270
+ children: [/* @__PURE__ */ jsx(DialogSheetHeadContext.Provider, {
259
271
  value: true,
260
- children: [backs.length > 0 ? backs : closes, rest]
272
+ children: /* @__PURE__ */ jsxs(PopupSheetReset, { children: [backs.length > 0 ? backs : closes, rest] })
261
273
  }), /* @__PURE__ */ jsx("div", {
262
274
  ref: setCommitSlot,
263
275
  "data-slot": "dialog-commit-slot",
@@ -18,7 +18,7 @@ export declare function DrawerContent({ className, children, portalProps, ...pro
18
18
  export type DrawerCloseProps = StyledProps<Drawer.Close.Props> & Pick<ButtonProps, "variant" | "size">;
19
19
  export declare function DrawerClose({ className, variant, size, children, render, "aria-label": ariaLabel, ...props }: DrawerCloseProps): import("react").JSX.Element;
20
20
  export type DrawerHeaderProps = useRender.ComponentProps<"div">;
21
- export declare function DrawerHeader({ className, render, ...props }: DrawerHeaderProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
21
+ export declare function DrawerHeader({ className, render, children, ...props }: DrawerHeaderProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
22
22
  export type DrawerFooterProps = useRender.ComponentProps<"div">;
23
23
  export declare function DrawerFooter({ className, render, ...props }: DrawerFooterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
24
24
  export type DrawerBodyProps = useRender.ComponentProps<"div">;
@@ -1,11 +1,12 @@
1
1
  import { cn } from "../../cn.mjs";
2
+ import { PopupSheetContext, PopupSheetReset } from "../../lib/popup-boundary.mjs";
2
3
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
3
4
  import { Button } from "../button/button.mjs";
4
5
  import { DialogSheetReset } from "../dialog/dialog-sheet-context.mjs";
5
6
  import { drawer } from "./drawer.recipe.mjs";
6
7
  import { useParentOverlayKind, withOverlayLayer } from "../../lib/overlay-depth.mjs";
7
8
  import { X } from "lucide-react";
8
- import { createContext, useContext, useMemo } from "react";
9
+ import { createContext, useContext, useMemo, useState } from "react";
9
10
  import { jsx, jsxs } from "react/jsx-runtime";
10
11
  import { mergeProps } from "@base-ui/react/merge-props";
11
12
  import { useRender } from "@base-ui/react/use-render";
@@ -89,6 +90,11 @@ function DrawerContent({ className, children, portalProps, ...props }) {
89
90
  axis: swipeAxis,
90
91
  snapPoints: hasSnapPoints
91
92
  });
93
+ const [surface, setSurface] = useState(null);
94
+ const popupSheet = useMemo(() => surface ? {
95
+ head: () => surface.querySelector("[data-slot=\"drawer-header\"]"),
96
+ surface: () => surface
97
+ } : void 0, [surface]);
92
98
  return /* @__PURE__ */ jsxs(DrawerPortal, {
93
99
  ...portalProps,
94
100
  children: [modal === true ? /* @__PURE__ */ jsx(DrawerOverlay, {}) : null, /* @__PURE__ */ jsx(Drawer.Viewport, {
@@ -99,9 +105,13 @@ function DrawerContent({ className, children, portalProps, ...props }) {
99
105
  className: popup({ className }),
100
106
  ...props,
101
107
  children: [hasSnapPoints || swipeDirection === "down" ? /* @__PURE__ */ jsx(DrawerSwipeHandle, {}) : null, /* @__PURE__ */ jsx(Drawer.Content, {
108
+ ref: setSurface,
102
109
  "data-slot": "drawer-content",
103
110
  className: content(),
104
- children: /* @__PURE__ */ jsx(DialogSheetReset, { children })
111
+ children: /* @__PURE__ */ jsx(DialogSheetReset, { children: /* @__PURE__ */ jsx(PopupSheetContext.Provider, {
112
+ value: popupSheet,
113
+ children
114
+ }) })
105
115
  })]
106
116
  })
107
117
  })]
@@ -130,14 +140,17 @@ function DrawerClose({ className, variant, size, children, render, "aria-label":
130
140
  children: children ?? /* @__PURE__ */ jsx(X, { className: closeIcon() })
131
141
  });
132
142
  }
133
- function DrawerHeader({ className, render, ...props }) {
143
+ function DrawerHeader({ className, render, children, ...props }) {
134
144
  const { hasSnapPoints, swipeAxis, swipeDirection } = useDrawerConfig();
135
145
  const { header } = drawer({
136
146
  side: swipeDirection,
137
147
  axis: swipeAxis,
138
148
  snapPoints: hasSnapPoints
139
149
  });
140
- const defaultProps = { className: header({ className }) };
150
+ const defaultProps = {
151
+ className: header({ className }),
152
+ children: /* @__PURE__ */ jsx(PopupSheetReset, { children })
153
+ };
141
154
  return useRender({
142
155
  defaultTagName: "div",
143
156
  render,
@@ -101,7 +101,7 @@ function DropdownMenuPopup({ className, ...props }) {
101
101
  });
102
102
  }
103
103
  function DropdownMenuFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
104
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container);
104
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container, void 0, props.anchor);
105
105
  return /* @__PURE__ */ jsx(Menu.Positioner, {
106
106
  ...collision,
107
107
  ...props
@@ -61,7 +61,7 @@ export type LightboxContentProps = StyledProps<Dialog.Popup.Props>;
61
61
  * A lightbox opened from state passes `finalFocus={openerRef}` so focus goes
62
62
  * back to what opened it.
63
63
  */
64
- export declare function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref, ...props }: LightboxContentProps): import("react").JSX.Element;
64
+ export declare function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref, children, ...props }: LightboxContentProps): import("react").JSX.Element;
65
65
  export type LightboxHeaderProps = useRender.ComponentProps<"div">;
66
66
  export declare function LightboxHeader({ render, className, ...props }: LightboxHeaderProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
67
67
  export type LightboxToolbarProps = useRender.ComponentProps<"div">;
@@ -1,6 +1,7 @@
1
1
  import { cn } from "../../cn.mjs";
2
2
  import { headEdgePull } from "../../lib/head-edges.mjs";
3
- import { OverlayLayerProvider, useParentOverlayKind } from "../../lib/overlay-depth.mjs";
3
+ import { OverlayLayer, useParentOverlayKind } from "../../lib/overlay-depth.mjs";
4
+ import { PopupSheetReset } from "../../lib/popup-boundary.mjs";
4
5
  import { tv } from "../../lib/tv.mjs";
5
6
  import { useMergedRef } from "../../lib/use-merged-ref.mjs";
6
7
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
@@ -25,9 +26,9 @@ const lightbox = tv({
25
26
  header: [
26
27
  "pointer-events-auto shrink-0",
27
28
  "flex items-center gap-3 pb-3",
28
- "pt-[calc(env(safe-area-inset-top,0px)+--spacing(3))]",
29
- "pl-[calc(env(safe-area-inset-left,0px)+--spacing(4))]",
30
- "pr-[calc(env(safe-area-inset-right,0px)+--spacing(4))]",
29
+ "pt-[calc(env(safe-area-inset-top,0px)+(--spacing(3)))]",
30
+ "pl-[calc(env(safe-area-inset-left,0px)+(--spacing(4)))]",
31
+ "pr-[calc(env(safe-area-inset-right,0px)+(--spacing(4)))]",
31
32
  "bg-transparent shadow-none",
32
33
  headEdgePull
33
34
  ],
@@ -71,7 +72,7 @@ const lightbox = tv({
71
72
  "focus-visible:outline-transparent focus-visible:shadow-focus",
72
73
  "[&_svg]:size-5.5"
73
74
  ],
74
- infoDock: ["pointer-events-none absolute right-6 bottom-20 left-6 z-raised flex justify-center"],
75
+ infoDock: ["pointer-events-none absolute inset-x-6 bottom-20 z-raised flex justify-center"],
75
76
  infoBar: [
76
77
  "pointer-events-auto flex items-center",
77
78
  "w-[clamp(144px,20vw,224px)] max-w-[calc(100vw-48px)] min-w-36",
@@ -87,7 +88,7 @@ const lightbox = tv({
87
88
  "*:data-[slot=button]:after:inset-x-[-3px] *:data-[slot=button]:first:after:left-[-9.5px] *:data-[slot=button]:last:after:right-[-9.5px]",
88
89
  "pointer-coarse:*:data-[slot=button]:after:inset-y-[-9.5px]"
89
90
  ],
90
- thumbnailStrip: ["pointer-events-none absolute right-0 bottom-4 left-0 z-raised", "flex items-center justify-center gap-1.5"],
91
+ thumbnailStrip: ["pointer-events-none absolute inset-x-0 bottom-4 z-raised", "flex items-center justify-center gap-1.5"],
91
92
  thumbnail: [
92
93
  "group/lightbox-thumb pointer-events-auto relative size-12 overflow-hidden",
93
94
  "transition-[border-color,opacity,box-shadow] duration-150",
@@ -127,7 +128,7 @@ function Lightbox({ closeOnModW = true, defaultOpen = false, modal = true, onOpe
127
128
  onOpenChange: handleOpenChange,
128
129
  open,
129
130
  ...props,
130
- children: [modWClose, /* @__PURE__ */ jsx(OverlayLayerProvider, {
131
+ children: [modWClose, /* @__PURE__ */ jsx(OverlayLayer, {
131
132
  kind: "dialog",
132
133
  modal: true,
133
134
  children: /* @__PURE__ */ jsx(LightboxModalContext.Provider, {
@@ -281,7 +282,7 @@ function routeScrollKey(event) {
281
282
  * A lightbox opened from state passes `finalFocus={openerRef}` so focus goes
282
283
  * back to what opened it.
283
284
  */
284
- function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref, ...props }) {
285
+ function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref, children, ...props }) {
285
286
  const documentMode = useContext(LightboxModalContext) === "trap-focus";
286
287
  const nested = useParentOverlayKind() != null;
287
288
  const { backdrop, content } = lightbox({ nested });
@@ -307,7 +308,8 @@ function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref,
307
308
  finalFocus: finalFocus ?? documentMode,
308
309
  className: content({ className }),
309
310
  onKeyDown: handleKeyDown,
310
- ...props
311
+ ...props,
312
+ children: /* @__PURE__ */ jsx(PopupSheetReset, { children })
311
313
  })] });
312
314
  }
313
315
  function LightboxHeader({ render, className, ...props }) {
@@ -137,7 +137,7 @@ function NotificationCenterPortal(props) {
137
137
  });
138
138
  }
139
139
  function NotificationCenterFramePositioner({ collisionBoundary, collisionPadding, container, defaultCollisionPadding, ...props }) {
140
- const collision = usePopupCollision(collisionPadding, collisionBoundary, defaultCollisionPadding, container);
140
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, defaultCollisionPadding, container, void 0, props.anchor);
141
141
  return /* @__PURE__ */ jsx(Popover.Positioner, {
142
142
  ...collision,
143
143
  ...props
@@ -71,7 +71,7 @@ function PopoverPopup({ className, ...props }) {
71
71
  });
72
72
  }
73
73
  function PopoverFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
74
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container);
74
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container, void 0, props.anchor);
75
75
  return /* @__PURE__ */ jsx(Popover.Positioner, {
76
76
  ...collision,
77
77
  ...props
@@ -70,7 +70,7 @@ function PreviewCardPopup({ className, ...props }) {
70
70
  });
71
71
  }
72
72
  function PreviewCardFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
73
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container);
73
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container, void 0, props.anchor);
74
74
  return /* @__PURE__ */ jsx(PreviewCard.Positioner, {
75
75
  ...collision,
76
76
  ...props
@@ -344,7 +344,7 @@ function SelectPopup({ className, ...props }) {
344
344
  });
345
345
  }
346
346
  function SelectFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
347
- const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
347
+ const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container, void 0, props.anchor);
348
348
  return /* @__PURE__ */ jsx(Select.Positioner, {
349
349
  ...collision,
350
350
  ...props
@@ -169,7 +169,7 @@ const CONTENT_COLLISION_AVOIDANCE = {
169
169
  * off center, by up to half the left and right insets.
170
170
  */
171
171
  function TooltipFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
172
- const collision = usePopupCollision(void 0, collisionBoundary, collisionPadding ?? 5, container, 0);
172
+ const collision = usePopupCollision(void 0, collisionBoundary, collisionPadding ?? 5, container, 0, props.anchor);
173
173
  return /* @__PURE__ */ jsx(Tooltip.Positioner, {
174
174
  ...collision,
175
175
  ...props
@@ -1,4 +1,5 @@
1
- import { useMemo, useState } from "react";
1
+ import { sheetPopupBoundary, usePopupSheet, visualViewportShift } from "./popup-boundary.mjs";
2
+ import { useLayoutEffect, useMemo, useState } from "react";
2
3
  // src/lib/frame-insets.ts
3
4
  const NO_FRAME_INSETS = Object.freeze({
4
5
  top: 0,
@@ -127,15 +128,20 @@ function portalsToBody(container) {
127
128
  return element == null || element === document.body || element === document.documentElement;
128
129
  }
129
130
  /**
130
- * The window inset by the frame insets, as a collision boundary rect. Its size
131
- * is read each time Base UI places the popup (it copies the rect's fields), so
132
- * it follows the window when it resizes while the popup is open.
131
+ * The window inset by the frame insets, as a collision boundary rect. Its
132
+ * fields are read each time Base UI places the popup (it copies them), so it
133
+ * follows the window when it resizes while the popup is open, and WebKit's
134
+ * visual viewport when it pans (visualViewportShift).
133
135
  */
134
136
  function frameCollisionBoundary(insets) {
135
137
  const root = () => document.documentElement;
136
138
  return {
137
- x: insets.left,
138
- y: insets.top,
139
+ get x() {
140
+ return insets.left + visualViewportShift().x;
141
+ },
142
+ get y() {
143
+ return insets.top + visualViewportShift().y;
144
+ },
139
145
  get width() {
140
146
  return Math.max(0, root().clientWidth - insets.left - insets.right);
141
147
  },
@@ -191,16 +197,54 @@ function frameCollision(padding, boundary, insets, gap, container) {
191
197
  * inset, and `gap` (8px) inside each inset, the insets as the boundary
192
198
  * (frameCollision). A `collisionPadding` the caller passes wins as it is, and
193
199
  * a `collisionBoundary` the caller passes is kept. Call it from a part inside
194
- * the popup's portal, which mounts each time the popup opens.
200
+ * the popup's portal, which mounts each time the popup opens, and spread the
201
+ * result on the positioner.
202
+ *
203
+ * In a sheet (a phone's DialogSheetContent, a Drawer, an app's own under
204
+ * SheetPopupBoundary) a popup with no `collisionBoundary` of its own takes the
205
+ * sheet's: the area under its head, decided for this popup each time it is
206
+ * placed (sheetPopupBoundary), whatever its portal container. Pass the
207
+ * positioner's own `anchor` as `anchor`: the sheet then decides by it, not by
208
+ * the trigger it would look up in the page. Outside a sheet nothing changes,
209
+ * and no `ref` is returned.
195
210
  */
196
- function usePopupCollision(collisionPadding, collisionBoundary, defaultPadding, container, gap = 8) {
211
+ function usePopupCollision(collisionPadding, collisionBoundary, defaultPadding, container, gap = 8, anchor) {
197
212
  const insets = useFrameInsets(container);
198
- return useMemo(() => collisionPadding === void 0 ? frameCollision(defaultPadding, collisionBoundary, insets, gap, container) : {
199
- collisionPadding,
200
- collisionBoundary
213
+ const sheet = usePopupSheet();
214
+ const [positioner] = useState(() => ({ current: null }));
215
+ const [latest] = useState(() => {
216
+ let current = anchor;
217
+ return {
218
+ read: () => current,
219
+ set: (next) => {
220
+ current = next;
221
+ }
222
+ };
223
+ });
224
+ useLayoutEffect(() => {
225
+ latest.set(anchor);
226
+ }, [latest, anchor]);
227
+ const sheetBoundary = useMemo(() => sheet && collisionBoundary === void 0 ? sheetPopupBoundary(sheet, () => positioner.current, latest.read) : void 0, [
228
+ sheet,
229
+ collisionBoundary,
230
+ positioner,
231
+ latest
232
+ ]);
233
+ const boundary = collisionBoundary ?? sheetBoundary;
234
+ return useMemo(() => {
235
+ const collision = collisionPadding === void 0 ? frameCollision(defaultPadding, boundary, insets, gap, container) : {
236
+ collisionPadding,
237
+ collisionBoundary: boundary
238
+ };
239
+ return sheetBoundary ? {
240
+ ...collision,
241
+ ref: positioner
242
+ } : collision;
201
243
  }, [
202
244
  collisionPadding,
203
- collisionBoundary,
245
+ boundary,
246
+ sheetBoundary,
247
+ positioner,
204
248
  defaultPadding,
205
249
  insets,
206
250
  gap,
@@ -24,6 +24,11 @@ export type OverlayLayerProviderProps = {
24
24
  * `useParentOverlayKind` inside it to pick its own scrim. Modal overlays only:
25
25
  * a non-modal one passes `modal={false}`, adds nothing, and must not call
26
26
  * `useParentOverlayKind` (it would read the layer below as its own).
27
+ *
28
+ * It is a surface of its own, modal or not, so what is inside starts outside
29
+ * any sheet around it: a popup opened in it isn't held inside a sheet it was
30
+ * opened from (dialog.md, "Popups in a sheet"). A sheet of the app's own puts
31
+ * its `SheetPopupBoundary` inside this provider.
27
32
  */
28
33
  export declare function OverlayLayerProvider({ kind, modal, children }: OverlayLayerProviderProps): import("react").JSX.Element;
29
34
  /**
@@ -1,8 +1,27 @@
1
+ import { PopupSheetReset } from "./popup-boundary.mjs";
1
2
  import { createContext, useContext, useMemo } from "react";
2
3
  import { jsx } from "react/jsx-runtime";
3
4
  // src/lib/overlay-depth.tsx
4
5
  const OverlayStackContext = createContext([]);
5
6
  /**
7
+ * Adds a modal surface to the stack for everything under it. raft-ui's own
8
+ * roots use this one: a root also holds its trigger, which is still in the
9
+ * surface it is written in, so it must not start outside that sheet as
10
+ * `OverlayLayerProvider` does.
11
+ */
12
+ function OverlayLayer({ kind, modal, children }) {
13
+ const stack = useContext(OverlayStackContext);
14
+ const value = useMemo(() => modal ? [...stack, kind] : stack, [
15
+ stack,
16
+ kind,
17
+ modal
18
+ ]);
19
+ return /* @__PURE__ */ jsx(OverlayStackContext.Provider, {
20
+ value,
21
+ children
22
+ });
23
+ }
24
+ /**
6
25
  * Tells everything under it that a modal surface is open, so a raft-ui
7
26
  * Dialog, AlertDialog or Drawer opened from inside draws the lighter nested
8
27
  * scrim (design/foundations/elevation.md#stacked-overlays). raft-ui's own
@@ -11,17 +30,17 @@ const OverlayStackContext = createContext([]);
11
30
  * `useParentOverlayKind` inside it to pick its own scrim. Modal overlays only:
12
31
  * a non-modal one passes `modal={false}`, adds nothing, and must not call
13
32
  * `useParentOverlayKind` (it would read the layer below as its own).
33
+ *
34
+ * It is a surface of its own, modal or not, so what is inside starts outside
35
+ * any sheet around it: a popup opened in it isn't held inside a sheet it was
36
+ * opened from (dialog.md, "Popups in a sheet"). A sheet of the app's own puts
37
+ * its `SheetPopupBoundary` inside this provider.
14
38
  */
15
39
  function OverlayLayerProvider({ kind, modal, children }) {
16
- const stack = useContext(OverlayStackContext);
17
- const value = useMemo(() => modal ? [...stack, kind] : stack, [
18
- stack,
40
+ return /* @__PURE__ */ jsx(OverlayLayer, {
19
41
  kind,
20
- modal
21
- ]);
22
- return /* @__PURE__ */ jsx(OverlayStackContext.Provider, {
23
- value,
24
- children
42
+ modal,
43
+ children: /* @__PURE__ */ jsx(PopupSheetReset, { children })
25
44
  });
26
45
  }
27
46
  /**
@@ -44,15 +63,15 @@ function useParentOverlayKind() {
44
63
  * children be a render function (`({ payload }) => …`), so keep that shape.
45
64
  */
46
65
  function withOverlayLayer(kind, modal, children) {
47
- if (typeof children === "function") return (...args) => /* @__PURE__ */ jsx(OverlayLayerProvider, {
66
+ if (typeof children === "function") return (...args) => /* @__PURE__ */ jsx(OverlayLayer, {
48
67
  kind,
49
68
  modal,
50
69
  children: children(...args)
51
70
  });
52
- return /* @__PURE__ */ jsx(OverlayLayerProvider, {
71
+ return /* @__PURE__ */ jsx(OverlayLayer, {
53
72
  kind,
54
73
  modal,
55
74
  children
56
75
  });
57
76
  }
58
- export { OverlayLayerProvider, useParentOverlayKind, withOverlayLayer };
77
+ export { OverlayLayer, OverlayLayerProvider, useParentOverlayKind, withOverlayLayer };
@@ -0,0 +1,24 @@
1
+ import { ReactNode, RefObject } from "react";
2
+ export type SheetPopupBoundaryProps = {
3
+ /** The sheet's head: popups stay under its bottom edge. */
4
+ head: RefObject<Element | null>;
5
+ /**
6
+ * The sheet's surface: popups stay above its bottom edge.
7
+ * @default The head's parent element.
8
+ */
9
+ surface?: RefObject<Element | null>;
10
+ children?: ReactNode;
11
+ };
12
+ /**
13
+ * For a sheet an app draws itself (its own surface, a head that stays put and
14
+ * a body that scrolls): every raft-ui popup opened under it (Select, Combobox,
15
+ * ComboboxSelect, DropdownMenu, ContextMenu, Popover, PreviewCard, Tooltip,
16
+ * NotificationCenter) flips and sizes inside the sheet, under its head
17
+ * (sheetPopupBoundary says when it doesn't). Wrap the sheet's body in it, or
18
+ * the head and the body: a popup anchored in the head is left alone either
19
+ * way. Put it inside the sheet's own `OverlayLayerProvider`, which starts
20
+ * without one. raft-ui's own sheets, `DialogSheetContent` on a phone and
21
+ * `Drawer`, do this by themselves, and a `collisionBoundary` passed to a popup
22
+ * still wins. It renders no element.
23
+ */
24
+ export declare function SheetPopupBoundary({ head, surface, children }: SheetPopupBoundaryProps): import("react").JSX.Element;