FN-6899: make right dock persistent

Make the right dock a persistent far-right surface with its own collapse control.

- Default the right dock to visible and preserve a collapsed rail instead of hiding it entirely.\n- Remove Header right-dock toggle props and routing so More views remains a menu and left-sidebar mode has no duplicate Header toggle.\n- Update docs, changeset, and regression tests for persistent dock behavior.\n\nFiles changed:\n .changeset/fn-6899-click-revealed-right-dock.md    |  5 ++\n docs/dashboard-guide.md                            |  6 +-\n packages/dashboard/app/App.tsx                     |  1 -\n .../mobile-feature-access-regression.test.tsx      | 32 ++------\n packages/dashboard/app/components/Header.tsx       | 57 +++-----------\n packages/dashboard/app/components/RightDock.css    | 18 +++++\n packages/dashboard/app/components/RightDock.tsx    | 86 ++++++++++++----------\n .../app/components/__tests__/Header.test.tsx       | 57 +++-----------\n .../app/components/__tests__/RightDock.test.tsx    | 31 ++++++--\n .../app/components/useRightDockController.tsx      |  4 +-\n 10 files changed, 132 insertions(+), 165 deletions(-)

Fusion-Task-Id: FN-6899

Fusion-Task-Lineage: 96c4ba70-eb73-4034-8254-6a192402d905
This commit is contained in:
gsxdsm
2026-06-21 23:37:25 -07:00
parent bb663a4705
commit a91388198b
10 changed files with 132 additions and 165 deletions

View File

