Files
olivv-cs-ComfyUI-FunPack/app/shell/layout.js
T
digital-garbageandClaude Opus 5 8bfd28950d app: the models and pipeline window
One wide modal, a card per group, and a group that opens into a node list
beside that node/'s parameters, saved a group at a time. Groups travel on the
slot, so a pipeline keeps its own arrangement and a new group is a slot
claiming a name nobody used.

Values are drafted until Save; adding, removing, replacing and moving a node
go to the server as they happen, because only it can say whether the result
still wires up.

An unset dropdown reads "not set" rather than showing the first file as though
it had been chosen -- v4 recorded that trap, and picking the entry the box
already shows fires no change, so the one click that looks like the fix does
nothing.

New: region.stack, a frame header, a modal footer that can be refilled.

Not yet: wiring. A socket says so where a control would be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 16:36:01 +03:00

113 lines
4.8 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
// media bin's view modes, the pipeline window and the wheel are not hidden
// below; they have not been built, and adding one is adding a region rather
// than removing a rule.
import { composer } from "../composer/composer.js";
import { offer } from "./mounts.js";
import { createTransport } from "./transport.js";
export function build(root, handlers = {}) {
// Left: what a project is made of. Empty until the media bin exists, and an
// empty state rather than an absent panel, because "there is nothing here
// yet" and "this part of the app is missing" must not look the same.
const assets = composer.panel.default({
title: "Assets",
body: composer.emptyState.default({
icon: "▤",
title: "Nothing in the bin",
hint: "Media you add to the project appears here.",
}),
});
// Centre: the result, and the prompt under it. The prompt is the one control
// that is always on screen, because it is the one always being edited.
const viewer = composer.viewer.media({ empty: "The result of the last run appears here." });
const preview = composer.panel.default({ title: "Preview", body: viewer });
const prompt = composer.panel.default({
title: "Prompt",
body: composer.hint.default({ text: "Modules that announced generation.prompt." }),
});
// The result gets the room. The prompt is a few lines and a button's worth of
// controls; at 38% of the height it was mostly empty space taken from the one
// thing on the page anybody is looking at. Draggable, so anyone writing a long
// prompt can take it back.
const centre = composer.splitPane.v({
size: 74,
label: "Preview and prompt",
panes: [preview, prompt],
});
// 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 generation = composer.panel.default({
title: "Generation",
body: composer.hint.default({ text: "Modules that announced a generation.* mount point." }),
});
const settings = composer.panel.default({
body: composer.hint.default({ text: "Modules that announced settings.general." }),
});
const properties = composer.group.default({
rows: [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 transport. One row, always visible, and the place a run is started and
// reported -- an inert setting is said HERE, next to Generate, not in the
// panel that would have enabled it.
const transport = createTransport(handlers);
// The top bar: the way into everything that is a WINDOW rather than a panel.
// It holds one button today and will hold the rest as they land -- a place
// that exists is what stops the next one being wedged into the transport row,
// which is the one row that has to keep saying what the run is doing.
const bar = composer.toolbar.default({
label: "FunPack",
items: [
composer.button.sm({
label: "Models and pipeline",
onClick: () => { if (handlers.onPipeline) handlers.onPipeline(); },
}),
],
});
const page = composer.frame.app({ header: bar, main: workspace, footer: transport });
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);
offer("generation.prompt", prompt.body);
offer("settings.general", settings.body);
offer("generation.model", generation.body);
offer("generation.latent", generation.body);
offer("generation.sampling", generation.body);
offer("generation.timing", generation.body);
offer("generation.post", generation.body);
return { workspace, assets, preview, viewer, prompt, generation, settings,
properties, transport, bar, page };
}