feat(team): wrap wide leaf rows into a grid + fit-to-screen button

Compact the tree so upper levels stay close together:
- a parent with >4 leaf children now lays them out in a square-ish grid
  (cols = ceil(sqrt(n))) instead of one wide row; connectors drop from a fixed
  bus Y so the elbows stay clean across grid rows;
- a 'Fit' toolbar button scales the whole chart to the viewport width via CSS
  zoom (reflows, so scroll shrinks) and toggles back to 100%.
Cuts the rebreak org width ~2400px→~1830px, and Fit shows the entire tree at once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-08 05:16:39 +02:00
parent 722c8598e9
commit 0d79e8fbd6

View File

@ -130,8 +130,13 @@ export function renderTeamHtml(cwd: string): string {
const rb = b.agent ? agentCfg[b.agent]?.role : undefined;
return (ROLE_ORDER[ra ?? ''] ?? 5) - (ROLE_ORDER[rb ?? ''] ?? 5) || a.label.localeCompare(b.label);
});
// Wrap a wide row of leaf children into a compact grid so a broad subtree
// doesn't spread the whole tree — keeps upper levels close together.
const allLeaves = kids.every((k) => !(childrenOf.get(k.id)?.length));
const wrap = allLeaves && kids.length > 4;
const cols = wrap ? Math.min(4, Math.ceil(Math.sqrt(kids.length))) : 0;
const childrenHtml = kids.length
? `<div class="children">${kids.map((k) => renderSubtree(k.id)).join('')}</div>`
? `<div class="children${wrap ? ' wrap' : ''}"${wrap ? ` style="--cols:${cols}"` : ''}>${kids.map((k) => renderSubtree(k.id)).join('')}</div>`
: '';
return `<div class="subtree">${renderNode(n)}${childrenHtml}</div>`;
}
@ -152,6 +157,13 @@ export function renderTeamHtml(cwd: string): string {
.edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; overflow: visible; }
.subtree { position: relative; z-index: 1; display: inline-flex; flex-direction: column; align-items: center; }
.children { display: flex; flex-direction: row; align-items: flex-start; justify-content: center; gap: 11px; margin-top: 42px; }
/* Wide leaf rows collapse into a compact grid (connectors drop from a fixed bus). */
.children.wrap { display: grid; grid-template-columns: repeat(var(--cols, 3), 168px); justify-content: center; column-gap: 11px; row-gap: 46px; }
.children.wrap .subtree { width: 168px; }
.team-toolbar { display: flex; justify-content: flex-end; gap: 8px; padding: 0 6px 8px; }
.fit-btn { min-height: 30px; padding: 0 13px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); color: var(--muted); font: 600 12px/1 var(--font-sans); cursor: pointer; transition: color 150ms, border-color 150ms; }
.fit-btn:hover { color: var(--text); border-color: var(--accent); }
.node {
position: relative;
@ -210,6 +222,9 @@ export function renderTeamHtml(cwd: string): string {
</head>
<body>
${pageHeader(config.projectName, 'team')}
<div class="team-toolbar">
<button class="fit-btn" id="fitBtn" type="button" title="Scale the whole chart to fit the screen">Fit</button>
</div>
<div class="scroll">
<main class="chart" id="chart">
<svg class="edges" id="edges" aria-hidden="true">
@ -257,7 +272,7 @@ export function renderTeamHtml(cwd: string): string {
var s = centerBottom(parent, box), e = centerTop(n, box);
// Sharp org-chart elbow: down from the parent to the shared bus, across, down to the child.
// Siblings share the same top Y, so their bus Y matches → one clean horizontal bus.
var busY = Math.round(s.y + (e.y - s.y) * 0.5);
var busY = Math.round(s.y + 20);
var sx = Math.round(s.x), ex = Math.round(e.x);
var fwd = 'M ' + sx + ' ' + Math.round(s.y) + ' L ' + sx + ' ' + busY + ' L ' + ex + ' ' + busY + ' L ' + ex + ' ' + Math.round(e.y);
var rev = 'M ' + ex + ' ' + Math.round(e.y) + ' L ' + ex + ' ' + busY + ' L ' + sx + ' ' + busY + ' L ' + sx + ' ' + Math.round(s.y);
@ -382,7 +397,27 @@ export function renderTeamHtml(cwd: string): string {
var sc = document.querySelector('.scroll');
if (sc) sc.scrollLeft = Math.max(0, (sc.scrollWidth - sc.clientWidth) / 2);
}
var rt; function relayout() { clearTimeout(rt); rt = setTimeout(layoutEdges, 120); }
// Fit-to-screen: CSS zoom reflows (so scrollWidth shrinks), unlike transform.
var fitted = false;
function toggleFit() {
var sc = document.querySelector('.scroll'), chart = document.getElementById('chart'), btn = document.getElementById('fitBtn');
if (!sc || !chart) return;
if (!fitted) {
chart.style.zoom = '1';
var natural = chart.scrollWidth || chart.getBoundingClientRect().width;
var k = Math.min(1, (sc.clientWidth - 10) / natural);
chart.style.zoom = String(k);
fitted = true; if (btn) btn.textContent = '100%';
setTimeout(layoutEdges, 40);
} else {
chart.style.zoom = '1'; fitted = false; if (btn) btn.textContent = 'Fit';
setTimeout(function(){ layoutEdges(); centerScroll(); }, 40);
}
}
var fb = document.getElementById('fitBtn');
if (fb) fb.addEventListener('click', toggleFit);
var rt; function relayout() { clearTimeout(rt); rt = setTimeout(function(){ if (fitted) { fitted = false; toggleFit(); } else layoutEdges(); }, 120); }
window.addEventListener('resize', relayout);
layoutEdges(); centerScroll();
setTimeout(function(){ layoutEdges(); centerScroll(); }, 350);