feat(ai): add resizable maximized chat panel
This commit is contained in:
1 parent
add44cf39e
commit
2927e01591
14 files changed
+182
-11
No files matched your search
@@ -0,0 +1,82 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { usePanelResize } from "@/composables/usePanelResize";
|
||||
|
||||
const storage = new Map<string, string>();
|
||||
const localStorageMock = {
|
||||
clear: () => storage.clear(),
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
};
|
||||
|
||||
function rect(left: number, width: number): DOMRect {
|
||||
return {
|
||||
bottom: 600,
|
||||
height: 600,
|
||||
left,
|
||||
right: left + width,
|
||||
top: 0,
|
||||
width,
|
||||
x: left,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect;
|
||||
}
|
||||
|
||||
describe("usePanelResize", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("localStorage", localStorageMock);
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("lets the AI panel consume the available editor width beyond the legacy 800px cap", () => {
|
||||
localStorage.setItem("dbx-ai-panel-width", "360");
|
||||
|
||||
const editor = document.createElement("div");
|
||||
const panel = document.createElement("div");
|
||||
const handle = document.createElement("div");
|
||||
panel.append(handle);
|
||||
document.body.append(editor, panel);
|
||||
|
||||
vi.spyOn(editor, "getBoundingClientRect").mockReturnValue(rect(300, 700));
|
||||
vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(1000, 360));
|
||||
|
||||
const { aiPanelWidth, startAiPanelResize } = usePanelResize();
|
||||
handle.addEventListener("mousedown", startAiPanelResize);
|
||||
handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 1000 }));
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { clientX: -1000 }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup"));
|
||||
|
||||
expect(aiPanelWidth.value).toBe(1060);
|
||||
expect(localStorage.getItem("dbx-ai-panel-width")).toBe("1060");
|
||||
});
|
||||
|
||||
it("resizes from the flex-shrunk width after a wide panel is restored in a narrow window", () => {
|
||||
localStorage.setItem("dbx-ai-panel-width", "1060");
|
||||
|
||||
const editor = document.createElement("div");
|
||||
const panel = document.createElement("div");
|
||||
const handle = document.createElement("div");
|
||||
panel.append(handle);
|
||||
document.body.append(editor, panel);
|
||||
|
||||
vi.spyOn(editor, "getBoundingClientRect").mockReturnValue(rect(300, 0));
|
||||
vi.spyOn(panel, "getBoundingClientRect").mockReturnValue(rect(300, 700));
|
||||
|
||||
const { aiPanelWidth, startAiPanelResize } = usePanelResize();
|
||||
handle.addEventListener("mousedown", startAiPanelResize);
|
||||
handle.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: 300 }));
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { clientX: 400 }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup"));
|
||||
|
||||
expect(aiPanelWidth.value).toBe(600);
|
||||
expect(localStorage.getItem("dbx-ai-panel-width")).toBe("600");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,20 @@
|
||||
import { ref, type Ref } from "vue";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
|
||||
const PANEL_MIN_WIDTH = 180;
|
||||
const DEFAULT_PANEL_MAX_WIDTH = 800;
|
||||
type PanelMaxWidth = number | ((handle: HTMLElement | null) => number);
|
||||
|
||||
function availableAiPanelMaxWidth(handle: HTMLElement | null) {
|
||||
const panel = handle?.parentElement;
|
||||
const flexibleContent = panel?.previousElementSibling as HTMLElement | null;
|
||||
if (!panel || !flexibleContent) return DEFAULT_PANEL_MAX_WIDTH;
|
||||
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const contentRect = flexibleContent.getBoundingClientRect();
|
||||
return Math.max(PANEL_MIN_WIDTH, panelRect.width + panelRect.left - contentRect.left);
|
||||
}
|
||||
|
||||
export function usePanelResize() {
|
||||
const sidebarWidth = ref(Number(safeLocalStorageGet("dbx-sidebar-width")) || 260);
|
||||
const aiPanelWidth = ref(Number(safeLocalStorageGet("dbx-ai-panel-width")) || 360);
|
||||
@@ -8,15 +22,21 @@ export function usePanelResize() {
|
||||
const sqlLibraryWidth = ref(Number(safeLocalStorageGet("dbx-sql-library-width")) || 288);
|
||||
const sqlFilePanelWidth = ref(Number(safeLocalStorageGet("dbx-sql-file-panel-width")) || 288);
|
||||
|
||||
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: "left" | "right") {
|
||||
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: "left" | "right", maxWidth: PanelMaxWidth = DEFAULT_PANEL_MAX_WIDTH) {
|
||||
return (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startWidth = widthRef.value;
|
||||
const resizeHandle = e.currentTarget as HTMLElement | null;
|
||||
const resolvedMaxWidth = typeof maxWidth === "function" ? maxWidth(resizeHandle) : maxWidth;
|
||||
const upperBound = Number.isFinite(resolvedMaxWidth) ? Math.max(PANEL_MIN_WIDTH, resolvedMaxWidth) : DEFAULT_PANEL_MAX_WIDTH;
|
||||
const renderedWidth = resizeHandle?.parentElement?.getBoundingClientRect().width;
|
||||
const requestedStartWidth = typeof renderedWidth === "number" && Number.isFinite(renderedWidth) && renderedWidth > 0 ? renderedWidth : widthRef.value;
|
||||
const startWidth = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, requestedStartWidth));
|
||||
widthRef.value = startWidth;
|
||||
|
||||
const onMouseMove = (ev: MouseEvent) => {
|
||||
const delta = ev.clientX - startX;
|
||||
widthRef.value = Math.max(180, Math.min(800, startWidth + (direction === "right" ? delta : -delta)));
|
||||
widthRef.value = Math.max(PANEL_MIN_WIDTH, Math.min(upperBound, startWidth + (direction === "right" ? delta : -delta)));
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
@@ -31,7 +51,7 @@ export function usePanelResize() {
|
||||
}
|
||||
|
||||
const startSidebarResize = startPanelResize(sidebarWidth, "dbx-sidebar-width", "right");
|
||||
const startAiPanelResize = startPanelResize(aiPanelWidth, "dbx-ai-panel-width", "left");
|
||||
const startAiPanelResize = startPanelResize(aiPanelWidth, "dbx-ai-panel-width", "left", availableAiPanelMaxWidth);
|
||||
const startHistoryResize = startPanelResize(historyWidth, "dbx-history-width", "left");
|
||||
const startSqlLibraryResize = startPanelResize(sqlLibraryWidth, "dbx-sql-library-width", "left");
|
||||
const startSqlFilePanelResize = startPanelResize(sqlFilePanelWidth, "dbx-sql-file-panel-width", "left");
|
||||
|
||||
Reference in new issue
Block a user