Unified · Split
| @@ -0,0 +1,11 @@ | ||
| 1 | +--- | |
| 2 | +"@executor-js/react": patch | |
| 3 | +--- | |
| 4 | + | |
| 5 | +**A click outside a dialog or sheet no longer discards the form inside it** | |
| 6 | + | |
| 7 | +Radix dismisses an overlay surface on any un-prevented outside interaction, and `DialogContent` and `SheetContent` only prevented that for clicks landing in a portaled combobox or select popup. Every other outside click fell through to dismissal, so a stray click on the page behind a form — after switching windows to copy an ID, for example — closed the surface and destroyed what the user had typed. These surfaces unmount their state on close by design, so nothing was recoverable. | |
| 8 | + | |
| 9 | +The default is now the opposite: an outside interaction keeps the surface open. Escape and the close button are unchanged and still close it. `DialogContent` and `SheetContent` take a new `dismissOnOutsideClick` prop for surfaces with nothing to lose — confirmations, pickers, and read-only panels — and the portaled-popup guard still applies there, so choosing a combobox option never dismisses. | |
| 10 | + | |
| 11 | +`CommandDialog` sets `dismissOnOutsideClick` on by default, because a command palette holds only a search string and clicking away is the expected way to leave it. | |
| @@ -401,7 +401,8 @@ function EnterpriseContactDialog() { · 400 unmodified lines | ||
| 401 | 401 | Contact us |
| 402 | 402 | </Button> |
| 403 | 403 | </DialogTrigger> |
| 404 | − <DialogContent> | |
| 404 | + {/* Static contact details only: clicking away dismisses it. */} | |
| 405 | + <DialogContent dismissOnOutsideClick> | |
| 405 | 406 | <DialogHeader> |
| 406 | 407 | <DialogTitle>Talk to us about Enterprise</DialogTitle> |
| 407 | 408 | <DialogDescription> |
| @@ -449,7 +450,8 @@ function SlackContactCta() { · 41 unmodified lines | ||
| 449 | 450 | <span aria-hidden>→</span> |
| 450 | 451 | </Button> |
| 451 | 452 | </DialogTrigger> |
| 452 | − <DialogContent> | |
| 453 | + {/* Static contact details only: clicking away dismisses it. */} | |
| 454 | + <DialogContent dismissOnOutsideClick> | |
| 453 | 455 | <DialogHeader> |
| 454 | 456 | <DialogTitle>Get in touch on Slack</DialogTitle> |
| 455 | 457 | <DialogDescription> |
| @@ -44,7 +44,8 @@ export function SupportSlot() { · 43 unmodified lines | ||
| 44 | 44 | <HelpIcon className="size-3.5 text-muted-foreground" /> |
| 45 | 45 | Get support |
| 46 | 46 | </Button> |
| 47 | − <DialogContent className="sm:max-w-[440px]"> | |
| 47 | + {/* Static links only: clicking away dismisses it. */} | |
| 48 | + <DialogContent dismissOnOutsideClick className="sm:max-w-[440px]"> | |
| 48 | 49 | <DialogHeader> |
| 49 | 50 | <DialogTitle className="font-display text-xl">Get support</DialogTitle> |
| 50 | 51 | <DialogDescription className="text-sm leading-relaxed"> |
| @@ -2357,15 +2357,12 @@ function AddAccountModalView(props: AddAccountModalProps) { · 2356 unmodified lines | ||
| 2357 | 2357 | // Non-modal for the same reason as the health-check editor sheet: a modal |
| 2358 | 2358 | // dialog's react-remove-scroll locks the wheel to the dialog subtree, so |
| 2359 | 2359 | // the operation combobox's PORTALED popup (and the modal body while it is |
| 2360 | − // open) cannot scroll. The overlay still dims and outside-click still | |
| 2361 | − // closes; the portaled-popup guard in DialogContent keeps option clicks | |
| 2362 | − // from dismissing. | |
| 2360 | + // open) cannot scroll. The overlay still dims, so the dialog keeps its | |
| 2361 | + // modal look. This form holds credentials and a half-built auth method, so | |
| 2362 | + // it takes DialogContent's default: an outside click does not dismiss it. | |
| 2363 | 2363 | <Dialog open={open} onOpenChange={onOpenChange} modal={false}> |
| 2364 | 2364 | <DialogContent |
| 2365 | 2365 | forceOverlay |
| 2366 | − onInteractOutside={ | |
| 2367 | − oauthClientHandoff?.action === "reconnect" ? (event) => event.preventDefault() : undefined | |
| 2368 | − } | |
| 2369 | 2366 | className={cn( |
| 2370 | 2367 | "max-h-[85vh] overflow-x-hidden overflow-y-auto", |
| 2371 | 2368 | (addingMethod && createCustomMethod) || oauthRegistering || oauthEditing |
| @@ -26,12 +26,17 @@ function CommandDialog({ · 25 unmodified lines | ||
| 26 | 26 | children, |
| 27 | 27 | className, |
| 28 | 28 | showCloseButton = true, |
| 29 | + dismissOnOutsideClick = true, | |
| 29 | 30 | ...props |
| 30 | 31 | }: React.ComponentProps<typeof Dialog> & { |
| 31 | 32 | title?: string; |
| 32 | 33 | description?: string; |
| 33 | 34 | className?: string; |
| 34 | 35 | showCloseButton?: boolean; |
| 36 | + /** A command palette holds nothing but a search string, and clicking away is | |
| 37 | + * the expected way to leave it, so this stays on — unlike `DialogContent`, | |
| 38 | + * whose default protects form state. */ | |
| 39 | + dismissOnOutsideClick?: boolean; | |
| 35 | 40 | }) { |
| 36 | 41 | return ( |
| 37 | 42 | <Dialog {...props}> |
| @@ -42,6 +47,7 @@ function CommandDialog({ · 4 unmodified lines | ||
| 42 | 47 | <DialogContent |
| 43 | 48 | className={cn("overflow-hidden p-0", className)} |
| 44 | 49 | showCloseButton={showCloseButton} |
| 50 | + dismissOnOutsideClick={dismissOnOutsideClick} | |
| 45 | 51 | > |
| 46 | 52 | <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> |
| 47 | 53 | {children} |
| @@ -2,6 +2,7 @@ import * as React from "react"; · 1 unmodified line | ||
| 2 | 2 | import { XIcon } from "lucide-react"; |
| 3 | 3 | import { Dialog as DialogPrimitive } from "radix-ui"; |
| 4 | 4 | |
| 5 | +import { applyOutsideDismissPolicy } from "../lib/outside-dismiss"; | |
| 5 | 6 | import { cn } from "../lib/utils"; |
| 6 | 7 | import { Button } from "./button"; |
| 7 | 8 | |
| @@ -37,25 +38,26 @@ function DialogOverlay({ · 29 unmodified lines | ||
| 37 | 38 | ); |
| 38 | 39 | } |
| 39 | 40 | |
| 40 | −// base-ui popups (combobox/select) portal their list OUTSIDE the dialog content, | |
| 41 | −// so clicking an option reads as an interaction outside the dialog and would | |
| 42 | −// dismiss it before the selection lands. Keep the dialog open for interactions | |
| 43 | −// that originate inside such a popup. | |
| 44 | −const PORTALED_POPUP_SELECTOR = "[data-slot='combobox-content'],[data-slot='select-content']"; | |
| 45 | − | |
| 46 | 41 | function DialogContent({ |
| 47 | 42 | className, |
| 48 | 43 | children, |
| 49 | 44 | showCloseButton = true, |
| 45 | + dismissOnOutsideClick = false, | |
| 50 | 46 | onInteractOutside, |
| 47 | + onPointerDownOutside, | |
| 51 | 48 | forceOverlay = false, |
| 52 | 49 | ...props |
| 53 | 50 | }: React.ComponentProps<typeof DialogPrimitive.Content> & { |
| 54 | 51 | showCloseButton?: boolean; |
| 52 | + /** Let a click outside the dialog close it. Off by default: a stray click on | |
| 53 | + * the page behind a form must not discard what the user typed. Turn it on | |
| 54 | + * for a dialog with nothing to lose — a confirmation, a picker, a read-only | |
| 55 | + * panel. Escape and the close button close either way. */ | |
| 56 | + dismissOnOutsideClick?: boolean; | |
| 55 | 57 | /** Pair with `<Dialog modal={false}>`: Radix renders no overlay in non-modal |
| 56 | 58 | * mode, so this renders a plain dim layer instead. It still eats outside |
| 57 | − * clicks (which dismiss via onInteractOutside), so the dialog keeps its | |
| 58 | − * modal look while the wheel stays free for portaled popups. */ | |
| 59 | + * clicks, so the dialog keeps its modal look while the wheel stays free for | |
| 60 | + * portaled popups. */ | |
| 59 | 61 | forceOverlay?: boolean; |
| 60 | 62 | }) { |
| 61 | 63 | return ( |
| @@ -66,12 +68,13 @@ function DialogContent({ · 4 unmodified lines | ||
| 66 | 68 | ) : null} |
| 67 | 69 | <DialogPrimitive.Content |
| 68 | 70 | data-slot="dialog-content" |
| 71 | + onPointerDownOutside={(event) => { | |
| 72 | + onPointerDownOutside?.(event); | |
| 73 | + applyOutsideDismissPolicy(event, dismissOnOutsideClick); | |
| 74 | + }} | |
| 69 | 75 | onInteractOutside={(event) => { |
| 70 | − const target = event.detail.originalEvent.target; | |
| 71 | − if (target instanceof Element && target.closest(PORTALED_POPUP_SELECTOR)) { | |
| 72 | − event.preventDefault(); | |
| 73 | − } | |
| 74 | 76 | onInteractOutside?.(event); |
| 77 | + applyOutsideDismissPolicy(event, dismissOnOutsideClick); | |
| 75 | 78 | }} |
| 76 | 79 | className={cn( |
| 77 | 80 | "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", |
| @@ -29,7 +29,8 @@ export function RemoveOAuthAppDialog(props: { · 28 unmodified lines | ||
| 29 | 29 | const inUse = connections.length > 0; |
| 30 | 30 | return ( |
| 31 | 31 | <Dialog open onOpenChange={(open: boolean) => (open ? undefined : props.onClose())}> |
| 32 | − <DialogContent> | |
| 32 | + {/* A confirmation with nothing to lose: clicking away cancels it. */} | |
| 33 | + <DialogContent dismissOnOutsideClick> | |
| 33 | 34 | <DialogHeader> |
| 34 | 35 | <DialogTitle>Remove {String(client.slug)}?</DialogTitle> |
| 35 | 36 | <DialogDescription> |
| @@ -4,6 +4,7 @@ import * as React from "react"; · 3 unmodified lines | ||
| 4 | 4 | import { XIcon } from "lucide-react"; |
| 5 | 5 | import { Dialog as SheetPrimitive } from "radix-ui"; |
| 6 | 6 | |
| 7 | +import { applyOutsideDismissPolicy } from "../lib/outside-dismiss"; | |
| 7 | 8 | import { cn } from "../lib/utils"; |
| 8 | 9 | |
| 9 | 10 | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { |
| @@ -38,34 +39,36 @@ function SheetOverlay({ · 28 unmodified lines | ||
| 38 | 39 | ); |
| 39 | 40 | } |
| 40 | 41 | |
| 41 | −// base-ui popups (combobox/select) portal their list OUTSIDE the dialog content, | |
| 42 | −// so clicking an option reads as an interaction outside the sheet and would | |
| 43 | −// dismiss it before the selection lands. Keep the sheet open for interactions | |
| 44 | −// that originate inside such a popup. | |
| 45 | −const PORTALED_POPUP_SELECTOR = "[data-slot='combobox-content'],[data-slot='select-content']"; | |
| 46 | − | |
| 47 | 42 | function SheetContent({ |
| 48 | 43 | className, |
| 49 | 44 | children, |
| 50 | 45 | side = "right", |
| 51 | 46 | showCloseButton = true, |
| 47 | + dismissOnOutsideClick = false, | |
| 52 | 48 | onInteractOutside, |
| 49 | + onPointerDownOutside, | |
| 53 | 50 | ...props |
| 54 | 51 | }: React.ComponentProps<typeof SheetPrimitive.Content> & { |
| 55 | 52 | side?: "top" | "right" | "bottom" | "left"; |
| 56 | 53 | showCloseButton?: boolean; |
| 54 | + /** Let a click outside the sheet close it. Off by default: a stray click on | |
| 55 | + * the page behind a form must not discard what the user typed. Turn it on | |
| 56 | + * for a sheet with nothing to lose — navigation, a picker, a read-only | |
| 57 | + * panel. Escape and the close button close either way. */ | |
| 58 | + dismissOnOutsideClick?: boolean; | |
| 57 | 59 | }) { |
| 58 | 60 | return ( |
| 59 | 61 | <SheetPortal> |
| 60 | 62 | <SheetOverlay /> |
| 61 | 63 | <SheetPrimitive.Content |
| 62 | 64 | data-slot="sheet-content" |
| 65 | + onPointerDownOutside={(event) => { | |
| 66 | + onPointerDownOutside?.(event); | |
| 67 | + applyOutsideDismissPolicy(event, dismissOnOutsideClick); | |
| 68 | + }} | |
| 63 | 69 | onInteractOutside={(event) => { |
| 64 | − const target = event.detail.originalEvent.target; | |
| 65 | − if (target instanceof Element && target.closest(PORTALED_POPUP_SELECTOR)) { | |
| 66 | − event.preventDefault(); | |
| 67 | − } | |
| 68 | 70 | onInteractOutside?.(event); |
| 71 | + applyOutsideDismissPolicy(event, dismissOnOutsideClick); | |
| 69 | 72 | }} |
| 70 | 73 | className={cn( |
| 71 | 74 | "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500", |
| @@ -168,6 +168,9 @@ function Sidebar({ · 167 unmodified lines | ||
| 168 | 168 | return ( |
| 169 | 169 | <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> |
| 170 | 170 | <SheetContent |
| 171 | + // Navigation only. Tapping the page behind it is the normal way to | |
| 172 | + // put the mobile nav away, and nothing is lost. | |
| 173 | + dismissOnOutsideClick | |
| 171 | 174 | data-sidebar="sidebar" |
| 172 | 175 | data-slot="sidebar" |
| 173 | 176 | data-mobile="true" |
| @@ -0,0 +1,77 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + applyOutsideDismissPolicy, | |
| 5 | + dismissesOnOutsideInteraction, | |
| 6 | + PORTALED_POPUP_SELECTOR, | |
| 7 | +} from "./outside-dismiss"; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * The property under test: a dialog or sheet keeps what the user typed. | |
| 11 | + * | |
| 12 | + * Radix closes an overlay surface whenever an outside interaction reaches it | |
| 13 | + * un-prevented, so "does not dismiss" is the thing that has to be asserted, not | |
| 14 | + * assumed. Escape is Radix's own `onEscapeKeyDown` path and never passes | |
| 15 | + * through here, which is why no case below can close a surface with a key. | |
| 16 | + */ | |
| 17 | +describe("dismissesOnOutsideInteraction", () => { | |
| 18 | + it("keeps the surface open by default", () => { | |
| 19 | + expect( | |
| 20 | + dismissesOnOutsideInteraction({ | |
| 21 | + dismissOnOutsideClick: false, | |
| 22 | + insidePortaledPopup: false, | |
| 23 | + }), | |
| 24 | + ).toBe(false); | |
| 25 | + }); | |
| 26 | + | |
| 27 | + it("closes the surface when it opts in", () => { | |
| 28 | + expect( | |
| 29 | + dismissesOnOutsideInteraction({ dismissOnOutsideClick: true, insidePortaledPopup: false }), | |
| 30 | + ).toBe(true); | |
| 31 | + }); | |
| 32 | + | |
| 33 | + it("never closes on a click inside a portaled popup, even when it opts in", () => { | |
| 34 | + // A combobox or select renders its list outside the surface, so choosing an | |
| 35 | + // option arrives as an outside interaction. Dismissing there would drop the | |
| 36 | + // selection before it lands. | |
| 37 | + expect( | |
| 38 | + dismissesOnOutsideInteraction({ dismissOnOutsideClick: true, insidePortaledPopup: true }), | |
| 39 | + ).toBe(false); | |
| 40 | + }); | |
| 41 | +}); | |
| 42 | + | |
| 43 | +/** A stand-in for the event Radix dispatches, recording whether it was blocked. */ | |
| 44 | +const outsideEvent = () => { | |
| 45 | + let prevented = false; | |
| 46 | + return { | |
| 47 | + event: { | |
| 48 | + detail: { originalEvent: { target: { nodeName: "DIV" } } }, | |
| 49 | + preventDefault: () => { | |
| 50 | + prevented = true; | |
| 51 | + }, | |
| 52 | + }, | |
| 53 | + prevented: () => prevented, | |
| 54 | + }; | |
| 55 | +}; | |
| 56 | + | |
| 57 | +describe("applyOutsideDismissPolicy", () => { | |
| 58 | + it("blocks a plain outside click by default", () => { | |
| 59 | + const outside = outsideEvent(); | |
| 60 | + applyOutsideDismissPolicy(outside.event, false); | |
| 61 | + expect(outside.prevented()).toBe(true); | |
| 62 | + }); | |
| 63 | + | |
| 64 | + it("lets a plain outside click through when the surface opts in", () => { | |
| 65 | + const outside = outsideEvent(); | |
| 66 | + applyOutsideDismissPolicy(outside.event, true); | |
| 67 | + expect(outside.prevented()).toBe(false); | |
| 68 | + }); | |
| 69 | +}); | |
| 70 | + | |
| 71 | +describe("PORTALED_POPUP_SELECTOR", () => { | |
| 72 | + it("covers both popup slots that portal out of a surface", () => { | |
| 73 | + expect(PORTALED_POPUP_SELECTOR).toBe( | |
| 74 | + "[data-slot='combobox-content'],[data-slot='select-content']", | |
| 75 | + ); | |
| 76 | + }); | |
| 77 | +}); | |
| @@ -0,0 +1,53 @@ | ||
| 1 | +/** | |
| 2 | + * Outside-interaction policy shared by `DialogContent` and `SheetContent`. | |
| 3 | + * | |
| 4 | + * Radix dismisses an overlay surface when an outside interaction is not | |
| 5 | + * default-prevented. That loses whatever the user typed, and a stray click on | |
| 6 | + * the page behind a form is easy to make. So the default here is the opposite | |
| 7 | + * of Radix's: an outside interaction keeps the surface open. Escape and the | |
| 8 | + * close button are unaffected — they still close. | |
| 9 | + * | |
| 10 | + * A surface with nothing to lose (a confirmation, a picker, a read-only panel) | |
| 11 | + * opts back in with `dismissOnOutsideClick`. | |
| 12 | + */ | |
| 13 | + | |
| 14 | +/** base-ui popups (combobox/select) portal their list OUTSIDE the surface, so a | |
| 15 | + * click on an option reads as an interaction outside it. Such a click must | |
| 16 | + * never dismiss, even when the surface opts in. */ | |
| 17 | +export const PORTALED_POPUP_SELECTOR = | |
| 18 | + "[data-slot='combobox-content'],[data-slot='select-content']"; | |
| 19 | + | |
| 20 | +/** True when the interaction started inside a popup this surface portals out. */ | |
| 21 | +export const isInsidePortaledPopup = (target: unknown): boolean => | |
| 22 | + typeof Element !== "undefined" && | |
| 23 | + target instanceof Element && | |
| 24 | + target.closest(PORTALED_POPUP_SELECTOR) !== null; | |
| 25 | + | |
| 26 | +/** | |
| 27 | + * Whether an outside interaction should close the surface. | |
| 28 | + * | |
| 29 | + * Pure so the decision is testable without a DOM: the caller does the element | |
| 30 | + * lookup and passes the answer in. | |
| 31 | + */ | |
| 32 | +export const dismissesOnOutsideInteraction = (input: { | |
| 33 | + readonly dismissOnOutsideClick: boolean; | |
| 34 | + readonly insidePortaledPopup: boolean; | |
| 35 | +}): boolean => input.dismissOnOutsideClick && !input.insidePortaledPopup; | |
| 36 | + | |
| 37 | +/** The shape Radix hands to `onInteractOutside` and `onPointerDownOutside`. */ | |
| 38 | +type OutsideInteractionEvent = { | |
| 39 | + readonly detail: { readonly originalEvent: { readonly target: unknown } }; | |
| 40 | + readonly preventDefault: () => void; | |
| 41 | +}; | |
| 42 | + | |
| 43 | +/** Apply the policy to a Radix outside-interaction event. */ | |
| 44 | +export const applyOutsideDismissPolicy = ( | |
| 45 | + event: OutsideInteractionEvent, | |
| 46 | + dismissOnOutsideClick: boolean, | |
| 47 | +): void => { | |
| 48 | + const dismisses = dismissesOnOutsideInteraction({ | |
| 49 | + dismissOnOutsideClick, | |
| 50 | + insidePortaledPopup: isInsidePortaledPopup(event.detail.originalEvent.target), | |
| 51 | + }); | |
| 52 | + if (!dismisses) event.preventDefault(); | |
| 53 | +}; | |
| @@ -670,7 +670,8 @@ export function AdminUsersPage() { · 669 unmodified lines | ||
| 670 | 670 | })} |
| 671 | 671 | |
| 672 | 672 | <Sheet open={selected !== null} onOpenChange={(open) => !open && setSelected(null)}> |
| 673 | − <SheetContent className="w-full gap-0 p-0 sm:max-w-xl"> | |
| 673 | + {/* A read-only detail panel: clicking away closes it. */} | |
| 674 | + <SheetContent dismissOnOutsideClick className="w-full gap-0 p-0 sm:max-w-xl"> | |
| 674 | 675 | {selected && ( |
| 675 | 676 | <> |
| 676 | 677 | <SheetHeader className="border-b border-border"> |
| @@ -540,7 +540,8 @@ function OrgApiKeysSectionBody() { · 539 unmodified lines | ||
| 540 | 540 | if (!open) setConfirmRevoke(null); |
| 541 | 541 | }} |
| 542 | 542 | > |
| 543 | − <DialogContent className="sm:max-w-[480px]"> | |
| 543 | + {/* A confirmation with nothing to lose: clicking away cancels it. */} | |
| 544 | + <DialogContent dismissOnOutsideClick className="sm:max-w-[480px]"> | |
| 544 | 545 | <DialogHeader> |
| 545 | 546 | <DialogTitle className="font-display text-xl">Revoke organization key</DialogTitle> |
| 546 | 547 | <DialogDescription className="text-sm leading-relaxed"> |
| @@ -496,7 +496,8 @@ function UpgradeDialog(props: { · 495 unmodified lines | ||
| 496 | 496 | }) { |
| 497 | 497 | return ( |
| 498 | 498 | <Dialog open={props.open} onOpenChange={props.onOpenChange}> |
| 499 | − <DialogContent className="sm:max-w-[400px]"> | |
| 499 | + {/* Informational only: nothing to lose, so clicking away dismisses it. */} | |
| 500 | + <DialogContent dismissOnOutsideClick className="sm:max-w-[400px]"> | |
| 500 | 501 | <DialogHeader> |
| 501 | 502 | <DialogTitle className="font-display text-xl">You are at your member limit</DialogTitle> |
| 502 | 503 | <DialogDescription className="text-sm leading-relaxed"> |