Files
olivv-cs-ComfyUI-FunPack/app/composer/elements/layout.css
T
digital-garbage 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.
2026-08-30 21:17:39 +03:00

357 lines
18 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
.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: 0 0 var(--control-col); min-inline-size: 0;
display: flex; align-items: center; justify-content: flex-end;
}
/* A control fills its column, or is at most as wide as it. Left to itself a
<select> takes the width of its widest option, which is how one row in a
list of five ended up 124px wider than the rest. */
.cx-settings-control > * { max-inline-size: 100%; }
.cx-settings-control > .cx-select,
.cx-settings-control > .cx-input,
.cx-settings-control > .cx-textarea,
.cx-settings-control > .cx-slider { inline-size: 100%; }
.cx-settings-row.cx-danger .cx-settings-label { color: var(--danger); }
/* Below the width where a label and a control fit side by side, they stop
sitting side by side. The control column will not give up its width, so in a
docked panel the label was squeezed to one word per line and its hint to a
four-character column -- readable as damage, not as a layout. The container
is the ROWS, not the viewport: the same group is used in a 190px panel and in
a 600px window, and only its own width decides.
AFTER the rules it overrides, and it has to stay there: a container query
adds no specificity, so written above them it lost -- and `flex: 0 0 180px`
in a COLUMN is 180px of HEIGHT, which turned four settings into a panel two
screens long. */
@container (max-width: 300px) {
.cx-settings-row { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
.cx-settings-control { flex: 0 0 auto; justify-content: flex-start; }
}
.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);
container-type: inline-size;
}
.cx-panel {
border: var(--bw) solid var(--line); border-radius: var(--r-4);
background: var(--elev-1); overflow: hidden;
box-shadow: var(--shadow-1);
/* 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;
}
/* Every panel head is the SAME height, whatever is in it. Padding-block plus a
min-height meant a head holding a control was 41px and a head holding only a
title was 34, so two panels side by side had their titles on different lines
-- the whole row read as if it had been assembled by hand. The height is the
band's; a control inside is centred in it. */
/* A zone is the app's own surface, not a card on it: no corners to round, no
shadow to cast, and no border of its own -- the hairline between it and the
next region belongs to the region, so two neighbours never draw two lines. */
.cx-zone { border: 0; border-radius: 0; box-shadow: none; }
.cx-panel-head {
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-5);
padding: 0 var(--sp-6); border-bottom: var(--bw) solid var(--line);
background: var(--grad-band);
block-size: var(--menubar-h); flex: none;
}
.cx-panel-title {
font-family: var(--font-display); font-size: var(--fs-3); font-weight: var(--fw-semibold);
display: flex; align-items: center; gap: var(--sp-3);
}
/* One accent mark per panel. Carried over from v4, where it is what makes a
head read as the top of something rather than as a strip of grey. */
.cx-panel-title::before {
content: ""; flex: none; inline-size: 5px; block-size: 5px;
border-radius: var(--r-pill); background: var(--accent);
}
.cx-panel-actions { display: flex; gap: var(--sp-2); }
/* A column with a gap, not a block: two things put in a body by two different
parts of the app had whatever space their own line boxes happened to leave,
which is how a prompt box ended up 8px under one label and 3px under the
next. The body decides the rhythm; what goes in it brings nothing. */
.cx-panel-body {
padding: var(--sp-6); flex: 1 1 auto; min-block-size: 0; overflow: auto;
display: flex; flex-direction: column; gap: var(--sp-5);
}
/* One child fills the body -- a viewer, a bin. Several are each their own
height and the body scrolls, which is what a panel of controls does. */
.cx-panel-body > .cx-viewer, .cx-panel-body > .cx-stack-fill { flex: 1 1 auto; }
.cx-panel-flush > .cx-panel-body { padding: 0; }
.cx-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.cx-action-bar {
display: flex; align-items: center; gap: var(--sp-6);
padding: var(--sp-5) var(--gutter);
border-top: var(--bw) solid var(--line); background: var(--grad-band);
position: sticky; bottom: 0;
}
/* What the app is SAYING sits at the start of the row and what is PRESSED at
the end. Everything shared one right-aligned group before, so "Ready" sat
against the Generate button with 1300px of empty bar to its left. */
.cx-action-bar-lead { display: flex; align-items: center; gap: var(--sp-5); min-inline-size: 0; }
.cx-action-bar-buttons { display: flex; align-items: center; gap: var(--sp-3); margin-inline-start: 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: var(--gutter); cursor: col-resize; }
.cx-split-v > .cx-split-bar { height: var(--gutter); 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; }
/* The same radius as a panel: these stand side by side in the properties
column, and two different corner radii on two boxes in one column reads as
two different kinds of thing. */
.cx-collapsible { border: var(--bw) solid var(--line); border-radius: var(--r-4); background: var(--elev-1); }
.cx-collapsible-head {
display: flex; align-items: center; gap: var(--sp-3);
padding: 0 var(--sp-6); block-size: var(--menubar-h); cursor: pointer; list-style: none;
font-size: var(--fs-5); background: var(--grad-band);
border-radius: calc(var(--r-4) - var(--bw));
}
.cx-collapsible[open] > .cx-collapsible-head {
border-end-start-radius: 0; border-end-end-radius: 0;
border-block-end: var(--bw) solid var(--line);
}
.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: var(--sp-6); padding-block-start: 0; }
.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;
background: var(--elev-1);
}
.cx-workspace-main > *, .cx-workspace-side > * { flex: 1 1 auto; min-block-size: 0; }
.cx-workspace-side {
/* A width the CONTENT can live in, not a share of the window. Sizing these
as a fraction of the viewport gave a 900px-wide window a 198px column, in
which "Loosen the starting image" wrapped over three lines and a section
heading broke mid-word. A panel is as wide as a settings row needs; when
the window cannot afford two of them it stops docking them (below). */
flex: 0 0 auto;
inline-size: clamp(268px, 24vw, 360px);
min-block-size: 0;
overflow: auto;
display: flex; flex-direction: column;
background: var(--elev-1);
/* The divider between one region and the next. One hairline, the same one a
splitter draws, so the app reads as a single surface that has been cut up
rather than as three things put next to each other. */
border-inline: var(--bw) 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;
overflow: hidden;
border-inline-width: 0;
}
/* The toggle sits in the head band's height, so it lines up with the panel
titles either side of it instead of floating a few pixels above them like a
mark left on the glass. */
/* The rails are the frame's own edge, so they carry the page colour rather than
a zone's -- the one strip of background the app shows. */
.cx-workspace-rail {
flex: 0 0 auto;
display: flex;
align-items: flex-start;
justify-content: center;
background: var(--elev-0);
padding-inline: var(--sp-1);
}
.cx-workspace-rail > * { block-size: var(--menubar-h); }
/* 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; }
/* Film grain over the app, and only over the app: no z-index, so anything that
opens above the frame -- a modal, a popover -- paints over it and stays
crisp. Fixed, so it does not scroll and cannot be captured by a scroller's
own stacking context. */
.cx-frame::after {
content: ""; position: fixed; inset: 0; pointer-events: none;
opacity: var(--grain-opacity); mix-blend-mode: var(--grain-blend);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.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-head, .cx-frame-foot { flex: 0 0 auto; }
/* Bands: edge to edge, the same height as a panel head, with the gutter inside
them. So the top bar reads as part of the frame rather than as a stray
button floating above the first panel. */
.cx-frame-head {
display: flex; align-items: center;
block-size: var(--menubar-h); padding-inline: var(--gutter);
border-block-end: var(--bw) solid var(--line); background: var(--grad-band);
}
.cx-frame-head > * { flex: 1 1 auto; min-inline-size: 0; }
/* Not enough room to dock. Below this the two panels plus a usable centre do
not fit -- three columns in 375px gave clipped headings and a preview no
wider than a thumbnail -- so a panel stops taking space from the centre and
sits over it instead. The rails do not move, so the way in and out is the
same control in the same place at every size.
The element closes both panels when it enters this mode and opens only one
at a time within it, because two overlays over a narrow centre is the same
problem with extra steps. What was open before is restored on the way out,
and none of that touches the remembered preference -- a window being small
is not the user changing their mind.
This is also what makes a phone possible later rather than a rewrite: the
difference between docked and overlaid is where the panel is drawn, not what
it is or how it is opened. */
@media (max-width: 760px) {
.cx-workspace { position: relative; }
.cx-workspace-side {
position: absolute;
inset-block: 0;
z-index: 1;
inline-size: min(86vw, 360px);
background: var(--elev-1);
box-shadow: var(--shadow-3);
}
.cx-workspace-left { inset-inline-start: 0; }
.cx-workspace-right { inset-inline-end: 0; }
/* Above the overlay, or the panel covers the only control that closes it --
which is the whole fault this arrangement was built to avoid, arriving by a
different route the moment the panel stopped taking its own space. */
.cx-workspace-rail { position: relative; z-index: 2; }
/* A collapsed overlay must not keep catching pointers over the centre. */
.cx-workspace-side.cx-collapsed { inline-size: 0; box-shadow: none; }
}
/* region.stack -- the app's redrawable host. Spacing only: what goes in it
brings its own everything else. */
.cx-stack { display: flex; flex-direction: column; min-block-size: 0; }
.cx-stack > * { min-inline-size: 0; }
.cx-stack-none { gap: 0; }
.cx-stack-sm { gap: var(--sp-4); }
.cx-stack-md { gap: var(--sp-6); }
.cx-stack-lg { gap: var(--sp-9); }
/* Filling the room it was given, rather than sizing to its content. Without a
definite height nothing inside can scroll: the whole page scrolls instead,
and a bar meant to hold the bottom scrolls away with it. */
.cx-stack-fill { flex: 1 1 auto; min-block-size: 0; }
/* A split inside a stack is what stretches. The rows around it -- a toolbar, a
banner, an action bar -- are each their own height. */
.cx-stack > .cx-split { flex: 1 1 auto; min-block-size: 0; }