feat: app/ui reproduces v4's screen with placeholders — menubar, zone heads, timeline header/toolbar/lanes, preview transport — on finer mount points; hubs may carry several features
This commit is contained in:
+2
-1
@@ -6,6 +6,7 @@ import { hostFor } from "./shell/mounts.js";
|
||||
import paths from "./modules.js";
|
||||
|
||||
const frame = build(document.getElementById("app"));
|
||||
const settings = createSettings({ menubar: hostFor("menubar") });
|
||||
const settings = createSettings({ menubar: hostFor("menubar.menus") });
|
||||
const result = await loadFeatures(paths);
|
||||
settings.addButton();
|
||||
window.FunPackUI = { frame, settings, ...result };
|
||||
|
||||
@@ -77,3 +77,9 @@
|
||||
animation: cx-spin 800ms linear infinite;
|
||||
}
|
||||
@keyframes cx-spin { to { transform: rotate(360deg); } }
|
||||
/* Render: the teal one, as v4 had it -- the output side of Generate. */
|
||||
.cx-btn-render {
|
||||
background: linear-gradient(var(--teal), color-mix(in srgb, var(--teal) 78%, black));
|
||||
color: var(--bg); border-color: var(--teal); font-weight: var(--fw-semibold);
|
||||
}
|
||||
.cx-btn-render:hover:not(:disabled) { filter: brightness(1.08); }
|
||||
|
||||
@@ -9,7 +9,7 @@ import { el } from "../internals/el.js";
|
||||
import { setText } from "../internals/text.js";
|
||||
import { roving } from "../internals/focus.js";
|
||||
|
||||
const TONES = new Set(["neutral", "primary", "danger", "ghost"]);
|
||||
const TONES = new Set(["neutral", "primary", "danger", "ghost", "render"]);
|
||||
|
||||
function base(size, { label, onClick, tone = "neutral", icon, disabled = false, busy = false, type = "button", title, pressed } = {}) {
|
||||
if (!TONES.has(tone)) throw new RangeError(`Unknown button tone "${tone}". Known: ${[...TONES].join(", ")}.`);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
display: inline-flex; align-items: center; gap: var(--sp-2);
|
||||
height: var(--ctl-h-sm); padding: 0 var(--sp-3);
|
||||
border: var(--bw) solid var(--line); border-radius: var(--r-pill);
|
||||
font-size: var(--fs-7); color: var(--muted); white-space: nowrap;
|
||||
font-size: var(--fs-6); color: var(--muted); white-space: nowrap;
|
||||
}
|
||||
.cx-chip-dot { width: 6px; height: 6px; border-radius: var(--r-pill); background: currentColor; }
|
||||
.cx-chip-good { color: var(--good); border-color: color-mix(in srgb, var(--good) 30%, var(--line)); }
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
--r-pill: 9999px;
|
||||
|
||||
/* ── controls (v4 shipped 11 distinct heights) ──────────────────────── */
|
||||
--ctl-h-sm: 20px; --ctl-h-md: 24px; --ctl-h-lg: 28px; --ctl-h-xl: 32px;
|
||||
--ctl-h-sm: 22px; --ctl-h-md: 24px; --ctl-h-lg: 28px; --ctl-h-xl: 32px;
|
||||
--ctl-h-icon: 18px; --ctl-h-icon-sm: 16px;
|
||||
--ctl-pad-x-sm: 6px; --ctl-pad-x-md: 8px;
|
||||
--ctl-pad-x-sm: 8px; --ctl-pad-x-md: 8px;
|
||||
--ctl-pad-x-lg: 10px; --ctl-pad-x-xl: 12px;
|
||||
--ctl-r: var(--r-3);
|
||||
--ctl-gap: var(--sp-3);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Stand-ins for the parts of v4's screen whose features are not ported yet: same place, same look,
|
||||
// inert. A real feature that mounts at one of these points replaces its row here (delete the row).
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
const btn = (label, tone = "ghost", extra = {}) => c.button.sm({ label, tone, disabled: true, ...extra });
|
||||
const menu = (label) => c.button.sm({ label, tone: "ghost" }); // inert, but not dimmed: a menu is never greyed in v4
|
||||
const dockTab = (label) => c.button.sm({ label, tone: "neutral", pressed: true, disabled: true });
|
||||
|
||||
const items = {
|
||||
"menubar.mode": () => [c.segmented.sm({ options: [{ value: "simple", label: "Simple" }, { value: "editor", label: "Editor" }], value: "editor" })],
|
||||
"menubar.menus": () => ["File", "Edit", "View", "Help"].map(menu),
|
||||
"menubar.right": () => [c.chip.neutral({ label: "saved" }), c.chip.good({ label: "ComfyUI live", dot: true })],
|
||||
|
||||
"assets.actions": () => [],
|
||||
assets: () => [c.toolbar.default({ items: [c.text.sm({ text: "Projects" })], trailing: [btn("+ New")] }),
|
||||
c.hint.default({ text: "Your projects appear here." })],
|
||||
|
||||
preview: () => [
|
||||
c.viewer.media({ empty: "No render yet. Use Generate in the timeline header." }),
|
||||
c.toolbar.default({ items: [btn("⏹"), btn("▶"), c.text.sm({ text: "00:00:00:00" }), btn("📌 Save frame")] }),
|
||||
],
|
||||
|
||||
inspector: () => [c.segmented.sm({ options: [{ value: "project", label: "Project" }, { value: "scene", label: "Scene" }], value: "scene" }),
|
||||
c.hint.default({ text: "Settings of the selected scene or the project." })],
|
||||
|
||||
"timeline.actions": () => [btn("▶ Generate", "primary"), btn("Selected"), btn("⧉ Render", "render"), btn("⚡ Auto Montage")],
|
||||
"timeline.status": () => [btn("⏱ Sampler", "neutral"), c.text.sm({ text: "0 clips" }),
|
||||
dockTab("Assets"), dockTab("Preview"), dockTab("Properties"), btn("◆ Composer", "neutral")],
|
||||
timeline: () => [
|
||||
c.toolbar.default({ items: [c.text.sm({ text: "00:00:00 / 00:00:00" }), btn("+ Add"), btn("Split"), btn("Remove"),
|
||||
btn("⤓ Export"), btn("Save to media bin"), btn("⊟ Separate audio"), btn("Remove audio")],
|
||||
trailing: [c.text.sm({ text: "J/K/L · S split · I/O in/out · +/- zoom" })] }),
|
||||
c.toolbar.default({ items: [c.text.sm({ text: "zoom" }), btn("−"), btn("+"), btn("fit")] }),
|
||||
c.ruler.default({ marks: [{ label: "00:00" }, { label: "00:02" }, { label: "00:04" }], total: 3 }),
|
||||
c.toolbar.default({ items: [c.text.sm({ text: "Video" }), c.hint.default({ text: "" })] }),
|
||||
c.toolbar.default({ items: [c.text.sm({ text: "Original" }), c.hint.default({ text: "" })] }),
|
||||
],
|
||||
};
|
||||
|
||||
export default Object.entries(items).map(([mount, build]) => ({
|
||||
id: `placeholder:${mount}`, mount,
|
||||
setup: ({ host }) => build().forEach((handle) => host.append(handle.node)),
|
||||
}));
|
||||
@@ -1,5 +1,6 @@
|
||||
// The one list of UI features. Adding a feature is a folder under ./features and a line here.
|
||||
export default [
|
||||
"./features/log/index.js",
|
||||
"./features/placeholders/index.js",
|
||||
"./features/temp/index.js",
|
||||
].map((path) => new URL(path, import.meta.url).href);
|
||||
|
||||
+21
-19
@@ -1,29 +1,31 @@
|
||||
// Loads UI features. A feature is a folder with an index.js hub:
|
||||
// export default { id, mount, needs?, setup({ host }) -> teardown? }
|
||||
// Loads UI features. A feature file's default export is one hub, or several:
|
||||
// { id, mount, needs?, setup({ host }) -> teardown? }
|
||||
// `mount` names a region (see mounts.js). A feature that cannot load, names a region nobody offers,
|
||||
// needs a service the app lacks, or throws in setup is hidden -- nothing of it reaches the screen.
|
||||
import { hostFor, offered } from "./mounts.js";
|
||||
|
||||
function mountOne(hub, has) {
|
||||
const host = hostFor(hub.mount);
|
||||
if (!host) throw new Error(`no region offers "${hub.mount}" (offered: ${offered().join(", ")})`);
|
||||
const lacking = (hub.needs || []).filter((name) => !has(name));
|
||||
if (lacking.length) throw new Error(`needs ${lacking.join(", ")}`);
|
||||
const before = [...host.childNodes];
|
||||
try {
|
||||
return { id: hub.id, teardown: hub.setup({ host }) };
|
||||
} catch (err) { // whatever it put on the page goes with it
|
||||
[...host.childNodes].filter((n) => !before.includes(n)).forEach((n) => n.remove());
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadFeatures(paths, { load = (path) => import(path), has = () => true } = {}) {
|
||||
const mounted = [], hidden = [];
|
||||
const hide = (id, err) => { hidden.push({ id, why: err.message }); console.warn(`[FunPack] ${id} hidden: ${err.message}`); };
|
||||
for (const path of paths) {
|
||||
let hub;
|
||||
try {
|
||||
hub = (await load(path)).default;
|
||||
const host = hostFor(hub.mount);
|
||||
if (!host) throw new Error(`no region offers "${hub.mount}" (offered: ${offered().join(", ")})`);
|
||||
const lacking = (hub.needs || []).filter((name) => !has(name));
|
||||
if (lacking.length) throw new Error(`needs ${lacking.join(", ")}`);
|
||||
const before = [...host.childNodes];
|
||||
try {
|
||||
mounted.push({ id: hub.id, teardown: hub.setup({ host }) });
|
||||
} catch (err) { // whatever it put on the page goes with it
|
||||
[...host.childNodes].filter((n) => !before.includes(n)).forEach((n) => n.remove());
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
hidden.push({ id: hub?.id || path, why: err.message });
|
||||
console.warn(`[FunPack] ${hub?.id || path} hidden: ${err.message}`);
|
||||
let hubs;
|
||||
try { hubs = [].concat((await load(path)).default); } catch (err) { hide(path, err); continue; }
|
||||
for (const hub of hubs) {
|
||||
try { mounted.push(mountOne(hub, has)); } catch (err) { hide(hub?.id || path, err); }
|
||||
}
|
||||
}
|
||||
return { mounted, hidden };
|
||||
|
||||
+12
-7
@@ -1,26 +1,31 @@
|
||||
// The app's regions, laid out as v4 had them: a menubar, then Assets | Preview | Properties side by
|
||||
// side with the Timeline under all three. Each region offers a mount point; no feature is named here.
|
||||
// side with the Timeline under all three. Every region offers mount points; no feature is named here.
|
||||
// menubar.mode | menubar.menus | menubar.right the bar
|
||||
// <zone> (body) | <zone>.actions (after the title) | <zone>.status (far end) for assets, preview, inspector, timeline
|
||||
import { composer } from "../composer/composer.js";
|
||||
import { offer } from "./mounts.js";
|
||||
|
||||
export function build(root) {
|
||||
const zone = (title, mount) => {
|
||||
const body = composer.region.stack({ gap: "sm", fill: true });
|
||||
offer(mount, body.node);
|
||||
return composer.panel.zone({ title, body });
|
||||
};
|
||||
const group = (mount) => {
|
||||
const bar = composer.toolbar.default({});
|
||||
offer(mount, bar.node);
|
||||
return bar;
|
||||
};
|
||||
const zone = (title, mount) => {
|
||||
const body = composer.region.stack({ gap: "sm", fill: true });
|
||||
offer(mount, body.node);
|
||||
return composer.panel.zone({ title, body, actions: [group(`${mount}.actions`)], status: [group(`${mount}.status`)] });
|
||||
};
|
||||
|
||||
const row = composer.region.stack({ gap: "none", children: [zone("Assets", "assets"), zone("Preview", "preview"), zone("Properties", "inspector")] });
|
||||
const main = composer.region.stack({ gap: "none", fill: true, children: [row, zone("Timeline", "timeline")] });
|
||||
row.node.classList.add("fp-row");
|
||||
main.node.classList.add("fp-main");
|
||||
|
||||
const header = composer.toolbar.default({ items: [composer.brand.default({}), group("menubar")], trailing: [group("menubar.right")] });
|
||||
const header = composer.toolbar.default({
|
||||
items: [composer.brand.default({}), group("menubar.mode"), group("menubar.menus")],
|
||||
trailing: [group("menubar.right")],
|
||||
});
|
||||
const frame = composer.frame.app({ header, main });
|
||||
root.append(frame.node);
|
||||
return frame;
|
||||
|
||||
@@ -46,6 +46,7 @@ export function createSettings({ menubar }) {
|
||||
return current;
|
||||
}
|
||||
|
||||
menubar.append(composer.button.sm({ label: "Settings", onClick: () => open() }).node);
|
||||
return { open, sections: () => [...sections] };
|
||||
// Added once the other menus are in, so it sits last as in v4.
|
||||
const addButton = () => menubar.append(composer.button.sm({ label: "Settings", onClick: () => open() }).node);
|
||||
return { open, addButton, sections: () => [...sections] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user