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.
204 lines
9.6 KiB
CSS
204 lines
9.6 KiB
CSS
.cx-field { display: flex; flex-direction: column; gap: var(--sp-2); }
|
||
.cx-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); }
|
||
.cx-field-error { font-size: var(--fs-6); color: var(--danger); }
|
||
|
||
.cx-settings-row {
|
||
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-7);
|
||
padding: var(--sp-4) 0; border-bottom: var(--bw) solid var(--line-2);
|
||
}
|
||
.cx-settings-row:last-child { border-bottom: 0; }
|
||
.cx-settings-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||
.cx-settings-label { font-size: var(--fs-4); line-height: var(--lh-4); color: var(--text); }
|
||
.cx-settings-control { flex: none; min-width: 140px; display: flex; justify-content: flex-end; }
|
||
.cx-settings-row.cx-danger .cx-settings-label { color: var(--danger); }
|
||
|
||
.cx-group { display: flex; flex-direction: column; gap: var(--sp-3); }
|
||
.cx-group-rows {
|
||
border: var(--bw) solid var(--line); border-radius: var(--r-4);
|
||
background: var(--elev-1); padding: 0 var(--sp-6);
|
||
}
|
||
|
||
.cx-panel {
|
||
border: var(--bw) solid var(--line); border-radius: var(--r-4);
|
||
background: var(--elev-1); overflow: hidden;
|
||
/* A column, so a panel given a height passes it to its body. Without this a
|
||
viewer asking for 100% of its parent got 100% of `auto` and collapsed to
|
||
one line, inside a panel that was the right size around it. */
|
||
display: flex; flex-direction: column; min-block-size: 0;
|
||
}
|
||
.cx-panel-head {
|
||
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-5);
|
||
padding: var(--sp-4) var(--sp-6); border-bottom: var(--bw) solid var(--line);
|
||
background: var(--elev-2); min-height: var(--menubar-h);
|
||
}
|
||
.cx-panel-title { font-family: var(--font-display); font-size: var(--fs-3); font-weight: var(--fw-semibold); }
|
||
.cx-panel-actions { display: flex; gap: var(--sp-2); }
|
||
.cx-panel-body { padding: var(--sp-6); flex: 1 1 auto; min-block-size: 0; overflow: auto; }
|
||
|
||
.cx-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
|
||
|
||
.cx-action-bar {
|
||
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-6);
|
||
padding: var(--sp-5) var(--sp-6);
|
||
border-top: var(--bw) solid var(--line); background: var(--elev-2);
|
||
position: sticky; bottom: 0;
|
||
}
|
||
.cx-action-bar-buttons { display: flex; gap: var(--sp-3); margin-left: auto; }
|
||
|
||
.cx-list-wrap { display: flex; flex-direction: column; gap: var(--sp-3); align-items: flex-start; }
|
||
.cx-list {
|
||
width: 100%; border: var(--bw) solid var(--line); border-radius: var(--r-3);
|
||
background: var(--elev-1); overflow: hidden;
|
||
}
|
||
.cx-list-row {
|
||
display: flex; align-items: center; gap: var(--sp-4);
|
||
padding: var(--sp-3) var(--sp-5); border-bottom: var(--bw) solid var(--line-2);
|
||
}
|
||
.cx-list-row:last-child { border-bottom: 0; }
|
||
.cx-list-label { font-size: var(--fs-4); flex: 1; min-width: 0; }
|
||
.cx-list-hint { font-size: var(--fs-7); color: var(--muted); }
|
||
.cx-list-control { flex: none; width: 92px; }
|
||
/* Separated from the row's own control by a rule, so the arrows read as
|
||
* "reorder this row" rather than "adjust that number". */
|
||
.cx-list-actions {
|
||
display: flex; gap: var(--sp-1); flex: none;
|
||
padding-left: var(--sp-4); margin-left: var(--sp-2);
|
||
border-left: var(--bw) solid var(--line-2);
|
||
}
|
||
.cx-list-move { color: var(--faint); }
|
||
.cx-list-move:hover:not(:disabled) { color: var(--accent); }
|
||
.cx-list-empty { padding: var(--sp-6); font-size: var(--fs-6); color: var(--faint); text-align: center; }
|
||
|
||
.cx-tabs { display: flex; gap: var(--sp-1); }
|
||
.cx-tab {
|
||
border: 0; background: transparent; color: var(--muted);
|
||
cursor: pointer; font-family: var(--font-ui); font-size: var(--fs-5);
|
||
transition: color var(--dur-1) var(--ease-standard);
|
||
}
|
||
.cx-tabs-underline { border-bottom: var(--bw) solid var(--line); gap: var(--sp-5); }
|
||
.cx-tabs-underline .cx-tab {
|
||
padding: var(--sp-3) 0; border-bottom: 2px solid transparent; margin-bottom: -1px;
|
||
}
|
||
.cx-tabs-underline .cx-tab.cx-on { color: var(--text); border-bottom-color: var(--accent); }
|
||
.cx-tabs-dock .cx-tab {
|
||
height: var(--ctl-h-md); padding: 0 var(--ctl-pad-x-md);
|
||
border: var(--bw) solid var(--line); border-radius: var(--r-3);
|
||
font-size: var(--fs-6);
|
||
}
|
||
.cx-tabs-dock .cx-tab.cx-on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
|
||
.cx-tab:hover { color: var(--text); }
|
||
|
||
.cx-split { display: flex; width: 100%; min-height: 120px; }
|
||
.cx-split-v { flex-direction: column; }
|
||
/* A column, so what is inside a pane inherits the pane's height instead of
|
||
sizing to its own content. A panel in a pane was the right size with a body
|
||
one line tall inside it, which looks like the panel is fine and the content
|
||
is empty. */
|
||
.cx-split-pane {
|
||
flex: 1 1 0; min-inline-size: 0; min-block-size: 0; overflow: auto;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.cx-split-pane > * { flex: 1 1 auto; min-block-size: 0; }
|
||
.cx-split > .cx-split-pane:first-child { flex: 0 0 auto; }
|
||
|
||
/* A horizontal split stacks rather than overflowing when there is not room for
|
||
two columns. The first pane carries an inline size, so on a narrow screen it
|
||
could not shrink and the whole page scrolled sideways -- 407px of content in
|
||
a 360px viewport. Wrapping rather than a media query because the kit sizes
|
||
itself from its container, not from the window. */
|
||
.cx-split-h { flex-wrap: wrap; }
|
||
.cx-split-h > .cx-split-pane { min-inline-size: min(100%, 260px); }
|
||
.cx-split-h > .cx-split-pane:first-child { max-inline-size: 100%; }
|
||
.cx-split-bar { flex: none; background: transparent; position: relative; }
|
||
.cx-split-h > .cx-split-bar { width: 7px; cursor: col-resize; }
|
||
.cx-split-v > .cx-split-bar { height: 7px; cursor: row-resize; }
|
||
.cx-split-bar::after {
|
||
content: ""; position: absolute; inset: 0; margin: auto;
|
||
background: var(--line); transition: background var(--dur-1) var(--ease-standard);
|
||
}
|
||
.cx-split-h > .cx-split-bar::after { width: 1px; }
|
||
.cx-split-v > .cx-split-bar::after { height: 1px; }
|
||
.cx-split-bar:hover::after, .cx-split-bar:focus-visible::after { background: var(--accent); }
|
||
.cx-split-bar:focus-visible { outline: none; }
|
||
|
||
.cx-collapsible { border: var(--bw) solid var(--line); border-radius: var(--r-3); background: var(--elev-1); }
|
||
.cx-collapsible-head {
|
||
display: flex; align-items: baseline; gap: var(--sp-3);
|
||
padding: var(--sp-3) var(--sp-5); cursor: pointer; list-style: none;
|
||
font-size: var(--fs-5);
|
||
}
|
||
.cx-collapsible-head::-webkit-details-marker { display: none; }
|
||
.cx-collapsible-head::before {
|
||
content: "›"; color: var(--faint); display: inline-block;
|
||
transition: transform var(--dur-1) var(--ease-standard);
|
||
}
|
||
.cx-collapsible[open] > .cx-collapsible-head::before { transform: rotate(90deg); }
|
||
.cx-collapsible-body { padding: 0 var(--sp-5) var(--sp-5); }
|
||
|
||
.cx-rail { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-2); }
|
||
.cx-rail-item {
|
||
display: flex; align-items: center; gap: var(--sp-4);
|
||
height: var(--ctl-h-xl); padding: 0 var(--sp-4);
|
||
border: 0; border-radius: var(--r-3); background: transparent;
|
||
color: var(--muted); font-size: var(--fs-5); cursor: pointer; text-align: left;
|
||
}
|
||
.cx-rail-item:hover { background: var(--hover); color: var(--text); }
|
||
.cx-rail-item.cx-on { background: var(--accent-soft); color: var(--accent-hi); }
|
||
.cx-rail-icon { width: 18px; text-align: center; }
|
||
|
||
/* Workspace: a centre that shrinks, panels docked either side.
|
||
The rails hold the toggles and never collapse, so the control for a panel is
|
||
never inside the panel it controls. */
|
||
.cx-workspace { display: flex; align-items: stretch; min-block-size: 0; block-size: 100%; inline-size: 100%; }
|
||
/* Flex containers, not blocks with overflow. A region whose height comes from
|
||
its content gives a vertical split inside it a basis of `auto`, so the split's
|
||
second pane resolves to zero and simply is not there -- the prompt under the
|
||
preview vanished exactly that way, with no error and nothing to see. */
|
||
.cx-workspace-main {
|
||
flex: 1 1 auto; min-inline-size: 0; min-block-size: 0;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.cx-workspace-main > *, .cx-workspace-side > * { flex: 1 1 auto; min-block-size: 0; }
|
||
.cx-workspace-side {
|
||
/* Allowed to shrink, and sized from the viewport rather than fixed: two
|
||
fixed 240px panels plus their rails are wider than a 480px window, and the
|
||
page then scrolls sideways -- the one thing a docked layout must never do,
|
||
because the transport bar goes with it. Narrow is cramped and legible; the
|
||
user can collapse either side. */
|
||
flex: 0 1 auto;
|
||
inline-size: clamp(0px, 22vw, 340px);
|
||
min-inline-size: 0;
|
||
min-block-size: 0;
|
||
overflow: auto;
|
||
display: flex; flex-direction: column;
|
||
padding: var(--sp-3);
|
||
border-inline: 1px solid var(--line);
|
||
}
|
||
/* Width nil rather than display:none -- the panel stays in the flow, stays
|
||
measurable, and can come back without being rebuilt.
|
||
|
||
Deliberately not animated. A remembered-closed panel would animate shut on
|
||
every page load, which reads as the app doing something rather than as a
|
||
remembered preference, and an in-flight width is a state anything measuring
|
||
the layout has to wait out. */
|
||
.cx-workspace-side.cx-collapsed {
|
||
inline-size: 0;
|
||
padding-inline: 0;
|
||
border-inline-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
.cx-workspace-rail {
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
padding: var(--sp-2) var(--sp-1);
|
||
}
|
||
|
||
/* The page frame. The one place a definite height is established, so everything
|
||
below can fill instead of each pane guessing at a percentage of nothing. */
|
||
.cx-frame { display: flex; flex-direction: column; block-size: 100%; min-block-size: 0; }
|
||
.cx-frame-main { flex: 1 1 auto; min-block-size: 0; display: flex; }
|
||
.cx-frame-main > * { flex: 1 1 auto; min-inline-size: 0; }
|
||
.cx-frame-foot { flex: 0 0 auto; }
|