From 26bd85d4c52890179eb3f53824b4e5151f5bb983 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 20 Jun 2026 16:32:39 -0700 Subject: [PATCH] FN-6800: center mobile nav icons in fixed slots Normalize mobile bottom navigation icon slots so unread indicators no longer skew perceived spacing. - Wrap every mobile nav icon in a consistent token-sized slot. - Keep chat unread dots inside the icon slot instead of protruding past it. - Extend MobileNavBar tests to assert icon-slot structure and spacing CSS. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-6800-mobile-nav-spacing.md | 5 ++++ packages/dashboard/app/components/MobileNavBar.css | 22 ++++++++++++----- packages/dashboard/app/components/MobileNavBar.tsx | 28 ++++++++++++++++------ .../app/components/__tests__/MobileNavBar.test.tsx | 24 ++++++++++++++++++- 4 files changed, 65 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-6800 Fusion-Task-Lineage: 66ac21cf-6761-4b2c-b2ae-723998ae2632 --- .changeset/fn-6800-mobile-nav-spacing.md | 5 ++++ .../dashboard/app/components/MobileNavBar.css | 22 +++++++++++---- .../dashboard/app/components/MobileNavBar.tsx | 28 ++++++++++++++----- .../__tests__/MobileNavBar.test.tsx | 24 +++++++++++++++- 4 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 .changeset/fn-6800-mobile-nav-spacing.md diff --git a/.changeset/fn-6800-mobile-nav-spacing.md b/.changeset/fn-6800-mobile-nav-spacing.md new file mode 100644 index 0000000000..c130f3c445 --- /dev/null +++ b/.changeset/fn-6800-mobile-nav-spacing.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix mobile bottom navigation icon alignment so unread indicators use a centered token-sized icon slot without visually skewing tab spacing. diff --git a/packages/dashboard/app/components/MobileNavBar.css b/packages/dashboard/app/components/MobileNavBar.css index 7249570bed..2e32ebb44b 100644 --- a/packages/dashboard/app/components/MobileNavBar.css +++ b/packages/dashboard/app/components/MobileNavBar.css @@ -78,8 +78,13 @@ FNXC:MobileNav 2026-06-20-02:04: Every mobile nav tab must be an equal-width column with a centered icon so inter-icon spacing stays uniform across tab counts, long labels, active state, badges, and status dots. Use a zero flex basis plus min-width:0 so intrinsic label width cannot bias flex distribution; badges and status dots stay absolutely positioned and out of flow. + +FNXC:MobileNav 2026-06-20-16:11: +Real-browser geometry showed the SVG centers were uniform after FN-6766, but status dots protruded past Chat/Mailbox icon boxes and made those icon clusters optically right-heavy. +Wrap every tab icon in the same token-sized icon slot and keep unread/pending dots inside that slot so icon centering and perceived inter-icon spacing stay identical across tab count, label length, badge, status-dot, and active variants. */ .mobile-nav-tab { + --mobile-nav-icon-size: calc(var(--space-lg) + var(--space-sm) - (var(--space-xs) / 2)); flex: 1 1 0; min-width: 0; display: flex; @@ -87,7 +92,7 @@ Use a zero flex basis plus min-width:0 so intrinsic label width cannot bias flex align-items: center; justify-content: center; gap: calc(var(--space-xs) / 2); - min-height: 36px; + min-height: calc(var(--space-xl) + var(--space-md)); padding: calc(var(--space-sm) - (var(--space-xs) / 2)) 0; background: none; border: none; @@ -109,19 +114,24 @@ Use a zero flex basis plus min-width:0 so intrinsic label width cannot bias flex } .mobile-nav-tab svg { - width: 22px; - height: 22px; + width: var(--mobile-nav-icon-size); + height: var(--mobile-nav-icon-size); } .mobile-nav-tab-icon-wrapper { position: relative; - display: inline-flex; + display: flex; + flex: 0 0 var(--mobile-nav-icon-size); + align-items: center; + justify-content: center; + width: var(--mobile-nav-icon-size); + height: var(--mobile-nav-icon-size); } .mobile-nav-chat-unread-dot { position: absolute; - top: calc(var(--space-xs) * -1); - right: calc(var(--space-xs) * -1); + top: 0; + right: 0; } .mobile-nav-tab-label { diff --git a/packages/dashboard/app/components/MobileNavBar.tsx b/packages/dashboard/app/components/MobileNavBar.tsx index 30a6cd049f..b8afb7dccd 100644 --- a/packages/dashboard/app/components/MobileNavBar.tsx +++ b/packages/dashboard/app/components/MobileNavBar.tsx @@ -328,7 +328,9 @@ export function MobileNavBar({ } }} > - + + + {t("nav.tasks", "Tasks")} @@ -340,7 +342,9 @@ export function MobileNavBar({ aria-selected={view === "agents"} onClick={() => onChangeView("agents")} > - + + + {t("nav.agents", "Agents")} @@ -352,7 +356,9 @@ export function MobileNavBar({ aria-selected={view === "missions"} onClick={() => onChangeView("missions")} > - + + + {t("nav.missions", "Missions")} @@ -407,7 +413,9 @@ export function MobileNavBar({ aria-selected={view === "command-center"} onClick={() => onChangeView("command-center")} > - + + + {t("nav.commandCenter", "Command Center")} @@ -420,7 +428,9 @@ export function MobileNavBar({ aria-selected={view === "skills"} onClick={() => onChangeView("skills")} > - + + + {t("nav.skills", "Skills")} )} @@ -439,7 +449,9 @@ export function MobileNavBar({ aria-selected={view === pluginTaskView || (view === "graph" && entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph")} onClick={() => onChangeView(entry.pluginId === "fusion-plugin-dependency-graph" && entry.view.viewId === "graph" ? "graph" : pluginTaskView)} > - + + + {entry.view.label} ); @@ -453,7 +465,9 @@ export function MobileNavBar({ aria-selected={false} onClick={() => setIsMoreOpen((prev) => !prev)} > - + + + {t("nav.more", "More")} diff --git a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx index b8867b12f8..6f9e5e37a2 100644 --- a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx +++ b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx @@ -45,12 +45,26 @@ function expectUniformMobileNavColumns(container: HTMLElement, expectedTabCount: expect(tabs).toHaveLength(expectedTabCount); const tabRule = extractRuleBlock(mobileNavCss, ".mobile-nav-tab"); + expect(tabRule).toContain("--mobile-nav-icon-size: calc(var(--space-lg) + var(--space-sm) - (var(--space-xs) / 2))"); expect(tabRule).toContain("flex: 1 1 0"); expect(tabRule).toContain("min-width: 0"); expect(tabRule).toContain("align-items: center"); expect(tabRule).toMatch(/padding:\s*[^;]+\s+0;/); expect(tabRule).not.toMatch(/margin-left|margin-right/); + const iconRule = extractRuleBlock(mobileNavCss, ".mobile-nav-tab svg"); + expect(iconRule).toContain("width: var(--mobile-nav-icon-size)"); + expect(iconRule).toContain("height: var(--mobile-nav-icon-size)"); + + const iconWrapperRule = extractRuleBlock(mobileNavCss, ".mobile-nav-tab-icon-wrapper"); + expect(iconWrapperRule).toContain("position: relative"); + expect(iconWrapperRule).toContain("display: flex"); + expect(iconWrapperRule).toContain("flex: 0 0 var(--mobile-nav-icon-size)"); + expect(iconWrapperRule).toContain("align-items: center"); + expect(iconWrapperRule).toContain("justify-content: center"); + expect(iconWrapperRule).toContain("width: var(--mobile-nav-icon-size)"); + expect(iconWrapperRule).toContain("height: var(--mobile-nav-icon-size)"); + const labelRule = extractRuleBlock(mobileNavCss, ".mobile-nav-tab-label"); expect(labelRule).toContain("width: 100%"); expect(labelRule).toContain("min-width: 0"); @@ -59,6 +73,10 @@ function expectUniformMobileNavColumns(container: HTMLElement, expectedTabCount: for (const tab of tabs) { expect(tab.className).toContain("mobile-nav-tab"); expect(tab.querySelector(".mobile-nav-tab-label")).toBeInTheDocument(); + const iconSlots = tab.querySelectorAll(":scope > .mobile-nav-tab-icon-wrapper"); + expect(iconSlots).toHaveLength(1); + expect(tab.querySelector(":scope > svg")).toBeNull(); + expect(iconSlots[0].querySelector("svg")).toBeInTheDocument(); } if (container.querySelector(".mobile-nav-tab-badge")) { @@ -66,7 +84,11 @@ function expectUniformMobileNavColumns(container: HTMLElement, expectedTabCount: } if (container.querySelector(".mobile-nav-chat-unread-dot")) { - expect(extractRuleBlock(mobileNavCss, ".mobile-nav-chat-unread-dot")).toContain("position: absolute"); + const dotRule = extractRuleBlock(mobileNavCss, ".mobile-nav-chat-unread-dot"); + expect(dotRule).toContain("position: absolute"); + expect(dotRule).toContain("top: 0"); + expect(dotRule).toContain("right: 0"); + expect(dotRule).not.toContain("*-1"); } }