FN-6696: promote Command Center in responsive navigation

Promote Command Center into fixed responsive nav positions while preserving overflow access for displaced views.

- Add tablet Header behavior that shows Command Center inline after Agents and moves Documents to the overflow menu.
- Add mobile top-level Command Center tab immediately after Mailbox and demote primary plugin tabs to More.
- Update navigation tests for tablet, desktop, mobile, and plugin overflow ordering.
- Add an xs font-size token for mailbox tab reuse.

Files changed:
 .../app/__tests__/tablet-header-controls.test.tsx  | 24 +++++++
 packages/dashboard/app/components/Header.tsx       | 82 ++++++++++++++++------
 packages/dashboard/app/components/MailboxModal.css |  4 +-
 packages/dashboard/app/components/MobileNavBar.tsx | 31 ++++----
 .../app/components/__tests__/Header.test.tsx       | 24 +++++++
 .../app/components/__tests__/MobileNavBar.test.tsx | 47 ++++++++++++-
 packages/dashboard/app/styles.css                  |  3 +-
 7 files changed, 174 insertions(+), 41 deletions(-)

Fusion-Task-Id: FN-6696

Fusion-Task-Lineage: e24bb740-7429-40f1-8612-d9af03486d26
This commit is contained in:
gsxdsm
2026-06-19 05:18:29 -07:00
parent 3383a095b7
commit 8b8e25c501
7 changed files with 174 additions and 41 deletions

View File

