diff --git a/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.test.tsx b/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.test.tsx
index ec73fb18d..a501078d1 100644
--- a/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.test.tsx
+++ b/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.test.tsx
@@ -13,4 +13,23 @@ describe("ReAuthBannerBar", () => {
expect(bar.style.transform).toBe("translate(-50%, -50%)");
expect(bar.style.zIndex).toBe("200");
});
+
+ it("caps its width instead of fixing it, so a narrow viewport cannot clip its controls", () => {
+ renderWithMantine(
+ contents,
+ );
+ const bar = screen.getByTestId("bar");
+ // Centered by `left: 50%` plus a -50% translate, so a width wider than the
+ // viewport would overflow both edges and clip the close button on one side
+ // and "Authorize again" on the other (#2218).
+ expect(bar.style.width).toBe("calc(100vw - 2rem)");
+ expect(bar.style.maxWidth).toBe("calc(26.25rem * var(--mantine-scale))");
+ });
+
+ it("takes its centering offset from the Paper theme rather than inline styles", () => {
+ renderWithMantine(
+ contents,
+ );
+ expect(screen.getByTestId("bar")).toHaveAttribute("data-variant", "reauth");
+ });
});
diff --git a/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.tsx b/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.tsx
index be0b51ddc..f8240e4b3 100644
--- a/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.tsx
+++ b/clients/web/src/components/groups/ReAuthBanner/ReAuthBannerBar.tsx
@@ -1,7 +1,10 @@
import { Paper } from "@mantine/core";
// The re-auth popup. A `Paper` so every static style is a prop; the stacking
-// order goes through `styles.root` since Mantine has no `z` prop.
+// order and the centering offset go through the `reauth` variant in
+// `theme/Paper.ts`, since Mantine exposes neither `transform` nor `zIndex` as a
+// style prop and flat CSS belongs in the theme layer rather than in inline
+// `styles` (#2218).
//
// Floats rather than spanning the top as a sticky full-bleed bar. The bar cost
// the whole view a band of vertical space for what is a notification about one
@@ -24,15 +27,21 @@ import { Paper } from "@mantine/core";
// "Authorize again" also clears the stale OAuth state, which a plain reconnect
// does not do. So it floats above the page and leaves it usable.
//
-// `transform` goes through `styles.root` for the same reason `zIndex` does:
-// Mantine exposes neither as a style prop.
+// The width is CAPPED at 420, not fixed at it. Because the banner is centered
+// by `left: 50%` plus a -50% translate, a fixed width wider than the viewport
+// overflows BOTH edges equally — clipping the close button on one side and
+// "Authorize again" on the other, which are the only two controls it has. That
+// is reachable on a narrow desktop window, not just a phone, since the element
+// is positioned against the viewport rather than a panel. `maw` caps it while
+// `w` keeps a 1rem gutter on each side so the shadow and radius still read.
export const ReAuthBannerBar = Paper.withProps({
+ variant: "reauth",
pos: "fixed",
top: "50%",
left: "50%",
- w: 420,
+ w: "calc(100vw - 2rem)",
+ maw: 420,
bg: "var(--mantine-color-body)",
shadow: "xl",
radius: "md",
- styles: { root: { transform: "translate(-50%, -50%)", zIndex: 200 } },
});
diff --git a/clients/web/src/theme/Paper.ts b/clients/web/src/theme/Paper.ts
index 8170703e2..39d455cd0 100644
--- a/clients/web/src/theme/Paper.ts
+++ b/clients/web/src/theme/Paper.ts
@@ -25,6 +25,17 @@ export const ThemePaper = Paper.extend({
},
};
}
+ if (props.variant === "reauth") {
+ return {
+ root: {
+ // Centers the fixed-position re-auth banner against the viewport.
+ // Neither property is available as a Mantine style prop, so the
+ // theme layer is where they belong (#2218).
+ transform: "translate(-50%, -50%)",
+ zIndex: 200,
+ },
+ };
+ }
if (props.variant === "panel") {
return {
root: {