From 0b52af94fa18add50c3e202b4b45bd508336b9be Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 13 Aug 2026 14:05:17 +0100 Subject: [PATCH] feat(webapp): restyle the modal and sheet close buttons (#4603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The close button on modals and slide-over panels is now a simpler icon-only button. The `Esc` key label moves out of the button and into a hover tooltip, delayed by 500ms. Modal with the new square close button in the top
right Hovering the close button shows a Close tooltip with
the Esc key ### Verified Both surfaces, driven in a real browser: no tooltip on open despite autofocus, hidden at 300ms of hover, "Close ยท Esc" at 700ms, hides on pointer leave, `Escape` closes, clicking the X closes with no orphaned tooltip, and the button stays keyboard-focusable (`tabIndex 0`). The `fullscreen` dialog variant flips the tooltip below to stay on-screen. `typecheck --filter webapp` passes; `format` and `lint` are clean. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) --- [Open workspace in Conductor](https://app.conductor.build/workspace/a7368189-9fbb-4edd-891c-43c633931bcf) Co-authored-by: Claude Opus 5 (1M context) --- .../app/components/primitives/Dialog.tsx | 19 ++---- .../primitives/ModalCloseButton.tsx | 58 +++++++++++++++++++ .../app/components/primitives/SheetV3.tsx | 11 ++-- 3 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 apps/webapp/app/components/primitives/ModalCloseButton.tsx diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index 3a7666eb8..b62bb01f2 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 000000000..8827a702e --- /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 922f28d5b..5bfc14285 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;