The app could run a pipeline and had nowhere to keep what it was making. A project is an ordered list of scenes, one JSON file each, and it is the only part of the app that has to survive a reload. A scene owns its prompt text and the result it produced, and nothing else. v4's Scene grew three dozen fields; each belongs to the feature that needs it. Per scene length and fps are deliberately absent — a regenerate uses the project's values, so a scene cropped on the timeline comes back whole. The prompt box edits the current scene rather than holding its own copy, so what is on screen and what a run uses cannot disagree. app: the region toggles are named buttons in the timeline bar v4's arrangement. The kit's fallback is one glyph on a rail at each edge of the window, which is right for a kit that cannot know what its panels are called and wrong here, where they have names and a bar to put them in. A pressed toggle was styled only inside a button group, so a standalone one carried aria-pressed and looked identical either way. gallery.strip is selectable like its siblings: a filmstrip is the one shape whose whole job is saying which of these you are on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
56 lines
2.2 KiB
JavaScript
56 lines
2.2 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.",
|
|
});
|
|
|
|
// 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: "sm", label: "Constructor", fill: true,
|
|
children: [empty] });
|
|
|
|
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,
|
|
/** Open, and mounted. Tests and the shell both ask. */
|
|
get isOpen() { return Boolean(window_); },
|
|
close() { if (window_) window_.close("close"); },
|
|
};
|
|
}
|