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.
325 lines
16 KiB
CSS
325 lines
16 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: 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;
|
||
/* 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. */
|
||
.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(--elev-2);
|
||
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); }
|
||
.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-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(--elev-2);
|
||
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);
|
||
}
|
||
.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%;
|
||
/* The gutter, in the one place that can apply it evenly. Regions carrying
|
||
their own padding gave a 7px gap on one side of the centre and 19 on the
|
||
other, and the three columns started at three different heights. */
|
||
gap: var(--gutter); padding: var(--gutter);
|
||
}
|
||
/* 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;
|
||
}
|
||
/* 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;
|
||
}
|
||
/* A closed panel is still a flex item, so the gutter is still applied on both
|
||
sides of nothing -- a double-width gap where a panel used to be, which is
|
||
exactly the kind of hole that reads as a bug. One of the two is taken back. */
|
||
.cx-workspace-left.cx-collapsed { margin-inline-end: calc(var(--gutter) * -1); }
|
||
.cx-workspace-right.cx-collapsed { margin-inline-start: calc(var(--gutter) * -1); }
|
||
/* 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. */
|
||
.cx-workspace-rail {
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
}
|
||
.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; }
|
||
.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(--elev-2);
|
||
}
|
||
.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; }
|