diff --git a/artifacts/tx-os/src/components/ui/dialog.tsx b/artifacts/tx-os/src/components/ui/dialog.tsx index fa003580..84295812 100644 --- a/artifacts/tx-os/src/components/ui/dialog.tsx +++ b/artifacts/tx-os/src/components/ui/dialog.tsx @@ -41,23 +41,32 @@ const DialogContent = React.forwardRef< const keyboardOpen = keyboardInset > 80 const dynamicStyle: React.CSSProperties = keyboardOpen ? { - // Anchor the dialog to the TOP of the visible visual viewport - // (right under the iOS status bar) and let it take the full - // available height above the keyboard. Anchoring (vs centering) - // is critical for tall forms — when the dialog is centered and - // the form has many fields, the footer slides into the middle - // of the visible area and the bottom half of the form ends up - // hidden behind the keyboard. With top-anchor + maxHeight, the - // dialog's own flex column (header / scrollable middle / - // footer) keeps Save/Cancel pinned above the keyboard and the - // rest of the form scrolls inside. + // Mobile-sheet behaviour when an on-screen keyboard is open. // - // The override of `transform` is necessary because the class - // string below applies `translate-y-[-50%]`; we need to keep - // the horizontal centering but cancel the vertical one. + // The base className puts the dialog at left:50%+translateX(-50%) + // (horizontal centering) and top:50%+translateY(-50%) (vertical + // centering). On iOS Safari with the soft keyboard open the + // layout viewport and the visual viewport diverge in BOTH axes + // (visualViewport.offsetLeft, .pageLeft and zoom factor can all + // shift), which made our previous "top-anchor + translateX(-50%)" + // override render as a narrow column shoved against one edge of + // the screen. + // + // To sidestep all of that we drop the centering entirely while + // the keyboard is open and pin the dialog as a top-anchored + // full-width sheet: explicit left/right insets, no transform, + // width:auto so the consumer's `w-[calc(100vw-1rem)]` or + // `max-w-3xl` no longer drive horizontal position. The + // dialog's internal `flex flex-col` (header / scrollable middle + // / sticky footer) then keeps Save/Cancel pinned above the + // keyboard and the rest of the form scrolls inside. top: `${offsetTop}px`, + left: "8px", + right: "8px", + width: "auto", + maxWidth: "none", maxHeight: `${height - 8}px`, - transform: "translateX(-50%)", + transform: "none", ...style, } : (style ?? {})