OverviewFilesCommitsIssuesDocsReleasesPeers

codetree commits / 7a31bd7

Nodes tab: hover highlighting, and dependency-of-dependency depth
Both asks, in the view they were asked about rather than in /floor. ## Highlighting Hovering a node lights its own wires, fades the rest to 6%, and dims every node it does not touch. Clicking one keeps its whole dependency closure on screen until you click it again, click the background, or press Escape. ## Dependencies of dependencies Clicking traces the FULL transitive closure, not one hop, with the distance carried by colour — amber at one hop, cooling through red toward violet as it goes out. Nodes in the closure carry an "N hops" badge. It uses the same dependencyClosure() and depthColor() the floor uses, so both views agree on what "depends on" means: a breadth-first walk BACKWARD along supplier -> consumer edges, shortest path wins, cycles terminate. Measured from the rendered DOM, selecting a module with real depth: selected node 1 hop badges 14 matching its measured reach of 14 modules dep edges 35 amber at 1 hop, red at 2 cold edges 70 dimmed nodes 41 Selecting a LEAF — something everything depends on but which depends on nothing — correctly lights nothing: its closure is itself. That is not a bug, and it is worth knowing before reading the view. ?select=<node title> deep-links a selection, like the floor, which is also the only way to review it without a mouse. ## Not verified The hover path shares the element bookkeeping with the click path, but headless Chromium cannot hover, so only the click path is measured. Hover is asserted by construction, not by evidence. Suite: 233 tests, 5126 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7a31bd71f7f8d165b827482cd20612671ca802af
by plankatron <fleeky@gmail.com> · 2026-09-21T01:04:22.000Z

Changed files (1) +98 -0

