Width, height and length belong to the PROJECT, not to a scene: a scene cropped on the timeline and regenerated comes back at the project's size, because the crop was a timeline decision and a regenerate is a new scene. They sit in the Constructor beside the prompt -- the two things settled in one sitting -- and travel to the graph on the path the prompt already uses. No new mechanism: the pipeline declares roles at `project.video` the same way it declares the prompt, so core still names no node and no setting. A role whose value is a number gets a number control instead of a box; that is the whole of the UI change. What the pipeline does not offer is not shown, so a graph with no length input simply has no Length. Bounds are the NODE's and stay there -- it already refuses a value outside them with a reason, beside Generate. A project file is the one thing here that outlives the code that wrote it, so everything read back out of one is checked: a whole positive number, or gone. Also, four faults the browser suite was already red with before this: * Six tests still clicked a rail toggle that moved into the timeline head. * At 420px the panel OVERLAYS the centre -- and the named toggles are in the centre, so the panel covered the control that closes it. That is the v4 trap this project forbids. They are not offered at that width now; the View menu, which nothing can cover, is the way in and out. * A 768px window laid out 860px wide and scrolled sideways. Root cause, and it was never about one component: a flex item's automatic minimum is its CONTENT, so every stack, split, panel body and frame region in the app handed its content's width upward. A scroller that cannot shrink below what it holds is not a scroller -- the strip of scenes made the WINDOW as wide as the strip. Floored at the five places every layout routes through. * A bin test counted `.cx-cell-glyph` across the whole page, so the timeline drawing cells with the same glyph in them broke a test about the bin.
69 lines
3.0 KiB
JavaScript
69 lines
3.0 KiB
JavaScript
// The Constructor: where a run is written, in a window of its own.
|
|
//
|
|
// v4 called this the Composer. The name is taken here -- `app/composer` is the
|
|
// element kit -- and two things called Composer in one codebase is one thing
|
|
// nobody can search for.
|
|
//
|
|
// The prompt is not a panel on the main window. It is the longest thing a
|
|
// person writes, it is edited in bursts and read rarely, and giving it a
|
|
// permanent third of the centre column cost the timeline the room it needs. So
|
|
// it opens, is written in, and closes.
|
|
//
|
|
// The HOST is built once, at startup, and outlives every opening. Two reasons,
|
|
// and both were faults before this file existed: a mount point has to exist
|
|
// when modules mount, which is long before anyone opens a window; and a window
|
|
// that rebuilt its contents on each open would throw away whatever was typed
|
|
// and not saved.
|
|
|
|
import { composer } from "../composer/composer.js";
|
|
|
|
export function createConstructor({ title = "Constructor", onChange } = {}) {
|
|
const empty = composer.emptyState.default({
|
|
icon: "✎",
|
|
title: "Nothing to write yet",
|
|
hint: "The pipeline decides which of its inputs are written here.",
|
|
});
|
|
|
|
// Two groups, by purpose: what this run SAYS, and what it is generated AT.
|
|
// Grouping them is the whole reason they share a window -- the prompt and the
|
|
// size of what it produces are settled in one sitting.
|
|
const written = composer.region.stack({ gap: "sm", label: "Prompt", children: [empty] });
|
|
// Labelled, because with five fields in a column the two purposes are not
|
|
// legible from the field names alone. Hidden by whoever fills it when the
|
|
// pipeline offers nothing to put here -- a heading over nothing is a lie.
|
|
const videoLabel = composer.label.section({ text: "Video settings" });
|
|
const video = composer.region.stack({ gap: "sm", label: "Video settings",
|
|
children: [videoLabel] });
|
|
|
|
// The host is a plain region: it is handed to the modal as a body and handed
|
|
// back when the modal closes, which is why the modal never owns it.
|
|
const host = composer.region.stack({ gap: "md", label: "Constructor", fill: true,
|
|
children: [written, video] });
|
|
|
|
let window_ = null;
|
|
|
|
function open() {
|
|
if (window_) return window_; // one window onto one prompt
|
|
window_ = composer.modal.generic({
|
|
title,
|
|
subtitle: "What this run is made of.",
|
|
size: "lg",
|
|
body: host,
|
|
actions: [composer.button.md({ label: "Done", tone: "primary",
|
|
onClick: () => window_ && window_.close("done") })],
|
|
onClose: () => { window_ = null; if (onChange) onChange(); },
|
|
});
|
|
if (onChange) onChange();
|
|
return window_;
|
|
}
|
|
|
|
return {
|
|
host, empty, open,
|
|
/** The two mount points, offered by the shell under these names. */
|
|
written, video,
|
|
/** Open, and mounted. Tests and the shell both ask. */
|
|
get isOpen() { return Boolean(window_); },
|
|
close() { if (window_) window_.close("close"); },
|
|
};
|
|
}
|