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
This commit is contained in:
5
.changeset/fn-6800-mobile-nav-spacing.md
Normal file
5
.changeset/fn-6800-mobile-nav-spacing.md
Normal file
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -328,7 +328,9 @@ export function MobileNavBar({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LayoutGrid />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<LayoutGrid />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{t("nav.tasks", "Tasks")}</span>
|
||||
</button>
|
||||
|
||||
@@ -340,7 +342,9 @@ export function MobileNavBar({
|
||||
aria-selected={view === "agents"}
|
||||
onClick={() => onChangeView("agents")}
|
||||
>
|
||||
<Bot />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<Bot />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{t("nav.agents", "Agents")}</span>
|
||||
</button>
|
||||
|
||||
@@ -352,7 +356,9 @@ export function MobileNavBar({
|
||||
aria-selected={view === "missions"}
|
||||
onClick={() => onChangeView("missions")}
|
||||
>
|
||||
<Target />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<Target />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{t("nav.missions", "Missions")}</span>
|
||||
</button>
|
||||
|
||||
@@ -407,7 +413,9 @@ export function MobileNavBar({
|
||||
aria-selected={view === "command-center"}
|
||||
onClick={() => onChangeView("command-center")}
|
||||
>
|
||||
<Gauge />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<Gauge />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{t("nav.commandCenter", "Command Center")}</span>
|
||||
</button>
|
||||
|
||||
@@ -420,7 +428,9 @@ export function MobileNavBar({
|
||||
aria-selected={view === "skills"}
|
||||
onClick={() => onChangeView("skills")}
|
||||
>
|
||||
<Zap />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<Zap />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{t("nav.skills", "Skills")}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -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)}
|
||||
>
|
||||
<PluginIcon />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<PluginIcon />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{entry.view.label}</span>
|
||||
</button>
|
||||
);
|
||||
@@ -453,7 +465,9 @@ export function MobileNavBar({
|
||||
aria-selected={false}
|
||||
onClick={() => setIsMoreOpen((prev) => !prev)}
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<span className="mobile-nav-tab-icon-wrapper">
|
||||
<MoreHorizontal />
|
||||
</span>
|
||||
<span className="mobile-nav-tab-label">{t("nav.more", "More")}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user