Make transfer panes resizable

This commit is contained in:
DonVoo 2026-07-13 09:14:24 +02:00
parent c7f6750d6c
commit 97a65dbbd4
3 changed files with 51 additions and 29 deletions

View file

@ -73,8 +73,8 @@ func renderStartPage(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<script src="/static/app.js?v=20260712-22" defer></script></head>
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -121,9 +121,9 @@ func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane str
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -697,9 +697,9 @@ func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Accou
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Konten-Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -743,9 +743,9 @@ func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []strin
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Archiv-mbox Verwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -791,9 +791,9 @@ func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, ed
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Benutzerverwaltung - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -865,9 +865,9 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -890,9 +890,13 @@ func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMod
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
<main class="transfer-workbench">
<aside class="pane pane-tree" id="left-folders">%s</aside>
<div class="pane-resizer" data-resize-index="0"></div>
<section class="pane pane-list" id="left-list">%s</section>
<div class="pane-resizer" data-resize-index="1"></div>
<section class="pane pane-read" id="transfer-preview">%s</section>
<div class="pane-resizer" data-resize-index="2"></div>
<section class="pane pane-list" id="right-list">%s</section>
<div class="pane-resizer" data-resize-index="3"></div>
<aside class="pane pane-tree" id="right-folders">%s</aside>
</main>
<footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`,
@ -1313,9 +1317,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quell-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>
@ -1357,9 +1361,9 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ziel-Postfach - Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-22">
<link rel="stylesheet" href="/static/style.css?v=20260713-5">
<script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-22" defer></script></head>
<script src="/static/app.js?v=20260713-5" defer></script></head>
<body class="ol2013">
<header class="ribbon">
<button class="app-menu-button" type="button" aria-label="Mail-Graveyard-Menue" aria-expanded="false" data-backstage-toggle><span class="mail-logo" aria-hidden="true"></span></button>

View file

@ -1,8 +1,19 @@
(function () {
const key = "mail-graveyard-pane-widths-v2";
const minWidths = [160, 220, 300];
const handleWidth = 6;
function storageKey(container) {
if (container.classList.contains("transfer-workbench")) return "mail-graveyard-transfer-widths-v1";
if (container.classList.contains("four-pane")) return "mail-graveyard-four-pane-widths-v1";
return "mail-graveyard-pane-widths-v2";
}
function minWidths(container) {
const count = panes(container).length;
if (container.classList.contains("transfer-workbench")) return [220, 240, 320, 240, 220];
if (count === 4) return [160, 220, 280, 280];
return [160, 220, 300];
}
function panes(container) {
return Array.from(container.querySelectorAll(".pane"));
}
@ -12,13 +23,15 @@
}
function clampWidths(widths, total) {
const next = widths.map((w, i) => Math.max(minWidths[i], Math.round(w || minWidths[i])));
const max = Math.max(total - handleWidth * (next.length - 1), minWidths.reduce((a, b) => a + b, 0));
const mins = minWidths(this);
const next = widths.map((w, i) => Math.max(mins[i] || mins[mins.length - 1], Math.round(w || mins[i] || mins[mins.length - 1])));
const minTotal = mins.slice(0, next.length).reduce((a, b) => a + b, 0);
const max = Math.max(total - handleWidth * (next.length - 1), minTotal);
const sum = next.reduce((a, b) => a + b, 0);
if (sum > max) {
let extra = sum - max;
for (let i = next.length - 1; i >= 0 && extra > 0; i--) {
const canTake = Math.max(0, next[i] - minWidths[i]);
const canTake = Math.max(0, next[i] - (mins[i] || mins[mins.length - 1]));
const take = Math.min(canTake, extra);
next[i] -= take;
extra -= take;
@ -29,20 +42,24 @@
function applyWidths(container, widths) {
const total = container.getBoundingClientRect().width;
const w = clampWidths(widths, total);
container.style.gridTemplateColumns =
`${w[0]}px ${handleWidth}px ${w[1]}px ${handleWidth}px minmax(${w[2]}px, 1fr)`;
localStorage.setItem(key, JSON.stringify(w));
const w = clampWidths.call(container, widths, total);
const columns = [];
w.forEach((width, index) => {
columns.push(index === w.length - 1 ? `minmax(${width}px, 1fr)` : `${width}px`);
if (index < w.length - 1) columns.push(`${handleWidth}px`);
});
container.style.gridTemplateColumns = columns.join(" ");
localStorage.setItem(storageKey(container), JSON.stringify(w));
}
function restore(container) {
try {
const saved = JSON.parse(localStorage.getItem(key) || "null");
if (Array.isArray(saved) && saved.length === 3) {
const saved = JSON.parse(localStorage.getItem(storageKey(container)) || "null");
if (Array.isArray(saved) && saved.length === panes(container).length) {
applyWidths(container, saved);
}
} catch (_) {
localStorage.removeItem(key);
localStorage.removeItem(storageKey(container));
}
}
@ -89,6 +106,7 @@
});
}
window.addEventListener("DOMContentLoaded", () => bind(document.querySelector(".transfer-workbench")));
window.addEventListener("DOMContentLoaded", () => bind(document.querySelector(".four-pane")));
window.addEventListener("DOMContentLoaded", () => bind(document.querySelector(".three-pane")));
})();

View file

@ -492,7 +492,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
.transfer-workbench {
flex: 1 1 auto;
display: grid;
grid-template-columns: 310px minmax(300px, 1fr) minmax(420px, 1.15fr) minmax(300px, 1fr) 310px;
grid-template-columns: 310px 6px minmax(300px, 1fr) 6px minmax(420px, 1.15fr) 6px minmax(300px, 1fr) 6px 310px;
min-height: 0;
}
.transfer-workbench .pane:last-child {