diff --git a/.changeset/fn-6766-mobile-nav-spacing.md b/.changeset/fn-6766-mobile-nav-spacing.md new file mode 100644 index 0000000000..2530d3e90a --- /dev/null +++ b/.changeset/fn-6766-mobile-nav-spacing.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix mobile bottom tab navigation icon spacing so every tab uses an equal-width column across optional tabs, badges, and status dots. diff --git a/packages/dashboard/app/components/MobileNavBar.css b/packages/dashboard/app/components/MobileNavBar.css index c6831588ea..7249570bed 100644 --- a/packages/dashboard/app/components/MobileNavBar.css +++ b/packages/dashboard/app/components/MobileNavBar.css @@ -74,23 +74,28 @@ } /* Individual tab button */ +/* +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. +*/ .mobile-nav-tab { - flex: 1; + flex: 1 1 0; + min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 2px; - min-width: 36px; + gap: calc(var(--space-xs) / 2); min-height: 36px; - padding: 6px 0; + padding: calc(var(--space-sm) - (var(--space-xs) / 2)) 0; background: none; border: none; color: var(--text-muted); font-size: 10px; line-height: 1.2; cursor: pointer; - transition: color 0.15s ease; + transition: color var(--transition-fast); position: relative; -webkit-tap-highlight-color: transparent; } @@ -120,11 +125,14 @@ } .mobile-nav-tab-label { + width: 100%; + min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; font-size: 10px; line-height: 1.2; + text-align: center; white-space: nowrap; } diff --git a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx index eb9da10526..b8867b12f8 100644 --- a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx +++ b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx @@ -1,3 +1,5 @@ +import { readFileSync } from "fs"; +import { resolve } from "path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { MobileNavBar } from "../MobileNavBar"; @@ -26,6 +28,48 @@ function mockViewport(mode: "mobile" | "desktop") { }); } +const mobileNavCss = readFileSync(resolve(process.cwd(), "app/components/MobileNavBar.css"), "utf8"); + +function extractRuleBlock(css: string, selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`)); + return match?.[1] ?? ""; +} + +function getRenderedMobileTabs(container: HTMLElement): HTMLElement[] { + return Array.from(container.querySelectorAll(".mobile-nav-bar > .mobile-nav-tab")); +} + +function expectUniformMobileNavColumns(container: HTMLElement, expectedTabCount: number) { + const tabs = getRenderedMobileTabs(container); + expect(tabs).toHaveLength(expectedTabCount); + + const tabRule = extractRuleBlock(mobileNavCss, ".mobile-nav-tab"); + 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 labelRule = extractRuleBlock(mobileNavCss, ".mobile-nav-tab-label"); + expect(labelRule).toContain("width: 100%"); + expect(labelRule).toContain("min-width: 0"); + expect(labelRule).toContain("text-align: center"); + + for (const tab of tabs) { + expect(tab.className).toContain("mobile-nav-tab"); + expect(tab.querySelector(".mobile-nav-tab-label")).toBeInTheDocument(); + } + + if (container.querySelector(".mobile-nav-tab-badge")) { + expect(extractRuleBlock(mobileNavCss, ".mobile-nav-tab-badge")).toContain("position: absolute"); + } + + if (container.querySelector(".mobile-nav-chat-unread-dot")) { + expect(extractRuleBlock(mobileNavCss, ".mobile-nav-chat-unread-dot")).toContain("position: absolute"); + } +} + const createDefaultProps = () => ({ view: "board" as const, onChangeView: vi.fn(), @@ -105,6 +149,57 @@ describe("MobileNavBar", () => { expect(screen.queryByTestId("mobile-nav-tab-skills")).toBeNull(); }); + it("keeps every mobile tab in an equal-width column across tab, active, badge, and status-dot variants", () => { + const sevenTabRender = render( + , + ); + expectUniformMobileNavColumns(sevenTabRender.container, 7); + expect(screen.getByTestId("mobile-nav-tab-command-center").className).toContain("mobile-nav-tab--active"); + expect(screen.getByLabelText("Unread chat response")).toBeInTheDocument(); + expect(screen.getByLabelText("Pending approvals")).toBeInTheDocument(); + expect(screen.getByTestId("mobile-nav-tab-mailbox").querySelector(".mobile-nav-tab-badge")?.textContent).toBe("7"); + sevenTabRender.unmount(); + + const eightTabRender = render( + , + ); + expectUniformMobileNavColumns(eightTabRender.container, 8); + expect(screen.getByTestId("mobile-nav-tab-skills").className).toContain("mobile-nav-tab--active"); + expect(screen.getByTestId("mobile-nav-tab-mailbox").querySelector(".mobile-nav-tab-badge")?.textContent).toBe("99+"); + eightTabRender.unmount(); + + const pluginVariantRender = render( + , + ); + expectUniformMobileNavColumns(pluginVariantRender.container, 8); + expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-spacing-check-wide")).toBeNull(); + fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); + expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-spacing-check-wide")).toBeDefined(); + }); + it("keeps Todos in the mobile More sheet when todoView is enabled", () => { const onOpenTodos = vi.fn(); render(