@botiverse/raft-ui 0.55.0 → 0.56.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 +23 -0
- package/dist/components/combobox/combobox.mjs +4 -4
- package/dist/components/combobox-select/combobox-select.mjs +4 -4
- package/dist/components/context-menu/context-menu.mjs +4 -4
- package/dist/components/dropdown-menu/dropdown-menu.mjs +4 -4
- package/dist/components/message-item/message-footer.recipe.mjs +2 -2
- package/dist/components/notification-center/notification-center.mjs +4 -4
- package/dist/components/popover/popover.mjs +4 -4
- package/dist/components/preview-card/preview-card.mjs +4 -4
- package/dist/components/select/select.mjs +4 -4
- package/dist/components/sidebar/sidebar-section.recipe.mjs +11 -4
- package/dist/components/tooltip/tooltip.mjs +11 -7
- package/dist/lib/frame-insets.mjs +91 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [0.56.1](https://github.com/botiverse/raft-design/compare/v0.56.0...v0.56.1) (2026-10-31)
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Sidebar section controls hold their place in the row while hidden**, so a long section title truncates before them at rest and its count stays visible; with a mouse they only fade in on hover, focus or with their menu open, and nothing in the row moves. The attention mark sits over that reserved place and hides when the controls show; on a touch screen both show at rest, side by side. ([#931](https://github.com/botiverse/raft-design/pull/931))
|
|
6
|
+
|
|
7
|
+
## [0.56.0](https://github.com/botiverse/raft-design/compare/v0.55.0...v0.56.0) (2026-10-31)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Sidebar section header:** its controls rest in the title's hint color and turn body color only on hover, focus or with their menu open; the controls end on the header's line, and the title starts where the rows' content starts (stdrc, 2026-10-31). ([#922](https://github.com/botiverse/raft-design/pull/922))
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Popups under window controls stay on their trigger.** The frame insets (macOS traffic lights, caption buttons) are now a collision boundary instead of collision padding, so Base UI's shift limiter no longer pushes a popup with an arrow off its trigger: a top-bar tooltip in the installed web app on macOS sat about 69px right of its icon, 38px on Windows and Linux desktop. Popups portaled into a container of their own keep their padding and their clipping ancestors. Tooltip, Popover, PreviewCard, Select, menus, Combobox and NotificationCenter share one hook. ([#927](https://github.com/botiverse/raft-design/pull/927))
|
|
16
|
+
- **MessageItem's Saved marker** is 24px with footnote text, like the reactions and badges beside it, so it no longer reads as a lighter grey. ([#923](https://github.com/botiverse/raft-design/pull/923))
|
|
17
|
+
|
|
18
|
+
### Design System
|
|
19
|
+
|
|
20
|
+
- **Window-control insets bound a popup; they don't pad it** (Page Structure › Frame insets). ([#927](https://github.com/botiverse/raft-design/pull/927))
|
|
21
|
+
- Docs: https://raft-design.botiverse.build/patterns/page-structure/
|
|
22
|
+
- **Panel header actions:** shown buttons, then ⋯, then Close ([#920](https://github.com/botiverse/raft-design/pull/920)); **Navigation:** a listed item opens in the side panel, Raft has no breadcrumbs ([#921](https://github.com/botiverse/raft-design/pull/921)).
|
|
23
|
+
|
|
1
24
|
## [0.55.0](https://github.com/botiverse/raft-design/compare/v0.54.0...v0.55.0) (2026-10-31)
|
|
2
25
|
|
|
3
26
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
3
|
import { Button } from "../button/button.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
5
5
|
import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
|
|
6
6
|
import { chipRemoveBase } from "../../lib/chip-remove.mjs";
|
|
7
7
|
import { Check, X } from "lucide-react";
|
|
@@ -283,10 +283,10 @@ function ComboboxSeparator({ className, ...props }) {
|
|
|
283
283
|
...props
|
|
284
284
|
});
|
|
285
285
|
}
|
|
286
|
-
function ComboboxFramePositioner({ collisionPadding, container, ...props }) {
|
|
287
|
-
const
|
|
286
|
+
function ComboboxFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
287
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
|
|
288
288
|
return /* @__PURE__ */ jsx(Combobox.Positioner, {
|
|
289
|
-
|
|
289
|
+
...collision,
|
|
290
290
|
...props
|
|
291
291
|
});
|
|
292
292
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useTheme } from "../../lib/theme/use-theme.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
4
4
|
import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
|
|
5
5
|
import { usePhoneMedia } from "../../lib/phone-media.mjs";
|
|
6
6
|
import { roomBelowIsShort } from "./combobox-select.list-side.mjs";
|
|
@@ -65,10 +65,10 @@ const PHONE_LIST_COLLISION = {
|
|
|
65
65
|
align: "shift",
|
|
66
66
|
fallbackAxisSide: "none"
|
|
67
67
|
};
|
|
68
|
-
function ComboboxSelectFramePositioner({ collisionPadding, container, ...props }) {
|
|
69
|
-
const
|
|
68
|
+
function ComboboxSelectFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
69
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
|
|
70
70
|
return /* @__PURE__ */ jsx(Combobox.Positioner, {
|
|
71
|
-
|
|
71
|
+
...collision,
|
|
72
72
|
...props
|
|
73
73
|
});
|
|
74
74
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
4
4
|
import { Kbd } from "../kbd/kbd.mjs";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { ContextMenu } from "@base-ui/react";
|
|
@@ -83,10 +83,10 @@ function ContextMenuPortal(props) {
|
|
|
83
83
|
...useThemeScopeProps(props)
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
-
function ContextMenuFramePositioner({ collisionPadding, container, ...props }) {
|
|
87
|
-
const
|
|
86
|
+
function ContextMenuFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
87
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
|
|
88
88
|
return /* @__PURE__ */ jsx(ContextMenu.Positioner, {
|
|
89
|
-
|
|
89
|
+
...collision,
|
|
90
90
|
...props
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -2,7 +2,7 @@ import { tv } from "../../lib/tv.mjs";
|
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
3
|
import { Button } from "../button/button.mjs";
|
|
4
4
|
import { Badge } from "../badge/badge.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
6
6
|
import { Kbd } from "../kbd/kbd.mjs";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { Menu } from "@base-ui/react/menu";
|
|
@@ -99,10 +99,10 @@ function DropdownMenuPopup({ className, ...props }) {
|
|
|
99
99
|
...props
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
-
function DropdownMenuFramePositioner({ collisionPadding, container, ...props }) {
|
|
103
|
-
const
|
|
102
|
+
function DropdownMenuFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
103
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container);
|
|
104
104
|
return /* @__PURE__ */ jsx(Menu.Positioner, {
|
|
105
|
-
|
|
105
|
+
...collision,
|
|
106
106
|
...props
|
|
107
107
|
});
|
|
108
108
|
}
|
|
@@ -5,8 +5,8 @@ const messageFooter = tv({
|
|
|
5
5
|
savedIndicator: [
|
|
6
6
|
"inline-flex min-w-0 max-w-full items-center gap-1 overflow-hidden whitespace-nowrap select-none",
|
|
7
7
|
"[&_svg]:shrink-0",
|
|
8
|
-
"h-
|
|
9
|
-
"text-
|
|
8
|
+
"h-6 rounded-chip pr-[6.5px] pl-[5.5px]",
|
|
9
|
+
"text-footnote font-bold text-foreground-muted",
|
|
10
10
|
"bg-fill-muted hover:overlay-medium active:overlay-medium",
|
|
11
11
|
"outline-none focus-visible:shadow-focus",
|
|
12
12
|
"[&_svg]:size-3.5 [&_svg]:text-foreground"
|
|
@@ -2,7 +2,7 @@ import { headEdgePull } from "../../lib/head-edges.mjs";
|
|
|
2
2
|
import { tv } from "../../lib/tv.mjs";
|
|
3
3
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
4
4
|
import { Button } from "../button/button.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
6
6
|
import { Status } from "../status/status.mjs";
|
|
7
7
|
import { EmptyState } from "../empty-state/empty-state.mjs";
|
|
8
8
|
import { AlertTriangle, Bell, CheckCircle2, Info } from "lucide-react";
|
|
@@ -132,10 +132,10 @@ function NotificationCenterTrigger({ className, children, ...props }) {
|
|
|
132
132
|
function NotificationCenterPortal(props) {
|
|
133
133
|
return /* @__PURE__ */ jsx(Popover.Portal, { ...useThemeScopeProps(props) });
|
|
134
134
|
}
|
|
135
|
-
function NotificationCenterFramePositioner({ collisionPadding, container, defaultCollisionPadding, ...props }) {
|
|
136
|
-
const
|
|
135
|
+
function NotificationCenterFramePositioner({ collisionBoundary, collisionPadding, container, defaultCollisionPadding, ...props }) {
|
|
136
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, defaultCollisionPadding, container);
|
|
137
137
|
return /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
138
|
-
|
|
138
|
+
...collision,
|
|
139
139
|
...props
|
|
140
140
|
});
|
|
141
141
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import { Popover } from "@base-ui/react/popover";
|
|
6
6
|
//#region src/components/popover/popover.tsx
|
|
@@ -60,10 +60,10 @@ function PopoverPopup({ className, ...props }) {
|
|
|
60
60
|
...props
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
-
function PopoverFramePositioner({ collisionPadding, container, ...props }) {
|
|
64
|
-
const
|
|
63
|
+
function PopoverFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
64
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container);
|
|
65
65
|
return /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
66
|
-
|
|
66
|
+
...collision,
|
|
67
67
|
...props
|
|
68
68
|
});
|
|
69
69
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import {
|
|
4
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
import { PreviewCard } from "@base-ui/react/preview-card";
|
|
7
7
|
//#region src/components/preview-card/preview-card.tsx
|
|
@@ -72,10 +72,10 @@ function PreviewCardPopup({ className, ...props }) {
|
|
|
72
72
|
...scopeProps
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
-
function PreviewCardFramePositioner({ collisionPadding, container, ...props }) {
|
|
76
|
-
const
|
|
75
|
+
function PreviewCardFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
76
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 5, container);
|
|
77
77
|
return /* @__PURE__ */ jsx(PreviewCard.Positioner, {
|
|
78
|
-
|
|
78
|
+
...collision,
|
|
79
79
|
...props
|
|
80
80
|
});
|
|
81
81
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
3
|
import { Button } from "../button/button.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
5
5
|
import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
|
|
6
6
|
import { Check, ChevronDown } from "lucide-react";
|
|
7
7
|
import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
@@ -264,10 +264,10 @@ function SelectPopup({ className, ...props }) {
|
|
|
264
264
|
...props
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
|
-
function SelectFramePositioner({ collisionPadding, container, ...props }) {
|
|
268
|
-
const
|
|
267
|
+
function SelectFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
268
|
+
const collision = usePopupCollision(collisionPadding, collisionBoundary, 8, container);
|
|
269
269
|
return /* @__PURE__ */ jsx(Select.Positioner, {
|
|
270
|
-
|
|
270
|
+
...collision,
|
|
271
271
|
...props
|
|
272
272
|
});
|
|
273
273
|
}
|
|
@@ -11,7 +11,8 @@ const sidebarSectionRecipe = tv({ slots: {
|
|
|
11
11
|
sidebarSectionHeader: [
|
|
12
12
|
"flex items-center justify-between",
|
|
13
13
|
"group/section-header",
|
|
14
|
-
"my-0 h-auto gap-2
|
|
14
|
+
"my-0 h-auto gap-2 pt-4 pb-0 pl-5",
|
|
15
|
+
"pr-[calc(max(0px,calc(8px-var(--scrollbar-size,0px)))+12px)]",
|
|
15
16
|
"not-pointer-fine:pb-1.5"
|
|
16
17
|
],
|
|
17
18
|
sidebarSectionDisclosure: [
|
|
@@ -27,6 +28,7 @@ const sidebarSectionRecipe = tv({ slots: {
|
|
|
27
28
|
sidebarSectionCount: ["font-mono text-mono-meta font-normal tabular-nums text-foreground-hint"],
|
|
28
29
|
sidebarSectionAttention: [
|
|
29
30
|
"inline-flex min-w-4.5 shrink-0 items-center justify-center",
|
|
31
|
+
"pointer-fine:absolute pointer-fine:top-1/2 pointer-fine:right-0 pointer-fine:-translate-y-1/2",
|
|
30
32
|
"group-data-open/section:hidden",
|
|
31
33
|
"group-data-popup-open/section-header:hidden",
|
|
32
34
|
"group-hover/section-header:hidden"
|
|
@@ -34,15 +36,20 @@ const sidebarSectionRecipe = tv({ slots: {
|
|
|
34
36
|
sidebarSectionTrailing: ["relative flex min-w-4.5 shrink-0 items-center justify-end"],
|
|
35
37
|
sidebarSectionControls: [
|
|
36
38
|
"flex shrink-0 items-center gap-1 not-pointer-fine:gap-5",
|
|
37
|
-
"pointer-events-none
|
|
38
|
-
"not-pointer-fine:
|
|
39
|
+
"pointer-events-none opacity-0",
|
|
40
|
+
"not-pointer-fine:pointer-events-auto not-pointer-fine:opacity-100",
|
|
39
41
|
"pointer-fine:group-hover/section-header:pointer-events-auto pointer-fine:group-hover/section-header:opacity-100",
|
|
40
42
|
"pointer-fine:focus-within:pointer-events-auto pointer-fine:focus-within:opacity-100",
|
|
41
|
-
"pointer-fine:has-data-popup-open:pointer-events-auto pointer-fine:has-data-popup-open:opacity-100"
|
|
43
|
+
"pointer-fine:has-data-popup-open:pointer-events-auto pointer-fine:has-data-popup-open:opacity-100",
|
|
44
|
+
"[&>[data-icon-inset]:last-child]:-mr-icon-inset"
|
|
42
45
|
],
|
|
43
46
|
sidebarSectionControl: [
|
|
44
47
|
"relative flex h-6 w-auto min-w-6 shrink-0 items-center justify-center p-0 -my-1",
|
|
45
48
|
"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
|
49
|
+
"[--icon-inset:calc((--spacing(6)-14px)/2)]",
|
|
50
|
+
"text-foreground-hint",
|
|
51
|
+
"hover:text-foreground active:text-foreground focus-visible:text-foreground",
|
|
52
|
+
"data-popup-open:bg-fill-medium data-popup-open:text-foreground",
|
|
46
53
|
"not-pointer-fine:after:absolute not-pointer-fine:after:inset-[-11px] not-pointer-fine:after:content-['']"
|
|
47
54
|
]
|
|
48
55
|
} });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { Children, Fragment, isValidElement
|
|
3
|
+
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
4
|
+
import { Children, Fragment, isValidElement } from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { Tooltip } from "@base-ui/react/tooltip";
|
|
7
7
|
//#region src/components/tooltip/tooltip.tsx
|
|
@@ -153,13 +153,17 @@ const CONTENT_COLLISION_AVOIDANCE = {
|
|
|
153
153
|
* Inside the portal, so it mounts each time the tooltip opens and reads the
|
|
154
154
|
* frame insets then (lib/frame-insets.ts): a tooltip that would open into
|
|
155
155
|
* one flips away from it, and one that would overlap it along its trigger
|
|
156
|
-
* shifts out of it.
|
|
156
|
+
* shifts out of it. It keeps to the inset itself, not past it.
|
|
157
|
+
*
|
|
158
|
+
* The insets are its collision boundary, not its padding (frameCollision):
|
|
159
|
+
* Base UI sizes the shift limiter from the padding, so a padding carrying a
|
|
160
|
+
* window-controls inset pushed every short tooltip off its trigger, its arrow
|
|
161
|
+
* off center, by up to half the left and right insets.
|
|
157
162
|
*/
|
|
158
|
-
function TooltipFramePositioner({ collisionPadding, container, ...props }) {
|
|
159
|
-
const
|
|
160
|
-
const padding = useMemo(() => frameCollisionPadding(collisionPadding ?? 5, insets, 0), [collisionPadding, insets]);
|
|
163
|
+
function TooltipFramePositioner({ collisionBoundary, collisionPadding, container, ...props }) {
|
|
164
|
+
const collision = usePopupCollision(void 0, collisionBoundary, collisionPadding ?? 5, container, 0);
|
|
161
165
|
return /* @__PURE__ */ jsx(Tooltip.Positioner, {
|
|
162
|
-
|
|
166
|
+
...collision,
|
|
163
167
|
...props
|
|
164
168
|
});
|
|
165
169
|
}
|
|
@@ -110,18 +110,102 @@ function useFrameInsets(container) {
|
|
|
110
110
|
const [insets] = useState(() => readFrameInsets(container));
|
|
111
111
|
return insets;
|
|
112
112
|
}
|
|
113
|
+
function hasInset(insets) {
|
|
114
|
+
return insets.top !== 0 || insets.right !== 0 || insets.bottom !== 0 || insets.left !== 0;
|
|
115
|
+
}
|
|
113
116
|
/**
|
|
114
|
-
*
|
|
115
|
-
* a
|
|
116
|
-
*
|
|
117
|
+
* Whether a popup's portal container is the document's own: none, the body or the root.
|
|
118
|
+
* A ref counts as a container of its own whatever it holds now: the caller memoizes on
|
|
119
|
+
* the ref object, which never changes, so reading `.current` here would take an empty
|
|
120
|
+
* ref (a first render, a keepMounted popup) for the body for good. Erring this way only
|
|
121
|
+
* keeps the padding, never clips the popup.
|
|
117
122
|
*/
|
|
118
|
-
function
|
|
123
|
+
function portalsToBody(container) {
|
|
124
|
+
if (container && "current" in container) return false;
|
|
125
|
+
const node = container;
|
|
126
|
+
const element = node && !(node instanceof Element) && "host" in node ? node.host : node;
|
|
127
|
+
return element == null || element === document.body || element === document.documentElement;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
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.
|
|
133
|
+
*/
|
|
134
|
+
function frameCollisionBoundary(insets) {
|
|
135
|
+
const root = () => document.documentElement;
|
|
136
|
+
return {
|
|
137
|
+
x: insets.left,
|
|
138
|
+
y: insets.top,
|
|
139
|
+
get width() {
|
|
140
|
+
return Math.max(0, root().clientWidth - insets.left - insets.right);
|
|
141
|
+
},
|
|
142
|
+
get height() {
|
|
143
|
+
return Math.max(0, root().clientHeight - insets.top - insets.bottom);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* How a popup keeps out of the frame insets: `padding` from the window's edge,
|
|
149
|
+
* and on each side with an inset, at least the inset plus `gap`
|
|
150
|
+
* (frameCollisionPadding).
|
|
151
|
+
*
|
|
152
|
+
* The insets bound the collision area (frameCollisionBoundary) and the padding
|
|
153
|
+
* keeps only what is left past them. They are not added to the padding,
|
|
154
|
+
* because Base UI also sizes its shift limiter from the padding (limitShift
|
|
155
|
+
* offset = arrow / 2 + (left + right) / 2, or top + bottom beside the trigger):
|
|
156
|
+
* a padding that carried a 70px window-controls inset pushed a short tooltip
|
|
157
|
+
* with an arrow up to that half-sum off its trigger with nothing in the way.
|
|
158
|
+
* The area the popup is kept in is the same either way.
|
|
159
|
+
*
|
|
160
|
+
* Only a popup portaled to the body (no `container`, or the body or the root
|
|
161
|
+
* element) takes the boundary. One portaled into a container of its own may
|
|
162
|
+
* sit inside an element that clips it (a dialog's scrolling body), and an
|
|
163
|
+
* explicit boundary would replace Base UI's default, the clipping ancestors,
|
|
164
|
+
* so it would flip and size against the window and be cut off.
|
|
165
|
+
*
|
|
166
|
+
* With no inset, a `boundary` of the caller's own (it bounds the popup as it
|
|
167
|
+
* is, so the insets go into the padding), or a container of its own, this is
|
|
168
|
+
* frameCollisionPadding.
|
|
169
|
+
*/
|
|
170
|
+
function frameCollision(padding, boundary, insets, gap, container) {
|
|
171
|
+
const padded = frameCollisionPadding(padding, insets, gap);
|
|
172
|
+
if (!hasInset(insets) || boundary !== void 0 || typeof document === "undefined" || !portalsToBody(container)) return {
|
|
173
|
+
collisionPadding: padded,
|
|
174
|
+
collisionBoundary: boundary
|
|
175
|
+
};
|
|
176
|
+
const full = padded;
|
|
177
|
+
const side = (key) => Math.max(0, full[key] - insets[key]);
|
|
178
|
+
return {
|
|
179
|
+
collisionPadding: {
|
|
180
|
+
top: side("top"),
|
|
181
|
+
right: side("right"),
|
|
182
|
+
bottom: side("bottom"),
|
|
183
|
+
left: side("left")
|
|
184
|
+
},
|
|
185
|
+
collisionBoundary: frameCollisionBoundary(insets)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A popup's collision padding and boundary (a tooltip, a popover, a menu, a
|
|
190
|
+
* select or combobox list, a preview card): `defaultPadding` where there is no
|
|
191
|
+
* inset, and `gap` (8px) inside each inset, the insets as the boundary
|
|
192
|
+
* (frameCollision). A `collisionPadding` the caller passes wins as it is, and
|
|
193
|
+
* 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.
|
|
195
|
+
*/
|
|
196
|
+
function usePopupCollision(collisionPadding, collisionBoundary, defaultPadding, container, gap = 8) {
|
|
119
197
|
const insets = useFrameInsets(container);
|
|
120
|
-
return useMemo(() => collisionPadding
|
|
198
|
+
return useMemo(() => collisionPadding === void 0 ? frameCollision(defaultPadding, collisionBoundary, insets, gap, container) : {
|
|
199
|
+
collisionPadding,
|
|
200
|
+
collisionBoundary
|
|
201
|
+
}, [
|
|
121
202
|
collisionPadding,
|
|
203
|
+
collisionBoundary,
|
|
122
204
|
defaultPadding,
|
|
123
|
-
insets
|
|
205
|
+
insets,
|
|
206
|
+
gap,
|
|
207
|
+
container
|
|
124
208
|
]);
|
|
125
209
|
}
|
|
126
210
|
//#endregion
|
|
127
|
-
export { NO_FRAME_INSETS, frameCollisionPadding, frameInsetsFrom, readFrameInsets, useFrameInsets,
|
|
211
|
+
export { NO_FRAME_INSETS, frameCollision, frameCollisionBoundary, frameCollisionPadding, frameInsetsFrom, readFrameInsets, useFrameInsets, usePopupCollision };
|