diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx index 10e46c222..1437f9f0d 100644 --- a/canvas/src/components/mobile/MobileChat.tsx +++ b/canvas/src/components/mobile/MobileChat.tsx @@ -756,7 +756,12 @@ export function MobileChat({ border: "none", outline: "none", background: "transparent", - fontSize: 14.5, + // iOS Safari/PWA zooms the viewport when a focused textarea + // has a computed font-size below 16px. 14.5 triggers that + // focus-zoom; the page looks broken until the user pinches + // back (#224, same class as desktop #1434 / sibling #225). + // 16px is the minimum that keeps focus from zooming. + fontSize: 16, lineHeight: 1.4, color: p.text, padding: "6px 0", diff --git a/canvas/src/components/mobile/MobileSpawn.tsx b/canvas/src/components/mobile/MobileSpawn.tsx index 34f6a9295..cdcbaf479 100644 --- a/canvas/src/components/mobile/MobileSpawn.tsx +++ b/canvas/src/components/mobile/MobileSpawn.tsx @@ -318,7 +318,12 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v border: `0.5px solid ${p.border}`, borderRadius: 12, fontFamily: MOBILE_FONT_MONO, - fontSize: 13.5, + // iOS Safari/PWA zooms the viewport when a focused input has + // a computed font-size below 16px; the layout jumps and the + // page looks broken until the user pinches back (#224 / #225, + // same class as desktop #1434). 16px is the minimum that + // suppresses that focus-zoom. + fontSize: 16, color: p.text, outline: "none", boxSizing: "border-box", diff --git a/canvas/src/components/mobile/__tests__/MobileChat.test.tsx b/canvas/src/components/mobile/__tests__/MobileChat.test.tsx index 0c8e24595..0aa69351d 100644 --- a/canvas/src/components/mobile/__tests__/MobileChat.test.tsx +++ b/canvas/src/components/mobile/__tests__/MobileChat.test.tsx @@ -263,6 +263,20 @@ describe("MobileChat — composer", () => { const sendBtn = container.querySelector('[aria-label="Send"]') as HTMLButtonElement; expect(sendBtn.disabled).toBe(true); }); + + // Regression #224: the composer textarea must render with font-size + // ≥ 16px. iOS Safari and PWAs auto-zoom the viewport when a focused + // input has a computed font-size below 16px — the layout jumps and + // the page looks broken until the user pinches back. Same class as + // desktop #1434 / sibling MobileSpawn #225. + it("composer textarea renders at font-size 16px or greater (iOS focus-zoom regression #224)", () => { + const { container } = renderChat(mockAgentId); + const textarea = container.querySelector("textarea") as HTMLTextAreaElement; + expect(textarea).toBeTruthy(); + const fs = Number.parseFloat(textarea.style.fontSize); + expect(Number.isFinite(fs)).toBe(true); + expect(fs).toBeGreaterThanOrEqual(16); + }); }); // ─── Tabs ───────────────────────────────────────────────────────────────────── diff --git a/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx b/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx index fb34825ef..03b0cffe3 100644 --- a/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx +++ b/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx @@ -93,6 +93,24 @@ describe("MobileSpawn — render", () => { expect(input).toBeTruthy(); }); + // Regression #224 / #225: the agent-name input must render with a + // font-size ≥ 16px. iOS Safari and PWAs auto-zoom the viewport when a + // focused input has a computed font-size below 16px — the layout + // jumps and the page looks broken until the user pinches back. + it("renders the name input at font-size 16px or greater (iOS focus-zoom regression)", () => { + apiGetSpy.mockResolvedValue(mockTemplates); + render(); + const input = document.querySelector( + 'input[aria-label="Agent name"]', + ) as HTMLInputElement | null; + expect(input).toBeTruthy(); + // Parse the inline style font-size — jsdom doesn't run a layout + // engine, so getComputedStyle reports the inline value verbatim. + const fs = Number.parseFloat(input!.style.fontSize); + expect(Number.isFinite(fs)).toBe(true); + expect(fs).toBeGreaterThanOrEqual(16); + }); + it("renders all 4 tier buttons", () => { apiGetSpy.mockResolvedValue(mockTemplates); render();