fix(canvas): re-centre Toolbar on canvas area when SidePanel is open
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) <noreply@anthropic.com>
This commit is contained in:
parent
a0ac72f725
commit
5eb5e38c59
@ -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<number>(() => {
|
||||
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);
|
||||
|
||||
@ -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 (
|
||||
<div className="fixed top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-zinc-900/80 backdrop-blur-md border border-zinc-800/60 rounded-xl px-4 py-2 shadow-xl shadow-black/20">
|
||||
<div
|
||||
className="fixed top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-zinc-900/80 backdrop-blur-md border border-zinc-800/60 rounded-xl px-4 py-2 shadow-xl shadow-black/20 transition-[margin-left] duration-200"
|
||||
style={toolbarOffsetStyle}
|
||||
>
|
||||
{/* Logo / Title */}
|
||||
<div className="flex items-center gap-2 pr-3 border-r border-zinc-800/60">
|
||||
<img src="/molecule-icon.png" alt="Molecule AI" className="w-5 h-5" />
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<Node<WorkspaceNodeData>>[]) => void;
|
||||
@ -115,6 +122,8 @@ export const useCanvasStore = create<CanvasState>((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<string>(),
|
||||
toggleNodeSelection: (id) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user