Merge pull request #449 from Molecule-AI/fix/issue-425-sidepanel-width-persist
fix(canvas): persist SidePanel width to localStorage (closes #425)
This commit is contained in:
commit
055efc535a
@ -19,6 +19,10 @@ import { ScheduleTab } from "./tabs/ScheduleTab";
|
||||
import { ChannelsTab } from "./tabs/ChannelsTab";
|
||||
import { summarizeWorkspaceCapabilities } from "@/store/canvas";
|
||||
|
||||
const SIDEPANEL_WIDTH_KEY = "molecule:sidepanel-width";
|
||||
const SIDEPANEL_DEFAULT_WIDTH = 480;
|
||||
const SIDEPANEL_MIN_WIDTH = 320;
|
||||
|
||||
const TABS: { id: PanelTab; label: string; icon: string }[] = [
|
||||
{ id: "chat", label: "Chat", icon: "◈" },
|
||||
{ id: "activity", label: "Activity", icon: "⊙" },
|
||||
@ -43,11 +47,19 @@ export function SidePanel() {
|
||||
s.nodes.find((n) => n.id === s.selectedNodeId)
|
||||
);
|
||||
|
||||
// Resizable panel width
|
||||
const [width, setWidth] = useState(480);
|
||||
// Resizable panel width — persisted across node selections via localStorage
|
||||
const [width, setWidth] = useState<number>(() => {
|
||||
if (typeof window === "undefined") return SIDEPANEL_DEFAULT_WIDTH;
|
||||
const saved = localStorage.getItem(SIDEPANEL_WIDTH_KEY);
|
||||
const parsed = saved ? parseInt(saved, 10) : NaN;
|
||||
return Number.isFinite(parsed) && parsed >= SIDEPANEL_MIN_WIDTH
|
||||
? parsed
|
||||
: SIDEPANEL_DEFAULT_WIDTH;
|
||||
});
|
||||
const widthRef = useRef(width); // tracks live drag value for the mouseup handler
|
||||
const dragging = useRef(false);
|
||||
const startX = useRef(0);
|
||||
const startWidth = useRef(480);
|
||||
const startWidth = useRef(SIDEPANEL_DEFAULT_WIDTH);
|
||||
|
||||
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@ -62,13 +74,20 @@ export function SidePanel() {
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!dragging.current) return;
|
||||
const delta = startX.current - e.clientX;
|
||||
const newWidth = Math.min(Math.max(startWidth.current + delta, 320), window.innerWidth * 0.8);
|
||||
const newWidth = Math.min(
|
||||
Math.max(startWidth.current + delta, SIDEPANEL_MIN_WIDTH),
|
||||
window.innerWidth * 0.8,
|
||||
);
|
||||
setWidth(newWidth);
|
||||
widthRef.current = newWidth; // keep ref in sync so mouseUp can persist it
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
if (!dragging.current) return;
|
||||
dragging.current = false;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
// Persist the final dragged width so it survives node re-selection
|
||||
localStorage.setItem(SIDEPANEL_WIDTH_KEY, String(widthRef.current));
|
||||
};
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
|
||||
afterEach(() => {
|
||||
@ -160,3 +160,59 @@ describe("SidePanel — ARIA tablist pattern", () => {
|
||||
expect(mockSetPanelTab).toHaveBeenCalledWith("events");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SidePanel — localStorage width persistence (issue #425)", () => {
|
||||
const STORAGE_KEY = "molecule:sidepanel-width";
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("falls back to 480px when localStorage has no saved width", () => {
|
||||
const { container } = render(<SidePanel />);
|
||||
const panel = container.firstChild as HTMLElement;
|
||||
// The outermost div has style={{ width }}
|
||||
expect(panel.style.width).toBe("480px");
|
||||
});
|
||||
|
||||
it("reads a valid saved width from localStorage on mount", () => {
|
||||
localStorage.setItem(STORAGE_KEY, "600");
|
||||
const { container } = render(<SidePanel />);
|
||||
const panel = container.firstChild as HTMLElement;
|
||||
expect(panel.style.width).toBe("600px");
|
||||
});
|
||||
|
||||
it("falls back to 480px when localStorage value is below minimum (320px)", () => {
|
||||
localStorage.setItem(STORAGE_KEY, "200");
|
||||
const { container } = render(<SidePanel />);
|
||||
const panel = container.firstChild as HTMLElement;
|
||||
expect(panel.style.width).toBe("480px");
|
||||
});
|
||||
|
||||
it("falls back to 480px when localStorage value is not a number", () => {
|
||||
localStorage.setItem(STORAGE_KEY, "notanumber");
|
||||
const { container } = render(<SidePanel />);
|
||||
const panel = container.firstChild as HTMLElement;
|
||||
expect(panel.style.width).toBe("480px");
|
||||
});
|
||||
|
||||
it("persists width to localStorage on mouseup after drag", () => {
|
||||
localStorage.setItem(STORAGE_KEY, "600");
|
||||
render(<SidePanel />);
|
||||
// Simulate a drag: mousedown on resize handle, mousemove, mouseup
|
||||
fireEvent.mouseDown(document.querySelector(".cursor-col-resize")!, {
|
||||
clientX: 100,
|
||||
});
|
||||
fireEvent.mouseMove(window, { clientX: 50 }); // dragged 50px left → wider
|
||||
fireEvent.mouseUp(window);
|
||||
// localStorage should have been updated to the new width
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
expect(saved).not.toBeNull();
|
||||
expect(parseInt(saved!, 10)).toBeGreaterThanOrEqual(320);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user