snatching amp's walkthrough for my own purposes mwhahaha traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: polish it up

+392 -35
+168 -18
src/index.ts
··· 21 21 const id = match[1]!; 22 22 const diagram = diagrams.get(id); 23 23 if (!diagram) { 24 - return new Response("Diagram not found", { status: 404 }); 24 + return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), { 25 + status: 404, 26 + headers: { "Content-Type": "text/html; charset=utf-8" }, 27 + }); 25 28 } 26 29 return new Response(generateViewerHTML(diagram), { 27 30 headers: { "Content-Type": "text/html; charset=utf-8" }, ··· 30 33 31 34 // List available diagrams 32 35 if (url.pathname === "/") { 33 - if (diagrams.size === 0) { 34 - return new Response( 35 - "<html><body style='font-family:system-ui;padding:40px;color:#666'><h2>Traverse</h2><p>No diagrams yet. Use the MCP tool to create one.</p></body></html>", 36 - { headers: { "Content-Type": "text/html" } }, 37 - ); 38 - } 39 - const links = [...diagrams.entries()] 40 - .map( 41 - ([id, d]) => 42 - `<li><a href="/diagram/${id}">${escapeHTML(d.summary)}</a></li>`, 43 - ) 44 - .join("\n"); 45 - return new Response( 46 - `<html><body style='font-family:system-ui;padding:40px'><h2>Traverse</h2><ul>${links}</ul></body></html>`, 47 - { headers: { "Content-Type": "text/html" } }, 48 - ); 36 + return new Response(generateIndexHTML(diagrams), { 37 + headers: { "Content-Type": "text/html; charset=utf-8" }, 38 + }); 49 39 } 50 40 51 - return new Response("Not found", { status: 404 }); 41 + return new Response(generate404HTML("Page not found", "There's nothing at this URL."), { 42 + status: 404, 43 + headers: { "Content-Type": "text/html; charset=utf-8" }, 44 + }); 52 45 }, 53 46 }); 54 47 ··· 108 101 // Connect MCP server to stdio transport 109 102 const transport = new StdioServerTransport(); 110 103 await server.connect(transport); 104 + 105 + function generate404HTML(title: string, message: string): string { 106 + return `<!DOCTYPE html> 107 + <html lang="en"> 108 + <head> 109 + <meta charset="UTF-8" /> 110 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 111 + <title>Traverse — ${escapeHTML(title)}</title> 112 + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='14' fill='%232563eb'/><path d='M10 12h12M10 16h12M10 20h12' stroke='white' stroke-width='2' stroke-linecap='round'/></svg>" /> 113 + <style> 114 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 115 + :root { 116 + --bg: #fafafa; --text: #1a1a1a; --text-muted: #666; 117 + --border: #e2e2e2; --code-bg: #f4f4f5; 118 + } 119 + @media (prefers-color-scheme: dark) { 120 + :root { 121 + --bg: #0a0a0a; --text: #e5e5e5; --text-muted: #a3a3a3; 122 + --border: #262626; --code-bg: #1c1c1e; 123 + } 124 + } 125 + body { 126 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 127 + background: var(--bg); color: var(--text); min-height: 100vh; 128 + display: flex; align-items: center; justify-content: center; 129 + } 130 + .container { text-align: center; padding: 20px; } 131 + .code { font-size: 64px; font-weight: 700; color: var(--text-muted); opacity: 0.3; } 132 + h1 { font-size: 20px; font-weight: 600; margin-top: 8px; } 133 + p { color: var(--text-muted); font-size: 14px; margin-top: 8px; } 134 + a { 135 + display: inline-block; margin-top: 24px; font-size: 13px; 136 + color: var(--text); text-decoration: none; 137 + border: 1px solid var(--border); border-radius: 6px; 138 + padding: 8px 16px; transition: all 0.15s; 139 + } 140 + a:hover { border-color: var(--text-muted); background: var(--code-bg); } 141 + </style> 142 + </head> 143 + <body> 144 + <div class="container"> 145 + <div class="code">404</div> 146 + <h1>${escapeHTML(title)}</h1> 147 + <p>${escapeHTML(message)}</p> 148 + <a href="/">Back to diagrams</a> 149 + </div> 150 + </body> 151 + </html>`; 152 + } 153 + 154 + function generateIndexHTML(diagrams: Map<string, WalkthroughDiagram>): string { 155 + const items = [...diagrams.entries()] 156 + .map( 157 + ([id, d]) => { 158 + const nodeCount = Object.keys(d.nodes).length; 159 + return `<a href="/diagram/${id}" class="diagram-item"> 160 + <span class="diagram-title">${escapeHTML(d.summary)}</span> 161 + <span class="diagram-meta">${nodeCount} node${nodeCount !== 1 ? "s" : ""}</span> 162 + </a>`; 163 + }, 164 + ) 165 + .join("\n"); 166 + 167 + const content = diagrams.size === 0 168 + ? `<div class="empty"> 169 + <div class="empty-icon"> 170 + <svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> 171 + <rect x="8" y="8" width="32" height="32" rx="4"/> 172 + <circle cx="20" cy="20" r="3"/><circle cx="28" cy="28" r="3"/> 173 + <path d="M22 21l4 5"/> 174 + </svg> 175 + </div> 176 + <p>No diagrams yet.</p> 177 + <p class="hint">Use the <code>walkthrough_diagram</code> MCP tool to create one.</p> 178 + </div>` 179 + : `<div class="diagram-list">${items}</div>`; 180 + 181 + return `<!DOCTYPE html> 182 + <html lang="en"> 183 + <head> 184 + <meta charset="UTF-8" /> 185 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 186 + <title>Traverse</title> 187 + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='14' fill='%232563eb'/><path d='M10 12h12M10 16h12M10 20h12' stroke='white' stroke-width='2' stroke-linecap='round'/></svg>" /> 188 + <style> 189 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 190 + :root { 191 + --bg: #fafafa; --bg-panel: #ffffff; --border: #e2e2e2; 192 + --text: #1a1a1a; --text-muted: #666; --accent: #2563eb; 193 + --code-bg: #f4f4f5; 194 + } 195 + @media (prefers-color-scheme: dark) { 196 + :root { 197 + --bg: #0a0a0a; --bg-panel: #141414; --border: #262626; 198 + --text: #e5e5e5; --text-muted: #a3a3a3; --accent: #3b82f6; 199 + --code-bg: #1c1c1e; 200 + } 201 + } 202 + body { 203 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 204 + background: var(--bg); color: var(--text); min-height: 100vh; 205 + } 206 + .header { 207 + padding: 48px 20px 32px; 208 + max-width: 520px; margin: 0 auto; 209 + } 210 + .header h1 { 211 + font-size: 24px; font-weight: 700; 212 + display: flex; align-items: center; gap: 10px; 213 + } 214 + .header h1 span { 215 + font-size: 11px; font-weight: 600; text-transform: uppercase; 216 + letter-spacing: 0.05em; color: var(--text-muted); 217 + background: var(--code-bg); padding: 3px 8px; 218 + border-radius: 4px; 219 + } 220 + .header p { color: var(--text-muted); font-size: 14px; margin-top: 8px; } 221 + .diagram-list { 222 + max-width: 520px; margin: 0 auto; padding: 0 20px 48px; 223 + display: flex; flex-direction: column; gap: 8px; 224 + } 225 + .diagram-item { 226 + display: flex; align-items: center; justify-content: space-between; 227 + padding: 14px 16px; border: 1px solid var(--border); 228 + border-radius: 8px; text-decoration: none; color: var(--text); 229 + transition: border-color 0.15s, background 0.15s; 230 + } 231 + .diagram-item:hover { 232 + border-color: var(--text-muted); background: var(--code-bg); 233 + } 234 + .diagram-title { font-size: 14px; font-weight: 500; } 235 + .diagram-meta { 236 + font-size: 12px; color: var(--text-muted); 237 + flex-shrink: 0; margin-left: 12px; 238 + } 239 + .empty { 240 + max-width: 520px; margin: 0 auto; padding: 60px 20px; 241 + text-align: center; color: var(--text-muted); 242 + } 243 + .empty-icon { margin-bottom: 16px; opacity: 0.4; } 244 + .empty p { font-size: 15px; } 245 + .empty .hint { font-size: 13px; margin-top: 8px; } 246 + .empty code { 247 + background: var(--code-bg); padding: 2px 6px; 248 + border-radius: 3px; font-size: 12px; 249 + } 250 + </style> 251 + </head> 252 + <body> 253 + <div class="header"> 254 + <h1>Traverse <span>v0.1</span></h1> 255 + <p>Interactive code walkthrough diagrams</p> 256 + </div> 257 + ${content} 258 + </body> 259 + </html>`; 260 + } 111 261 112 262 function escapeHTML(str: string): string { 113 263 return str
+224 -17
src/template.ts
··· 9 9 <meta charset="UTF-8" /> 10 10 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 11 11 <title>Traverse — ${escapeHTML(diagram.summary)}</title> 12 + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='14' fill='%232563eb'/><path d='M10 12h12M10 16h12M10 20h12' stroke='white' stroke-width='2' stroke-linecap='round'/></svg>" /> 12 13 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github-dark.min.css" id="hljs-dark" disabled /> 13 14 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github.min.css" id="hljs-light" /> 14 15 <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script> ··· 24 25 --text-muted: #666; 25 26 --accent: #2563eb; 26 27 --accent-hover: #1d4ed8; 28 + --accent-subtle: rgba(37, 99, 235, 0.08); 27 29 --node-hover: rgba(37, 99, 235, 0.08); 28 30 --code-bg: #f4f4f5; 29 31 --summary-bg: #f0f4ff; ··· 40 42 --text-muted: #a3a3a3; 41 43 --accent: #3b82f6; 42 44 --accent-hover: #60a5fa; 45 + --accent-subtle: rgba(59, 130, 246, 0.1); 43 46 --node-hover: rgba(59, 130, 246, 0.12); 44 47 --code-bg: #1c1c1e; 45 48 --summary-bg: #111827; ··· 55 58 min-height: 100vh; 56 59 } 57 60 61 + /* ── Summary bar with breadcrumb ── */ 58 62 .summary-bar { 63 + position: sticky; 64 + top: 0; 65 + z-index: 100; 59 66 padding: 12px 20px; 60 67 background: var(--summary-bg); 61 68 border-bottom: 1px solid var(--border); ··· 64 71 display: flex; 65 72 align-items: center; 66 73 gap: 8px; 74 + backdrop-filter: blur(12px); 75 + -webkit-backdrop-filter: blur(12px); 67 76 } 68 77 69 78 .summary-bar .label { ··· 72 81 text-transform: uppercase; 73 82 font-size: 11px; 74 83 letter-spacing: 0.05em; 84 + flex-shrink: 0; 85 + } 86 + 87 + .summary-bar .sep { 88 + color: var(--text-muted); 89 + flex-shrink: 0; 90 + font-size: 11px; 91 + } 92 + 93 + .summary-bar .breadcrumb-title { 94 + color: var(--text-muted); 95 + overflow: hidden; 96 + text-overflow: ellipsis; 97 + white-space: nowrap; 98 + } 99 + 100 + body.has-selection .summary-bar .breadcrumb-title { 101 + cursor: pointer; 102 + } 103 + 104 + body.has-selection .summary-bar .breadcrumb-title:hover { 105 + color: var(--text); 106 + } 107 + 108 + .summary-bar .header-sep, 109 + .summary-bar .header-node { 110 + display: none; 111 + } 112 + 113 + body.has-selection .summary-bar .header-sep, 114 + body.has-selection .summary-bar .header-node { 115 + display: inline; 116 + } 117 + 118 + .summary-bar .header-node { 119 + color: var(--text); 120 + font-weight: 500; 121 + overflow: hidden; 122 + text-overflow: ellipsis; 123 + white-space: nowrap; 75 124 } 76 125 77 126 .diagram-section { ··· 88 137 } 89 138 90 139 .diagram-section svg { 91 - max-height: none !important; 92 - height: auto !important; 140 + max-height: calc(100vh - 100px); 93 141 width: 100%; 94 142 } 95 143 ··· 124 172 .diagram-section .node g path { 125 173 fill: var(--bg) !important; 126 174 stroke: var(--text) !important; 175 + transition: fill 0.15s, stroke 0.15s, stroke-width 0.15s; 127 176 } 128 177 .diagram-section .node .label, 129 178 .diagram-section .node .nodeLabel, ··· 263 312 } 264 313 265 314 .diagram-section .node.selected :is(rect, circle, ellipse, polygon, path) { 266 - fill: var(--code-bg) !important; 267 - stroke-width: 2.5px !important; 315 + fill: var(--text) !important; 316 + stroke: var(--text) !important; 317 + stroke-width: 2px !important; 318 + } 319 + .diagram-section .node.selected .label, 320 + .diagram-section .node.selected .nodeLabel, 321 + .diagram-section .node.selected text, 322 + .diagram-section .node.selected foreignObject, 323 + .diagram-section .node.selected foreignObject div, 324 + .diagram-section .node.selected foreignObject span, 325 + .diagram-section .node.selected foreignObject p { 326 + color: var(--bg) !important; 327 + fill: var(--bg) !important; 268 328 } 269 329 270 330 /* Edge hover */ ··· 298 358 } 299 359 300 360 /* ── Detail section ── */ 361 + #detail-section { 362 + transition: opacity 0.15s ease; 363 + } 364 + 365 + #detail-section.fading { opacity: 0; } 366 + 301 367 .content-summary { 302 368 font-size: 20px; 303 369 font-weight: 600; ··· 396 462 397 463 .code-snippet { 398 464 margin-top: 12px; 465 + position: relative; 399 466 } 400 467 401 468 .code-snippet pre { ··· 406 473 font-size: 13px; 407 474 line-height: 1.5; 408 475 } 476 + 477 + .copy-btn { 478 + position: absolute; 479 + top: 8px; 480 + right: 8px; 481 + background: var(--bg-panel); 482 + border: 1px solid var(--border); 483 + border-radius: 5px; 484 + padding: 4px 6px; 485 + cursor: pointer; 486 + color: var(--text-muted); 487 + opacity: 0; 488 + transition: opacity 0.15s, color 0.15s, border-color 0.15s; 489 + display: flex; 490 + align-items: center; 491 + justify-content: center; 492 + } 493 + 494 + .copy-btn svg { width: 14px; height: 14px; } 495 + 496 + .code-snippet:hover .copy-btn { opacity: 1; } 497 + 498 + .copy-btn:hover { 499 + color: var(--accent); 500 + border-color: var(--accent); 501 + } 502 + 503 + .copy-btn.copied { 504 + color: #16a34a; 505 + border-color: #16a34a; 506 + opacity: 1; 507 + } 409 508 </style> 410 509 </head> 411 510 <body> 412 511 <div class="summary-bar"> 413 - <span class="label">Traverse</span> 414 - <span>${escapeHTML(diagram.summary)}</span> 512 + <a class="label" href="/" style="text-decoration:none;color:inherit">Traverse</a> 513 + <span class="sep">&rsaquo;</span> 514 + <span class="breadcrumb-title" id="breadcrumb-title">${escapeHTML(diagram.summary)}</span> 515 + <span class="sep header-sep">&rsaquo;</span> 516 + <span class="header-node" id="header-node"></span> 415 517 </div> 416 518 417 519 <div class="content-wrap"> ··· 430 532 431 533 const DIAGRAM_DATA = ${diagramJSON}; 432 534 535 + 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>'; 536 + 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>'; 537 + 433 538 function initTheme() { 434 539 const dark = window.matchMedia("(prefers-color-scheme: dark)").matches; 435 540 document.getElementById("hljs-dark").disabled = !dark; ··· 454 559 requestAnimationFrame(() => { 455 560 fitDiagram(); 456 561 attachClickHandlers(); 457 - renderAllNodes(); 562 + 563 + // Check URL hash for deep link 564 + const hash = window.location.hash.slice(1); 565 + if (hash && DIAGRAM_DATA.nodes[hash]) { 566 + const svg = document.querySelector(".diagram-section svg"); 567 + const nodeEl = svg && findNodeEl(svg, hash); 568 + if (nodeEl) { 569 + selectNode(hash, nodeEl, false); 570 + } else { 571 + renderAllNodes(); 572 + } 573 + } else { 574 + renderAllNodes(); 575 + } 458 576 }); 459 577 460 578 window.addEventListener("resize", fitDiagram); 579 + 580 + // Header breadcrumb title click to deselect 581 + document.getElementById("breadcrumb-title").addEventListener("click", (e) => { 582 + if (selectedNodeId) { 583 + e.stopPropagation(); 584 + deselectAll(); 585 + } 586 + }); 587 + 588 + // Handle browser back/forward 589 + window.addEventListener("hashchange", () => { 590 + const hash = window.location.hash.slice(1); 591 + if (!hash) { 592 + deselectAll(true); 593 + } else if (DIAGRAM_DATA.nodes[hash]) { 594 + const svg = document.querySelector(".diagram-section svg"); 595 + const nodeEl = svg && findNodeEl(svg, hash); 596 + if (nodeEl) selectNode(hash, nodeEl, false); 597 + } 598 + }); 461 599 } 462 600 463 601 function fitDiagram() { ··· 473 611 svg.removeAttribute("height"); 474 612 } 475 613 614 + function findNodeEl(svg, nodeId) { 615 + const nodeIds = Object.keys(DIAGRAM_DATA.nodes); 616 + const allNodes = svg.querySelectorAll(".node"); 617 + for (const nodeEl of allNodes) { 618 + const id = nodeEl.id; 619 + if (!id) continue; 620 + const matchedId = nodeIds.find(nid => 621 + id === nid || 622 + id.endsWith("-" + nid) || 623 + id.startsWith("flowchart-" + nid + "-") || 624 + id.includes("-" + nid + "-") 625 + ); 626 + if (matchedId === nodeId) return nodeEl; 627 + } 628 + return null; 629 + } 630 + 476 631 function attachClickHandlers() { 477 632 const svg = document.querySelector(".diagram-section svg"); 478 633 if (!svg) return; ··· 507 662 508 663 // Click outside to deselect 509 664 document.addEventListener("click", (e) => { 510 - if (!e.target.closest(".detail-section") && !e.target.closest(".node")) { 665 + if (!e.target.closest("#detail-section") && !e.target.closest(".node") && !e.target.closest(".summary-bar")) { 511 666 deselectAll(); 512 667 } 513 668 }); 514 669 } 515 670 516 671 let selectedEl = null; 672 + let selectedNodeId = null; 517 673 518 674 function renderNodeCard(nodeId, meta) { 519 675 let html = '<div class="node-card" data-card-id="' + escapeAttr(nodeId) + '">'; ··· 531 687 532 688 if (meta.codeSnippet) { 533 689 html += '<div class="section-label">Code</div>'; 534 - html += '<div class="code-snippet"><pre><code>' + escapeText(meta.codeSnippet) + "</code></pre></div>"; 690 + html += '<div class="code-snippet"><button class="copy-btn" title="Copy code">' + COPY_ICON + '</button><pre><code>' + escapeText(meta.codeSnippet) + "</code></pre></div>"; 535 691 } 536 692 537 693 html += '</div>'; ··· 546 702 } 547 703 section.innerHTML = html; 548 704 highlightAll(section); 705 + attachCopyButtons(section); 549 706 } 550 707 551 708 function highlightAll(container) { ··· 554 711 }); 555 712 } 556 713 557 - function selectNode(nodeId, el) { 714 + function attachCopyButtons(container) { 715 + container.querySelectorAll(".copy-btn").forEach(btn => { 716 + btn.addEventListener("click", (e) => { 717 + e.stopPropagation(); 718 + const code = btn.closest(".code-snippet").querySelector("code").textContent; 719 + navigator.clipboard.writeText(code).then(() => { 720 + btn.innerHTML = CHECK_ICON; 721 + btn.classList.add("copied"); 722 + setTimeout(() => { 723 + btn.innerHTML = COPY_ICON; 724 + btn.classList.remove("copied"); 725 + }, 1500); 726 + }); 727 + }); 728 + }); 729 + } 730 + 731 + function transitionContent(callback) { 732 + const section = document.getElementById("detail-section"); 733 + section.classList.add("fading"); 734 + setTimeout(() => { 735 + callback(); 736 + section.classList.remove("fading"); 737 + }, 150); 738 + } 739 + 740 + function selectNode(nodeId, el, pushState = true) { 558 741 const meta = DIAGRAM_DATA.nodes[nodeId]; 559 742 if (!meta) return; 560 743 561 744 if (selectedEl) selectedEl.classList.remove("selected"); 562 745 el.classList.add("selected"); 563 746 selectedEl = el; 747 + selectedNodeId = nodeId; 564 748 565 - const section = document.getElementById("detail-section"); 566 - section.innerHTML = renderNodeCard(nodeId, meta); 567 - highlightAll(section); 749 + // Update breadcrumbs 750 + document.body.classList.add("has-selection"); 751 + document.getElementById("header-node").textContent = meta.title; 568 752 569 - section.scrollIntoView({ behavior: "smooth", block: "start" }); 753 + // Update URL hash 754 + if (pushState) { 755 + history.pushState(null, "", "#" + nodeId); 756 + } 757 + 758 + transitionContent(() => { 759 + const section = document.getElementById("detail-section"); 760 + section.innerHTML = renderNodeCard(nodeId, meta); 761 + highlightAll(section); 762 + attachCopyButtons(section); 763 + }); 764 + 570 765 } 571 766 572 - function deselectAll() { 767 + function deselectAll(skipHistory) { 573 768 if (selectedEl) { 574 769 selectedEl.classList.remove("selected"); 575 770 selectedEl = null; 576 771 } 577 - renderAllNodes(); 578 - window.scrollTo({ top: 0, behavior: "smooth" }); 772 + selectedNodeId = null; 773 + 774 + // Update breadcrumbs 775 + document.body.classList.remove("has-selection"); 776 + document.getElementById("header-node").textContent = ""; 777 + 778 + // Clear hash 779 + if (!skipHistory) { 780 + history.pushState(null, "", window.location.pathname); 781 + } 782 + 783 + transitionContent(() => { 784 + renderAllNodes(); 785 + }); 579 786 } 580 787 581 788 function escapeText(s) {