diff --git a/packages/dashboard/app/components/MailboxModal.css b/packages/dashboard/app/components/MailboxModal.css index 61cccc1522..c6f9c2d1a5 100644 --- a/packages/dashboard/app/components/MailboxModal.css +++ b/packages/dashboard/app/components/MailboxModal.css @@ -871,37 +871,47 @@ The list pane is fixed to its inline `width` (`flex: 0 0 auto`) so the divider d } /* -FNXC:MailboxMobile 2026-07-17-13:43: -FN-8238 keeps the full-page mobile title above one compact action row: unread badge, -Compose, Mark all read, and Refresh remain visible together. This runtime-mode class -mirrors isMobileViewport(); CSS media and pointer queries cannot observe its physical-screen -or visualViewport classification, so desktop and tablet headers retain their shared chrome. +FNXC:MailboxMobile 2026-07-19-17:00: +FN-8407 supersedes FN-8238's title-then-actions mobile stack: Mailbox title and its +unread badge, Compose, Mark all read, and Refresh share one right-aligned row at the +320px boundary. Keep this override runtime-class-gated because CSS media and pointer +queries cannot observe isMobileViewport()'s physical-screen or visualViewport classification; +FN-8349 attempted this same change but finalized without landing it. */ .mailbox-view--mobile .view-header { - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; gap: var(--space-sm); } .mailbox-view--mobile .view-header__title { - flex: 0 0 100%; + flex: 1 1 auto; + min-width: 0; } .mailbox-view--mobile .view-header__actions { - flex: 1 1 100%; + flex: 0 1 auto; min-width: 0; flex-wrap: nowrap; - justify-content: flex-start; + justify-content: flex-end; gap: var(--space-xs); - margin-left: 0; + margin-left: auto; } .mailbox-view--mobile .view-header__actions .btn { min-width: 0; flex-shrink: 1; padding: var(--space-xs); +} + +.mailbox-view--mobile .view-header__actions .btn span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } +.mailbox-view--mobile .view-header__actions .btn svg, .mailbox-view--mobile .view-header__actions .btn-icon, .mailbox-view--mobile .mailbox-unread-badge { flex-shrink: 0; diff --git a/packages/dashboard/app/components/__tests__/MailboxView.test.tsx b/packages/dashboard/app/components/__tests__/MailboxView.test.tsx index e203843f46..9e20d9217d 100644 --- a/packages/dashboard/app/components/__tests__/MailboxView.test.tsx +++ b/packages/dashboard/app/components/__tests__/MailboxView.test.tsx @@ -2310,8 +2310,12 @@ describe("MailboxView", () => { it("defines class-gated, compact single-row mailbox mobile layout rules", () => { const css = loadAllAppCss(); - expect(css).toMatch(/\.mailbox-view--mobile\s+\.view-header\s*\{[^}]*flex-wrap:\s*wrap;[^}]*\}/); - expect(css).toMatch(/\.mailbox-view--mobile\s+\.view-header__actions\s*\{[^}]*flex:\s*1\s+1\s+100%;[^}]*min-width:\s*0;[^}]*flex-wrap:\s*nowrap;[^}]*margin-left:\s*0;[^}]*\}/); + expect(css).toMatch(/\.mailbox-view--mobile\s+\.view-header\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*align-items:\s*center;[^}]*\}/); + expect(css).toMatch(/\.mailbox-view--mobile\s+\.view-header__title\s*\{[^}]*flex:\s*1\s+1\s+auto;[^}]*min-width:\s*0;[^}]*\}/); + expect(css).toMatch(/\.mailbox-view--mobile\s+\.view-header__actions\s*\{[^}]*flex:\s*0\s+1\s+auto;[^}]*min-width:\s*0;[^}]*flex-wrap:\s*nowrap;[^}]*justify-content:\s*flex-end;[^}]*margin-left:\s*auto;[^}]*\}/); + expect(css).toMatch(/\.mailbox-view--mobile\s+\.view-header__actions\s+\.btn\s+span\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;[^}]*\}/); + expect(css).not.toMatch(/\.mailbox-view--mobile\s+\.view-header__title\s*\{[^}]*flex:\s*0\s+0\s+100%;[^}]*\}/); + expect(css).not.toMatch(/\.mailbox-view--mobile\s+\.view-header__actions\s*\{[^}]*flex:\s*1\s+1\s+100%;[^}]*\}/); expect(css).toMatch(/\.mailbox-view--mobile\s+\.mailbox-tabs\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*overflow-x:\s*auto;[^}]*\}/); expect(css).toMatch(/\.mailbox-view--mobile\s+\.mailbox-tab\s*\{[^}]*min-width:\s*0;[^}]*flex:\s*1\s+1\s+0;[^}]*flex-shrink:\s*1;[^}]*\}/); expect(css).toMatch(/\.mailbox-view--mobile\s+\.mailbox-message-detail-header\s*\{[^}]*flex-direction:\s*row;[^}]*flex-wrap:\s*nowrap;[^}]*\}/); @@ -2321,7 +2325,7 @@ describe("MailboxView", () => { expect(css).toMatch(/\.mailbox-view:not\(\.mailbox-view--mobile\)\s+\.mailbox-message-detail-header\s*\{[^}]*flex-direction:\s*column;[^}]*\}/); expect(css).toMatch(/\.mailbox-view:not\(\.mailbox-view--mobile\)\s+\.mailbox-message-detail-actions\s*\{[^}]*flex-wrap:\s*wrap;[^}]*\}/); - // The runtime class, not a height or pointer media proxy, is the only FN-8238 gate. + // The runtime class, not a height or pointer media proxy, is the only FN-8407 gate. expect(css).not.toMatch(/@media\s*\([^)]*(?:max-height:\s*480px|pointer:\s*coarse)[^)]*\)\s*\{[\s\S]*?\.mailbox-view--mobile/); }); diff --git a/packages/dashboard/scripts/browser-layout-smoke.mjs b/packages/dashboard/scripts/browser-layout-smoke.mjs index 301475507e..9b150d2ebe 100644 --- a/packages/dashboard/scripts/browser-layout-smoke.mjs +++ b/packages/dashboard/scripts/browser-layout-smoke.mjs @@ -182,6 +182,25 @@ export function createSmokeHtml() { `).join(""); + /* + FNXC:MailboxMobile 2026-07-19-17:00: + FN-8407 requires a real-browser 320px regression surface because jsdom cannot + measure the shared ViewHeader flex geometry. Exercise the unread Inbox's tightest + badge + Compose + Mark all read + Refresh row and both Compose + Refresh-only states. + */ + const mailboxMobileHeaderFixtures = [ + ["unread-inbox", '9'], + ["read-inbox", ''], + ["non-inbox", ''], + ].map(([state, actions]) => ` +
+
+

Mailbox

+
${actions}
+
+
+ `).join(""); + return ` @@ -265,6 +284,10 @@ export function createSmokeHtml() { ${taskDetailInlineRowFixtures} +
+ ${mailboxMobileHeaderFixtures} +
+