havre: a divider to drag between the list and the chat

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KjyFZEJivqQMhhCyyQau49
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-23 19:37:07 +00:00
1 parent d821cc5bee
commit 058d0b069e
1 file changed
+54
+54
View File
@@ -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();
})();