FN-6801: float sidebar collapse toggle

Replace the sidebar brand-row collapse control with a floating border toggle.\n\n- Remove the empty sidebar brand shell and keep the collapse control reachable in both expanded and rail modes.\n- Style the collapse button as a floating affordance on the sidebar edge.\n- Cover the missing brand shell, accessible toggle labels, pressed state, and persisted collapsed state in sidebar tests.\n- Update dashboard documentation to describe the border-mounted toggle.\n\nFiles changed:\n docs/dashboard-guide.md                            |  2 +-\n .../dashboard/app/components/LeftSidebarNav.css    | 30 +++++++++++-----------\n .../dashboard/app/components/LeftSidebarNav.tsx    | 28 ++++++++++----------\n .../components/__tests__/LeftSidebarNav.test.tsx   | 27 +++++++++++++++++++\n 4 files changed, 56 insertions(+), 31 deletions(-)

Fusion-Task-Id: FN-6801

Fusion-Task-Lineage: e1384565-c0cc-4564-b556-f5b74a501c41
This commit is contained in:
gsxdsm
2026-06-20 19:08:50 -07:00
parent 36b8950dbe
commit c8c76a2d1b
4 changed files with 56 additions and 31 deletions

View File

@@ -25,7 +25,7 @@ Enable **Left Sidebar Navigation** from **Settings → Experimental Features** t
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.
A small right-border toggle collapses or expands the sidebar without consuming a navigation row; collapsed rail mode keeps 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.
On mobile viewports (`<=768px`), the sidebar is not rendered even when the experiment is enabled. The existing bottom `MobileNavBar` remains the navigation surface.

View File

@@ -16,20 +16,25 @@ The experimental sidebar is a persistent desktop/tablet navigation replacement f
color: var(--text);
}
.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__collapse-toggle {
flex-shrink: 0;
}
.left-sidebar-nav__collapse-toggle--floating {
position: absolute;
top: var(--space-sm);
right: calc((var(--space-lg) + var(--space-xs)) * -1);
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: calc(var(--space-lg) + var(--space-md));
min-height: calc(var(--space-lg) + var(--space-md));
padding: var(--space-xs);
border-radius: var(--radius-full);
box-shadow: var(--shadow-sm);
}
.left-sidebar-nav__list {
display: flex;
flex: 1;
@@ -166,17 +171,12 @@ The experimental sidebar is a persistent desktop/tablet navigation replacement f
display: none;
}
.left-sidebar-nav--collapsed .left-sidebar-nav__brand,
.left-sidebar-nav--collapsed .left-sidebar-nav__item {
justify-content: center;
padding-right: 0;
padding-left: 0;
}
.left-sidebar-nav--collapsed .left-sidebar-nav__collapse-toggle {
width: auto;
}
@media (max-width: 768px) {
.left-sidebar-nav {
display: none;

View File

@@ -396,22 +396,20 @@ export function LeftSidebarNav({
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.
FNXC:Navigation 2026-06-20-12:00:
The sidebar collapse affordance must not consume a dedicated brand row now that logo, wordmark, and project controls live in Header. Float the single toggle on the resize border so expanded and rail states retain the same reachable click target without an empty header shell.
*/}
<div className="left-sidebar-nav__brand" data-testid="sidebar-nav-brand">
<button
type="button"
className="btn-icon left-sidebar-nav__collapse-toggle"
aria-label={isCollapsed ? t("nav.expandSidebar", "Expand sidebar") : t("nav.collapseSidebar", "Collapse sidebar")}
title={isCollapsed ? t("nav.expandSidebar", "Expand sidebar") : t("nav.collapseSidebar", "Collapse sidebar")}
aria-pressed={isCollapsed}
data-testid="sidebar-nav-collapse-toggle"
onClick={toggleCollapsed}
>
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
</button>
</div>
<button
type="button"
className="btn btn-icon left-sidebar-nav__collapse-toggle left-sidebar-nav__collapse-toggle--floating"
aria-label={isCollapsed ? t("nav.expandSidebar", "Expand sidebar") : t("nav.collapseSidebar", "Collapse sidebar")}
title={isCollapsed ? t("nav.expandSidebar", "Expand sidebar") : t("nav.collapseSidebar", "Collapse sidebar")}
aria-pressed={isCollapsed}
data-testid="sidebar-nav-collapse-toggle"
onClick={toggleCollapsed}
>
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
</button>
<nav className="left-sidebar-nav__list" aria-label={t("nav.primaryNavAriaLabel", "Primary navigation")}>
<div className="left-sidebar-nav__section">{primaryEntries.map(renderEntry)}</div>

View File

@@ -50,7 +50,9 @@ const pluginViews: PluginDashboardViewEntry[] = [
];
function expectNoSidebarBrandOrProjectAffordances(container: HTMLElement) {
expect(screen.queryByTestId("sidebar-nav-brand")).toBeNull();
expect(screen.queryByTestId("sidebar-nav-project-selector")).toBeNull();
expect(container.querySelector(".left-sidebar-nav__brand")).toBeNull();
expect(container.querySelector(".left-sidebar-nav__logo-mark")).toBeNull();
expect(container.querySelector(".left-sidebar-nav__wordmark")).toBeNull();
}
@@ -247,6 +249,28 @@ describe("LeftSidebarNav", () => {
},
);
it("removes the brand-row shell while keeping the floating toggle reachable in expanded and collapsed states", () => {
const { container } = renderSidebar();
const sidebar = screen.getByTestId("left-sidebar-nav");
const expandedToggle = screen.getByTestId("sidebar-nav-collapse-toggle");
expectNoSidebarBrandOrProjectAffordances(container);
expect(expandedToggle).toHaveClass("left-sidebar-nav__collapse-toggle--floating");
expect(expandedToggle).toHaveAttribute("aria-pressed", "false");
expect(expandedToggle).toHaveAccessibleName("Collapse sidebar");
expect(expandedToggle).toHaveAttribute("title", "Collapse sidebar");
fireEvent.click(expandedToggle);
const collapsedToggle = screen.getByTestId("sidebar-nav-collapse-toggle");
expect(sidebar.className).toContain("left-sidebar-nav--collapsed");
expectNoSidebarBrandOrProjectAffordances(container);
expect(collapsedToggle).toHaveClass("left-sidebar-nav__collapse-toggle--floating");
expect(collapsedToggle).toHaveAttribute("aria-pressed", "true");
expect(collapsedToggle).toHaveAccessibleName("Expand sidebar");
expect(collapsedToggle).toHaveAttribute("title", "Expand sidebar");
});
it("toggles collapsed rail mode, keeps bottom settings reachable, and restores it on remount", () => {
const firstRender = renderSidebar();
const sidebar = screen.getByTestId("left-sidebar-nav");
@@ -264,6 +288,9 @@ describe("LeftSidebarNav", () => {
firstRender.unmount();
renderSidebar();
expect(screen.getByTestId("left-sidebar-nav").className).toContain("left-sidebar-nav--collapsed");
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toHaveAttribute("aria-pressed", "true");
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toHaveAccessibleName("Expand sidebar");
expect(screen.getByTestId("sidebar-nav-collapse-toggle")).toHaveAttribute("title", "Expand sidebar");
expect(screen.getByTestId("sidebar-nav-settings")).toBeDefined();
expect(screen.getByTestId("sidebar-nav-board")).toBeDefined();
});