import "./ui_template_resizer.css"; import { readByKey, writeByKey } from "../A00_Common/b_page_state"; /* ============================================================================= * ui_template_resizer.ts * 공통 스플리터 — 패널 경계를 끌어 크기를 조절한다. * * 조절값은 등록표가 정한 저장소에 남긴다(취향은 브라우저, 나머지는 세션 — 2026-09-07). 같은 브라우저 세션에서는 페이지를 오가도 * 유지되고, 브라우저를 다시 열면 기본값으로 돌아간다(2026-08-01 사용자 지시). * 사용자별 영구 저장은 나중에 따로 검토한다. * ========================================================================== */ export interface PanelResizerOptions { /** vertical=위아래로 끌어 높이 조절, horizontal=좌우로 끌어 폭 조절 */ axis: "vertical" | "horizontal"; /** 크기를 적용할 패널 */ target: HTMLElement; /** * 크기를 담을 CSS 변수 이름(예: `--b05-profile-height`). * 인라인 width/height로 박으면 `.is-collapsed { height: 0 }` 같은 접기 규칙을 이겨 버려 * 패널이 접히지 않는다. 변수로 넘기면 접기 규칙이 그대로 이긴다. */ cssVar: string; /** 손잡이가 패널의 위/왼쪽에 있으면 -1 — 끌어올리거나 왼쪽으로 끌면 커진다. */ direction: 1 | -1; /** 최소 크기(px). 함수면 끌 때마다 다시 계산한다(열린 서브패널 최소높이 합 같은 동적 하한). */ min: number | (() => number); /** 최대 크기(px). 함수면 끌 때마다 다시 계산한다(부모의 70% 같은 상대 상한). */ max: number | (() => number); /** 세션 저장 키. 생략하면 저장하지 않는다. */ storageKey?: string; /** 크기가 바뀔 때마다 호출한다(차트 재계산 등). */ onResize?: (size: number) => void; } export interface PanelResizer { /** 패널 경계에 붙일 손잡이. 패널이 `position: relative`여야 제자리에 앉는다. */ root: HTMLElement; /** 저장된 크기를 다시 적용한다(재렌더 후 복원용). */ restore: () => void; /** 기본 크기로 되돌린다(저장값도 지운다). */ reset: () => void; dispose: () => void; } export function createPanelResizer(options: PanelResizerOptions): PanelResizer { const { axis, target, cssVar, direction, storageKey, onResize } = options; const root = document.createElement("div"); root.className = `ui-resizer ui-resizer--${axis}`; root.setAttribute("role", "separator"); root.setAttribute("aria-orientation", axis === "vertical" ? "horizontal" : "vertical"); const minOf = (): number => (typeof options.min === "function" ? options.min() : options.min); const maxOf = (): number => (typeof options.max === "function" ? options.max() : options.max); const clamp = (size: number): number => { const min = minOf(); const max = Math.max(min, maxOf()); return Math.min(max, Math.max(min, size)); }; function apply(size: number, persist: boolean): void { const next = clamp(size); target.style.setProperty(cssVar, `${Math.round(next)}px`); if (persist && storageKey) writeByKey(storageKey, String(Math.round(next))); onResize?.(next); } function restore(): void { if (!storageKey) return; const saved = Number(readByKey(storageKey)); // 저장한 뒤 창 크기가 바뀌었을 수 있으니 복원할 때도 상·하한을 다시 씌운다. if (Number.isFinite(saved) && saved > 0) apply(saved, false); } function reset(): void { target.style.removeProperty(cssVar); if (storageKey) writeByKey(storageKey, null); onResize?.(axis === "vertical" ? target.clientHeight : target.clientWidth); } let startPosition = 0; let startSize = 0; root.addEventListener("pointerdown", (event) => { // 왼쪽 버튼만 — 가운데·오른쪽 버튼은 브라우저 기본 동작에 맡긴다. if (event.button !== 0) return; event.preventDefault(); startPosition = axis === "vertical" ? event.clientY : event.clientX; startSize = axis === "vertical" ? target.clientHeight : target.clientWidth; root.classList.add("is-dragging"); // 끄는 동안 크기 transition이 걸려 있으면 손이 끄는 만큼 늦게 따라온다. target.classList.add("is-resizing"); root.setPointerCapture(event.pointerId); }); root.addEventListener("pointermove", (event) => { if (!root.hasPointerCapture(event.pointerId)) return; const current = axis === "vertical" ? event.clientY : event.clientX; apply(startSize + (current - startPosition) * direction, true); }); const stop = (event: PointerEvent): void => { if (!root.hasPointerCapture(event.pointerId)) return; root.releasePointerCapture(event.pointerId); root.classList.remove("is-dragging"); target.classList.remove("is-resizing"); }; root.addEventListener("pointerup", stop); root.addEventListener("pointercancel", stop); // 더블클릭하면 기본 크기로 되돌린다(잘못 끌었을 때 되돌리는 가장 빠른 길). root.addEventListener("dblclick", reset); restore(); return { root, restore, reset, dispose() { root.remove(); }, }; }