@@ -94,12 +94,36 @@ describe("tablet header controls", () => {
expect(screen.getByTitle("Board view")).toBeDefined();
expect(screen.getByTitle("List view")).toBeDefined();
expect(screen.getByTitle("Agents view")).toBeDefined();
expect(screen.getByTestId("view-toggle-command-center")).toBeDefined();
expect(screen.queryByTitle("Documents view")).toBeNull();
// Skills and Insights are NOT inline (they're in overflow)
expect(screen.queryByTitle("Skills view")).toBeNull();
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
expect(screen.queryByTitle("Insights view")).toBeNull();
});
it("places tablet Command Center inline immediately after Agents and Documents only in overflow", () => {
renderTabletHeader({ onChangeView: noop, showAgentsTab: true });
expect(screen.getByTestId("view-toggle-command-center").previousElementSibling).toBe(screen.getByTitle("Agents view"));
expect(screen.queryByTitle("Documents view")).toBeNull();
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-documents")).toBeDefined();
expect(screen.queryByTestId("view-overflow-command-center")).toBeNull();
});
it("keeps desktop Documents inline and Command Center in overflow", () => {
renderDesktopHeader({ onChangeView: noop, showAgentsTab: true });
expect(screen.getByTitle("Documents view")).toBeDefined();
expect(screen.queryByTestId("view-toggle-command-center")).toBeNull();
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-command-center")).toBeDefined();
expect(screen.queryByTestId("view-overflow-documents")).toBeNull();
});
it("renders view toggle overflow trigger on tablet when overflow items are available", () => {
renderTabletHeader({ onChangeView: noop, experimentalFeatures: { insights: true } });
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();

View File

@@ -260,9 +260,10 @@ export function Header({
experimentalFeatures?.memoryView ||
experimentalFeatures?.devServerView ||
!hideFullNav ||
isTablet ||
pluginDashboardViews.some((entry) => entry.view.placement !== "primary")
);
}, [onChangeView, experimentalFeatures, todosEnabled, showSkillsTab, hideFullNav, pluginDashboardViews]);
}, [onChangeView, experimentalFeatures, todosEnabled, showSkillsTab, hideFullNav, isTablet, pluginDashboardViews]);
const getEffectiveViewport = useCallback(() => {
const vv = window.visualViewport;
@@ -1018,6 +1019,23 @@ export function Header({
<Bot size={16} />
</button>
)}
{isTablet && (
/*
FNXC:Navigation 2026-06-19-12:00:
Tablet navigation promotes Command Center immediately after Agents while desktop keeps Command Center in the More-views overflow.
Documents moves to the tablet More-views overflow below to conserve horizontal space without changing desktop ordering.
*/
<button
className={`view-toggle-btn${view === "command-center" ? " active" : ""}`}
onClick={() => onChangeView("command-center")}
title={t("header.commandCenterView", "Command Center")}
aria-label={t("header.commandCenterView", "Command Center")}
aria-pressed={view === "command-center"}
data-testid="view-toggle-command-center"
>
<Gauge size={16} />
</button>
)}
<button
className={`view-toggle-btn${view === "missions" ? " active" : ""}`}
onClick={() => onChangeView("missions")}
@@ -1040,15 +1058,17 @@ export function Header({
<span className="status-dot status-dot--pending header-chat-unread-dot" aria-label={t("header.unreadChatResponse", "Unread chat response")} />
)}
</button>
<button
className={`view-toggle-btn${view === "documents" ? " active" : ""}`}
onClick={() => onChangeView("documents")}
title={t("header.documentsView", "Documents view")}
aria-label={t("header.documentsView", "Documents view")}
aria-pressed={view === "documents"}
>
<FileText size={16} />
</button>
{!isTablet && (
<button
className={`view-toggle-btn${view === "documents" ? " active" : ""}`}
onClick={() => onChangeView("documents")}
title={t("header.documentsView", "Documents view")}
aria-label={t("header.documentsView", "Documents view")}
aria-pressed={view === "documents"}
>
<FileText size={16} />
</button>
)}
<button
className={`view-toggle-btn${view === "mailbox" ? " active" : ""}`}
onClick={() => onChangeView("mailbox")}
@@ -1090,7 +1110,7 @@ export function Header({
<>
<button
ref={viewOverflowTriggerRef}
className={`view-toggle-btn${["research", "skills", "insights", "memory", "secrets", "reliability", "command-center", "dev-server", "devserver", "graph", "stash-recovery"].includes(view) || (experimentalFeatures?.evalsView && view === "evals") || (experimentalFeatures?.goalsView && view === "goalsView") || (todosEnabled && todosOpen) || isPluginViewId(view) ? " active" : ""}`}
className={`view-toggle-btn${["research", "skills", "insights", "memory", "secrets", "reliability", "dev-server", "devserver", "graph", "stash-recovery"].includes(view) || (!isTablet && view === "command-center") || (isTablet && view === "documents") || (experimentalFeatures?.evalsView && view === "evals") || (experimentalFeatures?.goalsView && view === "goalsView") || (todosEnabled && todosOpen) || isPluginViewId(view) ? " active" : ""}`}
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
title={t("header.moreViews", "More views")}
aria-label={t("header.moreViews", "More views")}
@@ -1230,18 +1250,34 @@ export function Header({
<Activity size={14} />
<span>{t("header.reliabilityView", "Reliability")}</span>
</button>
<button
className={`view-toggle-overflow-item${view === "command-center" ? " active" : ""}`}
onClick={() => {
onChangeView("command-center");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-command-center"
>
<Gauge size={14} />
<span>{t("header.commandCenterView", "Command Center")}</span>
</button>
{isTablet && (
<button
className={`view-toggle-overflow-item${view === "documents" ? " active" : ""}`}
onClick={() => {
onChangeView("documents");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-documents"
>
<FileText size={14} />
<span>{t("header.documentsView", "Documents view")}</span>
</button>
)}
{!isTablet && (
<button
className={`view-toggle-overflow-item${view === "command-center" ? " active" : ""}`}
onClick={() => {
onChangeView("command-center");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-command-center"
>
<Gauge size={14} />
<span>{t("header.commandCenterView", "Command Center")}</span>
</button>
)}
{experimentalFeatures?.devServerView && (
<button
className={`view-toggle-overflow-item${view === "dev-server" || view === "devserver" ? " active" : ""}`}

View File

@@ -789,7 +789,7 @@
.mailbox-modal .mailbox-tab {
flex-shrink: 0;
padding: var(--space-sm) var(--space-md);
font-size: 0.8rem;
font-size: var(--font-size-xs, 0.8rem);
}
.mailbox-modal .mailbox-content {
@@ -881,7 +881,7 @@
.mailbox-view .mailbox-tab {
flex-shrink: 0;
padding: var(--space-sm) var(--space-md);
font-size: 0.8rem;
font-size: var(--font-size-xs, 0.8rem);
}
.mailbox-view .mailbox-content {

View File

@@ -270,14 +270,19 @@ export function MobileNavBar({
const skillsEnabled = Boolean(showSkillsTab);
const todoViewEnabled = Boolean(experimentalFeatures?.todoView);
// Keep a maximum of one optional primary tab visible at once to preserve touch-target width.
// Keep optional primary tabs limited to preserve touch-target width.
// Overflowed destinations remain available in the More sheet.
const showSkillsTopLevel = skillsEnabled;
const showSkillsInMore = skillsEnabled && !showSkillsTopLevel;
const sortedPrimaryPluginViews = pluginDashboardViews
.filter((entry) => entry.view.placement === "primary")
.sort((a, b) => (a.view.order ?? Number.MAX_SAFE_INTEGER) - (b.view.order ?? Number.MAX_SAFE_INTEGER));
const MAX_PRIMARY_PLUGIN_TOP_LEVEL_TABS = 1;
/*
FNXC:Navigation 2026-06-19-12:05:
Mobile navigation adds Command Center as a fixed top-level tab immediately after Mailbox.
Primary plugin tabs, including Compound Engineering, are demoted to the More sheet so touch targets stay wide and Command Center is not duplicated.
*/
const MAX_PRIMARY_PLUGIN_TOP_LEVEL_TABS = 0;
const topLevelPrimaryPluginViews = sortedPrimaryPluginViews.slice(0, MAX_PRIMARY_PLUGIN_TOP_LEVEL_TABS);
const topLevelPluginViewKeys = new Set(
topLevelPrimaryPluginViews.map((entry) => `${entry.pluginId}:${entry.view.viewId}`),
@@ -289,7 +294,6 @@ export function MobileNavBar({
const isMoreActive =
view === "documents"
|| view === "reliability"
|| view === "command-center"
|| (Boolean(experimentalFeatures?.evalsView) && view === "evals")
|| (Boolean(experimentalFeatures?.goalsView) && view === "goalsView")
|| view === "research"
@@ -393,6 +397,18 @@ export function MobileNavBar({
)}
</button>
<button
type="button"
className={`mobile-nav-tab${view === "command-center" ? " mobile-nav-tab--active" : ""}`}
data-testid="mobile-nav-tab-command-center"
role="tab"
aria-selected={view === "command-center"}
onClick={() => onChangeView("command-center")}
>
<Gauge />
<span className="mobile-nav-tab-label">{t("nav.commandCenter", "Command Center")}</span>
</button>
{showSkillsTopLevel && (
<button
type="button"
@@ -671,15 +687,6 @@ export function MobileNavBar({
<span>{t("nav.reliability", "Reliability")}</span>
</button>
<button
type="button"
className="mobile-more-item"
data-testid="mobile-more-item-command-center"
onClick={() => handleMoreAction(() => onChangeView("command-center"))}
>
<Gauge />
<span>{t("nav.commandCenter", "Command Center")}</span>
</button>
{experimentalFeatures?.evalsView && (
<button
type="button"

View File

@@ -299,6 +299,30 @@ describe("Header", () => {
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
});
it("keeps desktop Documents inline and Command Center only in overflow", () => {
renderHeader({ onChangeView: noop, showAgentsTab: true }, "desktop");
expect(screen.getByTitle("Documents view")).toBeInTheDocument();
expect(screen.queryByTestId("view-toggle-command-center")).toBeNull();
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-command-center")).toBeInTheDocument();
expect(screen.queryByTestId("view-overflow-documents")).toBeNull();
});
it("promotes Command Center after Agents and moves Documents to overflow on tablet", () => {
renderHeader({ onChangeView: noop, showAgentsTab: true }, "tablet");
const agentsButton = screen.getByTitle("Agents view");
const commandCenterButton = screen.getByTestId("view-toggle-command-center");
expect(commandCenterButton.previousElementSibling).toBe(agentsButton);
expect(screen.queryByTitle("Documents view")).toBeNull();
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getByTestId("view-overflow-documents")).toBeInTheDocument();
expect(screen.queryByTestId("view-overflow-command-center")).toBeNull();
});
it("renders view overflow trigger when skills tab is enabled", () => {
renderHeader({ onChangeView: noop, showSkillsTab: true });
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();

View File

@@ -58,7 +58,7 @@ describe("MobileNavBar", () => {
mockViewport("mobile");
});
it("renders seven tab buttons (tasks + agents + missions + chat + mailbox + skills + more) when showSkillsTab is true", () => {
it("renders eight tab buttons (tasks + agents + missions + chat + mailbox + command center + skills + more) when showSkillsTab is true", () => {
render(<MobileNavBar {...createDefaultProps()} showSkillsTab={true} />);
expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeDefined();
@@ -66,6 +66,7 @@ describe("MobileNavBar", () => {
expect(screen.getByTestId("mobile-nav-tab-missions")).toBeDefined();
expect(screen.getByTestId("mobile-nav-tab-chat")).toBeDefined();
expect(screen.getByTestId("mobile-nav-tab-mailbox")).toBeDefined();
expect(screen.getByTestId("mobile-nav-tab-command-center")).toBeDefined();
expect(screen.getByTestId("mobile-nav-tab-skills")).toBeDefined();
expect(screen.queryByTestId("mobile-nav-tab-roadmaps")).toBeNull();
expect(screen.getByTestId("mobile-nav-tab-more")).toBeDefined();
@@ -173,7 +174,7 @@ describe("MobileNavBar", () => {
expect(props.onChangeView).toHaveBeenCalledWith("plugin:fusion-plugin-dependency-graph:queue");
});
it("limits primary plugin tabs on mobile and overflows extra primary views into More", () => {
it("demotes primary plugin tabs on mobile and renders them in More", () => {
render(
<MobileNavBar
{...createDefaultProps()}
@@ -190,10 +191,11 @@ describe("MobileNavBar", () => {
/>,
);
expect(screen.getByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph")).toBeDefined();
expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-graph")).toBeNull();
expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-dependency-graph-queue")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-dependency-graph-graph")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-dependency-graph-queue")).toBeDefined();
});
@@ -252,6 +254,22 @@ describe("MobileNavBar", () => {
expect(props.onChangeView).toHaveBeenCalledWith("mailbox");
});
it("places Command Center immediately after Mailbox and routes from the top-level tab", () => {
const props = createDefaultProps();
render(<MobileNavBar {...props} view="board" mailboxUnreadCount={3} mailboxPendingApprovalCount={1} />);
const mailboxTab = screen.getByTestId("mobile-nav-tab-mailbox");
const commandCenterTab = screen.getByTestId("mobile-nav-tab-command-center");
expect(mailboxTab.compareDocumentPosition(commandCenterTab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(commandCenterTab.previousElementSibling).toBe(mailboxTab);
fireEvent.click(commandCenterTab);
expect(props.onChangeView).toHaveBeenCalledWith("command-center");
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.queryByTestId("mobile-more-item-command-center")).toBeNull();
});
it("agents tab calls onChangeView with 'agents'", () => {
const props = createDefaultProps();
render(<MobileNavBar {...props} view="board" />);
@@ -401,6 +419,7 @@ describe("MobileNavBar", () => {
expect(screen.getByTestId("mobile-more-item-github")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-usage")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-projects")).toBeDefined();
expect(screen.queryByTestId("mobile-more-item-command-center")).toBeNull();
expect(screen.queryByTestId("mobile-more-item-chat")).toBeNull();
expect(screen.queryByTestId("mobile-more-item-roadmaps")).toBeNull();
expect(screen.queryByTestId("mobile-more-item-insights")).toBeNull();
@@ -413,6 +432,28 @@ describe("MobileNavBar", () => {
expect(screen.queryByTestId("mobile-more-item-roadmaps")).toBeNull();
});
it("renders Compound Engineering primary plugin only in the More sheet while Command Center follows Mailbox", () => {
render(
<MobileNavBar
{...createDefaultProps()}
pluginDashboardViews={[
{
pluginId: "fusion-plugin-compound-engineering",
view: { viewId: "compound-engineering", label: "Compound Engineering", componentPath: "./CompoundEngineeringView", icon: "Sparkles", placement: "primary", order: 36 },
},
]}
/>,
);
expect(screen.getByTestId("mobile-nav-tab-command-center").previousElementSibling).toBe(screen.getByTestId("mobile-nav-tab-mailbox"));
expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-compound-engineering-compound-engineering")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-compound-engineering-compound-engineering")).toBeDefined();
expect(screen.queryAllByTestId("mobile-more-item-plugin-fusion-plugin-compound-engineering-compound-engineering")).toHaveLength(1);
expect(screen.queryByTestId("mobile-more-item-command-center")).toBeNull();
});
it("suppresses legacy roadmaps entries when roadmap plugin view is registered", () => {
render(
<MobileNavBar

View File

@@ -120,6 +120,7 @@ html {
/* Typography */
--font-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
--font-mono: "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-size-xs: 0.8rem;
/* Spacing Scale */
--space-xs: 4px;
@@ -667,7 +668,7 @@ html .column.drag-over * {
* Runtime provider settings card — unified layout used by Hermes / OpenClaw /
* Paperclip cards. Header (large logo + name + status), description, form,
* footer action row.
* font-size values intentionally raw — no --font-size-* tokens defined yet
* FNXC:DashboardCssTokens 2026-06-19-05:07: Most font-size values remain intentionally raw; --font-size-xs exists for mobile tab-size reuse where token validity tests require a defined custom property.
* ------------------------------------------------------------------------- */
.runtime-card {