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>
255 lines
12 KiB
CSS
255 lines
12 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 {
|
||
/* 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;
|
||
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-head, .cx-frame-foot { flex: 0 0 auto; }
|
||
|
||
/* 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; }
|