From e39af80f43384a4946141a8eb5b4e3fde3880da8 Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Wed, 15 Jul 2026 14:02:35 -0700
Subject: [PATCH] fix(ui): let a caller's max-width override DialogContent's
default cap
DialogContent bakes a default `sm:max-w-lg` cap into its base class. Because
that default is scoped to the `sm` breakpoint, a caller passing a bare
`max-w-*` (e.g. `className="max-w-md"`) lands in a different responsive bucket:
tailwind-merge keeps both classes and the sm:-scoped default out-specifies the
caller at >=640px, so the override silently only applies below `sm` and the
modal renders at 32rem on desktop regardless of intent.
Apply the default only when the caller sets no max-width of their own. This is
byte-identical for callers with no width or an sm:-prefixed width (already
overriding the default) and for max-w-none dialogs, and fixes the ~20 dialogs
that pass a bare max-w. Extract the composition into a pure, unit-tested helper.
---
.../ui/dialog-content-class.test.ts | 55 +++++++++++++++++++
.../src/components/ui/dialog-content-class.ts | 37 +++++++++++++
src/renderer/src/components/ui/dialog.tsx | 11 +---
3 files changed, 94 insertions(+), 9 deletions(-)
create mode 100644 src/renderer/src/components/ui/dialog-content-class.test.ts
create mode 100644 src/renderer/src/components/ui/dialog-content-class.ts
diff --git a/src/renderer/src/components/ui/dialog-content-class.test.ts b/src/renderer/src/components/ui/dialog-content-class.test.ts
new file mode 100644
index 00000000000..cd0e93cd21e
--- /dev/null
+++ b/src/renderer/src/components/ui/dialog-content-class.test.ts
@@ -0,0 +1,55 @@
+import { describe, expect, it } from 'vitest'
+import { resolveDialogContentClassName } from './dialog-content-class'
+
+describe('resolveDialogContentClassName', () => {
+ it('applies the default sm:max-w-lg cap when the caller passes no className', () => {
+ const cls = resolveDialogContentClassName()
+ expect(cls).toContain('sm:max-w-lg')
+ expect(cls).toContain('max-w-[calc(100%-2rem)]')
+ })
+
+ it('keeps the default cap when the caller className has no max-width', () => {
+ const cls = resolveDialogContentClassName('gap-0 p-0')
+ expect(cls).toContain('sm:max-w-lg')
+ })
+
+ it('lets a bare max-w override win at every breakpoint by dropping the default', () => {
+ const cls = resolveDialogContentClassName('max-w-md')
+ expect(cls).toContain('max-w-md')
+ // The bug: without dropping the sm:-scoped default, it out-specifies the
+ // caller's bare max-w at >=sm and the modal stays 32rem wide.
+ expect(cls).not.toContain('sm:max-w-lg')
+ })
+
+ it('honors a custom pixel max-width', () => {
+ const cls = resolveDialogContentClassName('max-w-[360px]')
+ expect(cls).toContain('max-w-[360px]')
+ expect(cls).not.toContain('sm:max-w-lg')
+ })
+
+ it('keeps an sm:-scoped override and drops the redundant default', () => {
+ const cls = resolveDialogContentClassName('sm:max-w-2xl')
+ expect(cls).toContain('sm:max-w-2xl')
+ expect(cls).not.toContain('sm:max-w-lg')
+ })
+
+ it('preserves an intentionally uncapped (max-w-none) dialog', () => {
+ const cls = resolveDialogContentClassName('max-w-none sm:max-w-none')
+ expect(cls).toContain('max-w-none')
+ expect(cls).toContain('sm:max-w-none')
+ expect(cls).not.toContain('sm:max-w-lg')
+ })
+
+ it('respects an important max-width override', () => {
+ const cls = resolveDialogContentClassName('!max-w-[360px]')
+ expect(cls).toContain('!max-w-[360px]')
+ expect(cls).not.toContain('sm:max-w-lg')
+ })
+
+ it('appends caller layout classes after the base', () => {
+ const cls = resolveDialogContentClassName('gap-4 p-0 sm:max-w-[520px]')
+ expect(cls).toContain('gap-4')
+ expect(cls).toContain('p-0')
+ expect(cls).toContain('sm:max-w-[520px]')
+ })
+})
diff --git a/src/renderer/src/components/ui/dialog-content-class.ts b/src/renderer/src/components/ui/dialog-content-class.ts
new file mode 100644
index 00000000000..117ba8fe02d
--- /dev/null
+++ b/src/renderer/src/components/ui/dialog-content-class.ts
@@ -0,0 +1,37 @@
+import { cn } from '@/lib/utils'
+
+// Why: bg-background in dark mode is the same color as the canvas, and
+// border-border/50 is ~3.5% white over that canvas — both invisible. A
+// translucent surface, solid 14% border, dual shadow, and 2xl backdrop blur
+// match the dropdown-menu recipe (which already works) and read clearly in both
+// light and dark mode.
+const DIALOG_CONTENT_BASE_CLASS =
+ '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 border-black/14 bg-background/96 p-6 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl duration-200 outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] 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'
+
+/** The default desktop width cap. Applied only when the caller sets no
+ * max-width of their own (see resolveDialogContentClassName). */
+const DIALOG_CONTENT_DEFAULT_MAX_WIDTH = 'sm:max-w-lg'
+
+/** Matches a Tailwind `max-w-*` utility in a className string, with or without a
+ * variant prefix (`sm:`) or important (`!`) modifier. */
+const CALLER_MAX_WIDTH = /(?:^|[\s:!])max-w-/
+
+/**
+ * Compose the DialogContent className.
+ *
+ * The default cap is `sm:max-w-lg`, but because it is scoped to the `sm`
+ * breakpoint, a caller's unprefixed `max-w-*` lands in a different responsive
+ * bucket and tailwind-merge keeps both — so the default silently wins at ≥sm and
+ * the caller's width is ignored on desktop. Applying the default only when the
+ * caller has not set their own max-width lets a bare `className="max-w-md"` win
+ * at every breakpoint, while leaving callers that pass no width (default cap) or
+ * an `sm:`-prefixed width (already overrode the default) byte-for-byte unchanged.
+ */
+export function resolveDialogContentClassName(className?: string): string {
+ const hasCallerMaxWidth = className ? CALLER_MAX_WIDTH.test(className) : false
+ return cn(
+ DIALOG_CONTENT_BASE_CLASS,
+ !hasCallerMaxWidth && DIALOG_CONTENT_DEFAULT_MAX_WIDTH,
+ className
+ )
+}
diff --git a/src/renderer/src/components/ui/dialog.tsx b/src/renderer/src/components/ui/dialog.tsx
index 8b1badf9ab3..6196e4cc0ff 100644
--- a/src/renderer/src/components/ui/dialog.tsx
+++ b/src/renderer/src/components/ui/dialog.tsx
@@ -5,6 +5,7 @@ import { XIcon } from 'lucide-react'
import { Dialog as DialogPrimitive } from 'radix-ui'
import { cn } from '@/lib/utils'
+import { resolveDialogContentClassName } from '@/components/ui/dialog-content-class'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
@@ -58,15 +59,7 @@ function DialogContent({
{children}