From 058d0b069e69d0a8bfc4ee5c0ff10bf594b586b5 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Fri, 23 Oct 2026 19:37:07 +0000 Subject: [PATCH] havre: a divider to drag between the list and the chat Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KjyFZEJivqQMhhCyyQau49 --- ventures/havre/split.js | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ventures/havre/split.js diff --git a/ventures/havre/split.js b/ventures/havre/split.js new file mode 100644 index 0000000..7762609 --- /dev/null +++ b/ventures/havre/split.js @@ -0,0 +1,54 @@ +// split.js: on a wide screen, the line between the list (chats / characters / harbor) and the chat can be dragged, or +// moved with the arrow keys once focused. Double-click (or Home) puts it back. The width is kept in this browser. +// Below 1040px the two take turns on screen, so there is nothing to drag and the divider hides (style.css). +(() => { + const KEY = "havre.split", MIN = 320, CHAT_MIN = 380; + const get = () => { try { return JSON.parse(localStorage.getItem(KEY)); } catch { return null; } }; + const put = (v) => { try { v == null ? localStorage.removeItem(KEY) : localStorage.setItem(KEY, JSON.stringify(v)); } catch { /* not kept */ } }; + function start() { + const studio = document.getElementById("studio"), chars = document.getElementById("chars"), chat = document.getElementById("chat"); + if (!studio || !chars || !chat) return; + const bar = document.createElement("div"); + bar.className = "splitter"; + bar.tabIndex = 0; + bar.setAttribute("role", "separator"); + bar.setAttribute("aria-orientation", "vertical"); + bar.setAttribute("aria-controls", "chars"); + bar.setAttribute("aria-label", "Resize the list and the chat"); + bar.title = "drag to resize ยท double-click to reset"; + chat.before(bar); + const wide = () => window.matchMedia("(min-width: 1041px)").matches; + const clamp = (w) => Math.round(Math.max(MIN, Math.min(w, studio.clientWidth - CHAT_MIN - bar.offsetWidth))); + function set(w, keep = true) { + if (w == null || !wide()) { chars.style.flex = ""; bar.removeAttribute("aria-valuenow"); if (w == null && keep) put(null); return; } + w = clamp(w); + chars.style.flex = `0 0 ${w}px`; + bar.setAttribute("aria-valuenow", String(w)); + if (keep) put(w); + } + set(get(), false); + window.addEventListener("resize", () => set(get(), false)); + bar.addEventListener("pointerdown", (e) => { + if (!wide() || e.button !== 0) return; + e.preventDefault(); + bar.setPointerCapture(e.pointerId); + const left = chars.getBoundingClientRect().left; + document.body.classList.add("splitting"); + const move = (ev) => set(ev.clientX - left); + const up = () => { bar.removeEventListener("pointermove", move); bar.removeEventListener("pointerup", up); bar.removeEventListener("pointercancel", up); document.body.classList.remove("splitting"); }; + bar.addEventListener("pointermove", move); + bar.addEventListener("pointerup", up); + bar.addEventListener("pointercancel", up); + }); + bar.addEventListener("dblclick", () => set(null)); + bar.addEventListener("keydown", (e) => { + const now = chars.getBoundingClientRect().width, step = e.shiftKey ? 80 : 20; + if (e.key === "ArrowLeft") set(now - step); + else if (e.key === "ArrowRight") set(now + step); + else if (e.key === "Home") set(null); + else return; + e.preventDefault(); + }); + } + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); +})();