Files
kandev/apps/web/hooks/use-quick-chat-width.ts
2026-07-15 09:13:52 +01:00

136 lines
4.2 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { getLocalStorage, setLocalStorage } from "@/lib/local-storage";
const STORAGE_KEY = "kandev.quickChat.dialogWidth";
const DEFAULT_VIEWPORT_RATIO = 0.8;
const MIN_WIDTH = 480;
const VIEWPORT_MARGIN = 32;
export const QUICK_CHAT_WIDTH_LIMITS = {
storageKey: STORAGE_KEY,
defaultViewportRatio: DEFAULT_VIEWPORT_RATIO,
minWidth: MIN_WIDTH,
viewportMargin: VIEWPORT_MARGIN,
} as const;
function viewportWidth(): number {
return typeof window === "undefined" ? 1200 : window.innerWidth;
}
export function clampQuickChatWidth(width: number, availableWidth = viewportWidth()): number {
const maximum = Math.max(0, availableWidth - VIEWPORT_MARGIN);
const minimum = Math.min(MIN_WIDTH, maximum);
return Math.round(Math.min(Math.max(width, minimum), maximum));
}
function readInitialWidth(): number {
const availableWidth = viewportWidth();
const stored = getLocalStorage<number | null>(STORAGE_KEY, null);
const preferred =
typeof stored === "number" && Number.isFinite(stored)
? stored
: availableWidth * DEFAULT_VIEWPORT_RATIO;
return clampQuickChatWidth(preferred, availableWidth);
}
function restoreBodyStyles() {
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
type ResizeEdge = "left" | "right";
export function useQuickChatWidth() {
const [width, setWidth] = useState(readInitialWidth);
const widthRef = useRef(width);
const dragRef = useRef<{
edge: ResizeEdge;
startX: number;
lastX: number;
startWidth: number;
dialog: HTMLElement;
} | null>(null);
const updateWidth = useCallback((nextWidth: number) => {
widthRef.current = nextWidth;
setWidth(nextWidth);
}, []);
const startResize = useCallback((edge: ResizeEdge, event: React.MouseEvent) => {
event.preventDefault();
const dialog = event.currentTarget.parentElement;
if (!dialog) return;
dragRef.current = {
edge,
startX: event.clientX,
lastX: event.clientX,
startWidth: widthRef.current,
dialog,
};
document.body.style.cursor = "ew-resize";
document.body.style.userSelect = "none";
}, []);
const handleLeftMouseDown = useCallback(
(event: React.MouseEvent) => startResize("left", event),
[startResize],
);
const handleRightMouseDown = useCallback(
(event: React.MouseEvent) => startResize("right", event),
[startResize],
);
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
const drag = dragRef.current;
if (!drag) return;
drag.lastX = event.clientX;
const pointerDelta = event.clientX - drag.startX;
const widthDelta = (drag.edge === "right" ? pointerDelta : -pointerDelta) * 2;
const nextWidth = clampQuickChatWidth(drag.startWidth + widthDelta);
widthRef.current = nextWidth;
drag.dialog.style.setProperty("--quick-chat-width", `${nextWidth}px`);
};
const handleMouseUp = () => {
if (!dragRef.current) return;
dragRef.current = null;
restoreBodyStyles();
setWidth(widthRef.current);
setLocalStorage(STORAGE_KEY, widthRef.current);
};
const handleWindowResize = () => {
const nextWidth = clampQuickChatWidth(widthRef.current);
const drag = dragRef.current;
if (drag) {
widthRef.current = nextWidth;
drag.startWidth = nextWidth;
drag.startX = drag.lastX;
drag.dialog.style.setProperty("--quick-chat-width", `${nextWidth}px`);
return;
}
updateWidth(nextWidth);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
window.addEventListener("resize", handleWindowResize);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
window.removeEventListener("resize", handleWindowResize);
if (dragRef.current) {
dragRef.current = null;
restoreBodyStyles();
}
};
}, [updateWidth]);
return {
width,
leftResizeHandleProps: { onMouseDown: handleLeftMouseDown },
rightResizeHandleProps: { onMouseDown: handleRightMouseDown },
};
}