dashboard.css + docs: migrate topology indent + icon/pending prose (#712 batch 12)

dashboard.css carried many CSS WHY-blocks duplicating prose
already in docs/web-ui.md (after PRs #722/#724/#728/#776).
Substantive new migration: topology indent geometry. Remaining
cookies collapse to docs pointers.

Moved to docs/web-ui.md::Topology tree:
- Indent + lane geometry paragraph (~18 lines new prose):
  per-depth 1.8em ladder rationale + hardcoded six levels +
  CSS Values 5 attr() partial-support caveat (Chromium-only as
  of 2026) + lane center continuation bars + bottom: -0.4em
  prefix extension to merge sibling vertical lines + horizontal
  stub at icon midline.

Collapsed in dashboard.css:
- Topology indent block (#363/#361) → 3-line pointer
- Tree prefix lanes block (#388) → 3-line pointer
- Tree prefix depth-step ladder (~6 lines) → 4-line tighter
  paraphrase (lane-meets-icon rationale stays inline as it's
  immediate context for the values below)
- Live cards icon-left split (#177/#344) → 6-line tighter
  paraphrase + docs pointer
- Icon selection-toggle hover/focus (#443) → docs pointer
- Icon img absolute positioning (#177) → docs pointer
- Icon favicon fallback (#195/#202) → docs pointer
- Pending state queued vs running (#769) → docs pointer
- Icon-only nav strip (#262/#333) → 7-line tighter + docs pointer
- Approval requested-at chip (#272) → 2-line pointer
- In-flight step indicator (#437) → 4-line pointer
- M0V3 affordance (#486) → 3-line pointer
- Cancel-X queued (#575) → 5-line pointer

dashboard.css: 51 → 36 cookies (counting hex-color false positives;
~15 actual issue refs scrubbed). Net ~50 lines of CSS WHY-prose
collapsed to brief docs pointers; ~18 lines of new substantive
prose migrated to docs/web-ui.md::Topology tree.
This commit is contained in:
iris 2026-05-31 14:47:04 +02:00 committed by mara
commit 9c72fd369a
2 changed files with 72 additions and 70 deletions

View file

@ -538,6 +538,26 @@ needed). The joint at the row's own depth column is `├` (more
siblings below) or `└` (last sibling at this depth — vertical siblings below) or `└` (last sibling at this depth — vertical
stops at the row's icon midline). stops at the row's icon midline).
**Indent + lane geometry.** Each depth level shifts the row right
by `1.8em` (the lane width). The per-depth ladders are hardcoded
for six levels — enough for any plausible hive topology, and the
typed `attr()` function from CSS Values 5 that would collapse
this to one rule is still partial-support (Chromium-only as of
2026). The `.tree-prefix` span sits absolutely positioned with
`left: -<depth>*1.8em` so its right edge meets the row content
(the icon) and its leftmost lane lines up with top-level rows'
icons at `x = 0`. Each `.tree-lane` is `flex: 0 0 1.8em` so all
lanes have equal width. Continuation bars are drawn at lane
center (`left: 0.6em`, `border-left: 1px solid currentColor`,
`top: 0; bottom: 0`) and extend through `.containers { gap: 0.4em }`
into the next sibling's prefix (`bottom: -0.4em` on the prefix
span itself) so adjacent ancestor lines visually merge into one
unbroken vertical line. The horizontal stub at a row's own joint
lands at the icon midline so the L/T meets the icon edge cleanly.
When every container has `parent = null` (pre-topology state) the
`[data-depth]` attribute is absent on every row and these rules
are no-ops — the layout reads exactly like the legacy flat list.
### Selection bar ### Selection bar
Per-card action buttons (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` / Per-card action buttons (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` /

View file

@ -189,16 +189,9 @@ a:hover {
background: rgba(24, 24, 37, 0.55); background: rgba(24, 24, 37, 0.55);
transition: opacity 200ms ease, border-color 200ms ease; transition: opacity 200ms ease, border-color 200ms ease;
} }
/* Topology indent (#363). Each depth level shifts the row right by one /* Topology indent ladder. See docs/web-ui.md::Topology tree (Indent
step; the .tree-prefix span (drawn by tabs.js::treePrefix) carries + lane geometry paragraph) for the 1.8em-per-depth-level
the / glyph and any continuation lines that thread through rationale + CSS-attr()-not-yet-portable caveat. */
ancestor columns. When every container has parent=null (pre-#361
state) `[data-depth]` is absent on every row and these rules are
no-ops the layout reads exactly like the legacy flat list.
Per-depth indent: hardcoded steps for 6 levels (sufficient for any
plausible hive topology) the typed `attr()` function from CSS
Values 5 would collapse this to one rule, but browser support is
still partial (Chromium-only as of 2026). */
.container-row[data-depth] { position: relative; } .container-row[data-depth] { position: relative; }
.container-row[data-depth="1"] { margin-left: 1.8em; } .container-row[data-depth="1"] { margin-left: 1.8em; }
.container-row[data-depth="2"] { margin-left: 3.6em; } .container-row[data-depth="2"] { margin-left: 3.6em; }
@ -206,16 +199,9 @@ a:hover {
.container-row[data-depth="4"] { margin-left: 7.2em; } .container-row[data-depth="4"] { margin-left: 7.2em; }
.container-row[data-depth="5"] { margin-left: 9em; } .container-row[data-depth="5"] { margin-left: 9em; }
.container-row[data-depth="6"] { margin-left: 10.8em; } .container-row[data-depth="6"] { margin-left: 10.8em; }
/* Tree prefix sits in the left margin and paints the connecting /* Tree prefix lanes DOM-painted, not text-glyph-painted. See
/ / lanes as CSS rules rather than text glyphs (#388). Each docs/web-ui.md::Topology tree for the full-row-height vertical
ancestor depth gets its own `.tree-lane` so we can paint a bar rationale + icon-midline joint alignment. */
full-row-height vertical bar that extends through the
.containers row gap into the next sibling text box-drawing
glyphs only fill one text line, which left visible breaks
between rows once cards grew taller than one line of text (5em
square icons + multi-line body). The horizontal stub at the row's
own joint lands at the icon midline so the L/T meets the icon
edge cleanly. */
.container-row .tree-prefix { .container-row .tree-prefix {
position: absolute; position: absolute;
/* Extend into the `.containers { gap: 0.4em }` below so vertical /* Extend into the `.containers { gap: 0.4em }` below so vertical
@ -228,12 +214,10 @@ a:hover {
user-select: none; user-select: none;
color: var(--purple-dim); color: var(--purple-dim);
} }
/* Each depth step is one 1.8em lane wide same step as the row's /* Prefix-left-edge ladder. Each depth step is 1.8em (matches the
own margin-left ladder above, so the rightmost lane (the joint) row indent ladder above) so the prefix's right edge meets the
sits flush against the row content (the icon). The prefix's left icon and its leftmost lane lines up with top-level rows' icons
edge is depth*1.8em LEFT of the row's left edge, so its right at x = 0. */
edge meets the icon, and the leftmost lane lines up with the
top-level rows' icons at x=0. */
.container-row[data-depth="1"] .tree-prefix { left: -1.8em; } .container-row[data-depth="1"] .tree-prefix { left: -1.8em; }
.container-row[data-depth="2"] .tree-prefix { left: -3.6em; } .container-row[data-depth="2"] .tree-prefix { left: -3.6em; }
.container-row[data-depth="3"] .tree-prefix { left: -5.4em; } .container-row[data-depth="3"] .tree-prefix { left: -5.4em; }
@ -277,14 +261,12 @@ a:hover {
width: 2em; width: 2em;
border-top: 1px solid currentColor; border-top: 1px solid currentColor;
} }
/* Live cards get the icon-left / body-right split; tombstone rows keep /* Live cards get the icon-left / body-right split; tombstone rows
the plain stacked block layout. The icon is a background-image div keep the plain stacked block layout. The icon's fixed 5em width
with no intrinsic size, so its load state can never reflow the row + aspect-ratio-derived height avoid the align-self: stretch
(issue #177). It used to `align-self: stretch` to fill the body feedback loop that used to make different-content cards show
height, but with state badges / rate-limit pills / etc. wrapping the different-sized icons. See docs/web-ui.md::Container row (Icon
head row, the body grew taller and the square icon grew with it layout + load strategy) for the load-state reflow rationale. */
so two cards with different content showed different-sized icons
(issue #344). Fixed at 5em now; height follows from aspect-ratio. */
.container-row:not(.tombstone) { .container-row:not(.tombstone) {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -298,9 +280,10 @@ a:hover {
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 6px; border-radius: 6px;
background-color: rgba(17, 17, 27, 0.6); background-color: rgba(17, 17, 27, 0.6);
/* #443 icon is the selection toggle. Cursor + hover ring make /* Icon doubles as the selection toggle cursor + hover ring
that affordable without a chrome change. The :focus-visible ring make that affordable without a chrome change. The
covers keyboard activation (Enter / Space). */ :focus-visible ring covers keyboard activation (Enter / Space).
See docs/web-ui.md::Selection bar for the toggle semantics. */
cursor: pointer; cursor: pointer;
transition: box-shadow 120ms ease, transform 120ms ease; transition: box-shadow 120ms ease, transform 120ms ease;
} }
@ -321,9 +304,9 @@ a:hover {
.container-row.selected > .container-icon { .container-row.selected > .container-icon {
box-shadow: 0 0 0 2px var(--purple), 0 0 12px -4px var(--purple); box-shadow: 0 0 0 2px var(--purple), 0 0 12px -4px var(--purple);
} }
/* The icon image fills the square wrapper and is taken out of flow /* Icon image absolutely positioned, fills the square wrapper.
(absolute) so its load state pending, loaded, broken can never See docs/web-ui.md::Container row (Icon layout + load strategy)
contribute intrinsic size or reflow the row. (issue #177) */ for the load-state-can't-reflow-row rationale. */
.container-row:not(.tombstone) > .container-icon > .container-icon-img { .container-row:not(.tombstone) > .container-icon > .container-icon-img {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -331,8 +314,9 @@ a:hover {
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
/* When the <img> fails to load it falls back to the dimmed hyperhive /* When the <img> fails to load it falls back to the dimmed
mark, standing in for the unreachable agent icon (issues #195, #202). */ hyperhive mark see docs/web-ui.md::Container row for the
fire-and-forget load + favicon fallback chain. */
.container-row:not(.tombstone) > .container-icon.icon-unreachable { .container-row:not(.tombstone) > .container-icon.icon-unreachable {
filter: grayscale(1); filter: grayscale(1);
opacity: 0.4; opacity: 0.4;
@ -341,11 +325,11 @@ a:hover {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
/* Pending state splits queued vs running (#769): queued ops show /* Pending state splits queued vs running queued ops show only
only the pending-state badge the row sits unhighlighted so a the pending-state badge (no row tint), running ops keep the
long queue doesn't paint half the SW4RM tab amber. Running ops amber row tint AND get a rotating amber ring on the icon. See
keep the amber row tint AND get a rotating amber ring on the docs/web-ui.md::Container row (Pending-state derivation) for
agent icon so it's obvious which container is actually moving. */ the priority order. */
.container-row.pending .actions { opacity: 0.4; pointer-events: none; } .container-row.pending .actions { opacity: 0.4; pointer-events: none; }
.container-row.pending-running { .container-row.pending-running {
border-color: var(--amber); border-color: var(--amber);
@ -385,11 +369,13 @@ a:hover {
font-weight: bold; font-weight: bold;
} }
.container-row .head .meta { margin-left: auto; } .container-row .head .meta { margin-left: auto; }
/* Icon-only nav strip in the head row the per-container backend- /* Icon-only nav strip in the head row backend-supplied per-
supplied link list (issue #262). Inline-flex + gap so a longer list container link list. Inline-flex + gap so a longer list (e.g.
(e.g. with `dashboardLinks` extras) doesn't cram (issue #333). Each with `dashboardLinks` extras) doesn't cram. Each link gets a
link gets a comfortable hit target with a subtle hover so the comfortable hit target with a subtle hover so the icons read
icons read as interactive rather than decorative. */ as interactive rather than decorative. See
docs/web-ui.md::Container row Line 1 for the link-list source
of truth. */
.container-row .head .nav-strip { .container-row .head .nav-strip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -605,9 +591,8 @@ code {
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.3em; gap: 0.3em;
} }
/* When the approval was requested right-aligned in the head row; /* Approval requested-at chip right-aligned, goes amber after
goes amber once it has been pending 1h so a stale request stands 1h. See docs/web-ui.md::Approval card. */
out at a glance (issue #272). */
.approval-ts { .approval-ts {
margin-left: auto; margin-left: auto;
color: var(--muted); color: var(--muted);
@ -836,11 +821,10 @@ code {
.rqe-source-approval { color: var(--green); border-color: var(--green); } .rqe-source-approval { color: var(--green); border-color: var(--green); }
.rqe-when { color: var(--muted); font-size: 0.85em; } .rqe-when { color: var(--muted); font-size: 0.85em; }
.rqe-reason { color: var(--muted); font-size: 0.85em; flex: 1 1 auto; } .rqe-reason { color: var(--muted); font-size: 0.85em; flex: 1 1 auto; }
/* #437: in-flight step indicator on running queue entries sub-line /* In-flight step indicator on running queue entries cyan sub-
below the main row, indented under the state glyph + kind. Cyan line below the main row, wraps to its own line via flex-basis:
keeps it visually grouped with the running spinner instead of 100%. See docs/web-ui.md::R3BU1LD QU3U3 for the step-annotation
blending into the muted reason/timing chips. flex-basis: 100% so semantics. */
it always wraps to its own line. */
.rqe-step { .rqe-step {
flex-basis: 100%; flex-basis: 100%;
margin: 0.1em 0 0 1.8em; margin: 0.1em 0 0 1.8em;
@ -858,12 +842,11 @@ code {
white-space: pre-wrap; white-space: pre-wrap;
} }
/* #575: cancel-X for queued rebuild_queue entries. Quiet by default /* Cancel-X for queued rebuild_queue entries sits at far right
(sits at the far right via margin-left: auto), lights red on hover. via margin-left: auto, lights red on hover. Mirrors the side-
Renders only when `state === 'queued'` per renderQueueEntry's panel-close glyph shape () so it stays unobtrusive in a list
guard Running / terminal rows don't get the affordance. Mirrors row. See docs/web-ui.md::R3BU1LD QU3U3 for the queued-only
the side-panel-close glyph shape () without the full B6N-style gating. */
`btn-deny` width so it stays unobtrusive in a list row. */
.rqe-cancel { .rqe-cancel {
margin-left: auto; margin-left: auto;
} }
@ -942,10 +925,9 @@ ul form.inline { display: inline-block; }
.btn-restart { color: var(--cyan); border-color: var(--cyan); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .btn-restart { color: var(--cyan); border-color: var(--cyan); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
.btn-stop { color: var(--pink); border-color: var(--pink); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .btn-stop { color: var(--pink); border-color: var(--pink); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
.btn-start { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .btn-start { color: var(--green); border-color: var(--green); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
/* #486 M0V3 affordance (selection bar). Mauve picks up the same /* M0V3 affordance (selection bar) mauve reads as "structural
accent the question-override / mid-status surfaces use; reads as change" rather than the destructive red / amber chrome of
"structural change" rather than the destructive red / amber chrome destroy / rebuild. See docs/web-ui.md::Selection bar. */
of destroy / rebuild. */
.btn-move { color: var(--mauve); border-color: var(--mauve); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } .btn-move { color: var(--mauve); border-color: var(--mauve); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
.btn-talk { color: var(--cyan); border-color: var(--cyan); } .btn-talk { color: var(--cyan); border-color: var(--cyan); }
.btn-spawn { color: var(--amber); border-color: var(--amber); } .btn-spawn { color: var(--amber); border-color: var(--amber); }