function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } function getVarPx(el: HTMLElement, name: string, fallback: number) { const v = getComputedStyle(el).getPropertyValue(name).trim(); if (!v) return fallback; const s = v.toLowerCase(); if (s.endsWith("px")) { const n = Number(s.slice(0, -2)); return Number.isFinite(n) ? n : fallback; } const n = Number(s); return Number.isFinite(n) ? n : fallback; } function getVarPct(el: HTMLElement, name: string, fallback: number) { const v = getComputedStyle(el).getPropertyValue(name).trim(); if (!v) return fallback; const s = v.toLowerCase(); if (s.endsWith("%")) { const n = Number(s.slice(0, -1)); return Number.isFinite(n) ? n : fallback; } const n = Number(s); return Number.isFinite(n) ? n : fallback; } function ensureFlexParent(parent: HTMLElement, axis: "row" | "column") { // Don't stomp on an existing layout if it's already flex in the right direction const cs = getComputedStyle(parent); if (cs.display !== "flex") parent.style.display = "flex"; parent.style.flexDirection = axis; parent.style.overflow = "hidden"; } function ensurePaneCanShrink(pane: HTMLElement) { // Critical for nested flex layouts (otherwise children overflow) pane.style.minWidth = "0"; pane.style.minHeight = "0"; } function setFixedSize( pane: HTMLElement, axis: "x" | "y", px: number, ) { // For flex: fixed pane should not grow/shrink, basis = px pane.style.flexGrow = "0"; pane.style.flexShrink = "0"; pane.style.flexBasis = `${px}px`; // Helps some browsers respect size if (axis === "x") { pane.style.width = `${px}px`; } else { pane.style.height = `${px}px`; } } function setFlexFill(pane: HTMLElement) { // Fill remaining space pane.style.flex = "1 1 auto"; } function enableFlexSplitters() { // Horizontal: A | hSplit | B (top/split/bottom) for (const splitter of document.querySelectorAll(".hSplit:not(.styleOnly)")) { const parent = splitter.parentElement as HTMLElement | null; if (!parent) continue; const kids = Array.from(parent.children) as HTMLElement[]; if (kids.length !== 3 || kids[1] !== splitter) { console.warn("hSplit parent must be A | splitter | B", parent); continue; } const a = kids[0]; const b = kids[2]; ensureFlexParent(parent, "column"); ensurePaneCanShrink(a); ensurePaneCanShrink(b); setFlexFill(b); // bottom fills const gap = splitter.getBoundingClientRect().height || 8; splitter.style.flex = `0 0 ${gap}px`; // Optional per-splitter CSS vars: // --split-default: 60% (of parent height) // --split-min-a: 80px // --split-min-b: 180px const defPct = getVarPct(splitter, "--split-default", 60); const minA = getVarPx(splitter, "--split-min-a", 80); const minB = getVarPx(splitter, "--split-min-b", 180); // Set initial size (A is fixed) { const r = parent.getBoundingClientRect(); const px = clamp((defPct / 100) * r.height, minA, r.height - gap - minB); setFixedSize(a, "y", px); } let dragging = false; splitter.addEventListener("pointerdown", (e) => { dragging = true; splitter.setPointerCapture(e.pointerId); document.body.style.cursor = "row-resize"; e.preventDefault(); }); splitter.addEventListener("pointermove", (e) => { if (!dragging) return; const r = parent.getBoundingClientRect(); const y = e.clientY - r.top; const maxA = r.height - gap - minB; const newA = clamp(y, minA, maxA); setFixedSize(a, "y", newA); }); splitter.addEventListener("pointerup", (e) => { dragging = false; document.body.style.cursor = ""; splitter.releasePointerCapture(e.pointerId); }); splitter.addEventListener("pointercancel", () => { dragging = false; document.body.style.cursor = ""; }); } // Vertical: A | vSplit | B (left/split/right) for (const splitter of document.querySelectorAll(".vSplit:not(.styleOnly)")) { const parent = splitter.parentElement as HTMLElement | null; if (!parent) continue; const kids = Array.from(parent.children) as HTMLElement[]; if (kids.length !== 3 || kids[1] !== splitter) { console.warn("vSplit parent must be A | splitter | B", parent); continue; } const a = kids[0]; const b = kids[2]; ensureFlexParent(parent, "row"); ensurePaneCanShrink(a); ensurePaneCanShrink(b); setFlexFill(a); // left fills const gap = splitter.getBoundingClientRect().width || 8; splitter.style.flex = `0 0 ${gap}px`; const defPct = getVarPct(splitter, "--split-default", 50); const minA = getVarPx(splitter, "--split-min-a", 220); const minB = getVarPx(splitter, "--split-min-b", 220); // Set initial size (B is fixed) { const r = parent.getBoundingClientRect(); const px = clamp((defPct / 100) * r.width, minB, r.width - gap - minA); setFixedSize(b, "x", px); } let dragging = false; splitter.addEventListener("pointerdown", (e) => { dragging = true; splitter.setPointerCapture(e.pointerId); document.body.style.cursor = "col-resize"; e.preventDefault(); }); splitter.addEventListener("pointermove", (e) => { if (!dragging) return; const r = parent.getBoundingClientRect(); // Right pane width = distance from right edge const xFromRight = r.right - e.clientX; const maxB = r.width - gap - minA; const newB = clamp(xFromRight, minB, maxB); setFixedSize(b, "x", newB); }); splitter.addEventListener("pointerup", (e) => { dragging = false; document.body.style.cursor = ""; splitter.releasePointerCapture(e.pointerId); }); splitter.addEventListener("pointercancel", () => { dragging = false; document.body.style.cursor = ""; }); } } enableFlexSplitters();