diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx
index 3a7666eb80..b62bb01f22 100644
--- a/apps/webapp/app/components/primitives/Dialog.tsx
+++ b/apps/webapp/app/components/primitives/Dialog.tsx
@@ -3,8 +3,7 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "~/utils/cn";
-import { XMarkIcon } from "@heroicons/react/24/solid";
-import { ShortcutKey } from "./ShortcutKey";
+import { ModalCloseButton } from "./ModalCloseButton";
const Dialog = DialogPrimitive.Root;
@@ -56,18 +55,10 @@ const DialogContent = React.forwardRef<
>
{children}
- {showCloseButton && (
-
-
-
- Close
-
- )}
+ {/* The default size-7 is the height this button had when it rendered the esc key alongside the
+ icon, so the vertical geometry dialogs align against (the top-11 divider, absolutely
+ positioned titles) is unchanged — it only gets narrower. */}
+ {showCloseButton && }
));
diff --git a/apps/webapp/app/components/primitives/ModalCloseButton.tsx b/apps/webapp/app/components/primitives/ModalCloseButton.tsx
new file mode 100644
index 0000000000..8827a702e4
--- /dev/null
+++ b/apps/webapp/app/components/primitives/ModalCloseButton.tsx
@@ -0,0 +1,58 @@
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import * as React from "react";
+import { CrossIcon } from "~/assets/icons/CrossIcon";
+import { cn } from "~/utils/cn";
+import { ShortcutKey } from "./ShortcutKey";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
+
+const CLOSE_TOOLTIP_DELAY_MS = 500;
+
+/**
+ * The close button for modal surfaces — Dialog and Sheet, which are both Radix Dialog underneath.
+ * Pass `className` to position it, and to override the default `size-7` box where a surface needs
+ * to keep a tighter header height.
+ */
+export function ModalCloseButton({ className }: { className?: string }) {
+ const [open, setOpen] = React.useState(false);
+ const openTimeout = React.useRef>();
+
+ const cancelOpen = () => clearTimeout(openTimeout.current);
+ React.useEffect(() => cancelOpen, []);
+
+ const close = () => {
+ cancelOpen();
+ setOpen(false);
+ };
+
+ return (
+
+ {/* The tooltip is driven by our own hover timer rather than Radix's: Radix opens tooltips
+ instantly on focus, and these surfaces autofocus this button whenever they hold no other
+ tabbable content, which would pop the tooltip open on mount and leave it there.
+ Radix-initiated opens are ignored; its closes (pointer leave, blur, click) are honoured. */}
+ !nextOpen && close()}>
+
+ {
+ if (event.pointerType === "touch") return;
+ cancelOpen();
+ openTimeout.current = setTimeout(() => setOpen(true), CLOSE_TOOLTIP_DELAY_MS);
+ }}
+ onPointerLeave={close}
+ className={cn(
+ "group flex size-7 items-center justify-center rounded-sm opacity-70 transition focus-custom hover:bg-background-hover hover:opacity-100 focus-visible:focus-custom disabled:pointer-events-none",
+ className
+ )}
+ >
+
+ Close
+
+
+
+ Close
+
+
+
+
+ );
+}
diff --git a/apps/webapp/app/components/primitives/SheetV3.tsx b/apps/webapp/app/components/primitives/SheetV3.tsx
index 922f28d5ba..5bfc14285e 100644
--- a/apps/webapp/app/components/primitives/SheetV3.tsx
+++ b/apps/webapp/app/components/primitives/SheetV3.tsx
@@ -1,9 +1,8 @@
-import { XMarkIcon } from "@heroicons/react/20/solid";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "~/utils/cn";
-import { ShortcutKey } from "./ShortcutKey";
+import { ModalCloseButton } from "./ModalCloseButton";
const Sheet = SheetPrimitive.Root;
@@ -91,11 +90,9 @@ const SheetTitle = React.forwardRef<
{...props}
>
{children}
-
-
-
- Close
-
+ {/* size-6 rather than the default size-7 keeps this header row at the height it had when the
+ button rendered the esc key alongside the icon — it only gets narrower. */}
+
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;