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>
v4's arrangement, which is the one that reads as an editor: a zone head
holds what acts on THAT zone, so the app grows a bar of its own for what
acts on the app.
* The menu bar. Brand, View and Settings, and whether ComfyUI is still on
the other end. Only menus with something real in them exist -- an empty
File menu is a promise the app does not keep.
* The prompt is written in the CONSTRUCTOR, a window. v4 called this the
Composer; that name is taken here by the element kit, and two things
called Composer in one repo is one thing nobody can search for. It is
the longest thing a person writes and the least often read, and a
permanent third of the centre column is the room the timeline needs.
The host is built at startup and outlives every opening, so a window
that opens twice does not throw away what was typed.
* The timeline replaces it, and Generate moves into its head with what
the run is doing at the far end of the same head. No bar across the
bottom: one that spans the window belongs to no region.
Four faults found on the way, three of them mine and one much older:
* THE OVERLAY ROOT WAS A STACKING CONTEXT. It documents at length that it
must never be one -- and then set `position: fixed`, which makes one on
its own. So the whole z ladder was measured inside that single element:
an open menu at z 500 painted UNDER a button at z 1. Every overlay in
the app was one positive z-index away from being buried, and the only
reason nobody had seen it is that almost nothing claims a z-index. The
root is static and zero-sized now; every layer in it is fixed already.
* Generate's hover ran `filter: brightness()`, which lightens the LABEL
along with the fill under it -- the one thing that had to stay readable
lost contrast in both themes. Only the fill moves now, and which way it
moves depends on the theme: dark's accent carries near-black text so it
goes lighter, light's carries white so it goes darker. Measured: no
gradient stop under 4.1:1, hovered or at rest.
* A fixed-height head could not shrink below the width of everything in
it, and a head now holds a title, three controls and a status. That
min-content became the centre column's, so a 768px window laid out
878px wide and scrolled sideways. It wraps now, as v4's does.
* A CSS patch of mine silently did nothing because its anchor text had
moved and I had left the assertion off that one replacement. Every
patch asserts now.
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.
55 element groups across text, buttons, choice, input, slider, layout, status,
overlays, gallery and the wheel picker. Each lands in a registry-driven
catalogue that shows every variant in both themes, so an element that fails to
register is visibly absent rather than quietly missing.
rule_content_is_data iterates the REGISTRY, not a list: a new element is
attacked with markup the moment it calls define(), and one without a demo fails
the suite. That is announcement-not-enumeration applied to the tests.
Fixes found while building: checkbox silently ignored its label (inverted
logic left it with aria-label=toggle), the overlay root set z-index 0 and so
became a stacking context that trapped every overlay it was built to free, and
raising a window left its peers' z stale because z is derived from position --
claim() now notifies a rung when a peer moves.
The wheel highlights the actual sector rather than a box around the label: the
lit shape is the region that picks the item, which is what makes it aimable.