const STORAGE_KEY = 'pi-column-ratio'; const DEFAULT_RATIO = 1 / 1.83; export function createColumnResizer(layout, handle) { const desktop = matchMedia('(min-width: 851px)'); const style = getComputedStyle(layout); const minimumChat = parseFloat(style.getPropertyValue('--conversation-min')); const minimumWorkspace = parseFloat(style.getPropertyValue('--workspace-min')); let ratio = DEFAULT_RATIO, drag, frame = 0; try { const saved = Number(localStorage.getItem(STORAGE_KEY)); if (Number.isFinite(saved) && saved > 0 && saved < 1) ratio = saved; } catch { /* The divider also works when browser storage is unavailable. */ } function apply() { if (!desktop.matches) return; const width = layout.getBoundingClientRect().width; if (!width) return; const chat = Math.max(minimumChat, Math.min(width - minimumWorkspace, width * ratio)); layout.style.setProperty('--conversation-width', `${chat}px`); const percent = Math.round(chat / width * 100); handle.setAttribute('aria-valuemin', String(Math.round(minimumChat / width * 100))); handle.setAttribute('aria-valuemax', String(Math.round((width - minimumWorkspace) / width * 100))); handle.setAttribute('aria-valuenow', String(percent)); handle.setAttribute('aria-valuetext', `Chat ${percent}%, workspace ${100 - percent}%`); } function save() { try { localStorage.setItem(STORAGE_KEY, String(ratio)); } catch {} } function setWidth(pixels) { const width = layout.getBoundingClientRect().width; ratio = Math.max(minimumChat, Math.min(width - minimumWorkspace, pixels)) / width; apply(); } function flushDrag() { cancelAnimationFrame(frame); frame = 0; if (drag) setWidth(drag.x - layout.getBoundingClientRect().left - drag.offset); } function finish(commit) { if (!drag) return; if (commit) flushDrag(); else { cancelAnimationFrame(frame); frame = 0; ratio = drag.initialRatio; } const pointer = drag.pointer; drag = undefined; if (handle.hasPointerCapture(pointer)) handle.releasePointerCapture(pointer); document.documentElement.classList.remove('is-resizing-columns'); apply(); if (commit) save(); } function reset() { finish(false); ratio = DEFAULT_RATIO; apply(); save(); } handle.addEventListener('pointerdown', event => { if (!desktop.matches || event.button !== 0 || !event.isPrimary || drag) return; event.preventDefault(); handle.focus({ preventScroll: true }); const boundary = layout.querySelector('.conversation').getBoundingClientRect().right; drag = { pointer: event.pointerId, initialRatio: ratio, x: event.clientX, offset: event.clientX - boundary }; handle.setPointerCapture(event.pointerId); document.documentElement.classList.add('is-resizing-columns'); }); handle.addEventListener('pointermove', event => { if (!drag || event.pointerId !== drag.pointer) return; drag.x = event.clientX; if (!frame) frame = requestAnimationFrame(flushDrag); }); handle.addEventListener('pointerup', event => { if (!drag || event.pointerId !== drag.pointer) return; drag.x = event.clientX; finish(true); }); handle.addEventListener('pointercancel', () => finish(false)); handle.addEventListener('lostpointercapture', () => finish(false)); handle.addEventListener('dblclick', reset); handle.addEventListener('keydown', event => { if (!desktop.matches) return; if (event.key === 'Escape' && drag) { event.preventDefault(); finish(false); return; } if (!['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter'].includes(event.key)) return; event.preventDefault(); if (event.key === 'Enter') { reset(); return; } finish(false); const width = layout.querySelector('.conversation').getBoundingClientRect().width; const step = event.shiftKey ? 48 : 16; setWidth(event.key === 'Home' ? 0 : event.key === 'End' ? Infinity : width + (event.key === 'ArrowLeft' ? -step : step)); save(); }); window.addEventListener('blur', () => finish(false)); desktop.addEventListener('change', () => { finish(false); apply(); }); // Keep the preferred ratio on window resize, clamping only its displayed size. new ResizeObserver(apply).observe(layout); apply(); }