depwire-cli 0.1.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.
@@ -0,0 +1,579 @@
1
+ // Arc Diagram Renderer using D3.js
2
+ let graphData = null;
3
+ let svg = null;
4
+ let g = null;
5
+ let filePositions = new Map();
6
+ let selectedFile = null;
7
+ let selectedArc = null;
8
+ let ws = null;
9
+
10
+ async function init() {
11
+ try {
12
+ const response = await fetch('/api/graph');
13
+ graphData = await response.json();
14
+
15
+ // Update header
16
+ document.getElementById('projectName').textContent = graphData.projectName;
17
+ document.getElementById('stats').innerHTML = `
18
+ <div class="stat-item"><span class="stat-label">Files:</span> <span class="stat-value">${graphData.stats.totalFiles}</span></div>
19
+ <div class="stat-item"><span class="stat-label">Symbols:</span> <span class="stat-value">${graphData.stats.totalSymbols}</span></div>
20
+ <div class="stat-item"><span class="stat-label">Edges:</span> <span class="stat-value">${graphData.stats.totalCrossFileEdges}</span></div>
21
+ `;
22
+
23
+ // Render diagram
24
+ renderArcDiagram();
25
+
26
+ // Setup interactions
27
+ setupSearch();
28
+ setupExport();
29
+
30
+ // Setup WebSocket for live updates
31
+ setupWebSocket();
32
+
33
+ // Handle window resize
34
+ window.addEventListener('resize', () => {
35
+ renderArcDiagram();
36
+ });
37
+
38
+ } catch (error) {
39
+ console.error('Failed to load graph data:', error);
40
+ document.getElementById('detailPanel').querySelector('.detail-content').innerHTML =
41
+ '<p style="color: #ff4a4a;">Failed to load graph data. Please check the console.</p>';
42
+ }
43
+ }
44
+
45
+ function setupWebSocket() {
46
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
47
+ const wsUrl = `${protocol}//${window.location.host}`;
48
+
49
+ ws = new WebSocket(wsUrl);
50
+
51
+ ws.onopen = () => {
52
+ console.log('WebSocket connected — live updates enabled');
53
+ showNotification('Live updates enabled', 'success');
54
+ };
55
+
56
+ ws.onmessage = async (event) => {
57
+ const message = JSON.parse(event.data);
58
+
59
+ if (message.type === 'refresh') {
60
+ console.log('Graph updated — refreshing visualization...');
61
+ showNotification('Graph updated', 'info');
62
+
63
+ // Re-fetch graph data
64
+ try {
65
+ const response = await fetch('/api/graph');
66
+ graphData = await response.json();
67
+
68
+ // Update header stats
69
+ document.getElementById('stats').innerHTML = `
70
+ <div class="stat-item"><span class="stat-label">Files:</span> <span class="stat-value">${graphData.stats.totalFiles}</span></div>
71
+ <div class="stat-item"><span class="stat-label">Symbols:</span> <span class="stat-value">${graphData.stats.totalSymbols}</span></div>
72
+ <div class="stat-item"><span class="stat-label">Edges:</span> <span class="stat-value">${graphData.stats.totalCrossFileEdges}</span></div>
73
+ `;
74
+
75
+ // Re-render diagram
76
+ renderArcDiagram();
77
+ } catch (error) {
78
+ console.error('Failed to refresh graph data:', error);
79
+ showNotification('Failed to refresh', 'error');
80
+ }
81
+ }
82
+ };
83
+
84
+ ws.onclose = () => {
85
+ console.log('WebSocket disconnected — attempting reconnect in 3s...');
86
+ showNotification('Connection lost — reconnecting...', 'warning');
87
+ setTimeout(() => {
88
+ setupWebSocket();
89
+ }, 3000);
90
+ };
91
+
92
+ ws.onerror = (error) => {
93
+ console.error('WebSocket error:', error);
94
+ };
95
+ }
96
+
97
+ function showNotification(message, type = 'info') {
98
+ const notification = document.createElement('div');
99
+ notification.className = `notification notification-${type}`;
100
+ notification.textContent = message;
101
+ document.body.appendChild(notification);
102
+
103
+ // Fade in
104
+ setTimeout(() => {
105
+ notification.classList.add('show');
106
+ }, 10);
107
+
108
+ // Fade out and remove
109
+ setTimeout(() => {
110
+ notification.classList.remove('show');
111
+ setTimeout(() => {
112
+ notification.remove();
113
+ }, 300);
114
+ }, 3000);
115
+ }
116
+
117
+ function renderArcDiagram() {
118
+ const container = document.querySelector('.diagram-container');
119
+ const width = container.clientWidth;
120
+ const height = container.clientHeight;
121
+
122
+ // Reset state
123
+ filePositions.clear();
124
+ selectedFile = null;
125
+ selectedArc = null;
126
+
127
+ // Clear existing SVG
128
+ d3.select('#diagram').selectAll('*').remove();
129
+
130
+ svg = d3.select('#diagram')
131
+ .attr('width', width)
132
+ .attr('height', height);
133
+
134
+ g = svg.append('g');
135
+
136
+ // Add zoom behavior
137
+ const zoom = d3.zoom()
138
+ .scaleExtent([0.5, 4])
139
+ .on('zoom', (event) => {
140
+ g.attr('transform', event.transform);
141
+ });
142
+
143
+ svg.call(zoom);
144
+
145
+ // Layout calculations
146
+ const margin = { top: 60, right: 40, bottom: 120, left: 40 };
147
+ const plotWidth = width - margin.left - margin.right;
148
+ const plotHeight = height - margin.top - margin.bottom;
149
+ const baseline = margin.top + plotHeight;
150
+
151
+ // Calculate file bar positions
152
+ const totalSymbols = d3.sum(graphData.files, d => d.symbolCount);
153
+ const minBarWidth = 4;
154
+ const gap = 2;
155
+
156
+ let x = margin.left;
157
+ filePositions.clear();
158
+
159
+ graphData.files.forEach(file => {
160
+ const barWidth = Math.max(minBarWidth, (file.symbolCount / totalSymbols) * plotWidth * 0.8);
161
+ filePositions.set(file.path, {
162
+ x: x + barWidth / 2,
163
+ width: barWidth,
164
+ file: file
165
+ });
166
+ x += barWidth + gap;
167
+ });
168
+
169
+ // Get directory colors
170
+ const directories = [...new Set(graphData.files.map(f => f.directory))];
171
+ const colorScale = d3.scaleOrdinal()
172
+ .domain(directories)
173
+ .range(['#4a9eff', '#7c3aed', '#ec4899', '#f59e0b', '#10b981', '#06b6d4']);
174
+
175
+ // Calculate max distance for color scaling
176
+ const positions = Array.from(filePositions.values());
177
+ const maxDistance = d3.max(graphData.arcs, arc => {
178
+ const sourcePos = filePositions.get(arc.sourceFile);
179
+ const targetPos = filePositions.get(arc.targetFile);
180
+ if (!sourcePos || !targetPos) return 0;
181
+ return Math.abs(targetPos.x - sourcePos.x);
182
+ });
183
+
184
+ // Draw arcs
185
+ const arcs = g.selectAll('.arc')
186
+ .data(graphData.arcs)
187
+ .enter()
188
+ .append('path')
189
+ .attr('class', 'arc')
190
+ .attr('d', d => {
191
+ const sourcePos = filePositions.get(d.sourceFile);
192
+ const targetPos = filePositions.get(d.targetFile);
193
+ if (!sourcePos || !targetPos) return null;
194
+
195
+ const x1 = sourcePos.x;
196
+ const x2 = targetPos.x;
197
+ const distance = Math.abs(x2 - x1);
198
+ const height = distance * 0.4;
199
+ const midX = (x1 + x2) / 2;
200
+
201
+ return `M ${x1} ${baseline} Q ${midX} ${baseline - height} ${x2} ${baseline}`;
202
+ })
203
+ .attr('stroke', d => {
204
+ const sourcePos = filePositions.get(d.sourceFile);
205
+ const targetPos = filePositions.get(d.targetFile);
206
+ if (!sourcePos || !targetPos) return '#4a9eff';
207
+
208
+ const distance = Math.abs(targetPos.x - sourcePos.x);
209
+ const t = distance / maxDistance;
210
+ return d3.interpolateRainbow(t);
211
+ })
212
+ .attr('stroke-width', d => Math.min(4, 1 + Math.log(d.edgeCount)))
213
+ .on('mouseover', handleArcHover)
214
+ .on('mouseout', handleArcOut)
215
+ .on('click', handleArcClick);
216
+
217
+ // Draw file bars
218
+ const bars = g.selectAll('.file-bar')
219
+ .data(graphData.files)
220
+ .enter()
221
+ .append('rect')
222
+ .attr('class', 'file-bar')
223
+ .attr('x', d => {
224
+ const pos = filePositions.get(d.path);
225
+ return pos.x - pos.width / 2;
226
+ })
227
+ .attr('y', baseline)
228
+ .attr('width', d => filePositions.get(d.path).width)
229
+ .attr('height', 8)
230
+ .attr('fill', d => colorScale(d.directory))
231
+ .on('mouseover', handleBarHover)
232
+ .on('mouseout', handleBarOut)
233
+ .on('click', handleBarClick);
234
+
235
+ // Draw file labels
236
+ const labels = g.selectAll('.file-label')
237
+ .data(graphData.files)
238
+ .enter()
239
+ .append('text')
240
+ .attr('class', 'file-label')
241
+ .attr('x', d => filePositions.get(d.path).x)
242
+ .attr('y', baseline + 20)
243
+ .attr('transform', d => `rotate(-45, ${filePositions.get(d.path).x}, ${baseline + 20})`)
244
+ .attr('text-anchor', 'end')
245
+ .text(d => d.path.split('/').pop());
246
+
247
+ // Reset view button
248
+ svg.append('text')
249
+ .attr('x', 10)
250
+ .attr('y', 20)
251
+ .attr('fill', '#4a9eff')
252
+ .attr('font-size', '12px')
253
+ .attr('cursor', 'pointer')
254
+ .text('↺ Reset View')
255
+ .on('click', () => {
256
+ svg.transition()
257
+ .duration(750)
258
+ .call(zoom.transform, d3.zoomIdentity);
259
+ clearSelection();
260
+ });
261
+ }
262
+
263
+ function handleArcHover(event, d) {
264
+ if (selectedArc) return;
265
+
266
+ // Highlight arc
267
+ d3.select(event.currentTarget).classed('highlighted', true);
268
+
269
+ // Dim other arcs
270
+ d3.selectAll('.arc').filter(arc => arc !== d).classed('dimmed', true);
271
+
272
+ // Highlight connected file bars
273
+ d3.selectAll('.file-bar')
274
+ .filter(f => f.path === d.sourceFile || f.path === d.targetFile)
275
+ .classed('highlighted', true);
276
+
277
+ d3.selectAll('.file-bar')
278
+ .filter(f => f.path !== d.sourceFile && f.path !== d.targetFile)
279
+ .classed('dimmed', true);
280
+
281
+ // Show tooltip
282
+ showTooltip(event, `
283
+ <div class="tooltip-line"><strong>${d.sourceFile}</strong> → <strong>${d.targetFile}</strong></div>
284
+ <div class="tooltip-line"><span class="tooltip-label">Edges:</span> ${d.edgeCount}</div>
285
+ <div class="tooltip-line"><span class="tooltip-label">Types:</span> ${d.edgeKinds.join(', ')}</div>
286
+ `);
287
+
288
+ // Update detail panel
289
+ updateDetailPanel(`
290
+ <p class="detail-title">Arc: ${d.sourceFile} → ${d.targetFile}</p>
291
+ <p class="detail-info"><span class="detail-label">Edge count:</span> ${d.edgeCount}</p>
292
+ <p class="detail-info"><span class="detail-label">Edge types:</span> ${d.edgeKinds.join(', ')}</p>
293
+ `);
294
+ }
295
+
296
+ function handleArcOut(event, d) {
297
+ if (selectedArc) return;
298
+
299
+ d3.select(event.currentTarget).classed('highlighted', false);
300
+ d3.selectAll('.arc').classed('dimmed', false);
301
+ d3.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
302
+
303
+ hideTooltip();
304
+ resetDetailPanel();
305
+ }
306
+
307
+ function handleArcClick(event, d) {
308
+ event.stopPropagation();
309
+
310
+ if (selectedArc === d) {
311
+ // Deselect
312
+ selectedArc = null;
313
+ handleArcOut(event, d);
314
+ } else {
315
+ // Select
316
+ selectedArc = d;
317
+ selectedFile = null;
318
+
319
+ d3.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
320
+ d3.select(event.currentTarget).classed('highlighted', true);
321
+ d3.selectAll('.arc').filter(arc => arc !== d).classed('dimmed', true);
322
+
323
+ d3.selectAll('.file-bar')
324
+ .classed('highlighted', false)
325
+ .classed('dimmed', false)
326
+ .filter(f => f.path === d.sourceFile || f.path === d.targetFile)
327
+ .classed('highlighted', true);
328
+
329
+ d3.selectAll('.file-bar')
330
+ .filter(f => f.path !== d.sourceFile && f.path !== d.targetFile)
331
+ .classed('dimmed', true);
332
+ }
333
+ }
334
+
335
+ function handleBarHover(event, d) {
336
+ if (selectedFile) return;
337
+
338
+ // Highlight bar
339
+ d3.select(event.currentTarget).classed('highlighted', true);
340
+
341
+ // Highlight connected arcs
342
+ const connectedArcs = graphData.arcs.filter(arc =>
343
+ arc.sourceFile === d.path || arc.targetFile === d.path
344
+ );
345
+
346
+ d3.selectAll('.arc')
347
+ .classed('highlighted', arc => connectedArcs.includes(arc))
348
+ .classed('dimmed', arc => !connectedArcs.includes(arc));
349
+
350
+ // Dim other bars
351
+ d3.selectAll('.file-bar').filter(f => f !== d).classed('dimmed', true);
352
+
353
+ // Show tooltip
354
+ showTooltip(event, `
355
+ <div class="tooltip-line"><strong>${d.path}</strong></div>
356
+ <div class="tooltip-line"><span class="tooltip-label">Symbols:</span> ${d.symbolCount}</div>
357
+ <div class="tooltip-line"><span class="tooltip-label">Incoming:</span> ${d.incomingCount} connections</div>
358
+ <div class="tooltip-line"><span class="tooltip-label">Outgoing:</span> ${d.outgoingCount} connections</div>
359
+ `);
360
+
361
+ // Update detail panel
362
+ updateDetailPanel(`
363
+ <p class="detail-title">File: ${d.path}</p>
364
+ <p class="detail-info"><span class="detail-label">Directory:</span> ${d.directory}</p>
365
+ <p class="detail-info"><span class="detail-label">Symbols:</span> ${d.symbolCount}</p>
366
+ <p class="detail-info"><span class="detail-label">Incoming connections:</span> ${d.incomingCount}</p>
367
+ <p class="detail-info"><span class="detail-label">Outgoing connections:</span> ${d.outgoingCount}</p>
368
+ `);
369
+ }
370
+
371
+ function handleBarOut(event, d) {
372
+ if (selectedFile) return;
373
+
374
+ d3.select(event.currentTarget).classed('highlighted', false);
375
+ d3.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
376
+ d3.selectAll('.file-bar').classed('dimmed', false);
377
+
378
+ hideTooltip();
379
+ resetDetailPanel();
380
+ }
381
+
382
+ function handleBarClick(event, d) {
383
+ event.stopPropagation();
384
+
385
+ if (selectedFile === d) {
386
+ // Deselect
387
+ selectedFile = null;
388
+ handleBarOut(event, d);
389
+ } else {
390
+ // Select
391
+ selectedFile = d;
392
+ selectedArc = null;
393
+
394
+ const connectedArcs = graphData.arcs.filter(arc =>
395
+ arc.sourceFile === d.path || arc.targetFile === d.path
396
+ );
397
+
398
+ d3.selectAll('.arc')
399
+ .classed('highlighted', arc => connectedArcs.includes(arc))
400
+ .classed('dimmed', arc => !connectedArcs.includes(arc));
401
+
402
+ d3.selectAll('.file-bar')
403
+ .classed('highlighted', f => f === d)
404
+ .classed('dimmed', f => f !== d);
405
+ }
406
+ }
407
+
408
+ function showTooltip(event, content) {
409
+ const tooltip = document.getElementById('tooltip');
410
+ tooltip.innerHTML = content;
411
+ tooltip.style.left = (event.pageX + 10) + 'px';
412
+ tooltip.style.top = (event.pageY + 10) + 'px';
413
+ tooltip.classList.add('show');
414
+
415
+ // Update position on mouse move
416
+ d3.select('body').on('mousemove.tooltip', (e) => {
417
+ tooltip.style.left = (e.pageX + 10) + 'px';
418
+ tooltip.style.top = (e.pageY + 10) + 'px';
419
+ });
420
+ }
421
+
422
+ function hideTooltip() {
423
+ document.getElementById('tooltip').classList.remove('show');
424
+ d3.select('body').on('mousemove.tooltip', null);
425
+ }
426
+
427
+ function updateDetailPanel(content) {
428
+ document.getElementById('detailPanel').querySelector('.detail-content').innerHTML = content;
429
+ }
430
+
431
+ function resetDetailPanel() {
432
+ updateDetailPanel('<p class="detail-hint">Hover over arcs or files to see connection details</p>');
433
+ }
434
+
435
+ function clearSelection() {
436
+ selectedFile = null;
437
+ selectedArc = null;
438
+ d3.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
439
+ d3.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
440
+ resetDetailPanel();
441
+ }
442
+
443
+ function setupSearch() {
444
+ const searchInput = document.getElementById('searchInput');
445
+
446
+ searchInput.addEventListener('input', (e) => {
447
+ const query = e.target.value.toLowerCase().trim();
448
+
449
+ if (!query) {
450
+ clearSelection();
451
+ return;
452
+ }
453
+
454
+ // Find matching files
455
+ const matchingFiles = graphData.files.filter(f =>
456
+ f.path.toLowerCase().includes(query)
457
+ );
458
+
459
+ if (matchingFiles.length === 0) {
460
+ clearSelection();
461
+ return;
462
+ }
463
+
464
+ // Highlight matching files and their arcs
465
+ const matchingPaths = new Set(matchingFiles.map(f => f.path));
466
+
467
+ d3.selectAll('.file-bar')
468
+ .classed('highlighted', f => matchingPaths.has(f.path))
469
+ .classed('dimmed', f => !matchingPaths.has(f.path));
470
+
471
+ const connectedArcs = graphData.arcs.filter(arc =>
472
+ matchingPaths.has(arc.sourceFile) || matchingPaths.has(arc.targetFile)
473
+ );
474
+
475
+ d3.selectAll('.arc')
476
+ .classed('highlighted', arc => connectedArcs.includes(arc))
477
+ .classed('dimmed', arc => !connectedArcs.includes(arc));
478
+
479
+ // Update detail panel
480
+ updateDetailPanel(`
481
+ <p class="detail-title">Search Results: ${matchingFiles.length} file(s)</p>
482
+ ${matchingFiles.slice(0, 10).map(f => `<p class="detail-info">${f.path}</p>`).join('')}
483
+ ${matchingFiles.length > 10 ? '<p class="detail-info">...</p>' : ''}
484
+ `);
485
+ });
486
+
487
+ // Clear on Escape
488
+ searchInput.addEventListener('keydown', (e) => {
489
+ if (e.key === 'Escape') {
490
+ searchInput.value = '';
491
+ clearSelection();
492
+ }
493
+ });
494
+ }
495
+
496
+ function setupExport() {
497
+ const exportButton = document.getElementById('exportButton');
498
+ const exportMenu = document.getElementById('exportMenu');
499
+ const exportSvg = document.getElementById('exportSvg');
500
+ const exportPng = document.getElementById('exportPng');
501
+
502
+ exportButton.addEventListener('click', () => {
503
+ exportMenu.classList.toggle('show');
504
+ });
505
+
506
+ document.addEventListener('click', (e) => {
507
+ if (!e.target.closest('.export-dropdown')) {
508
+ exportMenu.classList.remove('show');
509
+ }
510
+ });
511
+
512
+ exportSvg.addEventListener('click', () => {
513
+ exportToSVG();
514
+ exportMenu.classList.remove('show');
515
+ });
516
+
517
+ exportPng.addEventListener('click', () => {
518
+ exportToPNG();
519
+ exportMenu.classList.remove('show');
520
+ });
521
+ }
522
+
523
+ function exportToSVG() {
524
+ const svgElement = document.getElementById('diagram');
525
+ const serializer = new XMLSerializer();
526
+ const svgString = serializer.serializeToString(svgElement);
527
+
528
+ const blob = new Blob([svgString], { type: 'image/svg+xml' });
529
+ const url = URL.createObjectURL(blob);
530
+
531
+ const link = document.createElement('a');
532
+ link.href = url;
533
+ link.download = `depwire-${graphData.projectName}.svg`;
534
+ link.click();
535
+
536
+ URL.revokeObjectURL(url);
537
+ }
538
+
539
+ function exportToPNG() {
540
+ const svgElement = document.getElementById('diagram');
541
+ const svgString = new XMLSerializer().serializeToString(svgElement);
542
+ const canvas = document.createElement('canvas');
543
+ const ctx = canvas.getContext('2d');
544
+
545
+ const img = new Image();
546
+ const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
547
+ const url = URL.createObjectURL(svgBlob);
548
+
549
+ img.onload = () => {
550
+ canvas.width = svgElement.clientWidth;
551
+ canvas.height = svgElement.clientHeight;
552
+ ctx.fillStyle = '#1a1a2e';
553
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
554
+ ctx.drawImage(img, 0, 0);
555
+
556
+ canvas.toBlob((blob) => {
557
+ const pngUrl = URL.createObjectURL(blob);
558
+ const link = document.createElement('a');
559
+ link.href = pngUrl;
560
+ link.download = `depwire-${graphData.projectName}.png`;
561
+ link.click();
562
+
563
+ URL.revokeObjectURL(pngUrl);
564
+ URL.revokeObjectURL(url);
565
+ });
566
+ };
567
+
568
+ img.src = url;
569
+ }
570
+
571
+ // Clear selection on clicking background
572
+ d3.select('body').on('click', () => {
573
+ if (!event.target.closest('.arc') && !event.target.closest('.file-bar')) {
574
+ clearSelection();
575
+ }
576
+ });
577
+
578
+ // Initialize on page load
579
+ init();
@@ -0,0 +1,48 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Depwire Visualization</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Crect width='128' height='128' rx='28' fill='%230d1b2a'/%3E%3Crect x='32' y='24' width='6' height='80' rx='3' fill='%2300d4aa' opacity='.9'/%3E%3Cpath d='M35 28Q96 28 96 64Q96 100 35 100' fill='none' stroke='%2300d4aa' stroke-width='3.5' stroke-linecap='round' opacity='.85'/%3E%3Cpath d='M35 38Q82 38 82 64Q82 90 35 90' fill='none' stroke='%2300b4d8' stroke-width='3' stroke-linecap='round' opacity='.7'/%3E%3Cpath d='M35 48Q68 48 68 64Q68 80 35 80' fill='none' stroke='%2348cae4' stroke-width='2.5' stroke-linecap='round' opacity='.6'/%3E%3Ccircle cx='35' cy='28' r='4' fill='%2300d4aa'/%3E%3Ccircle cx='35' cy='100' r='4' fill='%2300d4aa'/%3E%3Ccircle cx='96' cy='64' r='4' fill='%2348cae4'/%3E%3C/svg%3E">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div class="header">
12
+ <div class="header-left">
13
+ <h1 class="title">
14
+ <span class="title-icon">📊</span>
15
+ <span class="title-text">Depwire</span>
16
+ <span class="project-name" id="projectName"></span>
17
+ </h1>
18
+ <div class="stats" id="stats"></div>
19
+ </div>
20
+ <div class="header-right">
21
+ <input type="text" id="searchInput" placeholder="Search files..." class="search-input">
22
+ <div class="export-dropdown">
23
+ <button class="export-button" id="exportButton">Export ▼</button>
24
+ <div class="export-menu" id="exportMenu">
25
+ <button class="export-option" id="exportSvg">Download SVG</button>
26
+ <button class="export-option" id="exportPng">Download PNG</button>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="main-container">
33
+ <div class="diagram-container">
34
+ <svg id="diagram"></svg>
35
+ </div>
36
+ <div class="detail-panel" id="detailPanel">
37
+ <div class="detail-content">
38
+ <p class="detail-hint">Hover over arcs or files to see connection details</p>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="tooltip" id="tooltip"></div>
44
+
45
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
46
+ <script src="arc.js"></script>
47
+ </body>
48
+ </html>