Two pieces of the "port v4's exact UI onto v5's backend" plan: - core/contract.py: new optional ModuleSpec.category field (closed set: continuity, guidance, conditioning, sampling, post, system), validated in core/schema.py, threaded through core/registry.py's FIELDS and to_manifest(). Presentation grouping, independent of STAGE (execution order) -- a module can be stage="sampling" and category="guidance" at once. Only the 4 existing modules that actually render a settings panel (alg, sharpen, appearance, full_control) got a category assigned; the other 12 have none to categorize (no SETTINGS block) and stay uncategorised, which is valid. - v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied verbatim into app/legacy/. core/routes.py's "/" route now serves legacy/index.html instead of the composer entry point; a <base> tag in that file resolves its own relative script/link tags against /funpack/app/legacy/ regardless of what URL it's served at, so every file moved as a unit with zero per-file path edits. Verified live: all 61 assets load 200 OK, the real v4 onboarding screen renders pixel-for- pixel (it's the actual DOM/CSS, not a reimplementation), and several v4 API calls (/api/media, /api/projects, /api/git/status) already succeed against v5's real backend by coincidence of shared route naming -- the rest 404/405 as expected, since api.js's ~90 functions haven't been rewired to v5's actual routes yet (next piece of this plan). The old composer entry point (app/index.html, app/composer/**) stays in the tree, unreferenced, until a later cleanup pass once nothing imports it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
183 lines
7.2 KiB
JavaScript
183 lines
7.2 KiB
JavaScript
// Reusable OS-style floating window: drag, close, minimize (roll-up in place), maximize/restore.
|
|
// Foundation for Composer now; Models / Engine settings can adopt it later.
|
|
//
|
|
// const win = window.FloatingWindow.create({ id, title, subtitle, width, height, onClose });
|
|
// win.open(); win.body // mount your content into win.body
|
|
//
|
|
// Minimize collapses the window to its title bar where it sits (a "window shade"); click the
|
|
// bar or the button again to expand. One shared z-counter so click-to-front works.
|
|
(function () {
|
|
const { el, clear } = window.dom;
|
|
let zTop = 4000;
|
|
|
|
function create(opts) {
|
|
opts = opts || {};
|
|
const W = Math.max(opts.minWidth || 360, opts.width || 560);
|
|
const H = Math.max(opts.minHeight || 240, opts.height || 480);
|
|
|
|
const root = el("div", "fw");
|
|
root.style.width = W + "px";
|
|
root.style.height = H + "px";
|
|
root.hidden = true;
|
|
|
|
// ── chrome ────────────────────────────────────────────────────────────────
|
|
const bar = el("div", "fw-bar");
|
|
const titleWrap = el("div", "fw-title");
|
|
const dot = el("span", "fw-dot");
|
|
const titleText = el("span", "fw-title-text", opts.title || "Window");
|
|
titleWrap.append(dot, titleText);
|
|
if (opts.subtitle) titleWrap.append(el("span", "fw-sub", opts.subtitle));
|
|
|
|
const ctrls = el("div", "fw-ctrls");
|
|
const bMin = el("button", "fw-btn fw-min", "—"); bMin.title = "Minimize";
|
|
const bMax = el("button", "fw-btn fw-max", "▢"); bMax.title = "Maximize";
|
|
const bClose = el("button", "fw-btn fw-close", "✕"); bClose.title = "Close";
|
|
ctrls.append(bMin, bMax, bClose);
|
|
bar.append(titleWrap, ctrls);
|
|
|
|
const body = el("div", "fw-body");
|
|
const grip = el("div", "fw-grip"); grip.title = "Resize";
|
|
root.append(bar, body, grip);
|
|
document.body.append(root);
|
|
|
|
// ── state ─────────────────────────────────────────────────────────────────
|
|
let placed = false; // has it been centred / positioned yet
|
|
let maximized = false;
|
|
let pre = null; // geometry saved before maximize
|
|
let collapsed = false; // rolled up to the title bar in place
|
|
let preH = null; // height saved before collapse
|
|
|
|
function front() { root.style.zIndex = ++zTop; }
|
|
|
|
function center() {
|
|
const w = root.offsetWidth, h = root.offsetHeight;
|
|
const x = Math.max(8, Math.round((window.innerWidth - w) / 2));
|
|
const y = Math.max(48, Math.round((window.innerHeight - h) / 2.3));
|
|
root.style.left = x + "px"; root.style.top = y + "px";
|
|
}
|
|
|
|
function clampIntoView() {
|
|
const w = root.offsetWidth, h = root.offsetHeight;
|
|
let x = parseInt(root.style.left || "0", 10);
|
|
let y = parseInt(root.style.top || "0", 10);
|
|
x = Math.min(Math.max(8 - w + 80, x), window.innerWidth - 80);
|
|
y = Math.min(Math.max(44, y), window.innerHeight - 44);
|
|
root.style.left = x + "px"; root.style.top = y + "px";
|
|
}
|
|
|
|
function isOpen() { return !root.hidden; }
|
|
|
|
function open() {
|
|
root.hidden = false;
|
|
if (!placed) { center(); placed = true; }
|
|
else clampIntoView();
|
|
front();
|
|
if (opts.onOpen) opts.onOpen();
|
|
}
|
|
|
|
function close() {
|
|
root.hidden = true;
|
|
if (opts.onClose) opts.onClose();
|
|
}
|
|
|
|
function toggle() { isOpen() ? close() : open(); }
|
|
|
|
// Roll the window up to just its title bar, in place. Toggles.
|
|
function minimize() {
|
|
if (collapsed) { expand(); return; }
|
|
if (maximized) restore();
|
|
preH = root.style.height;
|
|
collapsed = true;
|
|
root.classList.add("fw-collapsed");
|
|
root.style.height = "";
|
|
bMin.textContent = "+"; bMin.title = "Expand";
|
|
front();
|
|
}
|
|
|
|
function expand() {
|
|
if (!collapsed) return;
|
|
collapsed = false;
|
|
root.classList.remove("fw-collapsed");
|
|
if (preH) root.style.height = preH;
|
|
bMin.textContent = "—"; bMin.title = "Minimize";
|
|
clampIntoView();
|
|
front();
|
|
}
|
|
|
|
function maximize() {
|
|
if (collapsed) expand();
|
|
if (maximized) { restore(); return; }
|
|
pre = { left: root.style.left, top: root.style.top, w: root.style.width, h: root.style.height };
|
|
maximized = true;
|
|
root.classList.add("fw-maxed");
|
|
front();
|
|
}
|
|
|
|
function restore() {
|
|
if (!maximized) return;
|
|
maximized = false;
|
|
root.classList.remove("fw-maxed");
|
|
if (pre) { root.style.left = pre.left; root.style.top = pre.top; root.style.width = pre.w; root.style.height = pre.h; }
|
|
}
|
|
|
|
bMin.onclick = minimize;
|
|
bMax.onclick = maximize;
|
|
bClose.onclick = close;
|
|
bar.ondblclick = (e) => { if (e.target.closest(".fw-btn")) return; collapsed ? expand() : maximize(); };
|
|
root.addEventListener("mousedown", front, true);
|
|
|
|
// ── drag by titlebar ────────────────────────────────────────────────────────
|
|
bar.addEventListener("mousedown", (e) => {
|
|
if (e.target.closest(".fw-btn") || maximized) return;
|
|
e.preventDefault();
|
|
const sx = e.clientX, sy = e.clientY;
|
|
const ox = parseInt(root.style.left || "0", 10);
|
|
const oy = parseInt(root.style.top || "0", 10);
|
|
document.body.classList.add("fw-dragging");
|
|
const move = (ev) => {
|
|
root.style.left = (ox + ev.clientX - sx) + "px";
|
|
root.style.top = Math.max(44, oy + ev.clientY - sy) + "px";
|
|
};
|
|
const up = () => {
|
|
document.removeEventListener("mousemove", move);
|
|
document.removeEventListener("mouseup", up);
|
|
document.body.classList.remove("fw-dragging");
|
|
};
|
|
document.addEventListener("mousemove", move);
|
|
document.addEventListener("mouseup", up);
|
|
});
|
|
|
|
// ── resize by grip ────────────────────────────────────────────────────────
|
|
grip.addEventListener("mousedown", (e) => {
|
|
if (maximized) return;
|
|
e.preventDefault(); e.stopPropagation();
|
|
const sx = e.clientX, sy = e.clientY;
|
|
const ow = root.offsetWidth, oh = root.offsetHeight;
|
|
document.body.classList.add("fw-dragging");
|
|
const move = (ev) => {
|
|
root.style.width = Math.max(opts.minWidth || 360, ow + ev.clientX - sx) + "px";
|
|
root.style.height = Math.max(opts.minHeight || 240, oh + ev.clientY - sy) + "px";
|
|
};
|
|
const up = () => {
|
|
document.removeEventListener("mousemove", move);
|
|
document.removeEventListener("mouseup", up);
|
|
document.body.classList.remove("fw-dragging");
|
|
};
|
|
document.addEventListener("mousemove", move);
|
|
document.addEventListener("mouseup", up);
|
|
});
|
|
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && isOpen()) close();
|
|
});
|
|
|
|
return {
|
|
root, body, titleEl: titleText,
|
|
open, close, toggle, minimize, expand, maximize, restore, isOpen,
|
|
setTitle(t) { titleText.textContent = t; },
|
|
};
|
|
}
|
|
|
|
window.FloatingWindow = { create };
|
|
})();
|