|
@@ -266,6 +266,13 @@
|
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 8px;
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 8px;
|
|
|
box-shadow: 0 12px 40px rgba(0,0,0,.5); z-index: 300; animation: winIn .15s ease both;
|
|
box-shadow: 0 12px 40px rgba(0,0,0,.5); z-index: 300; animation: winIn .15s ease both;
|
|
|
}
|
|
}
|
|
|
|
|
+ /* window switcher menu (header): same look, anchored below the header */
|
|
|
|
|
+ .winmenu { position: fixed; top: 42px; right: 12px; bottom: auto;
|
|
|
|
|
+ display: flex; flex-direction: column; gap: 2px;
|
|
|
|
|
+ background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 8px;
|
|
|
|
|
+ box-shadow: 0 12px 40px rgba(0,0,0,.5); z-index: 300; animation: winIn .15s ease both; }
|
|
|
|
|
+ .winmenu .dock-chip { text-align: left; }
|
|
|
|
|
+ .winmenu .wmax-on { font-weight: 600; }
|
|
|
|
|
|
|
|
/* --- confirm dialog ------------------------------------------------------- */
|
|
/* --- confirm dialog ------------------------------------------------------- */
|
|
|
.dlg-back[hidden] { display: none; }
|
|
.dlg-back[hidden] { display: none; }
|
|
@@ -365,6 +372,7 @@
|
|
|
<button class="wbtn" id="max-min" title="minimize">–</button>
|
|
<button class="wbtn" id="max-min" title="minimize">–</button>
|
|
|
<button class="wbtn" id="max-close" title="close">×</button></div>
|
|
<button class="wbtn" id="max-close" title="close">×</button></div>
|
|
|
<div class="spacer"></div>
|
|
<div class="spacer"></div>
|
|
|
|
|
+ <button class="tbtn" id="winmenu" title="all open windows (switch without losing maximize state)">▦</button>
|
|
|
<div class="ssec" id="sse"><span class="dot"></span><span id="sse-t">connecting…</span></div>
|
|
<div class="ssec" id="sse"><span class="dot"></span><span id="sse-t">connecting…</span></div>
|
|
|
<button class="tbtn" id="theme">◐</button>
|
|
<button class="tbtn" id="theme">◐</button>
|
|
|
</header>
|
|
</header>
|
|
@@ -443,20 +451,62 @@ const winRegistry = new Set();
|
|
|
const logWins = new Map(); // attach_id -> { win, pre, buf, paused, ... }
|
|
const logWins = new Map(); // attach_id -> { win, pre, buf, paused, ... }
|
|
|
const front = (el) => { el.style.zIndex = ++ztop; };
|
|
const front = (el) => { el.style.zIndex = ++ztop; };
|
|
|
|
|
|
|
|
-// At most one maximized window: its title shows in the header with a
|
|
|
|
|
-// restore button (the window itself has no frame left to click).
|
|
|
|
|
-let maxedHandle = null;
|
|
|
|
|
|
|
+// Multiple windows can be maximized at once — each keeps its own state —
|
|
|
|
|
+// but only one is shown: the rest stay maximized and merely hidden, and
|
|
|
|
|
+// the header switcher (▦) moves between them without losing anything.
|
|
|
|
|
+let maxedHandle = null; // the maximized window currently on screen
|
|
|
function showMaxTitle(h) {
|
|
function showMaxTitle(h) {
|
|
|
- if (maxedHandle && maxedHandle !== h) maxedHandle.unmaximize();
|
|
|
|
|
|
|
+ if (maxedHandle && maxedHandle !== h) maxedHandle.win.style.display = "none";
|
|
|
maxedHandle = h;
|
|
maxedHandle = h;
|
|
|
|
|
+ h.win.style.display = "";
|
|
|
|
|
+ front(h.win);
|
|
|
$("#max-title").textContent = h.title;
|
|
$("#max-title").textContent = h.title;
|
|
|
$("#maxinfo").hidden = false;
|
|
$("#maxinfo").hidden = false;
|
|
|
}
|
|
}
|
|
|
function hideMaxTitle(h) {
|
|
function hideMaxTitle(h) {
|
|
|
if (maxedHandle !== h) return;
|
|
if (maxedHandle !== h) return;
|
|
|
maxedHandle = null;
|
|
maxedHandle = null;
|
|
|
- $("#maxinfo").hidden = true;
|
|
|
|
|
|
|
+ // a still-maximized window behind this one comes to the front of the
|
|
|
|
|
+ // maximized stack — the layer never goes back to bare
|
|
|
|
|
+ const others = [...winRegistry].filter(
|
|
|
|
|
+ (x) => x !== h && x.isMaximized() && !x.isClosed() && x.win.isConnected,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (others.length) showMaxTitle(others[others.length - 1]);
|
|
|
|
|
+ else $("#maxinfo").hidden = true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Bring any open window forward: un-minimize if needed, and if it is
|
|
|
|
|
+// maximized, make it the one shown (hiding the other maximized ones).
|
|
|
|
|
+function activateWindow(h) {
|
|
|
|
|
+ if (h.isMinimized()) h.restore();
|
|
|
|
|
+ if (h.isMaximized()) {
|
|
|
|
|
+ for (const o of winRegistry)
|
|
|
|
|
+ if (o !== h && o.isMaximized() && !o.isClosed() && o.win.isConnected) o.win.style.display = "none";
|
|
|
|
|
+ h.win.style.display = "";
|
|
|
|
|
+ showMaxTitle(h);
|
|
|
|
|
+ } else front(h.win);
|
|
|
|
|
+ saveWinState();
|
|
|
|
|
+}
|
|
|
|
|
+let winMenuEl = null;
|
|
|
|
|
+function toggleWinMenu() {
|
|
|
|
|
+ if (winMenuEl) { winMenuEl.remove(); winMenuEl = null; return; }
|
|
|
|
|
+ const m = document.createElement("div");
|
|
|
|
|
+ m.className = "winmenu";
|
|
|
|
|
+ const open = [...winRegistry].filter((h) => !h.isClosed() && h.win.isConnected);
|
|
|
|
|
+ const glyph = (h) => (h.isMaximized() ? "⛶ " : h.isMinimized() ? "– " : "▪ ");
|
|
|
|
|
+ for (const h of open) {
|
|
|
|
|
+ const b = document.createElement("button");
|
|
|
|
|
+ b.className = "dock-chip" + (h === maxedHandle ? " wmax-on" : "");
|
|
|
|
|
+ b.textContent = glyph(h) + h.label;
|
|
|
|
|
+ b.title = (h.isMaximized() ? "maximized" : h.isMinimized() ? "minimized" : "floating") + " — click to show";
|
|
|
|
|
+ b.onclick = () => { winMenuEl.remove(); winMenuEl = null; activateWindow(h); };
|
|
|
|
|
+ m.appendChild(b);
|
|
|
|
|
+ }
|
|
|
|
|
+ document.body.appendChild(m);
|
|
|
|
|
+ winMenuEl = m;
|
|
|
|
|
+ setTimeout(() => addEventListener("click", () => { m.remove(); winMenuEl = null; }, { once: true }), 0);
|
|
|
}
|
|
}
|
|
|
|
|
+$("#winmenu").onclick = (e) => { e.stopPropagation(); toggleWinMenu(); };
|
|
|
|
|
|
|
|
// --- window layout persistence -------------------------------------------
|
|
// --- window layout persistence -------------------------------------------
|
|
|
// The dashboard has no session: geometry, minimized/closed/maximized and
|
|
// The dashboard has no session: geometry, minimized/closed/maximized and
|
|
@@ -525,8 +575,10 @@ function makeWin({ id, title, short, x, y, w, h, bodyHtml, onClosed, extraCls, r
|
|
|
$("#winlayer").appendChild(win);
|
|
$("#winlayer").appendChild(win);
|
|
|
const closed = { value: false };
|
|
const closed = { value: false };
|
|
|
const doMin = () => {
|
|
const doMin = () => {
|
|
|
- unmax(); // minimize a normal-shaped window; the dock chip restores it
|
|
|
|
|
|
|
+ // minimize keeps the maximize state: a maximized window minimized goes
|
|
|
|
|
+ // to the maximized stack (▦ switcher) and comes back full-screen
|
|
|
win.style.display = "none";
|
|
win.style.display = "none";
|
|
|
|
|
+ if (maxedHandle === handle) hideMaxTitle(handle);
|
|
|
renderDock();
|
|
renderDock();
|
|
|
saveWinState();
|
|
saveWinState();
|
|
|
};
|
|
};
|
|
@@ -626,7 +678,9 @@ function makeWin({ id, title, short, x, y, w, h, bodyHtml, onClosed, extraCls, r
|
|
|
return { left: g.left, top: g.top, width: g.width, height: g.height };
|
|
return { left: g.left, top: g.top, width: g.width, height: g.height };
|
|
|
},
|
|
},
|
|
|
restore() { win.style.display = ""; closed.value = false; front(win); renderDock(); },
|
|
restore() { win.style.display = ""; closed.value = false; front(win); renderDock(); },
|
|
|
- isMinimized: () => win.style.display === "none",
|
|
|
|
|
|
|
+ // a hidden maximized window is display:none but NOT minimized — it
|
|
|
|
|
+ // waits in the maximized stack, reachable from the ▦ switcher
|
|
|
|
|
+ isMinimized: () => win.style.display === "none" && !geom,
|
|
|
isClosed: () => closed.value,
|
|
isClosed: () => closed.value,
|
|
|
};
|
|
};
|
|
|
winRegistry.add(handle);
|
|
winRegistry.add(handle);
|
|
@@ -681,9 +735,9 @@ function toggleDockMenu() {
|
|
|
b.onclick = () => { closeDockMenu(); fn(); };
|
|
b.onclick = () => { closeDockMenu(); fn(); };
|
|
|
m.appendChild(b);
|
|
m.appendChild(b);
|
|
|
};
|
|
};
|
|
|
- item("topo", () => { (wins.topo.isMinimized() ? wins.topo.restore() : front(wins.topo.win)); });
|
|
|
|
|
- item("state", () => { (wins.state.isMinimized() ? wins.state.restore() : front(wins.state.win)); });
|
|
|
|
|
- item("jobs", () => { (wins.jobs.isMinimized() ? wins.jobs.restore() : front(wins.jobs.win)); });
|
|
|
|
|
|
|
+ item("topo", () => activateWindow(wins.topo));
|
|
|
|
|
+ item("state", () => activateWindow(wins.state));
|
|
|
|
|
+ item("jobs", () => activateWindow(wins.jobs));
|
|
|
item("recipes", openRecipes);
|
|
item("recipes", openRecipes);
|
|
|
item("logs…", openLogsPicker);
|
|
item("logs…", openLogsPicker);
|
|
|
item("raw", openRaw);
|
|
item("raw", openRaw);
|
|
@@ -1543,7 +1597,7 @@ let recipesWin = null;
|
|
|
// viewer and live updates stay one implementation (paths prefix-rewritten
|
|
// viewer and live updates stay one implementation (paths prefix-rewritten
|
|
|
// server-side, theme inherited through the iframe's color-scheme).
|
|
// server-side, theme inherited through the iframe's color-scheme).
|
|
|
function openRecipes(g) {
|
|
function openRecipes(g) {
|
|
|
- if (recipesWin) { recipesWin.restore(); return recipesWin; } // also un-hides a minimized one
|
|
|
|
|
|
|
+ if (recipesWin) { activateWindow(recipesWin); return recipesWin; } // shows a minimized/hidden-max one too
|
|
|
const r = (k, d) => (g && Number.isFinite(parseFloat(g[k])) ? parseFloat(g[k]) : d);
|
|
const r = (k, d) => (g && Number.isFinite(parseFloat(g[k])) ? parseFloat(g[k]) : d);
|
|
|
recipesWin = makeWin({
|
|
recipesWin = makeWin({
|
|
|
id: "win-recipes", title: "Recipes", x: r("left", 24), y: r("top", 24),
|
|
id: "win-recipes", title: "Recipes", x: r("left", 24), y: r("top", 24),
|
|
@@ -1558,7 +1612,7 @@ function openRecipes(g) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function openRaw(g) {
|
|
async function openRaw(g) {
|
|
|
- if (rawWin) { rawWin.restore(); return rawWin; }
|
|
|
|
|
|
|
+ if (rawWin) { activateWindow(rawWin); return rawWin; }
|
|
|
const r = (k, d) => (g && Number.isFinite(parseFloat(g[k])) ? parseFloat(g[k]) : d);
|
|
const r = (k, d) => (g && Number.isFinite(parseFloat(g[k])) ? parseFloat(g[k]) : d);
|
|
|
const handle = makeWin({
|
|
const handle = makeWin({
|
|
|
title: "raw", x: r("left", 20), y: r("top", 20),
|
|
title: "raw", x: r("left", 20), y: r("top", 20),
|