dashboard: connect tree-prefix vertical bars across taller rows (#388)

The container-row tree-prefix used text box-drawing glyphs (├ └ │)
positioned with `top: 0.6em` — a single text-line tall. Once rows
grew past one line (5em square icon + multi-line body), the `│`
columns of consecutive siblings no longer touched, leaving visible
breaks in the tree.

Replace the text-glyph string with structured DOM: one `.tree-lane`
per depth column. Continuation lanes (`.lane-line`) paint a 1px
border-left spanning the full row height + the `.containers` gap
below, so adjacent siblings' bars visually merge into one unbroken
vertical. The row's own joint lane is `├` (branch — bar continues
below) or `└` (last — bar stops at icon midline), with a horizontal
stub at 3.1em (row padding-top + icon half-height) reaching to the
icon edge.

Joint y / stub width are derived from the 5em icon + 0.6em row
padding-top + 0.8em row padding-left so they meet the icon cleanly.
This commit is contained in:
iris 2026-05-25 00:32:15 +02:00 committed by Mara
commit 7743c07380
2 changed files with 89 additions and 20 deletions

View file

@ -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 <img> absolutely positioned inside a wrapper div: