authorgravatar for 136640686+MatthewFeroz@users.noreply.github.comMatthew Feroz <136640686+MatthewFeroz@users.noreply.github.com> 2026-10-10 15:20:57-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 17:20:57-05:00
logf92afe79c921dd7de20204e83d62fed0491b4e54
tree3725a671eafc1b6eec57367f90da8b7621000afc
parenta8bdfdb54181fb5c9d43f2747112c137a804d016
signature Signed by PGP key B5690EEEBB952194

fix(web): find bar and thread details panel stop covering each other (#17858)

Co-authored-by: Yash Singh <saiansh2525@gmail.com>

11 files changed, 313 insertions(+), 82 deletions(-)

apps/web/src/components/ChatView.tsx-5
......@@ -7998,11 +7998,6 @@ export default function ChatView(props: ChatViewProps) {
79987998 const [isThreadFindActive, setIsThreadFindActive] = useState(false);
79997999 const openThreadFind = useCallback(() => threadFindControlsRef.current?.open(), []);
80008000 const closeThreadFind = useCallback(() => threadFindControlsRef.current?.close(), []);
8001 // The details popover hangs off the header over the find bar; opening find dismisses it.
8002 useEffect(() => {
8003 if (!isThreadFindActive || threadPanelPresentation !== "popover" || !activeThreadRef) return;
8004 useRightPanelStore.getState().setThreadPanelOpen(activeThreadRef, "popover", false);
8005 }, [activeThreadRef, isThreadFindActive, threadPanelPresentation]);
80068001
80078002 useEffect(() => {
80088003 const handler = (event: globalThis.KeyboardEvent) => {
apps/web/src/components/chat/ChatCanvas.test.tsx created+119
......@@ -0,0 +1,119 @@
1// @vitest-environment jsdom
2
3import { act, useLayoutEffect } from "react";
4import { createRoot, type Root } from "react-dom/client";
5import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
6import { ChatCanvas } from "./ChatCanvas";
7import { useChatCanvas } from "./ChatCanvasContext";
8
9const resize = vi.hoisted(() => ({ callbacks: new Set<() => void>() }));
10vi.mock("../../lib/observeResize", () => ({
11 observeResize: (_elements: unknown, callback: () => void) => {
12 resize.callbacks.add(callback);
13 return () => resize.callbacks.delete(callback);
14 },
15}));
16
17let root: Root;
18let container: HTMLDivElement;
19let fontSize = 16;
20let canvas: ReturnType<typeof useChatCanvas>;
21
22function Preview({ x }: { x: number }) {
23 const current = useChatCanvas();
24 useLayoutEffect(() => {
25 canvas = current;
26 }, [current]);
27 const reportPreview = current?.reportPreview;
28 useLayoutEffect(() => {
29 reportPreview?.({
30 key: "browser:one",
31 width: 240,
32 source: { width: 240, height: 365 },
33 position: { x, y: 12 },
34 lastInteraction: "drag",
35 });
36 }, [reportPreview, x]);
37 return null;
38}
39
40beforeEach(() => {
41 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
42 fontSize = 16;
43 vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(1800);
44 vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(1000);
45 vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (
46 this: HTMLElement,
47 ) {
48 // A nonzero canvas origin catches viewport coordinates used as canvas coordinates.
49 if (this.hasAttribute("data-thread-find-bar")) {
50 const gap = 0.75 * fontSize;
51 return new DOMRect(
52 200 + 1800 - gap - 17.5 * fontSize,
53 100 + gap,
54 17.5 * fontSize,
55 2.25 * fontSize,
56 );
57 }
58 return new DOMRect(200, 100, 1800, 1000);
59 });
60 vi.spyOn(window, "getComputedStyle").mockReturnValue({
61 paddingLeft: "48px",
62 width: "736px",
63 minWidth: "640px",
64 } as CSSStyleDeclaration);
65 container = document.createElement("div");
66 document.body.append(container);
67 root = createRoot(container);
68});
69
70afterEach(async () => {
71 await act(() => root.unmount());
72 container.remove();
73 resize.callbacks.clear();
74 vi.restoreAllMocks();
75 vi.unstubAllGlobals();
76});
77
78async function renderFind(open: boolean, previewX = 1548) {
79 await act(() =>
80 root.render(
81 <ChatCanvas composerOverlayElement={null} detailsCardTopInset={open ? 48 : 0}>
82 {open ? <div data-thread-find-bar /> : null}
83 <Preview x={previewX} />
84 </ChatCanvas>,
85 ),
86 );
87}
88
89it.each([16, 24])(
90 "keeps a preview below Find and releases its space on close at a %spx root font",
91 async (size) => {
92 fontSize = size;
93 await renderFind(false);
94 expect(canvas?.layout.frame?.y).toBe(12);
95
96 await renderFind(true);
97 expect(canvas?.detailsCardTopInset).toBe(3 * size);
98 expect(canvas?.layout.frame?.y).toBe(3 * size + 12);
99
100 await renderFind(false);
101 expect(canvas?.detailsCardTopInset).toBe(0);
102 expect(canvas?.layout.frame?.y).toBe(12);
103
104 await renderFind(true);
105 expect(canvas?.layout.frame?.y).toBe(3 * size + 12);
106 },
107);
108
109it("remeasures the open bar when its font size changes without resizing the canvas", async () => {
110 await renderFind(true, 1200);
111 expect(canvas?.layout.frame?.y).toBe(12);
112
113 fontSize = 24;
114 await act(() => {
115 for (const callback of resize.callbacks) callback();
116 });
117 expect(canvas?.detailsCardTopInset).toBe(72);
118 expect(canvas?.layout.frame?.y).toBe(84);
119});
apps/web/src/components/chat/ChatCanvas.tsx+28-3
......@@ -47,6 +47,9 @@ export function ChatCanvas({
4747 minChatWidth: 640,
4848 composerHeight: 0,
4949 timelineGutter: 0,
50 findBarLeft: 0,
51 findBarRight: 0,
52 findBarBottom: 0,
5053 });
5154 const reportPreview = useCallback((next: ChatCanvasPreview) => {
5255 setPreview((current) =>
......@@ -69,9 +72,16 @@ export function ChatCanvas({
6972 const element = elementRef.current;
7073 const probe = widthProbeRef.current;
7174 if (!element || !probe) return;
75 const findBar =
76 detailsCardTopInset > 0 ? element.querySelector<HTMLElement>("[data-thread-find-bar]") : null;
7277 const measure = () => {
78 const canvasBounds = element.getBoundingClientRect();
79 const findBarBounds = findBar?.getBoundingClientRect();
7380 const styles = getComputedStyle(probe);
7481 const next = {
82 findBarLeft: findBarBounds ? findBarBounds.left - canvasBounds.left : 0,
83 findBarRight: findBarBounds ? findBarBounds.right - canvasBounds.left : 0,
84 findBarBottom: findBarBounds ? findBarBounds.bottom - canvasBounds.top : 0,
7585 width: element.clientWidth,
7686 height: element.clientHeight,
7787 padding: Number.parseFloat(styles.paddingLeft),
......@@ -92,22 +102,37 @@ export function ChatCanvas({
92102 };
93103 measure();
94104 const observed: Element[] = [element, probe];
105 if (findBar) observed.push(findBar);
95106 if (composerOverlayElement) observed.push(composerOverlayElement);
96107 if (timelineElement) observed.push(timelineElement);
97108 return observeResize(observed, measure);
98 }, [composerOverlayElement, timelineElement]);
109 }, [composerOverlayElement, timelineElement, detailsCardTopInset]);
99110 const context = useMemo(() => {
100111 const container = { width: measurements.width, height: measurements.height };
112 const findBar =
113 detailsCardTopInset > 0
114 ? {
115 left: measurements.findBarLeft,
116 right: measurements.findBarRight,
117 bottom: measurements.findBarBottom,
118 }
119 : null;
101120 return {
102121 container,
103122 lane: { padding: measurements.padding, minChatWidth: measurements.minChatWidth },
104 layout: resolveChatCanvasLayout({ ...measurements, container, preview, detailsCard }),
123 layout: resolveChatCanvasLayout({
124 ...measurements,
125 container,
126 preview,
127 detailsCard,
128 findBar,
129 }),
105130 previewKey: preview?.key ?? null,
106131 reportPreview,
107132 clearPreview,
108133 registerTimeline,
109134 reportDetailsCard,
110 detailsCardTopInset,
135 detailsCardTopInset: findBar?.bottom ?? 0,
111136 };
112137 }, [
113138 measurements,
apps/web/src/components/chat/ThreadDetailsCard.tsx+31-7
......@@ -7,6 +7,7 @@ import { selectThreadPanelOpen, useRightPanelStore } from "../../rightPanelStore
77import type { ThreadPanelPresentation } from "../../rightPanelLayout";
88import { useChatCanvas } from "./ChatCanvasContext";
99import {
10 THREAD_DETAILS_CARD_GAP,
1011 resolveThreadDetailsCardDensity,
1112 resolveThreadDetailsCardLayout,
1213} from "./threadDetailsCardLayout";
......@@ -45,6 +46,7 @@ export function ThreadDetailsCard({
4546 })
4647 : null;
4748 const mode = placement ? "inline" : "popover";
49 const topInset = canvas?.detailsCardTopInset ?? 0;
4850 const inlineOpen = useRightPanelStore((state) =>
4951 selectThreadPanelOpen(state.threadPanelVisibilityByThreadKey, threadRef, "inline"),
5052 );
......@@ -137,17 +139,38 @@ export function ThreadDetailsCard({
137139 <Popover
138140 handle={handle}
139141 open={mode === "popover" && popoverOpen}
140 onOpenChange={(open) =>
141 useRightPanelStore.getState().setThreadPanelOpen(threadRef, "popover", open)
142 }
142 onOpenChange={(open, details) => {
143 // The find bar sits beside the card, so clicking or focusing it does not
144 // dismiss the card. Escape still does.
145 const target =
146 details.reason === "focus-out"
147 ? (details.event as FocusEvent).relatedTarget
148 : details.reason === "outside-press"
149 ? details.event.target
150 : null;
151 if (!open && target instanceof Element && target.closest("[data-thread-find-bar]")) {
152 details.cancel();
153 return;
154 }
155 useRightPanelStore.getState().setThreadPanelOpen(threadRef, "popover", open);
156 }}
143157 >
144158 {placement ? (
145159 inlineOpen ? (
146160 <aside
147161 aria-label="Thread details"
148 className="absolute z-20"
162 // Slides down to make room for the find bar, but returns in the same frame
163 // the bar closes: a floating preview below moves up at once and would
164 // otherwise overlap the card mid-slide.
165 className={cn(
166 "absolute z-20",
167 topInset > 0 &&
168 "transition-[top] duration-150 ease-out motion-reduce:transition-none",
169 )}
149170 style={{
150 left: placement.x,
171 // Anchored to the right edge like the find bar, so both follow a resizing
172 // canvas in the same frame instead of waiting for its next measurement.
173 right: placement.right,
151174 top: placement.y,
152175 width: placement.width,
153176 maxHeight: height,
......@@ -162,10 +185,11 @@ export function ThreadDetailsCard({
162185 <PopoverPopup
163186 anchor={anchor}
164187 align="end"
165 alignOffset={0}
188 // Lines the card up with the find bar: the panel pads its card by 8px.
189 alignOffset={THREAD_DETAILS_CARD_GAP - 8}
166190 collisionAvoidance={{ side: "shift", align: "shift", fallbackAxisSide: "none" }}
167191 side="bottom"
168 sideOffset={0}
192 sideOffset={topInset}
169193 variant="panel"
170194 padding="none"
171195 >
apps/web/src/components/chat/ThreadFindBar.tsx+63-57
......@@ -59,65 +59,71 @@ export function ThreadFindBar(props: ThreadFindBarProps) {
5959 };
6060
6161 return (
62 <InputGroup
63 variant="popover"
64 size="lg"
65 onContextMenu={(event) => event.stopPropagation()}
66 role="search"
67 aria-label="Find in thread"
68 aria-busy={props.status === "loading" || props.counting === true}
69 className="absolute top-3 right-3 z-40 w-[min(24rem,calc(100%-1.5rem))]"
62 // Fades in while the details popover slides out from under it.
63 <div
64 data-thread-find-bar
65 className="absolute top-3 right-3 z-40 w-[min(var(--thread-details-panel-width),calc(100%-1.5rem))] transition-opacity duration-150 ease-out starting:opacity-0 motion-reduce:transition-none"
7066 >
71 <InputGroupInput
72 ref={inputRef}
73 type="search"
74 size="sm"
75 value={props.query}
67 <InputGroup
68 variant="popover"
69 size="lg"
70 onContextMenu={(event) => event.stopPropagation()}
71 role="search"
7672 aria-label="Find in thread"
77 placeholder="Find in thread"
78 maxLength={200}
79 spellCheck={false}
80 autoComplete="off"
81 onChange={(event) => props.onQueryChange(event.target.value)}
82 onKeyDown={handleKeyDown}
83 />
84 <InputGroupAddon align="inline-end" inset="pill">
85 <span
86 aria-live="polite"
87 className={cn(
88 "min-w-10 text-center text-xs tabular-nums",
89 noResults ? "text-destructive" : "text-muted-foreground",
90 )}
91 >
92 {label}
93 </span>
94 {props.status === "error" ? (
95 <Button size="xs" variant="ghost" onClick={props.onRetry}>
96 Retry
73 aria-busy={props.status === "loading" || props.counting === true}
74 className="w-full"
75 >
76 <InputGroupInput
77 ref={inputRef}
78 type="search"
79 size="sm"
80 value={props.query}
81 aria-label="Find in thread"
82 placeholder="Find in thread"
83 maxLength={200}
84 spellCheck={false}
85 autoComplete="off"
86 onChange={(event) => props.onQueryChange(event.target.value)}
87 onKeyDown={handleKeyDown}
88 />
89 <InputGroupAddon align="inline-end" inset="pill">
90 <span
91 aria-live="polite"
92 className={cn(
93 "min-w-10 text-center text-xs tabular-nums",
94 noResults ? "text-destructive" : "text-muted-foreground",
95 )}
96 >
97 {label}
98 </span>
99 {props.status === "error" ? (
100 <Button size="xs" variant="ghost" onClick={props.onRetry}>
101 Retry
102 </Button>
103 ) : null}
104 <Button
105 size="icon-xs"
106 variant="ghost"
107 aria-label="Previous match"
108 disabled={navigationDisabled}
109 onClick={props.onPrevious}
110 >
111 <ChevronUpIcon />
97112 </Button>
98 ) : null}
99 <Button
100 size="icon-xs"
101 variant="ghost"
102 aria-label="Previous match"
103 disabled={navigationDisabled}
104 onClick={props.onPrevious}
105 >
106 <ChevronUpIcon />
107 </Button>
108 <Button
109 size="icon-xs"
110 variant="ghost"
111 aria-label="Next match"
112 disabled={navigationDisabled}
113 onClick={props.onNext}
114 >
115 <ChevronDownIcon />
116 </Button>
117 <Button size="icon-xs" variant="ghost" aria-label="Close find" onClick={props.onClose}>
118 <XIcon />
119 </Button>
120 </InputGroupAddon>
121 </InputGroup>
113 <Button
114 size="icon-xs"
115 variant="ghost"
116 aria-label="Next match"
117 disabled={navigationDisabled}
118 onClick={props.onNext}
119 >
120 <ChevronDownIcon />
121 </Button>
122 <Button size="icon-xs" variant="ghost" aria-label="Close find" onClick={props.onClose}>
123 <XIcon />
124 </Button>
125 </InputGroupAddon>
126 </InputGroup>
127 </div>
122128 );
123129}
apps/web/src/components/chat/ThreadFindProvider.tsx+3-2
......@@ -1,8 +1,8 @@
11import {
22 createContext,
33 use,
4 useEffect,
54 useImperativeHandle,
5 useLayoutEffect,
66 type ReactNode,
77 type ComponentProps,
88 type Ref,
......@@ -49,7 +49,8 @@ export function ThreadFindProvider({
4949 find.open,
5050 find.close,
5151 ]);
52 useEffect(() => onOpenChange(find.isOpen), [find.isOpen, onOpenChange]);
52 // Before paint, so the details card makes room in the same frame the bar appears.
53 useLayoutEffect(() => onOpenChange(find.isOpen), [find.isOpen, onOpenChange]);
5354 return (
5455 <ThreadFindBarContext value={find.barProps}>
5556 <ThreadFindTimelineContext value={find.timelineProps}>{children}</ThreadFindTimelineContext>
apps/web/src/components/chat/chatCanvasLayout.test.ts+40
......@@ -135,6 +135,46 @@ describe("chat canvas layout", () => {
135135 expect(result.overlapsDetailsCard).toBe(false);
136136 expect(result.chat).toEqual({ left: 424, width: 736, insetStart: 0, insetEnd: 0 });
137137 });
138 it("keeps a dragged or resized preview below the open find bar", () => {
139 const findBar = { left: 1292, right: 1572, bottom: 48 };
140 const layout = (lastInteraction: "drag" | "resize") =>
141 resolveChatCanvasLayout({
142 container: { width: 1584, height: 988 },
143 maxChatWidth: 736,
144 composerHeight: 180,
145 findBar,
146 preview: {
147 ...preview,
148 width: 240,
149 source: { width: 240, height: 365 },
150 position: { x: 1332, y: 12 },
151 lastInteraction,
152 },
153 });
154 for (const lastInteraction of ["drag", "resize"] as const) {
155 const result = layout(lastInteraction);
156 expect(result.frame).toEqual({ x: 1332, y: 60, width: 240, height: 365 });
157 expect(result.overlapsDetailsCard).toBe(false);
158 expect(result.chat).toEqual({ left: 424, width: 736, insetStart: 0, insetEnd: 0 });
159 }
160 });
161 it("keeps a preview clear of a find bar wider than the inline details card", () => {
162 const result = resolveChatCanvasLayout({
163 container: { width: 1800, height: 1000 },
164 findBar: { left: 1362, right: 1782, bottom: 72 },
165 detailsCard: { left: 1508, right: 1788, bottom: 400 },
166 preview: {
167 ...preview,
168 width: 240,
169 source: { width: 240, height: 365 },
170 position: { x: 1200, y: 12 },
171 lastInteraction: "drag",
172 },
173 });
174 const frame = result.frame!;
175 expect(frame.x + frame.width <= 1362 || frame.y >= 84).toBe(true);
176 expect(result.overlapsDetailsCard).toBe(false);
177 });
138178 it("uses space beside the full card if a tall preview cannot fit below it", () => {
139179 const result = resolveChatCanvasLayout({
140180 container: { width: 1584, height: 988 },
apps/web/src/components/chat/chatCanvasLayout.ts+16-3
......@@ -32,6 +32,7 @@ export function resolveChatCanvasLayout({
3232 minChatWidth = 640,
3333 composerHeight = 0,
3434 detailsCard = null,
35 findBar = null,
3536}: {
3637 container: PreviewMiniPlayerSize;
3738 preview: ChatCanvasPreview | null;
......@@ -40,6 +41,8 @@ export function resolveChatCanvasLayout({
4041 minChatWidth?: number;
4142 composerHeight?: number;
4243 detailsCard?: PreviewMiniPlayerObstacles["detailsCard"];
44 /** The open find bar. It only keeps the floating preview clear; chat stays put. */
45 findBar?: PreviewMiniPlayerObstacles["detailsCard"];
4346}) {
4447 const centeredWidth = Math.max(0, Math.min(maxChatWidth, container.width - padding * 2));
4548 // A workspace card that does not fit beside the centered chat first moves
......@@ -90,13 +93,23 @@ export function resolveChatCanvasLayout({
9093 });
9194 }
9295 }
96 const findBarObstacles = findBar ? { detailsCard: findBar, composer: null } : undefined;
9397 const preferredFrame = {
9498 ...frame,
95 ...clampPreviewMiniPlayerPosition(frame, container, frame, undefined, minimumPreviewX),
99 ...clampPreviewMiniPlayerPosition(frame, container, frame, findBarObstacles, minimumPreviewX),
96100 };
97101 // A resize keeps its anchored edge and consumes card height first. A drag
98102 // clears the full card whenever it can, so moving alone never folds it.
99 const cardObstacle = preview.lastInteraction === "resize" ? null : detailsCard;
103 // The find bar can be wider than the inline card when the root font is enlarged.
104 const dragObstacle =
105 detailsCard && findBar
106 ? {
107 left: Math.min(detailsCard.left, findBar.left),
108 right: Math.max(detailsCard.right, findBar.right),
109 bottom: Math.max(detailsCard.bottom, findBar.bottom),
110 }
111 : (detailsCard ?? findBar);
112 const cardObstacle = preview.lastInteraction === "resize" ? findBar : dragObstacle;
100113 frame = {
101114 ...frame,
102115 ...clampPreviewMiniPlayerPosition(
......@@ -166,7 +179,7 @@ export function resolveChatCanvasLayout({
166179 frame,
167180 container,
168181 frame,
169 { composer: obstacles.composer, detailsCard: null },
182 { composer: obstacles.composer, detailsCard: findBar },
170183 minimumPreviewX,
171184 ),
172185 };
apps/web/src/components/chat/threadDetailsCardLayout.test.ts+3-2
......@@ -16,6 +16,7 @@ describe("workspace card", () => {
1616 it("pins to the top right at a fixed width", () => {
1717 expect(resolve(1600, 900)).toEqual({
1818 x: 1308,
19 right: 12,
1920 y: 12,
2021 width: 280,
2122 height: 876,
......@@ -30,7 +31,7 @@ describe("workspace card", () => {
3031 frame: null,
3132 topInset: 48,
3233 }),
33 ).toEqual({ x: 1308, y: 60, width: 280, height: 828 });
34 ).toEqual({ x: 1308, right: 12, y: 60, width: 280, height: 828 });
3435 });
3536 it("hides when a readable chat lane cannot fit beside it", () => {
3637 expect(resolve(1012, 900)).toMatchObject({ x: 720 });
......@@ -38,7 +39,7 @@ describe("workspace card", () => {
3839 });
3940 it("keeps the card at the top right while the preview is freely dragged vertically", () => {
4041 for (const y of [12, 170, 250, 400, 648]) {
41 expect(resolve(1600, 900, y)).toEqual({ x: 1308, y: 12, width: 280, height: 876 });
42 expect(resolve(1600, 900, y)).toEqual({ x: 1308, right: 12, y: 12, width: 280, height: 876 });
4243 }
4344 });
4445 it("keeps full height while a preview stays clear of the card", () => {
apps/web/src/components/chat/threadDetailsCardLayout.ts+4-2
......@@ -3,6 +3,8 @@ import { DETAILS_CARD_CLEARANCE } from "./chatCanvasLayout";
33
44/** Inset of the card from the canvas edges; the find bar shares it to line up. */
55export const THREAD_DETAILS_CARD_GAP = 12;
6// Keep in sync with --thread-details-panel-width, which sizes the popover and the find bar.
7const THREAD_DETAILS_CARD_WIDTH = 280;
68
79export function resolveThreadDetailsCardDensity(
810 height: number,
......@@ -32,8 +34,7 @@ export function resolveThreadDetailsCardLayout({
3234 topInset?: number;
3335}) {
3436 const gap = THREAD_DETAILS_CARD_GAP;
35 // Keep in sync with --thread-details-panel-width, which sizes the popover.
36 const width = 280;
37 const width = THREAD_DETAILS_CARD_WIDTH;
3738 const x = container.width - width - gap;
3839 if (x - DETAILS_CARD_CLEARANCE - lane.padding < lane.minChatWidth) return null;
3940 const y = gap + topInset;
......@@ -46,6 +47,7 @@ export function resolveThreadDetailsCardLayout({
4647 if (height < 160) return null;
4748 return {
4849 x,
50 right: gap,
4951 width,
5052 y,
5153 height,
apps/web/src/components/ui/popover.tsx+6-1
......@@ -72,10 +72,15 @@ function PopoverPopup({
7272 alignOffset={alignOffset}
7373 anchor={anchor}
7474 collisionAvoidance={collisionAvoidance}
75 // A panel's card already sits inside its own padding, so the panel can reach the edge.
76 collisionPadding={variant === "panel" ? 0 : undefined}
7577 className={cn(
7678 "h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-transform data-instant:transition-none",
7779 variant === "panel"
78 ? "z-(--z-sheet) w-[min(var(--thread-details-panel-width),var(--anchor-width))] transition-none"
80 ? // The card inside is the panel width, or the anchor less 12px per side; the viewport
81 // pads it by --spacing(2) per side. Panels slide when their offset changes, e.g. to make
82 // room for a bar below the header, even when opened from the keyboard (data-instant).
83 "z-(--z-sheet) w-[min(calc(var(--thread-details-panel-width)+--spacing(4)),calc(var(--anchor-width)---spacing(2)))] transition-[top] duration-150 ease-out data-instant:transition-[top] motion-reduce:transition-none motion-reduce:data-instant:transition-none"
7984 : "z-[130]",
8085 )}
8186 data-slot="popover-positioner"