tangled
alpha
login
or
join now
dunkirk.sh
/
traverse
1
fork
atom
snatching amp's walkthrough for my own purposes mwhahaha
traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c
1
fork
atom
overview
issues
pulls
pipelines
feat: add git hash and loading state
dunkirk.sh
1 month ago
15d4ca65
40afd145
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+92
-23
2 changed files
expand all
collapse all
unified
split
src
index.ts
template.ts
+48
-15
src/index.ts
···
5
5
import type { WalkthroughDiagram } from "./types.ts";
6
6
7
7
const PORT = parseInt(process.env.TRAVERSE_PORT || "4173", 10);
8
8
+
const GIT_HASH = await Bun.$`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => "dev");
8
9
9
10
// In-memory diagram store
10
11
const diagrams = new Map<string, WalkthroughDiagram>();
···
26
27
headers: { "Content-Type": "text/html; charset=utf-8" },
27
28
});
28
29
}
29
29
-
return new Response(generateViewerHTML(diagram), {
30
30
+
return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd()), {
30
31
headers: { "Content-Type": "text/html; charset=utf-8" },
31
32
});
32
33
}
···
39
40
40
41
// List available diagrams
41
42
if (url.pathname === "/") {
42
42
-
return new Response(generateIndexHTML(diagrams), {
43
43
+
return new Response(generateIndexHTML(diagrams, GIT_HASH), {
43
44
headers: { "Content-Type": "text/html; charset=utf-8" },
44
45
});
45
46
}
···
157
158
</html>`;
158
159
}
159
160
160
160
-
function generateIndexHTML(diagrams: Map<string, WalkthroughDiagram>): string {
161
161
+
function generateIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHash: string): string {
161
162
const items = [...diagrams.entries()]
162
163
.map(
163
164
([id, d]) => {
164
164
-
const nodeCount = Object.keys(d.nodes).length;
165
165
+
const nodes = Object.values(d.nodes);
166
166
+
const nodeCount = nodes.length;
167
167
+
const preview = nodes.slice(0, 4).map(n => escapeHTML(n.title));
168
168
+
const extra = nodeCount > 4 ? ` <span class="more">+${nodeCount - 4}</span>` : "";
169
169
+
const tags = preview.map(t => `<span class="tag">${t}</span>`).join("") + extra;
165
170
return `<a href="/diagram/${id}" class="diagram-item">
166
166
-
<span class="diagram-title">${escapeHTML(d.summary)}</span>
167
167
-
<span class="diagram-meta">${nodeCount} node${nodeCount !== 1 ? "s" : ""}</span>
171
171
+
<div class="diagram-header">
172
172
+
<span class="diagram-title">${escapeHTML(d.summary)}</span>
173
173
+
<span class="diagram-meta">${nodeCount} node${nodeCount !== 1 ? "s" : ""}</span>
174
174
+
</div>
175
175
+
<div class="diagram-tags">${tags}</div>
168
176
</a>`;
169
177
},
170
178
)
···
208
216
body {
209
217
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
210
218
background: var(--bg); color: var(--text); min-height: 100vh;
219
219
+
display: flex; flex-direction: column;
211
220
}
221
221
+
.main-content { flex: 1; }
212
222
.header {
213
223
padding: 48px 20px 32px;
214
224
max-width: 520px; margin: 0 auto;
···
226
236
.header p { color: var(--text-muted); font-size: 14px; margin-top: 8px; }
227
237
.diagram-list {
228
238
max-width: 520px; margin: 0 auto; padding: 0 20px 48px;
229
229
-
display: flex; flex-direction: column; gap: 8px;
239
239
+
display: flex; flex-direction: column; gap: 12px;
230
240
}
231
241
.diagram-item {
232
232
-
display: flex; align-items: center; justify-content: space-between;
233
233
-
padding: 14px 16px; border: 1px solid var(--border);
242
242
+
display: flex; flex-direction: column; gap: 10px;
243
243
+
padding: 16px; border: 1px solid var(--border);
234
244
border-radius: 8px; text-decoration: none; color: var(--text);
235
245
transition: border-color 0.15s, background 0.15s;
236
246
}
237
247
.diagram-item:hover {
238
248
border-color: var(--text-muted); background: var(--code-bg);
239
249
}
250
250
+
.diagram-header {
251
251
+
display: flex; align-items: center; justify-content: space-between;
252
252
+
}
240
253
.diagram-title { font-size: 14px; font-weight: 500; }
241
254
.diagram-meta {
242
255
font-size: 12px; color: var(--text-muted);
243
256
flex-shrink: 0; margin-left: 12px;
244
257
}
258
258
+
.diagram-tags {
259
259
+
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
260
260
+
}
261
261
+
.diagram-tags .tag {
262
262
+
font-size: 11px; color: var(--text-muted);
263
263
+
background: var(--code-bg); padding: 2px 8px;
264
264
+
border-radius: 4px;
265
265
+
}
266
266
+
.diagram-tags .more {
267
267
+
font-size: 11px; color: var(--text-muted); opacity: 0.6;
268
268
+
}
245
269
.empty {
246
270
max-width: 520px; margin: 0 auto; padding: 60px 20px;
247
271
text-align: center; color: var(--text-muted);
···
254
278
border-radius: 3px; font-size: 12px;
255
279
}
256
280
.site-footer {
257
257
-
padding: 32px 20px; text-align: center;
281
281
+
padding: 32px 20px;
258
282
font-size: 13px; color: var(--text-muted);
283
283
+
display: flex; justify-content: space-between; align-items: center;
259
284
}
260
285
.site-footer .heart { color: #e25555; }
261
286
.site-footer a { color: var(--text); text-decoration: none; }
262
287
.site-footer a:hover { text-decoration: underline; }
288
288
+
.site-footer .hash {
289
289
+
font-family: "SF Mono", "Fira Code", monospace;
290
290
+
font-size: 11px; opacity: 0.6;
291
291
+
color: var(--text-muted) !important;
292
292
+
}
263
293
</style>
264
294
</head>
265
295
<body>
266
266
-
<div class="header">
267
267
-
<h1>Traverse <span>v0.1</span></h1>
268
268
-
<p>Interactive code walkthrough diagrams</p>
296
296
+
<div class="main-content">
297
297
+
<div class="header">
298
298
+
<h1>Traverse <span>v0.1</span></h1>
299
299
+
<p>Interactive code walkthrough diagrams</p>
300
300
+
</div>
301
301
+
${content}
269
302
</div>
270
270
-
${content}
271
303
<footer class="site-footer">
272
272
-
Made with <span class="heart">♥</span> by <a href="https://dunkirk.sh">Kieran Klukas</a> · <a href="https://github.com/taciturnaxolotl/traverse">GitHub</a>
304
304
+
<span>Made with ❤️ by <a href="https://dunkirk.sh">Kieran Klukas</a></span>
305
305
+
<a class="hash" href="https://github.com/taciturnaxolotl/traverse/commit/${escapeHTML(gitHash)}">${escapeHTML(gitHash)}</a>
273
306
</footer>
274
307
</body>
275
308
</html>`;
+44
-8
src/template.ts
···
1
1
import type { WalkthroughDiagram } from "./types.ts";
2
2
3
3
-
export function generateViewerHTML(diagram: WalkthroughDiagram): string {
3
3
+
export function generateViewerHTML(diagram: WalkthroughDiagram, gitHash: string = "dev", projectRoot: string = ""): string {
4
4
const diagramJSON = JSON.stringify(diagram).replace(/<\//g, "<\\/");
5
5
6
6
return `<!DOCTYPE html>
···
56
56
background: var(--bg);
57
57
color: var(--text);
58
58
min-height: 100vh;
59
59
+
display: flex;
60
60
+
flex-direction: column;
59
61
}
60
62
61
63
/* ── Summary bar with breadcrumb ── */
···
77
79
78
80
.summary-bar .label {
79
81
font-weight: 600;
80
80
-
color: var(--accent);
82
82
+
color: var(--text-muted);
81
83
text-transform: uppercase;
82
84
font-size: 11px;
83
85
letter-spacing: 0.05em;
84
86
flex-shrink: 0;
87
87
+
text-decoration: none;
85
88
transition: color 0.15s;
86
89
}
87
90
···
139
142
padding: 24px;
140
143
border: 1px solid var(--border);
141
144
border-radius: 8px;
145
145
+
opacity: 0;
146
146
+
transition: opacity 0.3s ease;
147
147
+
}
148
148
+
149
149
+
.diagram-section.ready {
150
150
+
opacity: 1;
142
151
}
143
152
144
153
.diagram-section pre.mermaid {
···
364
373
max-width: 720px;
365
374
margin: 0 auto;
366
375
padding: 32px 20px;
376
376
+
flex: 1;
367
377
}
368
378
369
379
/* ── Detail section ── */
···
459
469
}
460
470
461
471
.links-list a {
462
462
-
color: var(--accent);
472
472
+
color: #5a7bc4;
463
473
text-decoration: none;
464
474
font-size: 13px;
465
475
font-family: "SF Mono", "Fira Code", monospace;
···
467
477
transition: color 0.15s;
468
478
}
469
479
470
470
-
.links-list a:hover { color: var(--accent-hover); text-decoration: underline; }
480
480
+
.links-list a:hover { color: #7b9ad8; text-decoration: underline; }
471
481
472
482
.code-snippet {
473
483
margin-top: 12px;
···
517
527
518
528
.site-footer {
519
529
padding: 32px 20px;
520
520
-
text-align: center;
521
530
font-size: 13px;
522
531
color: var(--text-muted);
532
532
+
display: flex;
533
533
+
justify-content: space-between;
534
534
+
align-items: center;
523
535
}
524
536
525
537
.site-footer .heart { color: #e25555; }
···
530
542
}
531
543
532
544
.site-footer a:hover { text-decoration: underline; }
545
545
+
546
546
+
.site-footer .hash {
547
547
+
font-family: "SF Mono", "Fira Code", monospace;
548
548
+
font-size: 11px;
549
549
+
color: var(--text-muted) !important;
550
550
+
opacity: 0.6;
551
551
+
}
533
552
</style>
534
553
</head>
535
554
<body>
536
555
<div class="summary-bar">
537
537
-
<a class="label" href="/" style="text-decoration:none;color:inherit">Traverse</a>
556
556
+
<a class="label" href="/">Traverse</a>
538
557
<span class="sep">›</span>
539
558
<span class="breadcrumb-title" id="breadcrumb-title">${escapeHTML(diagram.summary)}</span>
540
559
<span class="sep header-sep">›</span>
···
552
571
</div>
553
572
554
573
<footer class="site-footer">
555
555
-
Made with <span class="heart">♥</span> by <a href="https://dunkirk.sh">Kieran Klukas</a> · <a href="https://github.com/taciturnaxolotl/traverse">GitHub</a>
574
574
+
<span>Made with ❤️ by <a href="https://dunkirk.sh">Kieran Klukas</a></span>
575
575
+
<a class="hash" href="https://github.com/taciturnaxolotl/traverse/commit/${escapeHTML(gitHash)}">${escapeHTML(gitHash)}</a>
556
576
</footer>
557
577
558
578
<script type="module">
···
562
582
mermaid.registerLayoutLoaders(elkLayouts);
563
583
564
584
const DIAGRAM_DATA = ${diagramJSON};
585
585
+
const PROJECT_ROOT = ${JSON.stringify(projectRoot)};
565
586
566
587
const COPY_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="8" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5H11"/></svg>';
567
588
const CHECK_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5 6.5-7"/></svg>';
···
589
610
// Set viewBox so the SVG scales to fit the container
590
611
requestAnimationFrame(() => {
591
612
fitDiagram();
613
613
+
document.querySelector(".diagram-section").classList.add("ready");
592
614
attachClickHandlers();
593
615
594
616
// Check URL hash for deep link
···
616
638
}
617
639
});
618
640
641
641
+
// Escape key to deselect
642
642
+
document.addEventListener("keydown", (e) => {
643
643
+
if (e.key === "Escape" && selectedNodeId) deselectAll();
644
644
+
});
645
645
+
619
646
// Handle browser back/forward
620
647
window.addEventListener("hashchange", () => {
621
648
const hash = window.location.hash.slice(1);
···
711
738
html += '<div class="section-label">Related Files</div>';
712
739
html += '<ul class="links-list">';
713
740
meta.links.forEach(link => {
714
714
-
html += '<li><a href="' + escapeAttr(link.url) + '">' + escapeText(link.label) + "</a></li>";
741
741
+
const href = buildFileUrl(link.label, link.url);
742
742
+
html += '<li><a href="' + escapeAttr(href) + '">' + escapeText(link.label) + "</a></li>";
715
743
});
716
744
html += "</ul>";
717
745
}
···
814
842
transitionContent(() => {
815
843
renderAllNodes();
816
844
});
845
845
+
}
846
846
+
847
847
+
function buildFileUrl(label, url) {
848
848
+
// Parse line number from label like "src/index.ts:56-59" or "src/index.ts:56"
849
849
+
const lineMatch = label.match(/:(\d+)/);
850
850
+
const line = lineMatch ? lineMatch[1] : "1";
851
851
+
const filePath = PROJECT_ROOT + "/" + url;
852
852
+
return "vscode://file/" + filePath + ":" + line;
817
853
}
818
854
819
855
function escapeText(s) {