Files
olivv-cs-ComfyUI-FunPack/app/shell/layout.js
T
digital-garbage 16eec81a03 app: the app is one surface, not a tray of cards
Ran v4 side by side to see what the difference actually was, and it was
not spacing. v4 fills the window edge to edge and divides its zones with
hairlines; v5 floated rounded, shadowed cards on a visible page
background. One reads as an editing suite, the other as a dashboard.

So panel gains a second variant. "default" is still a card, for something
that floats -- inside a window, on a page. "zone" is a region of the app:
square, no shadow, no border of its own, divided from its neighbours by
the one hairline everything else uses. The workspace keeps no gutter
between regions; the preview zone is flush, because padding around a
picture is a frame nobody asked for, and its surround is letterbox black
rather than a panel colour.

The right column was two stacked panels with two heads for one thing.
It is one zone now.

And the surfaces have depth, derived from each theme's own elevation
scale so neither needs a rule of its own: a vertical fall on every band
and every raised control, the accent gradient and a bloom under a primary
button, a wash off the top-right of the page, tokenised thin scrollbars,
an accent mark on every zone title, and v4's film grain -- overlay at
3.5% on dark, multiply at 1.5% on light, and painted with no z-index so
anything that opens above the frame stays crisp.
2026-08-30 21:17:39 +03:00

135 lines
6.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";
export function build(root, handlers = {}) {
// 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." });
// 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,
});
const preview = composer.panel.zone({ title: "Preview", body: viewer, flush: true });
// 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.
const promptEmpty = composer.emptyState.default({
icon: "✎",
title: "No prompt here",
hint: "The pipeline decides which of its inputs appear on the main window.",
});
const prompt = composer.panel.zone({ title: "Prompt", body: promptEmpty });
// 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 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 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, promptEmpty.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, prompt, generation, settings,
properties, transport, bar, page };
}