From 292d3bd2a032e5091b062c6e0db9c5f95ed517fe Mon Sep 17 00:00:00 2001 From: Kyrylo Yevdokymov Date: Fri, 10 Jul 2026 17:37:02 +0300 Subject: [PATCH 1/3] fix(connections): cap dialog height and scroll overflowing content The connection dialogs (Cloud/Relay/Webhook/About) had no max-height: when the content grew past the viewport (e.g. SMTP Relay with the config-pin banner and Advanced section expanded on a short screen) the fixed-centered panel clipped at both edges and the action buttons became unreachable. Cap the shared shell at 85vh and make it a flex column: the title row stays pinned, the lead + form body scrolls (overflow-y: auto), and the shared DialogActions row is sticky at the bottom of the scroll area with an opaque background, so Save/Cancel/Remove stay visible at any viewport height. Spacing is preserved via a margin/padding swap on the actions row and applies to all four dialogs through dialogStyles. Co-Authored-By: Claude Fable 5 --- .../components/connections/dialogAtoms.tsx | 21 +++++++++--- .../components/connections/dialogStyles.ts | 32 ++++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/connections/dialogAtoms.tsx b/frontend/src/components/connections/dialogAtoms.tsx index ad4b0c4..9e20a3a 100644 --- a/frontend/src/components/connections/dialogAtoms.tsx +++ b/frontend/src/components/connections/dialogAtoms.tsx @@ -6,6 +6,8 @@ import { configBanner, configBannerCode, content, + contentBody, + contentHeader, dialogLead, dialogTitle, field, @@ -21,6 +23,11 @@ import { LockedFieldHint } from './LockedFieldHint' * paragraph. The actual form body is `children`. Callers mount the * body only when `open` so each open cycle gets fresh useState * initialisers without a reset-effect (the existing convention). + * + * The panel is capped at 85vh: the title stays pinned while the lead + + * body scroll, and the DialogActions row (sticky, see `actions` in + * dialogStyles) stays visible — so every control remains reachable on + * short viewports. */ export function ConnectionDialogShell({ open, @@ -42,11 +49,15 @@ export function ConnectionDialogShell({ {open && ( <> - -

{title}

-
-

{lead}

- {children} +
+ +

{title}

+
+
+
+

{lead}

+ {children} +
)}
diff --git a/frontend/src/components/connections/dialogStyles.ts b/frontend/src/components/connections/dialogStyles.ts index 9d13431..a30ee35 100644 --- a/frontend/src/components/connections/dialogStyles.ts +++ b/frontend/src/components/connections/dialogStyles.ts @@ -4,8 +4,9 @@ * AboutDialog). Keeps all dialogs visually locked to the same shell — * palette, spacing, and control styles flow from one place. * - * Convention: shells (`content`, `field`, `lockedHint`, `configBanner`) - * carry only the wrapper's own utilities. Per-element classes + * Convention: shells (`content`, `contentHeader`, `contentBody`, + * `field`, `lockedHint`, `configBanner`) carry only the wrapper's own + * utilities. Per-element classes * (`dialogTitle`, `dialogLead`, `fieldLabel`, `fieldInput`, etc.) are * applied directly to the elements they style at the dialog callsites. * This avoids parent-side `[&_…]:` selectors that route styles through @@ -16,16 +17,23 @@ import { inputBase } from '../../lib/styles' export const overlay = 'fixed inset-0 z-50 bg-black/60' -// Dialog shell — positioning + chrome only. Title/lead get their own -// classes at the callsite. +// Dialog shell — positioning + chrome only. Height is capped so tall +// dialogs (e.g. relay with Advanced expanded on a short viewport) never +// clip past the screen edges: the title (`contentHeader`) stays pinned, +// the form (`contentBody`) scrolls, and the sticky `actions` row keeps +// the buttons visible. Padding lives on header/body, not the panel, so +// the scroll area runs edge to edge. export const content = [ 'fixed top-1/2 left-1/2 z-[51] -translate-x-1/2 -translate-y-1/2', - 'w-[460px] max-w-[calc(100vw-32px)]', - 'rounded-[10px] border border-border-base bg-surface-raised text-fg', - 'px-6 pt-[22px] pb-5', + 'flex max-h-[85vh] w-[460px] max-w-[calc(100vw-32px)] flex-col', + 'overflow-hidden rounded-[10px] border border-border-base bg-surface-raised text-fg', 'shadow-[0_20px_60px_rgba(0,0,0,0.5)]', ].join(' ') +export const contentHeader = 'shrink-0 px-6 pt-[22px]' + +export const contentBody = 'min-h-0 overflow-y-auto px-6 pb-5' + export const dialogTitle = 'm-0 mb-1.5 text-[17px] font-semibold' export const dialogLead = 'm-0 mb-4 text-[13px] leading-[1.5] text-fg-muted' @@ -80,7 +88,15 @@ export const errorBox = [ 'bg-danger-soft border-danger-border text-danger', ].join(' ') -export const actions = 'mt-[18px] flex justify-end gap-2' +// Bottom button row. Sticky so the buttons stay reachable while +// contentBody scrolls. -mb-5/pb-5 shift the body's bottom padding onto +// the opaque row (nothing shows through beneath it when stuck), and +// mt-2.5 + pt-2 preserve the usual 18px gap above — the margin +// collapses with the previous sibling's, the padding stays opaque. +export const actions = [ + 'sticky bottom-0 mt-2.5 -mb-5 flex justify-end gap-2', + 'bg-surface-raised pt-2 pb-5', +].join(' ') // Variant-driven button. Use as `className={btn}` and set // `data-variant="primary" | "outline" | "danger-text"` on the element From 9e403b94bd5155ecd4a5138a1b6b5b4dc6ba52fa Mon Sep 17 00:00:00 2001 From: Kyrylo Yevdokymov Date: Fri, 10 Jul 2026 17:56:49 +0300 Subject: [PATCH 2/3] fixup! fix(connections): cap dialog height and scroll overflowing content --- .../src/components/connections/dialogStyles.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/connections/dialogStyles.ts b/frontend/src/components/connections/dialogStyles.ts index a30ee35..5c329bf 100644 --- a/frontend/src/components/connections/dialogStyles.ts +++ b/frontend/src/components/connections/dialogStyles.ts @@ -32,7 +32,12 @@ export const content = [ export const contentHeader = 'shrink-0 px-6 pt-[22px]' -export const contentBody = 'min-h-0 overflow-y-auto px-6 pb-5' +// No bottom padding here — the sticky `actions` row carries it (pb-5). +// Padding on the scroll container itself would sit BELOW the stuck row +// (sticky offsets account for margins, so the row can't be pulled into +// the padding zone with a negative margin) and scrolled content would +// show through the strip. +export const contentBody = 'min-h-0 overflow-y-auto px-6' export const dialogTitle = 'm-0 mb-1.5 text-[17px] font-semibold' @@ -89,12 +94,13 @@ export const errorBox = [ ].join(' ') // Bottom button row. Sticky so the buttons stay reachable while -// contentBody scrolls. -mb-5/pb-5 shift the body's bottom padding onto -// the opaque row (nothing shows through beneath it when stuck), and -// mt-2.5 + pt-2 preserve the usual 18px gap above — the margin -// collapses with the previous sibling's, the padding stays opaque. +// contentBody scrolls. The row owns the dialog's bottom padding (pb-5, +// opaque background) so it sticks truly flush with the scrollport +// bottom and nothing shows through beneath it; mt-2.5 + pt-2 preserve +// the usual gap above — the margin collapses with the previous +// sibling's, the padding stays opaque. export const actions = [ - 'sticky bottom-0 mt-2.5 -mb-5 flex justify-end gap-2', + 'sticky bottom-0 mt-2.5 flex justify-end gap-2', 'bg-surface-raised pt-2 pb-5', ].join(' ') From 232aa5ea482ee5dd24581df23843733eea6d12af Mon Sep 17 00:00:00 2001 From: Kyrylo Yevdokymov Date: Mon, 13 Jul 2026 13:22:36 +0300 Subject: [PATCH 3/3] [MT-23013] simplify dialog overflow styles --- .../components/connections/dialogAtoms.tsx | 6 ------ .../components/connections/dialogStyles.ts | 19 ++----------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/connections/dialogAtoms.tsx b/frontend/src/components/connections/dialogAtoms.tsx index 9e20a3a..70fd909 100644 --- a/frontend/src/components/connections/dialogAtoms.tsx +++ b/frontend/src/components/connections/dialogAtoms.tsx @@ -23,11 +23,6 @@ import { LockedFieldHint } from './LockedFieldHint' * paragraph. The actual form body is `children`. Callers mount the * body only when `open` so each open cycle gets fresh useState * initialisers without a reset-effect (the existing convention). - * - * The panel is capped at 85vh: the title stays pinned while the lead + - * body scroll, and the DialogActions row (sticky, see `actions` in - * dialogStyles) stays visible — so every control remains reachable on - * short viewports. */ export function ConnectionDialogShell({ open, @@ -195,4 +190,3 @@ export function DialogButton({ ) } - diff --git a/frontend/src/components/connections/dialogStyles.ts b/frontend/src/components/connections/dialogStyles.ts index 5c329bf..4f6c66b 100644 --- a/frontend/src/components/connections/dialogStyles.ts +++ b/frontend/src/components/connections/dialogStyles.ts @@ -17,12 +17,7 @@ import { inputBase } from '../../lib/styles' export const overlay = 'fixed inset-0 z-50 bg-black/60' -// Dialog shell — positioning + chrome only. Height is capped so tall -// dialogs (e.g. relay with Advanced expanded on a short viewport) never -// clip past the screen edges: the title (`contentHeader`) stays pinned, -// the form (`contentBody`) scrolls, and the sticky `actions` row keeps -// the buttons visible. Padding lives on header/body, not the panel, so -// the scroll area runs edge to edge. +// Cap dialog height while keeping the title outside the scroll area. export const content = [ 'fixed top-1/2 left-1/2 z-[51] -translate-x-1/2 -translate-y-1/2', 'flex max-h-[85vh] w-[460px] max-w-[calc(100vw-32px)] flex-col', @@ -32,11 +27,6 @@ export const content = [ export const contentHeader = 'shrink-0 px-6 pt-[22px]' -// No bottom padding here — the sticky `actions` row carries it (pb-5). -// Padding on the scroll container itself would sit BELOW the stuck row -// (sticky offsets account for margins, so the row can't be pulled into -// the padding zone with a negative margin) and scrolled content would -// show through the strip. export const contentBody = 'min-h-0 overflow-y-auto px-6' export const dialogTitle = 'm-0 mb-1.5 text-[17px] font-semibold' @@ -93,12 +83,7 @@ export const errorBox = [ 'bg-danger-soft border-danger-border text-danger', ].join(' ') -// Bottom button row. Sticky so the buttons stay reachable while -// contentBody scrolls. The row owns the dialog's bottom padding (pb-5, -// opaque background) so it sticks truly flush with the scrollport -// bottom and nothing shows through beneath it; mt-2.5 + pt-2 preserve -// the usual gap above — the margin collapses with the previous -// sibling's, the padding stays opaque. +// Keep actions reachable while the dialog body scrolls. export const actions = [ 'sticky bottom-0 mt-2.5 flex justify-end gap-2', 'bg-surface-raised pt-2 pb-5',