Files
olivv-cs-ComfyUI-FunPack/app/legacy/floating_window.js
T
digital-garbageandClaude Sonnet 5 29ac44ffcd feat: v4 UI port Phase A start -- module categories, legacy frontend served live
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>
2026-09-14 23:58:51 +03:00

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 };
})();