@@ -1998,7 +1998,6 @@ function AppInner() {
projectId={currentProject?.id}
mobileNavEnabled={isMobile}
leftSidebarNavActive={sidebarActive}
rightDockActive={rightDockActive} rightDockOpen={rightDock.open} onToggleRightDock={rightDock.toggle}
// Node switching props
availableNodes={nodes}
currentNode={currentNode}

View File

@@ -276,36 +276,30 @@ describe("Mobile Feature Access Regression Guard", () => {
}
});
it("right dock reroutes desktop and tablet More views without leaving the dropdown or chevron behind", () => {
it("desktop and tablet More views remain a dropdown rather than a Header right-dock toggle", () => {
for (const tier of ["desktop", "tablet"] as const) {
mockViewport(tier);
const onToggleRightDock = vi.fn();
const { unmount } = render(
<Header
view="board"
onChangeView={vi.fn()}
mobileNavEnabled={false}
showAgentsTab={true}
rightDockActive={true}
rightDockOpen={false}
onToggleRightDock={onToggleRightDock}
/>,
);
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
expect(trigger.querySelector(".lucide-panel-right")).toBeTruthy();
expect(trigger.querySelector(".lucide-chevron-down")).toBeNull();
expect(trigger.querySelector(".lucide-chevron-down")).toBeTruthy();
expect(trigger.querySelector(".lucide-panel-right")).toBeNull();
fireEvent.click(trigger);
expect(onToggleRightDock).toHaveBeenCalledOnce();
expect(screen.queryByRole("menu", { name: "More views" })).toBeNull();
expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument();
unmount();
}
});
it("right dock stays reachable as one standalone toggle when left sidebar nav is active", () => {
it("left sidebar nav leaves no duplicate Header right-dock toggle", () => {
for (const tier of ["desktop", "tablet"] as const) {
mockViewport(tier);
const onToggleRightDock = vi.fn();
const { unmount } = render(
<Header
view="board"
@@ -313,18 +307,11 @@ describe("Mobile Feature Access Regression Guard", () => {
mobileNavEnabled={false}
showAgentsTab={true}
leftSidebarNavActive={true}
rightDockActive={true}
rightDockOpen={true}
onToggleRightDock={onToggleRightDock}
/>,
);
const triggers = screen.getAllByTestId("view-toggle-overflow-trigger");
expect(triggers).toHaveLength(1);
expect(triggers[0].querySelector(".lucide-panel-right")).toBeTruthy();
expect(triggers[0]).toHaveAttribute("aria-pressed", "true");
fireEvent.click(triggers[0]);
expect(onToggleRightDock).toHaveBeenCalledOnce();
expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull();
expect(document.querySelector(".header-right-dock-toggle")).toBeNull();
unmount();
}
});
@@ -351,23 +338,18 @@ describe("Mobile Feature Access Regression Guard", () => {
it("right dock flag off keeps the desktop and tablet More views chevron dropdown", () => {
for (const tier of ["desktop", "tablet"] as const) {
mockViewport(tier);
const onToggleRightDock = vi.fn();
const { unmount } = render(
<Header
view="board"
onChangeView={vi.fn()}
mobileNavEnabled={false}
showAgentsTab={true}
rightDockActive={false}
rightDockOpen={false}
onToggleRightDock={onToggleRightDock}
/>,
);
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
expect(trigger.querySelector(".lucide-chevron-down")).toBeTruthy();
fireEvent.click(trigger);
expect(onToggleRightDock).not.toHaveBeenCalled();
expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument();
unmount();
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback, useMemo, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Settings, LayoutGrid, List, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, Grid3X3, Mail, MessageSquare, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge, PanelRight, ChevronDown, ChevronRight } from "lucide-react";
import { Settings, LayoutGrid, List, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, Grid3X3, Mail, MessageSquare, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge, ChevronDown, ChevronRight } from "lucide-react";
import "./Header.css";
// ProjectSelector styles used by the imported standalone component.
import "./ProjectSelector.css";
@@ -96,10 +96,6 @@ export interface HeaderProps {
mobileNavEnabled?: boolean;
/** When true on non-mobile screens, persistent left sidebar owns primary view navigation. */
leftSidebarNavActive?: boolean;
/** When true on tablet/desktop, the More views overflow trigger toggles the auxiliary right dock instead of a menu. */
rightDockActive?: boolean;
rightDockOpen?: boolean;
onToggleRightDock?: () => void;
/** Available nodes for the node selector */
availableNodes?: NodeConfig[];
/** Currently selected node (null for local) */
@@ -149,9 +145,6 @@ export function Header({
shellHost = { kind: "browser" },
mobileNavEnabled,
leftSidebarNavActive = false,
rightDockActive = false,
rightDockOpen = false,
onToggleRightDock,
availableNodes = [],
currentNode,
onSelectNode,
@@ -174,7 +167,10 @@ export function Header({
The hidden Header view-toggle location becomes the workflow-control portal slot only when left sidebar navigation is active on tablet/desktop. Mobile and flag-off paths keep workflow controls inline so the board/list chrome remains byte-identical.
*/
const hideHeaderViewNav = leftSidebarNavActive && !isMobile;
const shouldRouteMoreViewsToRightDock = rightDockActive && !isMobile && typeof onToggleRightDock === "function";
/*
FNXC:Navigation 2026-06-21-23:40:
The right dock is persistent and owns its own collapse control, so Header must not render a duplicate right-dock toggle or repurpose the More views overflow trigger on tablet/desktop.
*/
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
const [isNonMobileSearchOpen, setIsNonMobileSearchOpen] = useState(false);
// Track when user has explicitly closed the search (used for toggle visibility)
@@ -599,25 +595,6 @@ export function Header({
/>
)}
{/*
FNXC:Navigation 2026-06-21-00:00:
The default-on right dock changes the tablet/desktop More views affordance into a true panel toggle, so the icon must communicate a right panel and expose pressed/expanded state. When left-sidebar navigation hides Header view tabs, this standalone control keeps the dock reachable without duplicating the hidden overflow trigger; mobile and flag-off paths keep the legacy chevron menu.
*/}
{hideHeaderViewNav && shouldRouteMoreViewsToRightDock && (
<button
type="button"
className="btn-icon header-right-dock-toggle"
onClick={onToggleRightDock}
title={t("header.toggleRightDock", "Toggle right dock")}
aria-label={t("header.toggleRightDock", "Toggle right dock")}
aria-pressed={rightDockOpen}
aria-expanded={rightDockOpen}
data-testid="view-toggle-overflow-trigger"
>
<PanelRight size={16} />
</button>
)}
{/**
* FNXC:Header 2026-06-21-00:00:
* Desktop and tablet header search must render after the workflow portal slot so a populated WorkflowSwitcher appears left of the search icon while preserving the mobile search trigger's existing position and behavior.
@@ -759,29 +736,19 @@ export function Header({
<>
<button
ref={viewOverflowTriggerRef}
className={`view-toggle-btn${!shouldRouteMoreViewsToRightDock && (["research", "skills", "insights", "memory", "secrets", "dev-server", "devserver", "graph", "todos"].includes(view) || (isTablet && view === "documents") || (experimentalFeatures?.evalsView && view === "evals") || (experimentalFeatures?.goalsView && view === "goalsView") || isPluginViewId(view)) ? " active" : ""}`}
className={`view-toggle-btn${(["research", "skills", "insights", "memory", "secrets", "dev-server", "devserver", "graph", "todos"].includes(view) || (isTablet && view === "documents") || (experimentalFeatures?.evalsView && view === "evals") || (experimentalFeatures?.goalsView && view === "goalsView") || isPluginViewId(view)) ? " active" : ""}`}
onClick={() => {
if (shouldRouteMoreViewsToRightDock) {
setIsViewOverflowOpen(false);
onToggleRightDock?.();
return;
}
setIsViewOverflowOpen((prev) => !prev);
}}
title={shouldRouteMoreViewsToRightDock ? t("header.toggleRightDock", "Toggle right dock") : t("header.moreViews", "More views")}
aria-label={shouldRouteMoreViewsToRightDock ? t("header.toggleRightDock", "Toggle right dock") : t("header.moreViews", "More views")}
{...(shouldRouteMoreViewsToRightDock ? {
"aria-pressed": rightDockOpen,
"aria-expanded": rightDockOpen,
} : {
"aria-haspopup": "menu" as const,
"aria-expanded": isViewOverflowOpen,
})}
title={t("header.moreViews", "More views")}
aria-label={t("header.moreViews", "More views")}
aria-haspopup="menu"
aria-expanded={isViewOverflowOpen}
data-testid="view-toggle-overflow-trigger"
>
{shouldRouteMoreViewsToRightDock ? <PanelRight size={16} /> : <ChevronDown size={12} />}
<ChevronDown size={12} />
</button>
{!shouldRouteMoreViewsToRightDock && isViewOverflowOpen && (
{isViewOverflowOpen && (
<div
ref={viewOverflowRef}
className="view-toggle-overflow-menu"

View File

@@ -20,6 +20,12 @@ The right dock CSS uses a mobile media query as a belt-and-suspenders guard only
padding-bottom: var(--executor-footer-height);
}
.right-dock--collapsed {
width: calc(var(--space-2xl) + var(--space-sm));
min-width: calc(var(--space-2xl) + var(--space-sm));
max-width: calc(var(--space-2xl) + var(--space-sm));
}
.right-dock__resize-handle {
position: absolute;
inset-block: 0;
@@ -58,6 +64,12 @@ The right dock CSS uses a mobile media query as a belt-and-suspenders guard only
border-bottom: thin solid var(--border);
}
.right-dock--collapsed .right-dock__toolbar {
flex-direction: column;
justify-content: flex-start;
height: 100%;
}
.right-dock__tabs,
.right-dock__actions {
display: flex;
@@ -72,6 +84,12 @@ The right dock CSS uses a mobile media query as a belt-and-suspenders guard only
scrollbar-width: thin;
}
.right-dock--collapsed .right-dock__tabs,
.right-dock--collapsed .right-dock__actions {
flex-direction: column;
overflow: visible;
}
.right-dock__tab {
flex-shrink: 0;
color: var(--text-muted);

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { Maximize2, X } from "lucide-react";
import { Maximize2, PanelRight } from "lucide-react";
import {
findOverflowViewEntry,
getVisibleOverflowViewEntries,
@@ -29,8 +29,8 @@ export function readStoredRightDockWidth(): number {
}
export function readStoredRightDockOpen(): boolean {
if (typeof window === "undefined") return false;
return window.localStorage.getItem(RIGHT_DOCK_OPEN_STORAGE_KEY) === "true";
if (typeof window === "undefined") return true;
return window.localStorage.getItem(RIGHT_DOCK_OPEN_STORAGE_KEY) !== "false";
}
export function persistRightDockOpen(open: boolean): void {
@@ -83,6 +83,9 @@ The right dock is an auxiliary tablet/desktop surface: it remembers the last ove
FNXC:Navigation 2026-06-21-20:14:
FN-6882 splits right-dock entries into launcher actions and inline views. Action tabs invoke their existing Header handlers without replacing the Files body; only inline entries persist selection or expand into the modal.
FNXC:Navigation 2026-06-21-23:40:
The right dock is persistent and visible by default on tablet/desktop project screens. Its in-dock collapse toggle replaces the removed Header right-dock toggle, keeping one far-right control surface while preserving a narrow rail for restoring the panel.
*/
export function RightDock({
open,
@@ -118,10 +121,11 @@ export function RightDock({
persistRightDockView(key);
}, [renderProps, visibilityOptions]);
const closeDock = useCallback(() => {
persistRightDockOpen(false);
onOpenChange(false);
}, [onOpenChange]);
const toggleCollapsed = useCallback(() => {
const nextOpen = !open;
persistRightDockOpen(nextOpen);
onOpenChange(nextOpen);
}, [onOpenChange, open]);
const handleResizeStart = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
@@ -167,32 +171,35 @@ export function RightDock({
persistRightDockWidth(nextWidth);
}, [width]);
if (!open || !selectedEntry) {
if (!selectedEntry) {
return null;
}
const SelectedIcon = selectedEntry.icon;
const dockWidth = open ? `${width}px` : undefined;
return (
<aside
className={`right-dock${footerVisible ? " right-dock--with-footer" : ""}`}
style={{ width: `${width}px` }}
className={`right-dock${open ? "" : " right-dock--collapsed"}${footerVisible ? " right-dock--with-footer" : ""}`}
style={dockWidth ? { width: dockWidth } : undefined}
aria-label="Right dock"
data-testid="right-dock"
>
<div
className="right-dock__resize-handle"
role="separator"
aria-orientation="vertical"
aria-valuemin={RIGHT_DOCK_MIN_WIDTH}
aria-valuemax={RIGHT_DOCK_MAX_WIDTH}
aria-valuenow={width}
aria-label="Resize right dock"
tabIndex={0}
data-testid="right-dock-resize-handle"
onPointerDown={handleResizeStart}
onKeyDown={handleResizeKeyDown}
/>
{open ? (
<div
className="right-dock__resize-handle"
role="separator"
aria-orientation="vertical"
aria-valuemin={RIGHT_DOCK_MIN_WIDTH}
aria-valuemax={RIGHT_DOCK_MAX_WIDTH}
aria-valuenow={width}
aria-label="Resize right dock"
tabIndex={0}
data-testid="right-dock-resize-handle"
onPointerDown={handleResizeStart}
onKeyDown={handleResizeKeyDown}
/>
) : null}
<div className="right-dock__toolbar">
<div className="right-dock__tabs" role="tablist" aria-label="Right dock views">
{entries.map((entry) => {
@@ -216,7 +223,7 @@ export function RightDock({
})}
</div>
<div className="right-dock__actions">
{selectedEntry.render ? (
{open && selectedEntry.render ? (
<button
type="button"
className="btn-icon right-dock__expand"
@@ -230,23 +237,28 @@ export function RightDock({
) : null}
<button
type="button"
className="btn-icon right-dock__close"
aria-label="Close right dock"
title="Close right dock"
data-testid="right-dock-close"
onClick={closeDock}
className="btn-icon right-dock__collapse-toggle"
aria-label={open ? "Collapse right dock" : "Expand right dock"}
title={open ? "Collapse right dock" : "Expand right dock"}
aria-expanded={open}
data-testid="right-dock-collapse-toggle"
onClick={toggleCollapsed}
>
<X size={16} />
<PanelRight size={16} />
</button>
</div>
</div>
<div className="right-dock__header">
<SelectedIcon size={16} />
<div className="right-dock__title" role="heading" aria-level={3}>{selectedEntry.label}</div>
</div>
<div className="right-dock__body" role="tabpanel" aria-label={selectedEntry.label} data-testid="right-dock-body">
{selectedEntry.render?.(renderProps)}
</div>
{open ? (
<>
<div className="right-dock__header">
<SelectedIcon size={16} />
<div className="right-dock__title" role="heading" aria-level={3}>{selectedEntry.label}</div>
</div>
<div className="right-dock__body" role="tabpanel" aria-label={selectedEntry.label} data-testid="right-dock-body">
{selectedEntry.render?.(renderProps)}
</div>
</>
) : null}
</aside>
);
}

View File

@@ -269,65 +269,33 @@ describe("Header", () => {
expect(screen.queryByTestId("view-overflow-stash-recovery")).toBeNull();
});
it.each(["desktop", "tablet"] as const)("routes More views to the right dock panel toggle on %s", (tier) => {
const onToggleRightDock = vi.fn();
const { rerender } = renderHeader({
onChangeView: noop,
rightDockActive: true,
rightDockOpen: false,
onToggleRightDock,
}, tier);
it.each(["desktop", "tablet"] as const)("keeps More views as a chevron dropdown instead of a right-dock toggle on %s", (tier) => {
renderHeader({ onChangeView: noop, todosEnabled: true }, tier);
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
expect(trigger.querySelector(".lucide-panel-right")).toBeTruthy();
expect(trigger.querySelector(".lucide-chevron-down")).toBeNull();
expect(trigger).toHaveAttribute("aria-pressed", "false");
expect(trigger).not.toHaveAttribute("aria-haspopup");
expect(trigger.querySelector(".lucide-chevron-down")).toBeTruthy();
expect(trigger.querySelector(".lucide-panel-right")).toBeNull();
expect(trigger).toHaveAttribute("aria-haspopup", "menu");
expect(trigger).not.toHaveAttribute("aria-pressed");
fireEvent.click(trigger);
expect(onToggleRightDock).toHaveBeenCalledTimes(1);
expect(screen.queryByRole("menu", { name: "More views" })).toBeNull();
rerender(
<Header
onOpenSettings={noop}
onOpenGitHubImport={noop}
onChangeView={noop}
rightDockActive={true}
rightDockOpen={true}
onToggleRightDock={onToggleRightDock}
/>,
);
expect(screen.getByTestId("view-toggle-overflow-trigger")).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument();
});
it.each(["desktop", "tablet"] as const)("keeps one standalone right dock toggle when left sidebar hides view nav on %s", (tier) => {
const onToggleRightDock = vi.fn();
it.each(["desktop", "tablet"] as const)("renders no duplicate Header right-dock toggle when left sidebar hides view nav on %s", (tier) => {
renderHeader({
onChangeView: noop,
leftSidebarNavActive: true,
rightDockActive: true,
rightDockOpen: true,
onToggleRightDock,
todosEnabled: true,
}, tier);
const triggers = screen.getAllByTestId("view-toggle-overflow-trigger");
expect(triggers).toHaveLength(1);
expect(triggers[0].querySelector(".lucide-panel-right")).toBeTruthy();
expect(triggers[0]).toHaveAttribute("aria-pressed", "true");
fireEvent.click(triggers[0]);
expect(onToggleRightDock).toHaveBeenCalledTimes(1);
expect(screen.queryByRole("menu", { name: "More views" })).toBeNull();
expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull();
expect(document.querySelector(".header-right-dock-toggle")).toBeNull();
});
it("keeps the legacy chevron dropdown on mobile even when right dock props are present", () => {
const onToggleRightDock = vi.fn();
it("keeps the legacy chevron dropdown on mobile", () => {
renderHeader({
onChangeView: noop,
mobileNavEnabled: false,
rightDockActive: true,
rightDockOpen: false,
onToggleRightDock,
}, "mobile");
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
@@ -335,7 +303,6 @@ describe("Header", () => {
expect(trigger.querySelector(".lucide-panel-right")).toBeNull();
expect(trigger).toHaveAttribute("aria-haspopup", "menu");
fireEvent.click(trigger);
expect(onToggleRightDock).not.toHaveBeenCalled();
expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument();
});

View File

@@ -50,10 +50,7 @@ describe("RightDock", () => {
});
it("renders Files by default and restores only persisted inline views", () => {
const onOpenChange = vi.fn();
const { unmount } = render(
<RightDock open={true} onOpenChange={onOpenChange} renderProps={renderProps} />,
);
const { unmount } = render(<RightDock open={true} onOpenChange={vi.fn()} renderProps={renderProps} />);
expect(screen.getByTestId("right-dock-tab-files")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
@@ -62,7 +59,7 @@ describe("RightDock", () => {
expect(window.localStorage.getItem(RIGHT_DOCK_VIEW_STORAGE_KEY)).toBeNull();
unmount();
render(<RightDock open={true} onOpenChange={onOpenChange} renderProps={renderProps} />);
render(<RightDock open={true} onOpenChange={vi.fn()} renderProps={renderProps} />);
expect(screen.getByTestId("right-dock-tab-files")).toHaveAttribute("aria-selected", "true");
});
@@ -148,11 +145,11 @@ describe("RightDock", () => {
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
});
it("closes internally and clamps then persists resize width", () => {
it("collapses internally and clamps then persists resize width", () => {
const onOpenChange = vi.fn();
render(<RightDock open={true} onOpenChange={onOpenChange} renderProps={renderProps} />);
fireEvent.click(screen.getByTestId("right-dock-close"));
fireEvent.click(screen.getByTestId("right-dock-collapse-toggle"));
expect(onOpenChange).toHaveBeenCalledWith(false);
expect(window.localStorage.getItem(RIGHT_DOCK_OPEN_STORAGE_KEY)).toBe("false");
@@ -174,6 +171,26 @@ describe("RightDock", () => {
expect(screen.getByTestId("right-dock-resize-handle")).toHaveAttribute("aria-valuenow", "400");
});
it("shows an in-dock collapse toggle and keeps the collapsed rail persistent", () => {
const onOpenChange = vi.fn();
const { rerender } = render(<RightDock open={true} onOpenChange={onOpenChange} renderProps={renderProps} />);
expect(screen.getByTestId("right-dock-collapse-toggle")).toHaveAttribute("aria-expanded", "true");
expect(screen.getByTestId("right-dock-body")).toBeInTheDocument();
expect(screen.getByTestId("right-dock-resize-handle")).toBeInTheDocument();
expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual(toolTabIds);
rerender(<RightDock open={false} onOpenChange={onOpenChange} renderProps={renderProps} />);
expect(screen.getByTestId("right-dock")).toHaveClass("right-dock--collapsed");
expect(screen.getByTestId("right-dock-collapse-toggle")).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByTestId("right-dock-body")).toBeNull();
expect(screen.queryByTestId("right-dock-resize-handle")).toBeNull();
expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual(toolTabIds);
fireEvent.click(screen.getByTestId("right-dock-collapse-toggle"));
expect(onOpenChange).toHaveBeenLastCalledWith(true);
expect(window.localStorage.getItem(RIGHT_DOCK_OPEN_STORAGE_KEY)).toBe("true");
});
it("renders the expanded modal through the same registry and restores focus on close", async () => {
const onClose = vi.fn();
const focusButton = document.createElement("button");

View File

@@ -46,8 +46,8 @@ export interface RightDockController {
}
/*
FNXC:Navigation 2026-06-21-12:20:
App owns whether the default-on right dock is available, but this controller owns the persisted open state and shared view props so App.tsx does not duplicate each overflow destination's dock/modal rendering contract.
FNXC:Navigation 2026-06-21-23:40:
The right dock is visible by default and collapses from inside the dock. Keep the persisted open/collapsed state in this controller so App and Header do not need duplicate right-dock toggle wiring.
*/
export function useRightDockController(input: RightDockControllerInput): RightDockController {
const [open, setOpen] = useState(readStoredRightDockOpen);