v4's Simple mode is the Editor with about ten `display: none !important` rules over it, which is not a smaller app but the same one wearing a mask. It broke in the way that arrangement always breaks: a collapsed column was marked [hidden], nothing can slide a display:none element into view, and the panel button did nothing at all and said nothing. So the same shape is built the other way round -- a region that does not exist yet is ABSENT, and the toggles live in rails that are siblings of the panels, never inside what they hide. Two kit elements, because both are layout primitives rather than app code: frame.app establishes the one definite height everything else fills, and workspace.docked is the centre-shrinks-between-two-panels arrangement. Three faults found by looking at it rather than by reasoning about it: * The prompt was in the DOM with no pixels. A vertical split inside a parent whose height came from its content resolves its second pane to zero, and nothing anywhere reports that. * The whole app rendered as a strip at the top of an empty window: `100%` of a `min-height` is `auto`. * Two fixed-width panels plus their rails are wider than a 480px window, and the page scrolled sideways -- taking the transport bar with it. The collapse is deliberately not animated: a remembered-closed panel would animate shut on every load, which reads as the app doing something rather than as a preference being restored. Also: jsdom gives a document with no url an OPAQUE ORIGIN, where reading window.localStorage throws on the property access itself. Everything that remembers a preference was silently taking its fallback path in every jsdom test, so "it is remembered" could not be tested at the cheap layer and looked like it worked.
102 lines
4.0 KiB
JavaScript
102 lines
4.0 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";
|
|
|
|
export function build(root) {
|
|
// 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 preview = composer.panel.default({
|
|
title: "Preview",
|
|
body: composer.emptyState.default({
|
|
icon: "▶",
|
|
title: "Nothing generated yet",
|
|
hint: "The result of the last run appears here.",
|
|
}),
|
|
});
|
|
const prompt = composer.panel.default({
|
|
title: "Prompt",
|
|
body: composer.hint.default({ text: "Modules that announced generation.prompt." }),
|
|
});
|
|
const centre = composer.splitPane.v({
|
|
size: 62,
|
|
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 = composer.actionBar.sticky({
|
|
note: "Ready",
|
|
actions: [composer.button.lg({ label: "Generate", tone: "primary" })],
|
|
});
|
|
|
|
const page = composer.frame.app({ 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, prompt, generation, settings, properties, transport, page };
|
|
}
|