diff --git a/frontend/packages/dashboard/src/app.js b/frontend/packages/dashboard/src/app.js index 49394ce..d2d8d93 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/frontend/packages/dashboard/src/app.js @@ -649,16 +649,29 @@ window.marked = marked; } return out; } - function treePrefix({ depth, ancestorIsLast, isLast }) { - if (depth === 0) return ''; - let s = ''; - // Ancestor at depth 0 (root) doesn't get a vertical-line column — - // roots are separated visually as top-level rows already. + // Builds the .tree-prefix DOM for a row at the given depth. Each + // lane is its own positioned child so CSS can paint full-height + // vertical bars that bridge the gap between sibling rows — text + // box-drawing glyphs only paint one text-line tall, which left + // visible breaks between rows once we grew taller-than-one-line + // container cards (#388). Ancestor lanes are either continuation + // (vertical bar top→bottom+gap) or blank; the joint at this row's + // own depth is ├ (branch — vertical continues below) or └ (last — + // vertical stops at the row's icon midline). CSS at + // `.container-row .tree-prefix` paints the bars + horizontal stub. + function treePrefixDom({ depth, ancestorIsLast, isLast }) { + if (depth === 0) return null; + const prefix = el('span', { class: 'tree-prefix', 'aria-hidden': 'true' }); + // Ancestor columns (depth 1..depth-1). Skip depth 0 (root has no + // continuation column — top-level rows are separated visually as + // top-level rows already). for (let d = 1; d < depth; d++) { - s += ancestorIsLast[d] ? ' ' : '│ '; + const cls = ancestorIsLast[d] ? 'tree-lane lane-blank' : 'tree-lane lane-line'; + prefix.append(el('span', { class: cls })); } - s += isLast ? '└─ ' : '├─ '; - return s; + const jointCls = 'tree-lane lane-joint ' + (isLast ? 'lane-joint-last' : 'lane-joint-branch'); + prefix.append(el('span', { class: jointCls })); + return prefix; } function renderContainers(s) { @@ -731,10 +744,8 @@ window.marked = marked; // legacy flat layout (every container at depth 0) is bit- // identical to today's render — no glyph, no indent. if (node.depth > 0) li.dataset.depth = String(node.depth); - const prefix = treePrefix(node); - if (prefix) { - li.prepend(el('span', { class: 'tree-prefix', 'aria-hidden': 'true' }, prefix)); - } + const prefix = treePrefixDom(node); + if (prefix) li.prepend(prefix); // Full-height square agent icon, left of the card body. The // icon is an absolutely positioned inside a wrapper div: diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 2747613..fa44bbb 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -204,18 +204,76 @@ a:hover { .container-row[data-depth="4"] { margin-left: 7.2em; } .container-row[data-depth="5"] { margin-left: 9em; } .container-row[data-depth="6"] { margin-left: 10.8em; } +/* Tree prefix sits in the left margin and paints the connecting + ├ / └ / │ lanes as CSS rules rather than text glyphs (#388). Each + ancestor depth gets its own `.tree-lane` so we can paint a + 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 { - /* Tree-line glyphs sit in the left margin so the rest of the - container card layout (icon + body flex split) stays unchanged. - Mono font for column alignment with the ├─ / └─ / │ bricks. */ position: absolute; - left: -1.6em; - top: 0.6em; - color: var(--purple-dim); - font-family: inherit; - white-space: pre; + /* Extend into the `.containers { gap: 0.4em }` below so vertical + bars connect to the next sibling's prefix without a visual gap. */ + top: 0; + bottom: -0.4em; + display: flex; + flex-direction: row; pointer-events: none; user-select: none; + color: var(--purple-dim); +} +/* Each depth step is one 1.8em lane wide — same step as the row's + own margin-left ladder above, so the rightmost lane (the joint) + sits flush against the row content (the icon). The prefix's left + edge is depth*1.8em LEFT of the row's left edge, so its right + 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="2"] .tree-prefix { left: -3.6em; } +.container-row[data-depth="3"] .tree-prefix { left: -5.4em; } +.container-row[data-depth="4"] .tree-prefix { left: -7.2em; } +.container-row[data-depth="5"] .tree-prefix { left: -9em; } +.container-row[data-depth="6"] .tree-prefix { left: -10.8em; } +.tree-prefix .tree-lane { + flex: 0 0 1.8em; + position: relative; +} +/* Continuation bar — full row + gap below (so two adjacent + ancestor-line lanes from sibling rows visually merge into one + unbroken vertical line). Drawn at lane center (0.6em from left) + to keep it visually centered in the 1.8em column. */ +.tree-prefix .lane-line::before, +.tree-prefix .lane-joint::before { + content: ''; + position: absolute; + left: 0.6em; + top: 0; + bottom: 0; + border-left: 1px solid currentColor; +} +/* Last-child joint (└): vertical bar stops at the icon midline (no + continuation below this row). 3.1em ≈ row padding-top 0.6em + + icon half-height 2.5em, matching the horizontal stub's y below. */ +.tree-prefix .lane-joint-last::before { + bottom: auto; + height: 3.1em; +} +/* Horizontal stub from the joint's vertical bar across the lane + and through the row's padding-left to the icon left edge. Lands + at the icon midline so the ├/└ meets the icon cleanly. + Width = lane-right (1.2em from lane center) + row padding-left + (0.8em) = 2em. */ +.tree-prefix .lane-joint::after { + content: ''; + position: absolute; + left: 0.6em; + top: 3.1em; + width: 2em; + border-top: 1px solid currentColor; } /* Live cards get the icon-left / body-right split; tombstone rows keep the plain stacked block layout. The icon is a background-image div