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.
192 lines
8.9 KiB
CSS
192 lines
8.9 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;
|
||
}
|
||
.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); }
|
||
|
||
.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; }
|
||
.cx-split-pane { flex: 1 1 0; min-width: 0; min-height: 0; overflow: auto; }
|
||
.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; }
|