c4e5ceac6412e21c0ff77e48635d2ccc2bfb8f0b
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a600fe3e29 |
fix: real timeline -- full-width layout, playhead reentrancy, panel overlap
Round 1 + round 2 of adversarial review on the single-track timeline
(
|
||
|
|
fd292259e7 |
core, app: media import -- a library separate from the run-output bin
Files the user brings IN (a reference image, an imported clip) now have a real store: core/media.py, /api/media routes, and a "Media" tab beside the bin's "Results" tab in the Assets zone. Upload, list, fetch, delete all round-trip through the real server; nothing wires into a pipeline input yet, since no model module or Studio conditioning takes a reference image today. Two adversarial review rounds found and fixed three real issues along the way: MAX_BYTES (512MB) was dead code because aiohttp's own client_max_size capped every upload far lower first in both prod and dev, with a JSON-parse error leaking to the user on rejection instead of a clean message; the index.json read-modify-write had no guard against concurrent writers; and the devserver's bare Application didn't match ComfyUI's own 100MB default, which also broke an unrelated e2e assertion once the review polluted the real, unisolated media/ directory testing the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
739a720adc |
app, core: Settings matches v4 -- retractable rail, fixed frame, real About
Three things the unified Settings window was still missing next to v4's own, all found by looking at it next to v4 rather than in isolation: - The sidebar is a 52px icon rail now, expanding to a full list on hover/focus and overlaying the content instead of a splitPane's permanently-docked two-pane shape -- v4's own sidebar, not an approximation of it. Fixing this surfaced two real CSS bugs along the way: filterList's own min-width:220px silently defeated the collapse (clipping the search box to "Sear" instead of showing an icon), and .cx-filter-list's unset overflow-x (computed to auto once overflow-y was set) put a horizontal scrollbar under it. Centering the icon in the collapsed row needed the label's flex-basis AND the row's own gap to both go to zero -- an invisible label still claims its share of both otherwise, and the icon renders off-centre. - The modal now has a real height (v4's own 660px) rather than sizing to whichever section is showing, so switching between a short section (About) and a long one (Node packs) doesn't visibly resize the whole window. That exposed a THIRD latent bug: .cx-modal-body never had flex:1 1 auto, invisible until a modal had a real height to fill -- every prior modal shrink-wrapped its content, so head+body+foot always summed to exactly the card's height by accident. Fixed at the shared rule, not just for Settings. - About now shows what v4's does: commit and dirty state, a codename when the checkout has one, and the machine's real hardware/software facts (chip, memory, GPU or MPS, disk, OS, ComfyUI/Python/Torch/ attention-backend versions, hostname) -- a new core/sysinfo.py (ported from v4's own, individually guarded so a missing probe never takes the panel down) behind a new GET /funpack/api/system, injected into settings_window.js the same way every other network call here is. Also: a nav row's tone (the coloured icon chip) is now validated against its closed set the same way button.js already validates its own tone -- an unknown one used to render silently wrong instead of failing loudly. |
||
|
|
563b1c6f02 |
app: Settings looks designed instead of default -- icon chips, real sidebar, centred About
A bare glyph character had no framing at all; filterList's icon now gets a coloured chip, drawn from a fixed tone (accent/danger/warn/good/neutral) -- never a raw colour from data, the same rule el()'s style refusal already holds everywhere else. Each Settings section picks one. The sidebar was the filterList element's small dropdown-list shape (a 240px-capped box with its own border, floating in a large pane) rather than a real full-height sidebar; .cx-filter-full opts a host into the pane version without changing the compact one pipeline_window's own node picker still uses. Round one had a per-row hint line under the icon+label, which doesn't exist in v4's own sidebar and broke alignment the moment a hint wrapped (icon and label baseline-aligned against a hint floating as an independent sibling rather than stacked under its own label). Fixed the real bug -- label+hint are a column now, not loose flex siblings -- then removed the hint entirely to match v4: a section's subtitle belongs in its own head once picked, not repeated under every row whether picked or not. About is centred -- mark, name, version/branch -- rather than a plain settings-row list, the one section built fresh rather than mounting an existing window. |
||
|
|
9a4fb4eca0 |
app, core: what the project is generated at
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. |
||
|
|
d3b0ba0fa9 |
app: the accent button per theme, and room around a setting
* Generate was washed out on light and the fade downwards was why. Light's accent carries WHITE text, which caps how bright the fill can go before the label starts disappearing into it -- so the button sits AT the accent now and DARKENS under the pointer, where white only reads better. Dark's accent carries near-black text and has no such ceiling, so it does the opposite: rest at the accent, lighter on hover. Measured in both, at rest and hovered: no gradient stop under 4:1. * A section label sat directly on the dividing line above it -- the collapsible's body had no top padding of its own, so "APPEARANCE" read as belonging to the divider rather than to the card under it. * A checkbox in this app is a switch with a sentence under it, not a tick in a list. At 4px of padding the label and its hint sat hard against the top and bottom of their card, and a hint wrapping to two lines had nowhere to wrap into. * Row content is weighted to the left now: hard against the border, a card read as leaning, because what sits opposite a control is usually the short half of a wrapped line rather than anything at all. And one regression caught in the screenshot: the settings row stacks below a CONTAINER width, and that threshold was written as 300px -- which happened to be just above what the properties column resolved to. Trimming 4px of padding off two nested boxes widened it past the line, and every setting in that column went back to being shredded, in a commit about something else. It is 360 now, which is what the numbers say it should always have been: a 180px control column needs at least as much again beside it. |
||
|
|
62f92ecaf8 |
app: a menu bar, a timeline, and the prompt in a window of its own
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. |
||
|
|
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. |
||
|
|
549e430806 |
app: one gutter, one head height, one control column
The layout was assembled by hand and read like it. Measured at 1440x900: the three columns started at three different heights, a panel head was 41px when it held a control and 34 when it did not, the gap left of the centre was 7px and right of it 19, the properties column sat 11px further in than the others inside a border that meant nothing, and five controls in one list had four different widths -- a <select> takes the width of its widest option, so the row listing checkpoint filenames was 124px wider than its neighbours. Now: --gutter is the space between regions, between two panels stacked in a column, and inside the bands; the head band height is the panel head height is the collapsible head height; --control-col gives every settings control the same two edges. The frame's bands run edge to edge and the content is inset by the gutter, so the app has an outline instead of a margin that changed per region. Also here, all of it visible in the same screenshot: * The status sat against the Generate button with 1300px of empty bar to its left -- everything was in one right-aligned group. What the app says is at the start of the row now, what you press is at the end. * A panel body is a column with a gap: two things put in it by two parts of the app had whatever space their line boxes happened to leave. * A container query adds no specificity, so the narrow-panel rule written above the rules it overrides lost -- and flex-basis in a column is HEIGHT, which turned four settings into a panel two screens long. * The theme you picked was thrown away on every load: the appearance module applied its own declared default over the choice the page had already applied from storage. It adopts the live value now, and a panel is built after setup so its control shows what setup settled on. |
||
|
|
1b0f489950 |
core, app: what the panels hold now reaches the run
Every module panel was decoration. The values lived in the browser and nothing sent them, so a modifier switched on was one the run never heard about. They now travel with the pipeline on every check, and core places them wherever a module says they belong -- announced, not known, so the node that carries them is not privileged. Settings with nowhere to go are said beside Generate rather than swallowed, and the warning survives the run it is about: the status line is redrawn by every message, so a warning put there was gone the moment the run said Queued. Also: a settings row in a docked panel squeezed its label to one word per line, because the control keeps a 140px floor and took it out of the label. Below the width where they fit side by side, they stack. |
||
|
|
8bfd28950d |
app: the models and pipeline window
One wide modal, a card per group, and a group that opens into a node list beside that node/'s parameters, saved a group at a time. Groups travel on the slot, so a pipeline keeps its own arrangement and a new group is a slot claiming a name nobody used. Values are drafted until Save; adding, removing, replacing and moving a node go to the server as they happen, because only it can say whether the result still wires up. An unset dropdown reads "not set" rather than showing the first file as though it had been chosen -- v4 recorded that trap, and picking the entry the box already shows fires no change, so the one click that looks like the fix does nothing. New: region.stack, a frame header, a modal footer that can be refilled. Not yet: wiring. A socket says so where a control would be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78b623b2f7 |
app: panels sized by what they hold, and out of the way when they do not fit
The columns were a share of the viewport, which is not a measurement of anything. A 900x900 window -- an ordinary shape -- gave the properties column 198px, in which "Loosen the starting image" wrapped over three lines and a section heading broke mid-word; 375px put three columns side by side with clipped headings and a preview no wider than a thumbnail. A panel is now as wide as a settings row needs, and below 760px it stops taking space from the centre and overlays it instead. The element decides what is OPEN and the stylesheet decides how it is DRAWN, so entering that mode closes both panels and only one opens at a time -- two overlays over a narrow centre is the same problem with an extra step. What was open is restored on the way out, and none of it touches the remembered preference: a window being small is not the user changing their mind. The rails paint above the overlay. Without that the panel covered the only control that closes it, which is the exact fault this arrangement exists to avoid, arriving by a different route the moment the panel stopped taking its own space. Also: the prompt was holding 38% of the height for two lines of text. The result gets the room; the divider is still draggable. |
||
|
|
f37f526268 |
app: Generate wired to a real run -- queued, progress, cancel, result
ComfyUI's own queue and its own socket, not a second one alongside them. A
FunPack queue would be a second thing tracking what is running, and the one the
user believes is whichever spoke last -- so this speaks to /prompt, /interrupt
and /ws as any ComfyUI client does and holds only a view of the server's state.
The graph is asked for, never assembled here, for the same reason.
The client id is remembered: ComfyUI addresses progress to the client that
queued the run, so a reload with a fresh id hears nothing about the generation
already in flight -- the run continues, the GPU time is spent, and the app says
Ready.
Faults found on the way, each in code that already had passing tests:
* `[hidden]` was inert across the whole kit. The UA rule is element-level, so
`.cx-btn { display: inline-flex }` beats it -- Cancel was marked hidden, sat
there, and nothing reported anything. jsdom cannot see it: the attribute was
always correct. One reset rule, and a browser test over four element kinds.
* A panel passed no height to its body and a split pane passed none to its
panes, so the viewer asked for 100% of `auto` and collapsed to a single line
inside a panel that was the right size around it.
* subscribe() delivered the first state outside the guard every later delivery
gets, so one bad listener took down whoever was wiring the UI up -- at
startup, before anything existed to report it.
* A whole tests directory was never run: the test script globbed
composer/tests only. `node --test` discovers them itself now, so there is no
list to forget.
* `empty` was not in the content rule's key list, so three elements were never
asked to prove their empty-state text comes back as text rather than markup.
The dev server now mounts the REAL routes -- register() already took a route
table -- instead of reimplementing them over http.server. It answered POST
/api/pipeline with 501, so the edit-and-queue path could not be looked at, which
is the one thing the dev server exists for.
|
||
|
|
2bd604721c |
app: the base frame -- panels docked beside a preview that shrinks
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. |
||
|
|
bd0c4be42e |
composer: Playwright, and the two layout faults it found in its first run
The plan asked for jsdom AND Playwright from the start and only jsdom was ever built. jsdom has no layout engine: getBoundingClientRect answers zeros, container queries never fire, a theme is an attribute rather than a computed cascade. So every claim about POSITION or SIZE in this kit was unchecked, and the unit suite could not have noticed either of these. A popover bounded its height and not its width. `max-height: 60vh` was there from the start; there was no max-inline-size at all, so a long unbroken string -- a file path, a model filename -- made the popover wider than the screen and anchor.js could not save it: it can move a box, not shrink one. Measured at 1436px inside a 1280px viewport, off the right edge in all four corners. A horizontal split could not fit a narrow screen. The first pane carries an inline size and `flex: 0 0 auto`, so it would not shrink, and the page scrolled sideways at 360px -- 407px of content in a 360px viewport. It wraps now, which stacks the panes, rather than a media query: the kit sizes itself from its container and not from the window. Nine layout tests: every overlay corner, an overlay taller than the viewport, both themes resolving every token they use, light actually being lighter than dark, and no horizontal scrollbar at five widths. `npm run test:all` runs both suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
563a3fdd80 |
composer: edge panel, list row controls, and three fixes
wheel.half pins the centre to a screen edge and fans the items inward, for a
control that lives at the edge and has nowhere to open a full wheel. The arc
geometry is now shared with the full wheel, so the two cannot disagree about
which item a direction means. The hub is a glyph sized to the dead zone: the
cancel target you see is the one you hit.
list.rows takes a real control per row. A bare number printed beside the reorder
arrows reads as a stepper for that number, so moving a LoRA looked like editing
its weight.
setBusy re-derived disabled from the constructor's value, so finishing a busy
spell re-enabled a button that had been disabled since; busy and disabled are
tracked separately now.
An emptied number field committed as 0 and snapped to min, discarding the last
good value -- a browser normalises invalid text to empty, and Number('') is
finite so it sailed past the isFinite guard.
A context menu anchors to a POINT, and that stand-in reached the dismiss stack,
which threw on the first click anywhere.
|
||
|
|
094e056760 |
composer: the elements
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. |