Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e8f1cb91 | ||
|
|
9b85b15744 | ||
|
|
15d9ab268f | ||
|
|
058d0b069e |
No files matched your search
@@ -24,6 +24,7 @@
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script type="module" src="app.js"></script>
|
||||
<script src="update.js" defer></script>
|
||||
<script src="split.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#chat">skip to the chat</a>
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -144,6 +144,10 @@ input:disabled, textarea:disabled, select:disabled { background: var(--well); co
|
||||
/* ---------- the studio: characters | chat ---------- */
|
||||
.studio { display: flex; flex-wrap: wrap; align-items: flex-start; border-bottom: 2px solid var(--ink); }
|
||||
.chars { flex: 1.15 1 560px; min-width: 0; align-self: stretch; border-right: 1px solid var(--ink); }
|
||||
/* the line between the list and the chat, to drag (split.js) */
|
||||
.splitter { flex: 0 0 7px; align-self: stretch; margin-left: -4px; margin-right: -3px; position: relative; z-index: 2; cursor: col-resize; touch-action: none; }
|
||||
.splitter:hover, .splitter:focus-visible, .splitting .splitter { background: var(--hi); outline: none; }
|
||||
body.splitting { cursor: col-resize; user-select: none; }
|
||||
.chat {
|
||||
flex: 1 1 440px; min-width: 0; display: flex; flex-direction: column;
|
||||
position: sticky; top: 0; max-height: 100vh;
|
||||
@@ -504,6 +508,7 @@ p.note { padding: 8px 10px; border: 1px solid var(--ink); background: var(--hi);
|
||||
|
||||
/* ---------- narrow screens ---------- */
|
||||
@media (max-width: 1040px) {
|
||||
.splitter { display: none; }
|
||||
.chars { border-right: 0; border-bottom: 1px solid var(--ink); }
|
||||
/* one at a time, like a phone's messages: the list, or the chat. In the chat the left column's tabs stay above it,
|
||||
without their panels, so CHATS / CHARACTERS / HARBOR are always one tap away */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Checking fetches with cache: "reload", which also refreshes the browser's copies, so the reload gets the new files.
|
||||
(() => {
|
||||
const FILES = ["./", "app.js", "style.css", "workflows.js", "abilities.js", "card.js", "venice.js", "memory.js",
|
||||
"threads.js", "media.js", "backup.js", "theme.js"];
|
||||
"threads.js", "media.js", "backup.js", "theme.js", "split.js"];
|
||||
let base = null, shown = false, busy = false;
|
||||
async function print() {
|
||||
const parts = await Promise.all(FILES.map((f) => fetch(f, { cache: "reload" }).then((r) => (r.ok ? r.text() : "")).catch(() => null)));
|
||||
|
||||
Reference in new issue
Block a user