Skip to content

Commit b4dc346

Browse files
samejrclaude
andcommitted
feat(webapp): restyle the modal and sheet close buttons
Replace the close button on Dialog and Sheet with a square icon-only button using the project's CrossIcon. The esc key label moves out of the button and into a hover tooltip ("Close" + the Esc key) delayed by 500ms. Both surfaces are Radix Dialog underneath, so the treatment lives in one new ModalCloseButton primitive. Each surface keeps its existing height (size-7 for dialogs, size-6 for sheets) so nothing repositions - the buttons only get narrower. The tooltip is driven by its own hover timer rather than Radix's: Radix opens tooltips instantly on focus, and both surfaces autofocus the close button when they hold no other tabbable content, which would otherwise pop the tooltip open on mount and leave it there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent aca234d commit b4dc346

3 files changed

Lines changed: 67 additions & 21 deletions

File tree

apps/webapp/app/components/primitives/Dialog.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import * as React from "react";
44
import * as DialogPrimitive from "@radix-ui/react-dialog";
55
import { cn } from "~/utils/cn";
6-
import { XMarkIcon } from "@heroicons/react/24/solid";
7-
import { ShortcutKey } from "./ShortcutKey";
6+
import { ModalCloseButton } from "./ModalCloseButton";
87

98
const Dialog = DialogPrimitive.Root;
109

@@ -56,18 +55,10 @@ const DialogContent = React.forwardRef<
5655
>
5756
<hr className="absolute left-0 top-11 w-full" />
5857
{children}
59-
{showCloseButton && (
60-
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground group absolute right-2 top-2.25 flex items-center gap-1 rounded-sm p-1 py-1 pl-0 pr-1 opacity-70 transition focus-custom hover:bg-background-hover hover:opacity-100 focus-visible:focus-custom disabled:pointer-events-none">
61-
<ShortcutKey
62-
shortcut={{
63-
key: "esc",
64-
}}
65-
variant="medium"
66-
/>
67-
<XMarkIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
68-
<span className="sr-only">Close</span>
69-
</DialogPrimitive.Close>
70-
)}
58+
{/* The default size-7 is the height this button had when it rendered the esc key alongside the
59+
icon, so the vertical geometry dialogs align against (the top-11 divider, absolutely
60+
positioned titles) is unchanged — it only gets narrower. */}
61+
{showCloseButton && <ModalCloseButton className="absolute right-2 top-2.25" />}
7162
</DialogPrimitive.Content>
7263
</DialogPortal>
7364
));
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as DialogPrimitive from "@radix-ui/react-dialog";
2+
import * as React from "react";
3+
import { CrossIcon } from "~/assets/icons/CrossIcon";
4+
import { cn } from "~/utils/cn";
5+
import { ShortcutKey } from "./ShortcutKey";
6+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
7+
8+
const CLOSE_TOOLTIP_DELAY_MS = 500;
9+
10+
/**
11+
* The close button for modal surfaces — Dialog and Sheet, which are both Radix Dialog underneath.
12+
* Pass `className` to position it, and to override the default `size-7` box where a surface needs
13+
* to keep a tighter header height.
14+
*/
15+
export function ModalCloseButton({ className }: { className?: string }) {
16+
const [open, setOpen] = React.useState(false);
17+
const openTimeout = React.useRef<ReturnType<typeof setTimeout>>();
18+
19+
const cancelOpen = () => clearTimeout(openTimeout.current);
20+
React.useEffect(() => cancelOpen, []);
21+
22+
const close = () => {
23+
cancelOpen();
24+
setOpen(false);
25+
};
26+
27+
return (
28+
<TooltipProvider>
29+
{/* The tooltip is driven by our own hover timer rather than Radix's: Radix opens tooltips
30+
instantly on focus, and these surfaces autofocus this button whenever they hold no other
31+
tabbable content, which would pop the tooltip open on mount and leave it there.
32+
Radix-initiated opens are ignored; its closes (pointer leave, blur, click) are honoured. */}
33+
<Tooltip open={open} onOpenChange={(nextOpen) => !nextOpen && close()}>
34+
<TooltipTrigger asChild>
35+
<DialogPrimitive.Close
36+
onPointerEnter={(event) => {
37+
if (event.pointerType === "touch") return;
38+
cancelOpen();
39+
openTimeout.current = setTimeout(() => setOpen(true), CLOSE_TOOLTIP_DELAY_MS);
40+
}}
41+
onPointerLeave={close}
42+
className={cn(
43+
"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",
44+
className
45+
)}
46+
>
47+
<CrossIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
48+
<span className="sr-only">Close</span>
49+
</DialogPrimitive.Close>
50+
</TooltipTrigger>
51+
<TooltipContent className="flex items-center py-1.5 pl-2.5 pr-2 text-xs text-text-bright">
52+
Close
53+
<ShortcutKey shortcut={{ key: "esc" }} variant="medium" />
54+
</TooltipContent>
55+
</Tooltip>
56+
</TooltipProvider>
57+
);
58+
}

apps/webapp/app/components/primitives/SheetV3.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { XMarkIcon } from "@heroicons/react/20/solid";
21
import * as SheetPrimitive from "@radix-ui/react-dialog";
32
import { cva, type VariantProps } from "class-variance-authority";
43
import * as React from "react";
54
import { cn } from "~/utils/cn";
6-
import { ShortcutKey } from "./ShortcutKey";
5+
import { ModalCloseButton } from "./ModalCloseButton";
76

87
const Sheet = SheetPrimitive.Root;
98

@@ -91,11 +90,9 @@ const SheetTitle = React.forwardRef<
9190
{...props}
9291
>
9392
{children}
94-
<SheetPrimitive.Close className="flex items-center gap-1 rounded-sm p-1 pl-0 transition hover:bg-background-hover focus-visible:focus-custom disabled:pointer-events-none">
95-
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
96-
<XMarkIcon className="size-4 text-text-dimmed" />
97-
<span className="sr-only">Close</span>
98-
</SheetPrimitive.Close>
93+
{/* size-6 rather than the default size-7 keeps this header row at the height it had when the
94+
button rendered the esc key alongside the icon — it only gets narrower. */}
95+
<ModalCloseButton className="size-6" />
9996
</SheetPrimitive.Title>
10097
));
10198
SheetTitle.displayName = SheetPrimitive.Title.displayName;

0 commit comments

Comments
 (0)