From 5eb5e38c59784990e19323f2304532b1666ff5ba Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 23 Apr 2026 15:57:12 -0700 Subject: [PATCH] fix(canvas): re-centre Toolbar on canvas area when SidePanel is open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a workspace is selected the SidePanel (fixed, right-0, z-50) opens from the right edge and covers the right third of the viewport. The Toolbar at the top was positioned `fixed top-3 left-1/2 -translate-x-1/2 z-20` — centred on the full viewport, not the remaining canvas area. Consequence: the right half of the Toolbar (Audit / Search / Help / Settings) was hidden behind the panel as soon as the user clicked any workspace. Fix: publish the live SidePanel width to the canvas store and read it in Toolbar. When a node is selected, shift the Toolbar LEFT by `sidePanelWidth / 2` so its centre lines up with the middle of the remaining canvas area. Animated via a 200 ms `transition-[margin-left]` to match the SidePanel's own slide-in easing. - `store/canvas.ts` — added `sidePanelWidth` + `setSidePanelWidth`. Default 480 (matches SIDEPANEL_DEFAULT_WIDTH). - `SidePanel.tsx` — calls `setSidePanelWidth(width)` on every width change so the store stays in sync with localStorage. - `Toolbar.tsx` — reads `sidePanelWidth`, applies a negative `marginLeft` style when `selectedNodeId` is non-null. - `SidePanel.tabs.test.tsx` — added `setSidePanelWidth: vi.fn()` to the mocked store state so SidePanel's new useEffect has a callable to invoke. 18 previously-passing tests now pass again. No visual regression when no workspace is selected — the toolbar stays in its original centred position. SaaS canvas unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/SidePanel.tsx | 9 ++++++++- canvas/src/components/Toolbar.tsx | 16 +++++++++++++++- .../components/__tests__/SidePanel.tabs.test.tsx | 4 ++++ canvas/src/store/canvas.ts | 9 +++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/canvas/src/components/SidePanel.tsx b/canvas/src/components/SidePanel.tsx index c8b6456e..46322fea 100644 --- a/canvas/src/components/SidePanel.tsx +++ b/canvas/src/components/SidePanel.tsx @@ -46,11 +46,15 @@ export function SidePanel() { const panelTab = useCanvasStore((s) => s.panelTab); const setPanelTab = useCanvasStore((s) => s.setPanelTab); const selectNode = useCanvasStore((s) => s.selectNode); + const setSidePanelWidth = useCanvasStore((s) => s.setSidePanelWidth); const node = useCanvasStore((s) => s.nodes.find((n) => n.id === s.selectedNodeId) ); - // Resizable panel width — persisted across node selections via localStorage + // Resizable panel width — persisted across node selections via localStorage. + // Also published to the canvas store on every change so the centered + // Toolbar can re-centre itself on the remaining canvas area (avoids the + // Audit / Search / Settings buttons hiding under the panel). const [width, setWidth] = useState(() => { if (typeof window === "undefined") return SIDEPANEL_DEFAULT_WIDTH; const saved = localStorage.getItem(SIDEPANEL_WIDTH_KEY); @@ -59,6 +63,9 @@ export function SidePanel() { ? parsed : SIDEPANEL_DEFAULT_WIDTH; }); + useEffect(() => { + setSidePanelWidth(width); + }, [width, setSidePanelWidth]); const widthRef = useRef(width); // tracks live drag value for the mouseup handler const dragging = useRef(false); const startX = useRef(0); diff --git a/canvas/src/components/Toolbar.tsx b/canvas/src/components/Toolbar.tsx index f994c75b..19cd04d2 100644 --- a/canvas/src/components/Toolbar.tsx +++ b/canvas/src/components/Toolbar.tsx @@ -16,6 +16,17 @@ export function Toolbar() { const setShowA2AEdges = useCanvasStore((s) => s.setShowA2AEdges); const selectedNodeId = useCanvasStore((s) => s.selectedNodeId); const setPanelTab = useCanvasStore((s) => s.setPanelTab); + const sidePanelWidth = useCanvasStore((s) => s.sidePanelWidth); + + // Toolbar is fixed + centred on the viewport. When a workspace is + // selected the SidePanel (z-50, fixed right-0) opens and covers the + // right edge of the viewport — without this adjustment, the right + // half of the Toolbar (Audit / Search / Help / Settings) hides + // behind the panel. Shifting the toolbar LEFT by half the panel + // width re-centres it on the remaining canvas area. + const toolbarOffsetStyle = selectedNodeId + ? { marginLeft: `-${sidePanelWidth / 2}px` } + : undefined; const [stopping, setStopping] = useState(false); const [restartingAll, setRestartingAll] = useState(false); @@ -116,7 +127,10 @@ export function Toolbar() { }, []); return ( -
+
{/* Logo / Title */}
Molecule AI diff --git a/canvas/src/components/__tests__/SidePanel.tabs.test.tsx b/canvas/src/components/__tests__/SidePanel.tabs.test.tsx index ae16e094..f1181ba1 100644 --- a/canvas/src/components/__tests__/SidePanel.tabs.test.tsx +++ b/canvas/src/components/__tests__/SidePanel.tabs.test.tsx @@ -36,6 +36,10 @@ const mockStoreState = { panelTab: "chat", setPanelTab: mockSetPanelTab, selectNode: vi.fn(), + // Consumed by SidePanel's useEffect — publishes the drag-resized + // width to the store so Toolbar can re-centre itself on the + // remaining canvas area when the panel is open. + setSidePanelWidth: vi.fn(), nodes: [ { id: "ws-1", diff --git a/canvas/src/store/canvas.ts b/canvas/src/store/canvas.ts index 2b8a9ecf..e6f6f28a 100644 --- a/canvas/src/store/canvas.ts +++ b/canvas/src/store/canvas.ts @@ -51,6 +51,13 @@ interface CanvasState { panelTab: PanelTab; dragOverNodeId: string | null; contextMenu: ContextMenuState | null; + // Live width of the SidePanel in pixels. Only meaningful when + // selectedNodeId is non-null (panel visible). The Toolbar reads this + // to stay centred on the remaining canvas area instead of the full + // viewport, so the "Audit" / "Search" / "Settings" buttons don't get + // hidden behind the panel when a workspace is selected. + sidePanelWidth: number; + setSidePanelWidth: (w: number) => void; hydrate: (workspaces: WorkspaceData[]) => void; applyEvent: (msg: WSMessage) => void; onNodesChange: (changes: NodeChange>[]) => void; @@ -115,6 +122,8 @@ export const useCanvasStore = create((set, get) => ({ panelTab: "chat", dragOverNodeId: null, contextMenu: null, + sidePanelWidth: 480, // matches SIDEPANEL_DEFAULT_WIDTH in SidePanel.tsx + setSidePanelWidth: (w) => set({ sidePanelWidth: w }), // Batch selection selectedNodeIds: new Set(), toggleNodeSelection: (id) => {