diff --git a/src/components/PairQrCode.tsx b/src/components/PairQrCode.tsx index 82ab97d..2c1ef15 100644 --- a/src/components/PairQrCode.tsx +++ b/src/components/PairQrCode.tsx @@ -44,7 +44,11 @@ export function PairQrCode({ value, label, size = QR_SIZE }: PairQrCodeProps) { }, [value, size]) if (!src) { - return + // Sized to the requested QR so resolving doesn't shift the layout + // (callers render at 224 and 288; a fixed w-56 jumped for the latter). + return ( + + ) } return ( diff --git a/src/features/friends/AddFriendDialogView.tsx b/src/features/friends/AddFriendDialogView.tsx index acfaf02..707c955 100644 --- a/src/features/friends/AddFriendDialogView.tsx +++ b/src/features/friends/AddFriendDialogView.tsx @@ -17,7 +17,6 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' // The QR encoder (qrcode) and scanner (jsqr) are moderate vendor deps used @@ -127,7 +126,13 @@ export function AddFriendDialogView({ }: AddFriendDialogViewProps) { return ( - + {/* Wider than the primitive's max-w-lg so the card surface fits QR + + import controls side by side; the max-h + overflow guard keeps every + sub-surface (card / scanner / legacy) reachable at the 1024×640 + window minimum — the dialog is fixed-centered, so without it + overflow clips equally off both screen edges with nothing to + scroll (title, footer and close button all become unreachable). */} + {missingDisplayName ? ( ) : mode === 'legacy' ? ( @@ -245,16 +250,21 @@ function CardSurface({ {card.addHeading} {card.scanHint} - } - > - interpretImportText(text) !== null} - /> - + {/* Capped width: the scanner preview is a full-width square, and at + the widened dialog an uncapped square alone would outgrow the + 640px window minimum. 384px is plenty for camera aiming. */} + + } + > + interpretImportText(text) !== null} + /> + + setScanning(false)}> {strings.common.actions.cancel} @@ -271,107 +281,118 @@ function CardSurface({ {card.description} - - - {card.yourCodeHeading} - - {cardBuildError ? ( - - {card.codeError} - - ) : myCardLink ? ( - <> - {/* Larger than the legacy word QR (224): the card is a denser - ~200-char byte-mode payload, so more px-per-module keeps it - scannable from a laptop camera across a desk. */} - } - > - - - - {card.qrCaption} + {/* Two columns — QR left, import controls right — so the pane's + height stays inside the 1024×640 window minimum. The previous + single-column stack (QR at 320 above the import section) computed + to ~890px against an 800px default viewport, clipping the title + and footer off both screen edges. Left column is fixed at w-72 to + match the QR; an auto column would size to the captions' + unwrapped max-content instead. */} + + + + {card.yourCodeHeading} + + {cardBuildError ? ( + + {card.codeError} + ) : myCardLink ? ( + <> + {/* Larger than the legacy word QR (224): the card is a denser + ~200-char byte-mode payload, so more px-per-module keeps it + scannable from a laptop camera across a desk. 288 (not the + earlier 320) is the largest size that keeps the whole pane + inside the window minimum alongside the import column. */} + + } + > + + + + {card.qrCaption} + + void handleCopy()} + aria-label={card.copyAriaLabel} + > + {copied ? ( + <> + {card.copiedCta} + > + ) : ( + <> + {card.copyCta} + > + )} + + + {card.yourCodeCaption} + + > + ) : ( + + + {card.codeBuilding} + + )} + + + + + + {card.addHeading} + + {card.addBody} + + void handleCopy()} - aria-label={card.copyAriaLabel} + onClick={() => setScanning(true)} > - {copied ? ( - <> - {card.copiedCta} - > - ) : ( - <> - {card.copyCta} - > - )} + {card.scanCta} + + void handlePasteClipboard()} + > + {card.pasteCta} - - {card.yourCodeCaption} - - > - ) : ( - - - {card.codeBuilding} - )} - - - - - - - - {card.addHeading} - - {card.addBody} - - - setScanning(true)} - > - {card.scanCta} - - void handlePasteClipboard()} - > - {card.pasteCta} - - - setPasteValue(e.target.value)} - aria-label={card.inputAriaLabel} - rows={2} - autoCapitalize="none" - autoCorrect="off" - spellCheck={false} - className="font-mono text-xs" - /> - - - {card.addCta} - - - + setPasteValue(e.target.value)} + aria-label={card.inputAriaLabel} + rows={2} + autoCapitalize="none" + autoCorrect="off" + spellCheck={false} + className="font-mono text-xs" + /> + + + {card.addCta} + + + +
- {card.codeError} -
- {card.qrCaption} + {/* Two columns — QR left, import controls right — so the pane's + height stays inside the 1024×640 window minimum. The previous + single-column stack (QR at 320 above the import section) computed + to ~890px against an 800px default viewport, clipping the title + and footer off both screen edges. Left column is fixed at w-72 to + match the QR; an auto column would size to the captions' + unwrapped max-content instead. */} +
+ {card.codeError}
+ {card.qrCaption} +
+ {card.yourCodeCaption} +
{card.codeBuilding}
{card.addBody}
- {card.yourCodeCaption} -