maestro/ui/src/components/layout/VerticalResizeHandle.tsx
oss-sync d061ad08d8
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (e62f5c7)
2026-06-11 01:52:48 +00:00

77 lines
2.7 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
/** drag 中に呼ばれる。upperPct (0..100) を渡す。 */
onResize: (upperPct: number) => void;
/** drag 終了時に 1 度だけ呼ばれる。localStorage 保存用。 */
onResizeEnd: (upperPct: number) => void;
/** double-click でリセット。 */
onReset?: () => void;
/** 上下のパネルを含む親要素を識別する data-* selector。 */
parentSelector: string;
minUpperPct?: number;
minLowerPct?: number;
}
export function VerticalResizeHandle({
onResize, onResizeEnd, onReset, parentSelector,
minUpperPct = 20, minLowerPct = 15,
}: Props) {
const { t } = useTranslation('layout');
const onResizeRef = useRef(onResize);
const onResizeEndRef = useRef(onResizeEnd);
onResizeRef.current = onResize;
onResizeEndRef.current = onResizeEnd;
const draggingRef = useRef(false);
const lastPctRef = useRef<number | null>(null);
useEffect(() => {
const handleMove = (e: PointerEvent) => {
if (!draggingRef.current) return;
const parent = document.querySelector<HTMLElement>(parentSelector);
if (!parent) return;
const rect = parent.getBoundingClientRect();
const raw = ((e.clientY - rect.top) / rect.height) * 100;
const clamped = Math.max(minUpperPct, Math.min(100 - minLowerPct, raw));
lastPctRef.current = clamped;
onResizeRef.current(clamped);
};
const handleUp = () => {
if (!draggingRef.current) return;
draggingRef.current = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
if (lastPctRef.current !== null) onResizeEndRef.current(lastPctRef.current);
};
window.addEventListener('pointermove', handleMove);
window.addEventListener('pointerup', handleUp);
window.addEventListener('pointercancel', handleUp);
return () => {
window.removeEventListener('pointermove', handleMove);
window.removeEventListener('pointerup', handleUp);
window.removeEventListener('pointercancel', handleUp);
};
}, [parentSelector, minUpperPct, minLowerPct]);
return (
<div
role="separator"
aria-orientation="horizontal"
aria-label={t('resize.listPanel')}
onPointerDown={(e) => {
e.preventDefault();
draggingRef.current = true;
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
}}
onDoubleClick={onReset}
className="cursor-row-resize bg-transparent hover:bg-slate-300/60 transition-colors flex justify-center group"
style={{ height: 6, touchAction: 'none' }}
>
<div className="h-px self-center bg-hairline w-full group-hover:bg-slate-500/40" />
</div>
);
}