v4's arrangement, which is the one that reads as an editor: a zone head holds what acts on THAT zone, so the app grows a bar of its own for what acts on the app. * The menu bar. Brand, View and Settings, and whether ComfyUI is still on the other end. Only menus with something real in them exist -- an empty File menu is a promise the app does not keep. * The prompt is written in the CONSTRUCTOR, a window. v4 called this the Composer; that name is taken here by the element kit, and two things called Composer in one repo is one thing nobody can search for. It is the longest thing a person writes and the least often read, and a permanent third of the centre column is the room the timeline needs. The host is built at startup and outlives every opening, so a window that opens twice does not throw away what was typed. * The timeline replaces it, and Generate moves into its head with what the run is doing at the far end of the same head. No bar across the bottom: one that spans the window belongs to no region. Four faults found on the way, three of them mine and one much older: * THE OVERLAY ROOT WAS A STACKING CONTEXT. It documents at length that it must never be one -- and then set `position: fixed`, which makes one on its own. So the whole z ladder was measured inside that single element: an open menu at z 500 painted UNDER a button at z 1. Every overlay in the app was one positive z-index away from being buried, and the only reason nobody had seen it is that almost nothing claims a z-index. The root is static and zero-sized now; every layer in it is fixed already. * Generate's hover ran `filter: brightness()`, which lightens the LABEL along with the fill under it -- the one thing that had to stay readable lost contrast in both themes. Only the fill moves now, and which way it moves depends on the theme: dark's accent carries near-black text so it goes lighter, light's carries white so it goes darker. Measured: no gradient stop under 4.1:1, hovered or at rest. * A fixed-height head could not shrink below the width of everything in it, and a head now holds a title, three controls and a status. That min-content became the centre column's, so a 768px window laid out 878px wide and scrolled sideways. It wraps now, as v4's does. * A CSS patch of mine silently did nothing because its anchor text had moved and I had left the assertion off that one replacement. Every patch asserts now.
161 lines
7.1 KiB
JavaScript
161 lines
7.1 KiB
JavaScript
// The app's regions, and the mount points each one offers.
|
|
//
|
|
// A region declares what it can host. Core never learns these names, and no
|
|
// list of them exists anywhere else -- a module asks for one by name, and gets
|
|
// it or does not.
|
|
//
|
|
// The shape is the one v4 called Simple mode, built the other way round. v4's
|
|
// Simple mode was the Editor with about ten `display: none !important` rules
|
|
// over it, which is not a smaller app but the same app wearing a mask -- and it
|
|
// broke: a collapsed column was marked [hidden], and nothing can slide a
|
|
// display:none element into view, so a panel button did nothing at all and said
|
|
// nothing. So here a region that does not exist yet is ABSENT. The timeline,
|
|
// the picker wheel and the project wizard are not hidden below; they have not
|
|
// been built, and adding one is adding a region rather than removing a rule.
|
|
// The bin arrived exactly that way.
|
|
|
|
import { composer } from "../composer/composer.js";
|
|
import { offer } from "./mounts.js";
|
|
import { createTransport } from "./transport.js";
|
|
import { createBin } from "./bin.js";
|
|
import { createMenubar } from "./menubar.js";
|
|
import { createConstructor } from "./constructor.js";
|
|
|
|
export function build(root, handlers = {}) {
|
|
// The transport: Generate, Cancel, and what the run is doing. Built before the
|
|
// zones because its controls are handed to the zone that holds them.
|
|
const transport = createTransport(handlers);
|
|
|
|
// Built here rather than on first open: a mount point has to exist when
|
|
// modules mount, which is long before anyone opens a window.
|
|
const constructor = createConstructor();
|
|
|
|
// Centre: the result, and the timeline under it.
|
|
const viewer = composer.viewer.media({ empty: "The result of the last run appears here." });
|
|
|
|
// Left: what a project is made of, which for now is what it has produced. The
|
|
// bin owns the choice of what the viewer shows -- one place decides, so a
|
|
// click on an older result and a run finishing cannot both be right at once.
|
|
const bin = createBin({ onOpen: (item) => viewer.setSource(item.url, item.kind) });
|
|
const assets = composer.panel.zone({
|
|
title: "Assets",
|
|
actions: [bin.control],
|
|
body: bin.host,
|
|
});
|
|
// v4's preview head carries nothing but the name of the zone: what is being
|
|
// looked at is not something you act on.
|
|
const preview = composer.panel.zone({ title: "Preview", body: viewer, flush: true });
|
|
|
|
// The timeline: what the project IS, and where a run is started from.
|
|
//
|
|
// Generate lives here rather than in a bar across the bottom, because a bar
|
|
// that spans the window belongs to no region and acts on whatever happens to
|
|
// be in front -- which is a dashboard. v4 puts Generate in the timeline head,
|
|
// beside the thing it fills, and so does this.
|
|
//
|
|
// The warning is the first thing INSIDE the zone rather than in its head: it
|
|
// is a sentence, not a chip, and it has to sit under the button it is about
|
|
// -- an inert setting is said where the run is started, not where it was
|
|
// switched on.
|
|
const timelineEmpty = composer.emptyState.default({
|
|
icon: "▭",
|
|
title: "Nothing on the timeline",
|
|
hint: "What you generate lands here, in the order it plays.",
|
|
});
|
|
const timeline = composer.panel.zone({
|
|
title: "Timeline",
|
|
actions: [
|
|
...transport.actions,
|
|
composer.button.sm({
|
|
label: "Constructor",
|
|
onClick: () => { if (handlers.onConstructor) handlers.onConstructor(); },
|
|
}),
|
|
],
|
|
status: transport.status,
|
|
body: composer.region.stack({ gap: "sm", fill: true,
|
|
children: [transport.warning, timelineEmpty] }),
|
|
});
|
|
|
|
// The stand-ins below are EMPTY STATES, not labels: they say a region is
|
|
// empty, and settle() takes them down once it is not. A line reading "modules
|
|
// appear here" left above the modules that appeared is a region explaining
|
|
// itself to nobody.
|
|
|
|
// The result gets the room. Draggable, so a long timeline can take it back.
|
|
const centre = composer.splitPane.v({
|
|
size: 74,
|
|
label: "Preview and timeline",
|
|
panes: [preview, timeline],
|
|
});
|
|
|
|
// Right: everything about the run. Generation is open, settings folded away
|
|
// -- one is read constantly and the other rarely, and a fold says which is
|
|
// which without needing a second place to look. A settings WINDOW is a
|
|
// separate thing that has not been built; this is not a stand-in for it.
|
|
const generationEmpty = composer.emptyState.default({
|
|
icon: "◎",
|
|
title: "Nothing to set",
|
|
hint: "Modules with settings for a run appear here.",
|
|
});
|
|
const generation = composer.region.stack({ gap: "sm", label: "Generation",
|
|
children: [generationEmpty] });
|
|
|
|
const settingsEmpty = composer.emptyState.default({
|
|
icon: "⚙",
|
|
title: "Nothing to set",
|
|
hint: "Modules with settings that are not about one run appear here.",
|
|
});
|
|
// A host, not a panel: a bordered panel inside the collapsible's own bordered
|
|
// box is two frames drawn around one list of settings.
|
|
const settings = composer.region.stack({ gap: "sm", label: "Settings",
|
|
children: [settingsEmpty] });
|
|
// ONE zone, not two panels stacked. A region of the app is one area with one
|
|
// head on it; two heads in a column means two things, and there is only one
|
|
// thing here -- what the next run will do.
|
|
const properties = composer.panel.zone({
|
|
title: "Generation",
|
|
body: composer.region.stack({
|
|
gap: "sm", label: "Properties",
|
|
children: [generation, composer.collapsible.default({ label: "Settings", body: settings })],
|
|
}),
|
|
});
|
|
|
|
const workspace = composer.workspace.docked({
|
|
id: "main",
|
|
leftLabel: "Assets",
|
|
rightLabel: "Properties",
|
|
left: assets,
|
|
centre,
|
|
right: properties,
|
|
});
|
|
|
|
|
|
// The menu bar: who this is, what is not a zone, and whether ComfyUI is
|
|
// still on the other end. A zone head holds what acts on THAT zone, so
|
|
// anything that acts on the app has nowhere else to live.
|
|
const menubar = createMenubar({ workspace, onPipeline: handlers.onPipeline });
|
|
const bar = menubar;
|
|
|
|
const page = composer.frame.app({ header: bar, main: workspace });
|
|
|
|
root.replaceChildren(page.node);
|
|
|
|
// The mount point IS the contract with modules; the region behind it can be
|
|
// rearranged freely as long as the name survives.
|
|
offer("assets.library", assets.body);
|
|
// The prompt is written in a WINDOW, not in a zone: it is the longest thing a
|
|
// person writes and the least often read, and a permanent third of the centre
|
|
// column is the room the timeline needs.
|
|
offer("generation.prompt", constructor.host.node, constructor.empty.node);
|
|
offer("settings.general", settings.node, settingsEmpty.node);
|
|
// One host, five names. Each carries the same stand-in, and settle() takes it
|
|
// down once anything at all has mounted into the panel they share.
|
|
for (const point of ["model", "latent", "sampling", "timing", "post"]) {
|
|
offer(`generation.${point}`, generation.node, generationEmpty.node);
|
|
}
|
|
|
|
return { workspace, assets, bin, preview, viewer, timeline, generation, settings,
|
|
properties, transport, constructor, menubar,
|
|
connection: menubar.connection, bar, page };
|
|
}
|