From 60ad7479fd8028fcb4d02a9c760ab5d53159700e Mon Sep 17 00:00:00 2001 From: Root At Skic Date: Wed, 21 Oct 2026 22:39:30 +0300 Subject: [PATCH] Model graph: decision titles on two lines, refit after resize --- src/graph/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/graph/index.ts b/src/graph/index.ts index 5ad61e1..742d049 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -156,7 +156,12 @@ class GraphPage { } private elements() { - const nodes = this.graph.nodes.map((n) => ({ group: 'nodes' as const, data: { id: n.id, label: n.label, type: n.type ?? '', kind: n.kind, shared: n.shared ? 1 : 0, color: nodeColor(n) } })); + const shorten = (text: string, max: number) => { + if (text.length <= max) return text; + const cut = text.slice(0, max - 1); + return `${cut.slice(0, Math.max(cut.lastIndexOf(' '), max - 16))}\u2026`; + }; + const nodes = this.graph.nodes.map((n) => ({ group: 'nodes' as const, data: { id: n.id, label: n.kind === 'decision' ? shorten(n.label, 60) : n.label, type: n.type ?? '', kind: n.kind, shared: n.shared ? 1 : 0, color: nodeColor(n) } })); // In the layered layout a label sits at the end of the connection where fewer connections meet, so a // fan-out labels at its targets and a fan-in at its sources; what still shares an end is staggered. const outCount = new Map(); @@ -390,7 +395,7 @@ class GraphPage { return [ { selector: 'node', style: { shape: 'round-rectangle', width: (n: NodeSingular) => Math.min(240, Math.max(110, String(n.data('label')).length * 7.6 + 40)), height: 46, 'background-color': fill(dark ? 0.16 : 0.1), 'border-width': 1.5, 'border-color': 'data(color)', 'border-opacity': 0.9, label: 'data(label)', color: p.fg, 'font-size': 13, 'font-weight': 600, 'font-family': p.font, 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'ellipsis', 'text-max-width': '210px', 'overlay-opacity': 0, 'underlay-color': 'data(color)', 'underlay-padding': 6, 'underlay-opacity': 0, 'underlay-shape': 'round-rectangle', 'transition-property': 'underlay-opacity, border-width', 'transition-duration': 120 } }, { selector: 'node[shared = 0]', style: { 'border-style': 'dashed' } }, - { selector: 'node[kind = "decision"]', style: { shape: 'round-tag', height: 34, 'font-size': 11, 'font-weight': 500, 'background-color': fill(dark ? 0.12 : 0.08), 'text-max-width': '190px', width: (n: NodeSingular) => Math.min(220, Math.max(120, String(n.data('label')).length * 6.4 + 44)) } }, + { selector: 'node[kind = "decision"]', style: { shape: 'round-tag', height: (n: NodeSingular) => (String(n.data('label')).length > 30 ? 44 : 34), 'font-size': 11, 'font-weight': 500, 'background-color': fill(dark ? 0.12 : 0.08), 'text-wrap': 'wrap', 'text-max-width': '196px', 'line-height': 1.2, width: (n: NodeSingular) => Math.min(244, Math.max(120, String(n.data('label')).length * 6.4 + 48)) } }, { selector: 'node[kind = "registry"]', style: { shape: 'cut-rectangle', height: 32, 'font-size': 11, 'font-weight': 500, 'font-family': 'ui-monospace, Menlo, monospace', 'border-width': 1, 'border-color': p.muted, 'background-color': mix(dark ? '#94a3b8' : '#64748b', bg, 0.08), width: (n: NodeSingular) => Math.min(220, Math.max(110, String(n.data('label')).length * 6.8 + 36)) } }, { selector: 'node.hover', style: { 'underlay-opacity': dark ? 0.22 : 0.16, 'border-width': 2, 'background-color': fill(dark ? 0.28 : 0.18) } }, { selector: 'edge', style: { width: 1.4, 'line-color': p.edge, 'line-opacity': 0.85, 'target-arrow-color': p.edge, 'target-arrow-shape': 'triangle', 'arrow-scale': 0.8, 'curve-style': 'bezier', label: '', 'font-size': 10, 'font-family': p.font, color: p.muted, 'text-background-color': p.bg, 'text-background-opacity': 1, 'text-background-padding': '3px', 'text-background-shape': 'round-rectangle', 'text-rotation': 'autorotate', 'transition-property': 'width, line-color', 'transition-duration': 120 } }, @@ -793,7 +798,12 @@ class GraphPage { this.help.hidden = true; } }); - window.addEventListener('resize', () => this.cy.resize()); + let resizeTimer: number | undefined; + window.addEventListener('resize', () => { + this.cy.resize(); + window.clearTimeout(resizeTimer); + resizeTimer = window.setTimeout(() => this.cy.fit(this.cy.elements().not('.hidden'), 40), 150); + }); new MutationObserver(() => this.restyle()).observe(document.body, { attributes: true, attributeFilter: ['class'] }); window.addEventListener('message', (event) => { const message = event.data || {};