FN-6787: remove duplicate sidebar branding
Keep project identity controls in the Header while preserving sidebar navigation behavior. - Remove the Fusion logo, wordmark, and project selector from the experimental left sidebar. - Keep the sidebar collapse control reachable in expanded and collapsed modes. - Update sidebar tests and dashboard docs to cover the simplified navigation surface. Files changed: docs/dashboard-guide.md | 2 +- .../dashboard/app/components/LeftSidebarNav.css | 35 +----------- .../dashboard/app/components/LeftSidebarNav.tsx | 33 ++---------- .../components/__tests__/LeftSidebarNav.test.tsx | 63 +++++++++++++++++++++- 4 files changed, 67 insertions(+), 66 deletions(-) Fusion-Task-Id: FN-6787 Fusion-Task-Lineage: 18321939-4a68-48c2-bdbe-18e69e198a57
This commit is contained in:
@@ -23,7 +23,7 @@ Task Detail modal opens from onboarding, activity log, and task-to-task navigati
|
||||
|
||||
Enable **Left Sidebar Navigation** from **Settings → Experimental Features** to move the desktop/tablet project navigation out of the Header and into a persistent left sidebar.
|
||||
|
||||
When enabled on desktop or tablet project screens, the sidebar contains the Fusion brand, project selector, the primary destinations (Board, List, Agents, Command Center, Missions, Chat, Documents, Mailbox, and plugin primary views), Header overflow destinations as regular entries (Research, Insights, Skills, Memory, Secrets, Stash Recovery, Evals, Goals, Dev Server, Todos, and plugin overflow views when their flags/plugins are enabled), and a Settings button pinned to the bottom. The Header keeps its non-navigation controls but hides the view-toggle row and **More views** trigger so there is only one canonical navigation surface.
|
||||
When enabled on desktop or tablet project screens, the sidebar contains the primary destinations (Board, List, Agents, Command Center, Missions, Chat, Documents, Mailbox, and plugin primary views), Header overflow destinations as regular entries (Research, Insights, Skills, Memory, Secrets, Stash Recovery, Evals, Goals, Dev Server, Todos, and plugin overflow views when their flags/plugins are enabled), and a Settings button pinned to the bottom. The Header retains the Fusion brand and project selector, keeps its non-navigation controls, and hides the view-toggle row and **More views** trigger so there is only one canonical navigation surface.
|
||||
|
||||
The sidebar can be collapsed to an icon-only rail with accessible labels/titles preserved, and the expanded width can be resized from the right-edge separator. Collapsed state and expanded width are saved in browser `localStorage` (`fusion:left-sidebar-collapsed` and `fusion:left-sidebar-width`) and restored on reload.
|
||||
|
||||
|
||||
@@ -19,48 +19,17 @@ The experimental sidebar is a persistent desktop/tablet navigation replacement f
|
||||
.left-sidebar-nav__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: calc(var(--space-2xl) + var(--space-xl));
|
||||
}
|
||||
|
||||
.left-sidebar-nav__logo-mark {
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
flex-shrink: 0;
|
||||
color: var(--logo-accent);
|
||||
}
|
||||
|
||||
.left-sidebar-nav__wordmark {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.left-sidebar-nav__collapse-toggle {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-sidebar-nav__project-selector {
|
||||
padding: var(--space-sm);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.left-sidebar-nav__project-selector:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.left-sidebar-nav__project-selector .project-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-sidebar-nav__project-selector .project-selector-trigger {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.left-sidebar-nav__list {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -192,8 +161,6 @@ The experimental sidebar is a persistent desktop/tablet navigation replacement f
|
||||
min-width: var(--left-sidebar-nav-rail-width);
|
||||
}
|
||||
|
||||
.left-sidebar-nav--collapsed .left-sidebar-nav__wordmark,
|
||||
.left-sidebar-nav--collapsed .left-sidebar-nav__project-selector,
|
||||
.left-sidebar-nav--collapsed .left-sidebar-nav__label,
|
||||
.left-sidebar-nav--collapsed .left-sidebar-nav__badge {
|
||||
display: none;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "./LeftSidebarNav.css";
|
||||
import "./ProjectSelector.css";
|
||||
|
||||
/*
|
||||
FNXC:Navigation 2026-06-19-00:00:
|
||||
@@ -33,7 +32,6 @@ import type { ProjectInfo, PluginDashboardViewEntry } from "../api";
|
||||
import type { TaskView } from "../hooks/useViewState";
|
||||
import { buildPluginTaskViewId } from "../plugins/pluginViewRegistry";
|
||||
import { getPluginNavIcon } from "./pluginNavIcon";
|
||||
import { ProjectSelector as StandaloneProjectSelector } from "./ProjectSelector";
|
||||
|
||||
export interface LeftSidebarExperimentalFeatures {
|
||||
insights?: boolean;
|
||||
@@ -119,15 +117,6 @@ export interface LeftSidebarNavProps {
|
||||
onViewAllProjects?: () => void;
|
||||
}
|
||||
|
||||
function FusionLogo() {
|
||||
return (
|
||||
<svg className="left-sidebar-nav__logo-mark" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M12 2L3 7v10l9 5 9-5V7l-9-5z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" />
|
||||
<path d="M12 7v10M7.5 9.5l9 5M16.5 9.5l-9 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function formatCount(count: number): string {
|
||||
return count > 99 ? "99+" : String(count);
|
||||
}
|
||||
@@ -171,10 +160,6 @@ export function LeftSidebarNav({
|
||||
pluginDashboardViews = [],
|
||||
showAgentsTab = false,
|
||||
showSkillsTab = false,
|
||||
projects = [],
|
||||
currentProject = null,
|
||||
onSelectProject,
|
||||
onViewAllProjects,
|
||||
}: LeftSidebarNavProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const [sidebarWidth, setSidebarWidth] = useState(readStoredSidebarWidth);
|
||||
@@ -410,9 +395,11 @@ export function LeftSidebarNav({
|
||||
aria-label={t("nav.sidebarAriaLabel", "Sidebar navigation")}
|
||||
style={isCollapsed ? undefined : { width: sidebarWidth, minWidth: sidebarWidth }}
|
||||
>
|
||||
{/*
|
||||
FNXC:Navigation 2026-06-20-00:00:
|
||||
The sidebar no longer renders its own Fusion logo, wordmark, or project dropdown because those affordances already live in the top Header. Keep this brand row as the collapse control host so expanded and rail states retain a reachable toggle without leaving empty logo/project shells.
|
||||
*/}
|
||||
<div className="left-sidebar-nav__brand" data-testid="sidebar-nav-brand">
|
||||
<FusionLogo />
|
||||
<span className="left-sidebar-nav__wordmark">{t("dashboard.brandName", "Fusion")}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon left-sidebar-nav__collapse-toggle"
|
||||
@@ -426,18 +413,6 @@ export function LeftSidebarNav({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="left-sidebar-nav__project-selector" data-testid="sidebar-nav-project-selector">
|
||||
{onViewAllProjects ? (
|
||||
<StandaloneProjectSelector
|
||||
projects={projects}
|
||||
currentProject={currentProject}
|
||||
onSelect={onSelectProject}
|
||||
onViewAll={onViewAllProjects}
|
||||
allowSingleProject
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className="left-sidebar-nav__list" aria-label={t("nav.primaryNavAriaLabel", "Primary navigation")}>
|
||||
<div className="left-sidebar-nav__section">{primaryEntries.map(renderEntry)}</div>
|
||||
<div className="left-sidebar-nav__section left-sidebar-nav__section--secondary">{secondaryEntries.map(renderEntry)}</div>
|
||||
|
||||
@@ -2,9 +2,30 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { LeftSidebarNav } from "../LeftSidebarNav";
|
||||
import type { PluginDashboardViewEntry } from "../../api";
|
||||
import type { PluginDashboardViewEntry, ProjectInfo } from "../../api";
|
||||
import type { TaskView } from "../../hooks/useViewState";
|
||||
|
||||
const projects: ProjectInfo[] = [
|
||||
{
|
||||
id: "alpha",
|
||||
name: "Alpha",
|
||||
path: "/workspace/alpha",
|
||||
status: "active",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "2026-06-20T00:00:00.000Z",
|
||||
updatedAt: "2026-06-20T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "beta",
|
||||
name: "Beta",
|
||||
path: "/workspace/beta",
|
||||
status: "paused",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "2026-06-20T00:00:00.000Z",
|
||||
updatedAt: "2026-06-20T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const pluginViews: PluginDashboardViewEntry[] = [
|
||||
{
|
||||
pluginId: "fusion-plugin-primary",
|
||||
@@ -28,6 +49,12 @@ const pluginViews: PluginDashboardViewEntry[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function expectNoSidebarBrandOrProjectAffordances(container: HTMLElement) {
|
||||
expect(screen.queryByTestId("sidebar-nav-project-selector")).toBeNull();
|
||||
expect(container.querySelector(".left-sidebar-nav__logo-mark")).toBeNull();
|
||||
expect(container.querySelector(".left-sidebar-nav__wordmark")).toBeNull();
|
||||
}
|
||||
|
||||
function renderSidebar(overrides: Partial<ComponentProps<typeof LeftSidebarNav>> = {}) {
|
||||
const onChangeView = vi.fn();
|
||||
const props: ComponentProps<typeof LeftSidebarNav> = {
|
||||
@@ -61,7 +88,9 @@ describe("LeftSidebarNav", () => {
|
||||
});
|
||||
|
||||
it("renders core destinations, enabled overflow destinations, plugins, and bottom settings", () => {
|
||||
renderSidebar();
|
||||
const { container } = renderSidebar();
|
||||
|
||||
expectNoSidebarBrandOrProjectAffordances(container);
|
||||
|
||||
for (const testId of [
|
||||
"sidebar-nav-board",
|
||||
@@ -192,14 +221,43 @@ describe("LeftSidebarNav", () => {
|
||||
expect(screen.getByTestId(testId).getAttribute("aria-current")).toBe("page");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["without view-all callback", {}],
|
||||
["with empty project list", { projects: [], currentProject: null, onSelectProject: vi.fn(), onViewAllProjects: vi.fn() }],
|
||||
[
|
||||
"with a single project",
|
||||
{ projects: projects.slice(0, 1), currentProject: projects[0], onSelectProject: vi.fn(), onViewAllProjects: vi.fn() },
|
||||
],
|
||||
["with multiple projects", { projects, currentProject: projects[0], onSelectProject: vi.fn(), onViewAllProjects: vi.fn() }],
|
||||
] satisfies Array<[string, Partial<ComponentProps<typeof LeftSidebarNav>>]>)(
|
||||
"does not render duplicate sidebar brand or project selector %s",
|
||||
(_label, overrides) => {
|
||||
const { container } = renderSidebar(overrides);
|
||||
const sidebar = screen.getByTestId("left-sidebar-nav");
|
||||
|
||||
expectNoSidebarBrandOrProjectAffordances(container);
|
||||
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toBeDefined();
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByTestId("sidebar-nav-collapse-toggle"));
|
||||
expect(sidebar.className).toContain("left-sidebar-nav--collapsed");
|
||||
expectNoSidebarBrandOrProjectAffordances(container);
|
||||
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toBeDefined();
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeDefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("toggles collapsed rail mode, keeps bottom settings reachable, and restores it on remount", () => {
|
||||
const firstRender = renderSidebar();
|
||||
const sidebar = screen.getByTestId("left-sidebar-nav");
|
||||
|
||||
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toHaveAttribute("aria-pressed", "false");
|
||||
fireEvent.click(screen.getByTestId("sidebar-nav-collapse-toggle"));
|
||||
expect(sidebar.className).toContain("left-sidebar-nav--collapsed");
|
||||
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toHaveAttribute("aria-pressed", "true");
|
||||
expect(window.localStorage.getItem("fusion:left-sidebar-collapsed")).toBe("true");
|
||||
expect(screen.queryByTestId("sidebar-nav-resize-handle")).toBeNull();
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeDefined();
|
||||
expect(screen.getByTestId("sidebar-nav-settings").closest(".left-sidebar-nav__footer")).not.toBeNull();
|
||||
expect(within(sidebar).getAllByRole("button").at(-1)).toBe(screen.getByTestId("sidebar-nav-settings"));
|
||||
|
||||
@@ -207,6 +265,7 @@ describe("LeftSidebarNav", () => {
|
||||
renderSidebar();
|
||||
expect(screen.getByTestId("left-sidebar-nav").className).toContain("left-sidebar-nav--collapsed");
|
||||
expect(screen.getByTestId("sidebar-nav-settings")).toBeDefined();
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeDefined();
|
||||
});
|
||||
|
||||
it("clamps and persists drag resize width", () => {
|
||||
|
||||
Reference in New Issue
Block a user