lib/render-nodes.js +98 -0
……@@ -51,6 +51,11 @@ export const NODE_VIEW_CSS = `
5151.nv-frame { fill-opacity: 0.10; stroke-opacity: 0.55; stroke-width: 1.5; rx: 8; }
5252.nv-frame-label { font-size: 12px; font-weight: bold; fill-opacity: 0.95; pointer-events: none; }
5353.nv-junction { fill: #5a6570; stroke: none; }
54.nv-noodle.hot { stroke: #ffa657 !important; stroke-width: 2.2; }
55.nv-noodle.cold { opacity: 0.06; }
56.nv-noodle.dep { stroke-width: 2; }
57.nv-node.dim { opacity: 0.25; }
58.nv-hops { font-size: 8px; font-family: ui-monospace, monospace; }
5459.nv-noodle { stroke-linejoin: round; stroke-linecap: round; }
5560
5661#node-breadcrumb {
……@@ -103,6 +108,9 @@ var NV = {
103108 sourceCache: {},
104109 frames: [],
105110 circuit: true,
111 selected: null,
112 linkEls: [],
113 nodeEls: null,
106114 layoutPending: false,
107115 selected: null
108116}
……@@ -609,6 +617,8 @@ function nvRender () {
609617 // way, rather than a bezier drawn straight through everything between.
610618 var routed = {}
611619 var junctions = []
620 NV.linkEls = []
621 NV.nodeEls = {}
612622 if (NV.circuit && window.__ctRoute) {
613623 var boxes = []
614624 for (var bi = 0; bi < NV.nodes.length; bi++) {
……@@ -654,6 +664,7 @@ function nvRender () {
654664 ', ' + p2.x + ' ' + p2.y))
655665 path.setAttribute('class', 'nv-noodle')
656666 path.setAttribute('stroke', '#5a6570')
667 NV.linkEls.push({ el: path, from: lk.from, to: lk.to })
657668 noodleG.appendChild(path)
658669 }
659670
……@@ -669,6 +680,39 @@ function nvRender () {
669680 nvDrawFrames(frameG)
670681 for (var i = 0; i < NV.nodes.length; i++) nvDrawNode(nodeG, NV.nodes[i])
671682
683 // Dependencies of dependencies: clicking a node traces its WHOLE closure, not
684 // one hop, with distance carried by colour. Same shared implementation the
685 // floor uses, so both views agree about what "depends on" means.
686 if (NV.selected && window.__ct) {
687 var closure = window.__ct.dependencyClosure(NV.links, NV.selected)
688 for (var li = 0; li < NV.linkEls.length; li++) {
689 var le = NV.linkEls[li]
690 var d = closure.edges.get(le.from + ' -> ' + le.to)
691 if (d === undefined) {
692 le.el.classList.add('cold')
693 } else {
694 le.el.classList.add('dep')
695 le.el.setAttribute('stroke', window.__ct.depthColor(d, closure.maxDepth))
696 }
697 }
698 for (var ni = 0; ni < NV.nodes.length; ni++) {
699 var nn = NV.nodes[ni]
700 var nd = closure.nodes.get(nn.id)
701 if (nd === undefined) { if (nn.el) nn.el.classList.add('dim'); continue }
702 if (nd === 0 && nn.el) nn.el.classList.add('selected')
703 if (nd > 0 && nn.el) {
704 var hops = document.createElementNS('http://www.w3.org/2000/svg', 'text')
705 hops.setAttribute('class', 'nv-hops')
706 hops.setAttribute('x', NV_W - 7)
707 hops.setAttribute('y', nvNodeHeight(nn) - 5)
708 hops.setAttribute('text-anchor', 'end')
709 hops.setAttribute('fill', window.__ct.depthColor(nd, closure.maxDepth))
710 hops.textContent = nd + (nd === 1 ? ' hop' : ' hops')
711 nn.el.appendChild(hops)
712 }
713 }
714 }
715
672716 nvApplyTransform()
673717 nvBreadcrumb()
674718 nvLegend()
……@@ -744,8 +788,41 @@ function nvDrawNode (parent, n) {
744788 title.addEventListener('click', openIt)
745789 }
746790
791 // Hover traces this node's own wires; click keeps its whole dependency depth
792 // on screen until you click it again or hit Escape.
793 g.addEventListener('mouseenter', function () { if (!NV.selected) nvFocus(n) })
794 g.addEventListener('mouseleave', function () { if (!NV.selected) nvUnfocus() })
795 g.addEventListener('click', function (e) {
796 e.stopPropagation()
797 NV.selected = NV.selected === n.id ? null : n.id
798 nvRender()
799 })
800
747801 parent.appendChild(g)
748802 n.el = g
803 NV.nodeEls[n.id] = g
804}
805
806function nvFocus (node) {
807 var connected = {}
808 connected[node.id] = true
809 for (var i = 0; i < NV.linkEls.length; i++) {
810 var le = NV.linkEls[i]
811 var hot = le.from === node.id || le.to === node.id
812 le.el.classList.toggle('hot', hot)
813 le.el.classList.toggle('cold', !hot)
814 if (hot) connected[le.from === node.id ? le.to : le.from] = true
815 }
816 for (var id in NV.nodeEls) {
817 NV.nodeEls[id].classList.toggle('dim', !connected[id])
818 }
819}
820
821function nvUnfocus () {
822 for (var i = 0; i < NV.linkEls.length; i++) {
823 NV.linkEls[i].el.classList.remove('hot', 'cold')
824 }
825 for (var id in NV.nodeEls) NV.nodeEls[id].classList.remove('dim')
749826}
750827
751828function nvDrawSocket (g, n, sock, side, row) {
……@@ -827,6 +904,7 @@ function nvLegend () {
827904 '<span style="color:#6e7076">&#9675;</span> unmated &nbsp;' +
828905 '<span style="color:#f85149">&#9675;</span> unresolved' +
829906 '<br>left sockets demand &middot; right sockets supply' +
907 '<br>hover traces a node &middot; click for its full dependency depth' +
830908 '<br><label style="pointer-events:auto;cursor:pointer;color:#c9d1d9">' +
831909 '<input type="checkbox" id="nv-only-connected"' + (NV.onlyConnected ? ' checked' : '') +
832910 ' style="vertical-align:-1px;accent-color:#58a6ff"> collapse to only connected</label>' +
……@@ -958,6 +1036,16 @@ function nvInit () {
9581036 nvApplyTransform()
9591037 }, { passive: false })
9601038
1039 svg.addEventListener('click', function (e) {
1040 if (e.target === svg || e.target.id === 'nv-world') {
1041 if (NV.selected) { NV.selected = null; nvRender() }
1042 }
1043 })
1044
1045 document.addEventListener('keydown', function (e) {
1046 if (e.key === 'Escape' && NV.selected) { NV.selected = null; nvRender() }
1047 })
1048
9611049 svg.addEventListener('mousedown', function (e) {
9621050 NV.panning = true
9631051 NV.panSX = e.clientX - NV.panX
……@@ -976,5 +1064,15 @@ function nvInit () {
9761064 })
9771065
9781066 nvBuild()
1067
1068 // Deep-linkable, like every other view here, and the only way to review a
1069 // selection without a mouse: ?select=<node title>.
1070 var want = new URLSearchParams(location.search).get('select')
1071 if (want) {
1072 for (var si = 0; si < NV.nodes.length; si++) {
1073 if (NV.nodes[si].title === want) { NV.selected = NV.nodes[si].id; break }
1074 }
1075 if (NV.selected) nvRender()
1076 }
9791077}
9801078`