위젯 기능 추가

This commit is contained in:
Macbook
2026-06-14 01:03:51 +09:00
parent 95595f7e9e
commit 6936792ad4
57 changed files with 6728 additions and 540 deletions
@@ -0,0 +1,124 @@
import { useCallback, useEffect, useRef, type RefObject } from 'react';
import {
bindWindowDrag,
isPrimaryPointerButton,
} from '../utils/pointerDrag';
export interface Size2d {
width: number;
height: number;
}
export interface Position2d {
x: number;
y: number;
}
export interface FloatingWidgetResizeResult {
width: number;
height: number;
x?: number;
}
export type FloatingWidgetResizeCorner = 'se' | 'sw';
function clampSize(
width: number,
height: number,
minSize: Size2d,
maxW: number,
maxH: number,
): Size2d {
return {
width: Math.round(Math.max(minSize.width, Math.min(maxW, width))),
height: Math.round(Math.max(minSize.height, Math.min(maxH, height))),
};
}
export function useFloatingWidgetResize(
panelRef: RefObject<HTMLElement | null>,
size: Size2d,
minSize: Size2d,
onResize: (result: FloatingWidgetResizeResult) => void,
) {
const unbindDragRef = useRef<(() => void) | null>(null);
const endResize = useCallback(() => {
unbindDragRef.current?.();
unbindDragRef.current = null;
}, []);
useEffect(() => () => {
unbindDragRef.current?.();
}, []);
const startResize = useCallback((
e: React.PointerEvent,
corner: FloatingWidgetResizeCorner,
) => {
if (!isPrimaryPointerButton(e)) return;
e.preventDefault();
e.stopPropagation();
const handle = e.currentTarget as HTMLElement;
try {
handle.setPointerCapture(e.pointerId);
} catch {
/* ignore */
}
const el = panelRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const startX = e.clientX;
const startY = e.clientY;
const startW = size.width;
const startH = size.height;
const startRight = rect.right;
const maxH = window.innerHeight - rect.top - 8;
unbindDragRef.current?.();
unbindDragRef.current = bindWindowDrag(
(xy) => {
if (corner === 'se') {
const maxW = window.innerWidth - rect.left - 8;
const next = clampSize(
startW + xy.x - startX,
startH + xy.y - startY,
minSize,
maxW,
maxH,
);
onResize(next);
return;
}
/* sw — 우측 가장자리 고정, 좌측·하단 조절 */
let width = startRight - xy.x;
let x = xy.x;
if (x < 8) {
x = 8;
width = startRight - 8;
}
const maxW = startRight - 8;
const next = clampSize(width, startH + xy.y - startY, minSize, maxW, maxH);
onResize({ ...next, x: Math.round(startRight - next.width) });
},
endResize,
{ preventTouchScroll: true },
);
}, [panelRef, size.width, size.height, minSize, onResize, endResize]);
const onResizeSePointerDown = useCallback(
(e: React.PointerEvent) => startResize(e, 'se'),
[startResize],
);
const onResizeSwPointerDown = useCallback(
(e: React.PointerEvent) => startResize(e, 'sw'),
[startResize],
);
return { onResizeSePointerDown, onResizeSwPointerDown };
}