@tienne/gestalt 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/package.json +5 -2
  2. package/dist/skills/setup/SKILL.md +150 -0
  3. package/dist/src/cli/commands/graph-visualize.d.ts +11 -0
  4. package/dist/src/cli/commands/graph-visualize.d.ts.map +1 -0
  5. package/dist/src/cli/commands/graph-visualize.js +26 -0
  6. package/dist/src/cli/commands/graph-visualize.js.map +1 -0
  7. package/dist/src/cli/index.d.ts.map +1 -1
  8. package/dist/src/cli/index.js +14 -0
  9. package/dist/src/cli/index.js.map +1 -1
  10. package/dist/src/code-graph/embedding-provider.d.ts +15 -0
  11. package/dist/src/code-graph/embedding-provider.d.ts.map +1 -0
  12. package/dist/src/code-graph/embedding-provider.js +3 -0
  13. package/dist/src/code-graph/embedding-provider.js.map +1 -0
  14. package/dist/src/code-graph/engine.d.ts +28 -0
  15. package/dist/src/code-graph/engine.d.ts.map +1 -1
  16. package/dist/src/code-graph/engine.js +148 -0
  17. package/dist/src/code-graph/engine.js.map +1 -1
  18. package/dist/src/code-graph/providers/anthropic-summary.d.ts +8 -0
  19. package/dist/src/code-graph/providers/anthropic-summary.d.ts.map +1 -0
  20. package/dist/src/code-graph/providers/anthropic-summary.js +38 -0
  21. package/dist/src/code-graph/providers/anthropic-summary.js.map +1 -0
  22. package/dist/src/code-graph/providers/gemini-summary.d.ts +8 -0
  23. package/dist/src/code-graph/providers/gemini-summary.d.ts.map +1 -0
  24. package/dist/src/code-graph/providers/gemini-summary.js +43 -0
  25. package/dist/src/code-graph/providers/gemini-summary.js.map +1 -0
  26. package/dist/src/code-graph/providers/local-embedding.d.ts +10 -0
  27. package/dist/src/code-graph/providers/local-embedding.d.ts.map +1 -0
  28. package/dist/src/code-graph/providers/local-embedding.js +24 -0
  29. package/dist/src/code-graph/providers/local-embedding.js.map +1 -0
  30. package/dist/src/code-graph/providers/openai-summary.d.ts +8 -0
  31. package/dist/src/code-graph/providers/openai-summary.d.ts.map +1 -0
  32. package/dist/src/code-graph/providers/openai-summary.js +39 -0
  33. package/dist/src/code-graph/providers/openai-summary.js.map +1 -0
  34. package/dist/src/code-graph/rrf.d.ts +15 -0
  35. package/dist/src/code-graph/rrf.d.ts.map +1 -0
  36. package/dist/src/code-graph/rrf.js +25 -0
  37. package/dist/src/code-graph/rrf.js.map +1 -0
  38. package/dist/src/code-graph/storage.d.ts +7 -0
  39. package/dist/src/code-graph/storage.d.ts.map +1 -1
  40. package/dist/src/code-graph/storage.js +49 -0
  41. package/dist/src/code-graph/storage.js.map +1 -1
  42. package/dist/src/code-graph/summary-provider.d.ts +4 -0
  43. package/dist/src/code-graph/summary-provider.d.ts.map +1 -0
  44. package/dist/src/code-graph/summary-provider.js +3 -0
  45. package/dist/src/code-graph/summary-provider.js.map +1 -0
  46. package/dist/src/execute/passthrough-engine.d.ts +5 -0
  47. package/dist/src/execute/passthrough-engine.d.ts.map +1 -1
  48. package/dist/src/execute/passthrough-engine.js +20 -1
  49. package/dist/src/execute/passthrough-engine.js.map +1 -1
  50. package/dist/src/graph-viz/engine.d.ts +20 -0
  51. package/dist/src/graph-viz/engine.d.ts.map +1 -0
  52. package/dist/src/graph-viz/engine.js +81 -0
  53. package/dist/src/graph-viz/engine.js.map +1 -0
  54. package/dist/src/graph-viz/html-generator.d.ts +7 -0
  55. package/dist/src/graph-viz/html-generator.d.ts.map +1 -0
  56. package/dist/src/graph-viz/html-generator.js +699 -0
  57. package/dist/src/graph-viz/html-generator.js.map +1 -0
  58. package/dist/src/graph-viz/index.d.ts +6 -0
  59. package/dist/src/graph-viz/index.d.ts.map +1 -0
  60. package/dist/src/graph-viz/index.js +5 -0
  61. package/dist/src/graph-viz/index.js.map +1 -0
  62. package/dist/src/graph-viz/port-finder.d.ts +2 -0
  63. package/dist/src/graph-viz/port-finder.d.ts.map +1 -0
  64. package/dist/src/graph-viz/port-finder.js +26 -0
  65. package/dist/src/graph-viz/port-finder.js.map +1 -0
  66. package/dist/src/graph-viz/server.d.ts +26 -0
  67. package/dist/src/graph-viz/server.d.ts.map +1 -0
  68. package/dist/src/graph-viz/server.js +104 -0
  69. package/dist/src/graph-viz/server.js.map +1 -0
  70. package/dist/src/graph-viz/types.d.ts +11 -0
  71. package/dist/src/graph-viz/types.d.ts.map +1 -0
  72. package/dist/src/graph-viz/types.js +2 -0
  73. package/dist/src/graph-viz/types.js.map +1 -0
  74. package/dist/src/mcp/schemas.d.ts +11 -0
  75. package/dist/src/mcp/schemas.d.ts.map +1 -1
  76. package/dist/src/mcp/schemas.js +5 -0
  77. package/dist/src/mcp/schemas.js.map +1 -1
  78. package/dist/src/mcp/server.d.ts.map +1 -1
  79. package/dist/src/mcp/server.js +12 -3
  80. package/dist/src/mcp/server.js.map +1 -1
  81. package/dist/src/mcp/tools/code-graph-passthrough.d.ts.map +1 -1
  82. package/dist/src/mcp/tools/code-graph-passthrough.js +12 -0
  83. package/dist/src/mcp/tools/code-graph-passthrough.js.map +1 -1
  84. package/dist/src/mcp/tools/execute-passthrough.d.ts +1 -1
  85. package/dist/src/mcp/tools/execute-passthrough.d.ts.map +1 -1
  86. package/dist/src/mcp/tools/execute-passthrough.js +13 -3
  87. package/dist/src/mcp/tools/execute-passthrough.js.map +1 -1
  88. package/dist/src/mcp/tools/graph-visualize-passthrough.d.ts +13 -0
  89. package/dist/src/mcp/tools/graph-visualize-passthrough.d.ts.map +1 -0
  90. package/dist/src/mcp/tools/graph-visualize-passthrough.js +26 -0
  91. package/dist/src/mcp/tools/graph-visualize-passthrough.js.map +1 -0
  92. package/package.json +5 -2
  93. package/skills/setup/SKILL.md +150 -0
@@ -0,0 +1,699 @@
1
+ import { NodeKind, EdgeKind } from '../code-graph/types.js';
2
+ /**
3
+ * Generates a self-contained interactive HTML visualization for a code knowledge graph.
4
+ * Uses D3.js v7 force-directed layout with dark theme UI.
5
+ */
6
+ export function generateVisualizationHtml(nodes, edges) {
7
+ const nodesJson = JSON.stringify(nodes);
8
+ const edgesJson = JSON.stringify(edges);
9
+ // NodeKind / EdgeKind enum values serialized for use in inline JS
10
+ const nodeKindFile = NodeKind.File;
11
+ const nodeKindFunction = NodeKind.Function;
12
+ const nodeKindClass = NodeKind.Class;
13
+ const edgeKindImport = EdgeKind.IMPORTS_FROM;
14
+ const edgeKindCall = EdgeKind.CALLS;
15
+ return `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <title>Code Knowledge Graph</title>
21
+ <script src="https://d3js.org/d3.v7.min.js"></script>
22
+ <style>
23
+ *, *::before, *::after {
24
+ box-sizing: border-box;
25
+ margin: 0;
26
+ padding: 0;
27
+ }
28
+
29
+ body {
30
+ background: #0d1117;
31
+ color: #e6edf3;
32
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
33
+ display: flex;
34
+ height: 100vh;
35
+ overflow: hidden;
36
+ }
37
+
38
+ #graph-container {
39
+ flex: 1;
40
+ position: relative;
41
+ overflow: hidden;
42
+ }
43
+
44
+ svg {
45
+ width: 100%;
46
+ height: 100%;
47
+ display: block;
48
+ }
49
+
50
+ /* ─── Sidebar ─────────────────────────────────────────────── */
51
+ #sidebar {
52
+ width: 280px;
53
+ background: #161b22;
54
+ border-left: 1px solid #30363d;
55
+ display: flex;
56
+ flex-direction: column;
57
+ transition: transform 0.2s ease;
58
+ }
59
+
60
+ #sidebar-header {
61
+ padding: 16px 20px;
62
+ border-bottom: 1px solid #30363d;
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 10px;
66
+ }
67
+
68
+ #sidebar-header h2 {
69
+ font-size: 13px;
70
+ font-weight: 600;
71
+ color: #8b949e;
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.05em;
74
+ }
75
+
76
+ #sidebar-content {
77
+ flex: 1;
78
+ padding: 20px;
79
+ overflow-y: auto;
80
+ }
81
+
82
+ #node-placeholder {
83
+ color: #484f58;
84
+ font-size: 13px;
85
+ text-align: center;
86
+ margin-top: 40px;
87
+ line-height: 1.6;
88
+ }
89
+
90
+ #node-details {
91
+ display: none;
92
+ }
93
+
94
+ .detail-label {
95
+ font-size: 10px;
96
+ font-weight: 600;
97
+ color: #8b949e;
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.07em;
100
+ margin-bottom: 4px;
101
+ }
102
+
103
+ .detail-value {
104
+ font-size: 13px;
105
+ color: #e6edf3;
106
+ word-break: break-all;
107
+ margin-bottom: 16px;
108
+ padding: 8px 10px;
109
+ background: #0d1117;
110
+ border-radius: 6px;
111
+ border: 1px solid #30363d;
112
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
113
+ }
114
+
115
+ .detail-badge {
116
+ display: inline-block;
117
+ padding: 3px 8px;
118
+ border-radius: 999px;
119
+ font-size: 11px;
120
+ font-weight: 600;
121
+ margin-bottom: 16px;
122
+ }
123
+
124
+ .badge-file { background: #1f4068; color: #58a6ff; border: 1px solid #388bfd40; }
125
+ .badge-function { background: #1a3a1a; color: #3fb950; border: 1px solid #3fb95040; }
126
+ .badge-class { background: #3d2000; color: #f0883e; border: 1px solid #f0883e40; }
127
+ .badge-type { background: #2e1f5e; color: #bc8cff; border: 1px solid #bc8cff40; }
128
+
129
+ #connections-section h3 {
130
+ font-size: 11px;
131
+ font-weight: 600;
132
+ color: #8b949e;
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.07em;
135
+ margin-bottom: 8px;
136
+ }
137
+
138
+ .connection-item {
139
+ font-size: 12px;
140
+ color: #c9d1d9;
141
+ padding: 5px 8px;
142
+ border-radius: 4px;
143
+ margin-bottom: 3px;
144
+ background: #21262d;
145
+ white-space: nowrap;
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ cursor: pointer;
149
+ border: 1px solid transparent;
150
+ transition: border-color 0.15s;
151
+ }
152
+
153
+ .connection-item:hover {
154
+ border-color: #388bfd;
155
+ }
156
+
157
+ .connection-item .conn-kind {
158
+ font-size: 10px;
159
+ color: #8b949e;
160
+ margin-right: 4px;
161
+ }
162
+
163
+ /* ─── Stats Bar ───────────────────────────────────────────── */
164
+ #stats-bar {
165
+ position: absolute;
166
+ top: 12px;
167
+ left: 12px;
168
+ display: flex;
169
+ gap: 8px;
170
+ pointer-events: none;
171
+ }
172
+
173
+ .stat-chip {
174
+ background: #161b22cc;
175
+ border: 1px solid #30363d;
176
+ border-radius: 6px;
177
+ padding: 5px 10px;
178
+ font-size: 11px;
179
+ color: #8b949e;
180
+ backdrop-filter: blur(4px);
181
+ }
182
+
183
+ .stat-chip span {
184
+ color: #e6edf3;
185
+ font-weight: 600;
186
+ }
187
+
188
+ /* ─── Legend ─────────────────────────────────────────────── */
189
+ #legend {
190
+ position: absolute;
191
+ bottom: 16px;
192
+ left: 12px;
193
+ background: #161b22cc;
194
+ border: 1px solid #30363d;
195
+ border-radius: 8px;
196
+ padding: 10px 14px;
197
+ pointer-events: none;
198
+ backdrop-filter: blur(4px);
199
+ }
200
+
201
+ .legend-title {
202
+ font-size: 10px;
203
+ font-weight: 600;
204
+ color: #8b949e;
205
+ text-transform: uppercase;
206
+ letter-spacing: 0.07em;
207
+ margin-bottom: 8px;
208
+ }
209
+
210
+ .legend-item {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 7px;
214
+ margin-bottom: 5px;
215
+ font-size: 11px;
216
+ color: #8b949e;
217
+ }
218
+
219
+ .legend-dot {
220
+ width: 10px;
221
+ height: 10px;
222
+ border-radius: 50%;
223
+ flex-shrink: 0;
224
+ }
225
+
226
+ .legend-line {
227
+ width: 22px;
228
+ height: 2px;
229
+ flex-shrink: 0;
230
+ border-radius: 1px;
231
+ }
232
+
233
+ .legend-dashed {
234
+ background: repeating-linear-gradient(
235
+ 90deg,
236
+ #6e7681 0, #6e7681 4px,
237
+ transparent 4px, transparent 8px
238
+ );
239
+ }
240
+
241
+ /* ─── D3 Graph Styles ────────────────────────────────────── */
242
+ .node circle {
243
+ cursor: pointer;
244
+ transition: filter 0.15s;
245
+ }
246
+
247
+ .node circle:hover {
248
+ filter: brightness(1.3);
249
+ }
250
+
251
+ .node text {
252
+ pointer-events: none;
253
+ user-select: none;
254
+ font-size: 10px;
255
+ fill: #8b949e;
256
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
257
+ }
258
+
259
+ .link {
260
+ pointer-events: none;
261
+ }
262
+
263
+ .link.dimmed {
264
+ opacity: 0.08;
265
+ }
266
+
267
+ .node.dimmed circle {
268
+ opacity: 0.12;
269
+ }
270
+
271
+ .node.dimmed text {
272
+ opacity: 0.08;
273
+ }
274
+
275
+ .node.highlighted circle {
276
+ filter: brightness(1.4) drop-shadow(0 0 6px currentColor);
277
+ }
278
+
279
+ .node.highlighted text {
280
+ fill: #e6edf3;
281
+ }
282
+
283
+ /* ─── Controls ───────────────────────────────────────────── */
284
+ #controls {
285
+ position: absolute;
286
+ top: 12px;
287
+ right: 12px;
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: 4px;
291
+ }
292
+
293
+ .ctrl-btn {
294
+ width: 32px;
295
+ height: 32px;
296
+ background: #161b22cc;
297
+ border: 1px solid #30363d;
298
+ border-radius: 6px;
299
+ color: #8b949e;
300
+ font-size: 16px;
301
+ cursor: pointer;
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ transition: background 0.15s, color 0.15s;
306
+ backdrop-filter: blur(4px);
307
+ }
308
+
309
+ .ctrl-btn:hover {
310
+ background: #21262d;
311
+ color: #e6edf3;
312
+ }
313
+ </style>
314
+ </head>
315
+ <body>
316
+
317
+ <div id="graph-container">
318
+ <svg id="svg"></svg>
319
+
320
+ <div id="stats-bar">
321
+ <div class="stat-chip">Nodes: <span id="stat-nodes">0</span></div>
322
+ <div class="stat-chip">Edges: <span id="stat-edges">0</span></div>
323
+ </div>
324
+
325
+ <div id="controls">
326
+ <button class="ctrl-btn" id="btn-zoom-in" title="Zoom in">+</button>
327
+ <button class="ctrl-btn" id="btn-zoom-out" title="Zoom out">−</button>
328
+ <button class="ctrl-btn" id="btn-reset" title="Reset view">⌖</button>
329
+ </div>
330
+
331
+ <div id="legend">
332
+ <div class="legend-title">Legend</div>
333
+ <div class="legend-item"><div class="legend-dot" style="background:#388bfd;width:14px;height:14px;"></div> File</div>
334
+ <div class="legend-item"><div class="legend-dot" style="background:#3fb950;"></div> Function</div>
335
+ <div class="legend-item"><div class="legend-dot" style="background:#f0883e;"></div> Class / Type</div>
336
+ <div class="legend-item"><div class="legend-line legend-dashed"></div> Import</div>
337
+ <div class="legend-item"><div class="legend-line" style="background:#6e7681;"></div> Call / Other</div>
338
+ </div>
339
+ </div>
340
+
341
+ <div id="sidebar">
342
+ <div id="sidebar-header">
343
+ <h2>Node Details</h2>
344
+ </div>
345
+ <div id="sidebar-content">
346
+ <div id="node-placeholder">Click a node to<br/>inspect its details</div>
347
+ <div id="node-details"></div>
348
+ </div>
349
+ </div>
350
+
351
+ <script>
352
+ (function () {
353
+ 'use strict';
354
+
355
+ // ─── Embedded Data ─────────────────────────────────────────────
356
+ const RAW_NODES = ${nodesJson};
357
+ const RAW_EDGES = ${edgesJson};
358
+
359
+ const NODE_KIND_FILE = ${JSON.stringify(nodeKindFile)};
360
+ const NODE_KIND_FUNCTION = ${JSON.stringify(nodeKindFunction)};
361
+ const NODE_KIND_CLASS = ${JSON.stringify(nodeKindClass)};
362
+ const EDGE_KIND_IMPORT = ${JSON.stringify(edgeKindImport)};
363
+ const EDGE_KIND_CALL = ${JSON.stringify(edgeKindCall)};
364
+
365
+ // ─── Stats ─────────────────────────────────────────────────────
366
+ document.getElementById('stat-nodes').textContent = RAW_NODES.length;
367
+ document.getElementById('stat-edges').textContent = RAW_EDGES.length;
368
+
369
+ // ─── Helpers ───────────────────────────────────────────────────
370
+ function nodeColor(kind) {
371
+ if (kind === NODE_KIND_FILE) return '#388bfd';
372
+ if (kind === NODE_KIND_FUNCTION) return '#3fb950';
373
+ if (kind === NODE_KIND_CLASS) return '#f0883e';
374
+ return '#bc8cff'; // Type, etc.
375
+ }
376
+
377
+ function nodeRadius(kind) {
378
+ return kind === NODE_KIND_FILE ? 9 : 5;
379
+ }
380
+
381
+ function shortName(n) {
382
+ if (n.kind === NODE_KIND_FILE) {
383
+ const parts = n.filePath.split('/');
384
+ return parts[parts.length - 1] || n.name;
385
+ }
386
+ return n.name.length > 22 ? n.name.slice(0, 20) + '…' : n.name;
387
+ }
388
+
389
+ function badgeClass(kind) {
390
+ if (kind === NODE_KIND_FILE) return 'badge-file';
391
+ if (kind === NODE_KIND_FUNCTION) return 'badge-function';
392
+ if (kind === NODE_KIND_CLASS) return 'badge-class';
393
+ return 'badge-type';
394
+ }
395
+
396
+ // ─── D3 Setup ──────────────────────────────────────────────────
397
+ const width = document.getElementById('graph-container').clientWidth;
398
+ const height = document.getElementById('graph-container').clientHeight;
399
+
400
+ const svg = d3.select('#svg')
401
+ .attr('width', width)
402
+ .attr('height', height);
403
+
404
+ const defs = svg.append('defs');
405
+
406
+ // Arrow marker for directed edges (call edges)
407
+ defs.append('marker')
408
+ .attr('id', 'arrow')
409
+ .attr('viewBox', '0 -4 8 8')
410
+ .attr('refX', 12)
411
+ .attr('refY', 0)
412
+ .attr('markerWidth', 5)
413
+ .attr('markerHeight', 5)
414
+ .attr('orient', 'auto')
415
+ .append('path')
416
+ .attr('d', 'M0,-4L8,0L0,4')
417
+ .attr('fill', '#4d5566');
418
+
419
+ const zoomLayer = svg.append('g').attr('class', 'zoom-layer');
420
+
421
+ const zoom = d3.zoom()
422
+ .scaleExtent([0.1, 8])
423
+ .on('zoom', (event) => zoomLayer.attr('transform', event.transform));
424
+
425
+ svg.call(zoom);
426
+
427
+ // ─── Build graph data ──────────────────────────────────────────
428
+ const nodeMap = new Map(RAW_NODES.map(n => [n.id, n]));
429
+
430
+ // Filter edges to only those whose source and target exist
431
+ const validEdges = RAW_EDGES.filter(e =>
432
+ nodeMap.has(e.sourceId) && nodeMap.has(e.targetId)
433
+ );
434
+
435
+ const simNodes = RAW_NODES.map(n => ({ ...n }));
436
+ const simEdges = validEdges.map(e => ({
437
+ ...e,
438
+ source: e.sourceId,
439
+ target: e.targetId,
440
+ }));
441
+
442
+ // ─── Force Simulation ──────────────────────────────────────────
443
+ const simulation = d3.forceSimulation(simNodes)
444
+ .force('link', d3.forceLink(simEdges)
445
+ .id(d => d.id)
446
+ .distance(d => {
447
+ if (d.kind === EDGE_KIND_IMPORT) return 90;
448
+ return 60;
449
+ })
450
+ .strength(0.4)
451
+ )
452
+ .force('charge', d3.forceManyBody()
453
+ .strength(d => d.kind === NODE_KIND_FILE ? -280 : -120)
454
+ )
455
+ .force('center', d3.forceCenter(width / 2, height / 2))
456
+ .force('collision', d3.forceCollide().radius(d => nodeRadius(d.kind) + 6))
457
+ .alphaDecay(0.02);
458
+
459
+ // ─── Render Links ─────────────────────────────────────────────
460
+ const linkGroup = zoomLayer.append('g').attr('class', 'links');
461
+ const link = linkGroup.selectAll('line')
462
+ .data(simEdges)
463
+ .join('line')
464
+ .attr('class', 'link')
465
+ .attr('stroke', d => d.kind === EDGE_KIND_CALL ? '#4d5566' : '#3a3f4b')
466
+ .attr('stroke-width', d => d.kind === EDGE_KIND_CALL ? 1.2 : 0.8)
467
+ .attr('stroke-dasharray', d => d.kind === EDGE_KIND_IMPORT ? '4 3' : null)
468
+ .attr('stroke-opacity', d => d.kind === EDGE_KIND_CALL ? 0.7 : 0.45)
469
+ .attr('marker-end', d => d.kind === EDGE_KIND_CALL ? 'url(#arrow)' : null);
470
+
471
+ // ─── Render Nodes ─────────────────────────────────────────────
472
+ const nodeGroup = zoomLayer.append('g').attr('class', 'nodes');
473
+ const node = nodeGroup.selectAll('g')
474
+ .data(simNodes)
475
+ .join('g')
476
+ .attr('class', 'node')
477
+ .call(
478
+ d3.drag()
479
+ .on('start', (event, d) => {
480
+ if (!event.active) simulation.alphaTarget(0.3).restart();
481
+ d.fx = d.x;
482
+ d.fy = d.y;
483
+ })
484
+ .on('drag', (event, d) => {
485
+ d.fx = event.x;
486
+ d.fy = event.y;
487
+ })
488
+ .on('end', (event, d) => {
489
+ if (!event.active) simulation.alphaTarget(0);
490
+ d.fx = null;
491
+ d.fy = null;
492
+ })
493
+ )
494
+ .on('click', onNodeClick);
495
+
496
+ node.append('circle')
497
+ .attr('r', d => nodeRadius(d.kind))
498
+ .attr('fill', d => nodeColor(d.kind))
499
+ .attr('stroke', '#0d1117')
500
+ .attr('stroke-width', 1.5);
501
+
502
+ node.append('text')
503
+ .attr('dy', d => nodeRadius(d.kind) + 11)
504
+ .attr('text-anchor', 'middle')
505
+ .text(d => shortName(d));
506
+
507
+ // ─── Tick ─────────────────────────────────────────────────────
508
+ simulation.on('tick', () => {
509
+ link
510
+ .attr('x1', d => d.source.x)
511
+ .attr('y1', d => d.source.y)
512
+ .attr('x2', d => d.target.x)
513
+ .attr('y2', d => d.target.y);
514
+
515
+ node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
516
+ });
517
+
518
+ // ─── Click Interaction ────────────────────────────────────────
519
+ let selectedId = null;
520
+
521
+ function onNodeClick(event, d) {
522
+ event.stopPropagation();
523
+
524
+ if (selectedId === d.id) {
525
+ // Deselect
526
+ selectedId = null;
527
+ resetHighlight();
528
+ showPlaceholder();
529
+ return;
530
+ }
531
+
532
+ selectedId = d.id;
533
+ highlightNode(d);
534
+ showDetails(d);
535
+ }
536
+
537
+ svg.on('click', () => {
538
+ selectedId = null;
539
+ resetHighlight();
540
+ showPlaceholder();
541
+ });
542
+
543
+ function highlightNode(d) {
544
+ // Find directly connected node IDs
545
+ const connectedIds = new Set([d.id]);
546
+ simEdges.forEach(e => {
547
+ if (e.source.id === d.id || e.sourceId === d.id) connectedIds.add(e.target.id || e.targetId);
548
+ if (e.target.id === d.id || e.targetId === d.id) connectedIds.add(e.source.id || e.sourceId);
549
+ });
550
+
551
+ node
552
+ .classed('dimmed', n => !connectedIds.has(n.id))
553
+ .classed('highlighted', n => connectedIds.has(n.id));
554
+
555
+ link
556
+ .classed('dimmed', e => {
557
+ const sid = e.source.id || e.sourceId;
558
+ const tid = e.target.id || e.targetId;
559
+ return !(connectedIds.has(sid) && connectedIds.has(tid));
560
+ });
561
+ }
562
+
563
+ function resetHighlight() {
564
+ node.classed('dimmed', false).classed('highlighted', false);
565
+ link.classed('dimmed', false);
566
+ }
567
+
568
+ // ─── Sidebar: Details ─────────────────────────────────────────
569
+ function showPlaceholder() {
570
+ document.getElementById('node-placeholder').style.display = '';
571
+ document.getElementById('node-details').style.display = 'none';
572
+ }
573
+
574
+ function showDetails(d) {
575
+ document.getElementById('node-placeholder').style.display = 'none';
576
+
577
+ // Gather connections
578
+ const outgoing = [];
579
+ const incoming = [];
580
+ simEdges.forEach(e => {
581
+ const sid = e.source.id || e.sourceId;
582
+ const tid = e.target.id || e.targetId;
583
+ if (sid === d.id) {
584
+ const target = nodeMap.get(tid);
585
+ if (target) outgoing.push({ node: target, kind: e.kind });
586
+ }
587
+ if (tid === d.id) {
588
+ const source = nodeMap.get(sid);
589
+ if (source) incoming.push({ node: source, kind: e.kind });
590
+ }
591
+ });
592
+
593
+ const detailsEl = document.getElementById('node-details');
594
+ detailsEl.style.display = 'block';
595
+ detailsEl.innerHTML = \`
596
+ <div class="detail-label">Kind</div>
597
+ <div class="detail-badge \${badgeClass(d.kind)}">\${d.kind}</div>
598
+
599
+ <div class="detail-label">Name</div>
600
+ <div class="detail-value">\${escHtml(d.name)}</div>
601
+
602
+ <div class="detail-label">File</div>
603
+ <div class="detail-value">\${escHtml(d.filePath)}</div>
604
+
605
+ \${d.lineStart != null ? \`
606
+ <div class="detail-label">Lines</div>
607
+ <div class="detail-value">\${d.lineStart}\${d.lineEnd != null ? '–' + d.lineEnd : ''}</div>
608
+ \` : ''}
609
+
610
+ \${outgoing.length > 0 ? \`
611
+ <div id="connections-section">
612
+ <h3>Outgoing (\${outgoing.length})</h3>
613
+ \${outgoing.slice(0, 12).map(c => \`
614
+ <div class="connection-item" data-id="\${escAttr(c.node.id)}" title="\${escAttr(c.node.name)}">
615
+ <span class="conn-kind">\${c.kind}</span>\${escHtml(shortName(c.node))}
616
+ </div>
617
+ \`).join('')}
618
+ </div>
619
+ \` : ''}
620
+
621
+ \${incoming.length > 0 ? \`
622
+ <div id="connections-section" style="margin-top:12px;">
623
+ <h3>Incoming (\${incoming.length})</h3>
624
+ \${incoming.slice(0, 12).map(c => \`
625
+ <div class="connection-item" data-id="\${escAttr(c.node.id)}" title="\${escAttr(c.node.name)}">
626
+ <span class="conn-kind">\${c.kind}</span>\${escHtml(shortName(c.node))}
627
+ </div>
628
+ \`).join('')}
629
+ </div>
630
+ \` : ''}
631
+ \`;
632
+
633
+ // Click on connection item → jump to that node
634
+ detailsEl.querySelectorAll('.connection-item').forEach(item => {
635
+ item.addEventListener('click', () => {
636
+ const id = item.getAttribute('data-id');
637
+ const target = simNodes.find(n => n.id === id);
638
+ if (target) {
639
+ selectedId = target.id;
640
+ highlightNode(target);
641
+ showDetails(target);
642
+ panToNode(target);
643
+ }
644
+ });
645
+ });
646
+ }
647
+
648
+ function panToNode(d) {
649
+ const t = d3.zoomTransform(svg.node());
650
+ const cx = width / 2 - t.k * d.x;
651
+ const cy = height / 2 - t.k * d.y;
652
+ svg.transition().duration(400).call(
653
+ zoom.transform,
654
+ d3.zoomIdentity.translate(cx, cy).scale(t.k)
655
+ );
656
+ }
657
+
658
+ // ─── Controls ────────────────────────────────────────────────
659
+ document.getElementById('btn-zoom-in').addEventListener('click', () => {
660
+ svg.transition().duration(200).call(zoom.scaleBy, 1.4);
661
+ });
662
+ document.getElementById('btn-zoom-out').addEventListener('click', () => {
663
+ svg.transition().duration(200).call(zoom.scaleBy, 1 / 1.4);
664
+ });
665
+ document.getElementById('btn-reset').addEventListener('click', () => {
666
+ svg.transition().duration(400).call(
667
+ zoom.transform,
668
+ d3.zoomIdentity.translate(width / 2, height / 2).scale(1)
669
+ .translate(-width / 2, -height / 2)
670
+ );
671
+ });
672
+
673
+ // ─── Escape helpers ──────────────────────────────────────────
674
+ function escHtml(str) {
675
+ return String(str)
676
+ .replace(/&/g, '&amp;')
677
+ .replace(/</g, '&lt;')
678
+ .replace(/>/g, '&gt;')
679
+ .replace(/"/g, '&quot;');
680
+ }
681
+
682
+ function escAttr(str) {
683
+ return String(str).replace(/"/g, '&quot;');
684
+ }
685
+
686
+ // ─── Window resize ───────────────────────────────────────────
687
+ window.addEventListener('resize', () => {
688
+ const w = document.getElementById('graph-container').clientWidth;
689
+ const h = document.getElementById('graph-container').clientHeight;
690
+ svg.attr('width', w).attr('height', h);
691
+ simulation.force('center', d3.forceCenter(w / 2, h / 2)).alpha(0.1).restart();
692
+ });
693
+
694
+ })();
695
+ </script>
696
+ </body>
697
+ </html>`;
698
+ }
699
+ //# sourceMappingURL=html-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-generator.js","sourceRoot":"","sources":["../../../src/graph-viz/html-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE5D;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAsB,EAAE,KAAsB;IACtF,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAExC,kEAAkE;IAClE,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC;IACnC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,QAAQ,CAAC;IAC3C,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC;IACrC,MAAM,cAAc,GAAG,QAAQ,CAAC,YAAY,CAAC;IAC7C,MAAM,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC;IAEpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAqVa,SAAS;sBACT,SAAS;;+BAEA,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC;+BAC5B,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC;+BAChC,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;+BAC7B,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;+BAC9B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA8UnD,CAAC;AACT,CAAC"}
@@ -0,0 +1,6 @@
1
+ export type { GraphVisualizationResult, GraphVizServerOptions } from './types.js';
2
+ export * from './port-finder.js';
3
+ export * from './html-generator.js';
4
+ export * from './server.js';
5
+ export * from './engine.js';
6
+ //# sourceMappingURL=index.d.ts.map