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:
parent
722c8598e9
commit
0d79e8fbd6
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user