@@ -0,0 +1,660 @@
1 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Copyright CONFLICT LLC 2026 (weareconflict.com)
2 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
3 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ HTML template for the Navegador Graph Explorer.
4 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
5 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ A fully self-contained, single-file HTML page with:
6 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Force-directed graph visualisation via inline canvas + JS physics
7 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Search box that queries /api/search
8 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Node click → detail panel via /api/node/<name>
9 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Stats bar via /api/stats
10 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Zero external dependencies (no CDN, no frameworks)
11 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
12 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
13 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Colour palette per node label ─────────────────────────────────────────────
14 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ NODE_COLORS = {
15 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Function": "#4e9af1",
16 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Method": "#6cb4f5",
17 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Class": "#f4a93b",
18 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "File": "#a8d9a7",
19 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Module": "#82c9a0",
20 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Repository": "#e67e22",
21 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Variable": "#c39bd3",
22 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Import": "#a9cce3",
23 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Decorator": "#f1948a",
24 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Domain": "#f7dc6f",
25 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Concept": "#f9e79f",
26 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Rule": "#f0b27a",
27 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Decision": "#f8c471",
28 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "WikiPage": "#d2b4de",
29 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Person": "#fadbd8",
30 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "default": "#aaaaaa",
31 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
32 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
33 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
34 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _COLORS_JS = "\n".join(f" '{label}': '{color}'," for label, c
35 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ LICT LLC 2026 (weareconflict.com)
36 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
37 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ HTML template for the Navegador Graph Explorer.
38 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
39 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ A fully self-contained, single-file HTML page with:
40 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Force-directed graph visualisation via inline canvas + JS physics
41 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Search box that queries /api/search
42 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Node click → detail panel via /api/node/<name>
43 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Stats bar via /api/stats
44 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Zero external dependencies (no CDN, no frameworks)
45 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
46 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
47 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Colour palette per node label ─────────────────────────────────────────────
48 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ NODE_COLORS = {
49 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Function": "#4e9af1",
50 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Method": "#6cb4f5",
51 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Class": "#f4a93b",
52 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "File": "#a8d9a7",
53 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Module": "#82c9a0",
54 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Repository": "#e67e22",
55 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Variable": "#c39bd3",
56 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Import": "#a9cce3",
57 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Decorator": "#f1948a",
58 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Domain": "#f7dc6f",
59 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Concept": "#f9e79f",
60 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Rule": "#f0b27a",
61 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Decision": "#f8c471",
62 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "WikiPage": "#d2b4de",
63 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Person": "#fadbd8",
64 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "default": "#aaaaaa",
65 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
66 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
67 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _COLORS_JS = "\n".join(f" '{label}': '{color}'," for label, color in NODE_COLORS.items())
68 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
69 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ HTML_TEMPLATE = """<!DOCTYPE html>
70 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <html lang="en">
71 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <head>
72 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <meta charset="UTF-8">
73 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
74 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <title>Navegador Graph Explorer</title>
75 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <style>
76 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
77 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ body {{
78 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
79 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #1a1a2e;
80 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #e0e0e0;
81 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ height: 100vh;
82 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
83 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex-direction: column;
84 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow: hidden;
85 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
86 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #header {{
87 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #16213e;
88 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-bottom: 1px solid #0f3460;
89 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 10px 16px;
90 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
91 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ align-items: center;
92 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ gap: 12px;
93 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex-shrink: 0;
94 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
95 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #header h1 {{
96 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 1.1rem;
97 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #4e9af1;
98 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ letter-spacing: 0.05em;
99 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ white-space: nowrap;
100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #search-box {{
102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex: 1;
103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ max-width: 400px;
104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #0f3460;
105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border: 1px solid #4e9af1;
106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-radius: 6px;
107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 6px 12px;
108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #e0e0e0;
109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.9rem;
110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ outline: none;
111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #search-box::placeholder {{ color: #5a6a8a; }}
113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #search-box:focus {{ border-color: #6cb4f5; box-shadow: 0 0 0 2px rgba(78,154,241,0.2); }}
114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #stats-bar {{
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.75rem;
116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #7a8aaa;
117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ white-space: nowrap;
118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #main {{
120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex: 1;
122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow: hidden;
123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #canvas-wrap {{
125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex: 1;
126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ position: relative;
127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow: hidden;
128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #graph-canvas {{
130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: block;
131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ width: 100%;
132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ height: 100%;
133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cursor: grab;
134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #graph-canvas:active {{ cursor: grabbing; }}
136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #sidebar {{
137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ width: 300px;
138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #16213e;
139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-left: 1px solid #0f3460;
140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex-direction: column;
142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex-shrink: 0;
143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow: hidden;
144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #sidebar-title {{
146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 10px 14px;
147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.8rem;
148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #7a8aaa;
149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-bottom: 1px solid #0f3460;
150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ text-transform: uppercase;
151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ letter-spacing: 0.08em;
152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #search-results {{
154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-bottom: 1px solid #0f3460;
155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ max-height: 220px;
156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow-y: auto;
157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .search-result {{
159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 8px 14px;
160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cursor: pointer;
161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-bottom: 1px solid #0d1b35;
162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ transition: background 0.15s;
163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .search-result:hover {{ background: #0f3460; }}
165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .search-result .sr-name {{ font-size: 0.85rem; font-weight: 600; color: #c8d8f0; }}
166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .search-result .sr-meta {{ font-size: 0.72rem; color: #5a6a8a; margin-top: 2px; }}
167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #detail-panel {{
168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex: 1;
169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 14px;
170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow-y: auto;
171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.82rem;
172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #detail-panel h2 {{
174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 1rem;
175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #4e9af1;
176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin-bottom: 8px;
177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ word-break: break-all;
178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .detail-label {{
180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #5a6a8a;
181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.72rem;
182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ text-transform: uppercase;
183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ letter-spacing: 0.06em;
184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin-top: 10px;
185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin-bottom: 3px;
186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .detail-value {{
188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #c8d8f0;
189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ word-break: break-word;
190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .badge {{
192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: inline-block;
193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 2px 7px;
194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-radius: 10px;
195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.7rem;
196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-weight: 600;
197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin-right: 4px;
198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin-bottom: 4px;
199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .neighbor-item {{
201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #0f3460;
202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-radius: 4px;
203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 4px 8px;
204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin: 3px 0;
205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cursor: pointer;
206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ transition: background 0.15s;
207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ justify-content: space-between;
209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ align-items: center;
210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .neighbor-item:hover {{ background: #1a4a80; }}
212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .neighbor-name {{ color: #c8d8f0; font-size: 0.8rem; }}
213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .neighbor-type {{ color: #5a6a8a; font-size: 0.7rem; }}
214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #empty-hint {{
215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #3a4a6a;
216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ text-align: center;
217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ margin-top: 40px;
218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.85rem;
219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ line-height: 1.6;
220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #loading {{
222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ position: absolute;
223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ top: 50%;
224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ left: 50%;
225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ transform: translate(-50%, -50%);
226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #4e9af1;
227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-size: 0.9rem;
228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ pointer-events: none;
229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ::-webkit-scrollbar {{ width: 5px; }}
231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ::-webkit-scrollbar-track {{ background: #0d1b35; }}
232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ::-webkit-scrollbar-thumb {{ background: #0f3460; border-radius: 3px; }}
233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </style>
234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </head>
235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <body>
236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="header">
237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h1>navegador</h1>
238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input id="search-box" type="text" placeholder="Search nodes..." autocomplete="off">
239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="stats-bar">Loading…</div>
240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
241 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="main">
242 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="canvas-wrap">
243 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <canvas id="graph-canvas"></canvas>
244 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="loading">Loading graph…</div>
245 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
246 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="sidebar">
247 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="sidebar-title">Explorer</div>
248 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="search-results"></div>
249 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="detail-panel">
250 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="empty-hint">Click a node<br>or search above<br>to see details.</div>
251 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
252 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
253 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
254 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <script>
255 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (function() {
256 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'use strict';
257 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
258 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Colour palette ─────────────────────────────────────────────────────────
259 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const NODE_COLORS = {{
260 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ {colors}
261 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }};
262 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function nodeColor(label) {{
263 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return NODE_COLORS[label] || NODE_COLORS['default'];
264 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
265 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── State ──────────────────────────────────────────────────────────────────
267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let nodes = []; // {{id, label, name, x, y, vx, vy, ...props}}
268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let edges = []; // {{source_id, target_id, type}}
269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let nodeById = {{}}; // id → node
270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let selectedNode = null;
272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let hoveredNode = null;
273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Camera
275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let camX = 0, camY = 0, camScale = 1;
276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let isDragging = false, dragStartX = 0, dragStartY = 0, camStartX = 0, camStartY = 0;
277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let isDraggingNode = false, dragNode = null, dragNodeOffX = 0, dragNodeOffY = 0;
278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Physics
280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let physicsRunning = true;
281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const REPEL = 8000;
282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const ATTRACT = 0.04;
283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const EDGE_LEN = 120;
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const DAMPING = 0.85;
285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const MAX_VEL = 12;
286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Canvas setup ────────────────────────────────────────────────────────────
288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const canvas = document.getElementById('graph-canvas');
289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const ctx = canvas.getContext('2d');
290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const wrap = document.getElementById('canvas-wrap');
291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const loading = document.getElementById('loading');
292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function resize() {{
294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.width = wrap.clientWidth;
295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.height = wrap.clientHeight;
296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ window.addEventListener('resize', resize);
298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resize();
299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Fetch graph data ────────────────────────────────────────────────────────
301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadGraph() {{
302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {{
303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await fetch('/api/graph').then(r => r.json());
304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ initGraph(data.nodes || [], data.edges || []);
305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loading.style.display = 'none';
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadStats();
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} catch(e) {{
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loading.textContent = 'Error loading graph.';
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function initGraph(rawNodes, rawEdges) {{
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const cx = canvas.width / 2, cy = canvas.height / 2;
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nodeById = {{}};
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nodes = rawNodes.map((n, i) => {{
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const angle = (i / Math.max(rawNodes.length, 1)) * 2 * Math.PI;
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const r = Math.min(cx, cy) * 0.6;
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const node = {{
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ id: n.id,
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ label: n.label || 'default',
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ name: n.name || n.id,
322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ x: cx + r * Math.cos(angle),
323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ y: cy + r * Math.sin(angle),
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ vx: 0, vy: 0,
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ props: n.props || {{}},
326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }};
327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nodeById[n.id] = node;
328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return node;
329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }});
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ edges = rawEdges.map(e => ({{
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ source_id: e.source,
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ target_id: e.target,
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type: e.type || '',
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}));
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadStats() {{
338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {{
339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const s = await fetch('/api/stats').then(r => r.json());
340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const bar = document.getElementById('stats-bar');
341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ bar.textContent = `${{s.nodes}} nodes · ${{s.edges}} edges`;
342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} catch(_) {{}}
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Physics simulation ──────────────────────────────────────────────────────
346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function tick() {{
347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!physicsRunning || nodes.length === 0) return;
348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const cx = canvas.width / 2, cy = canvas.height / 2;
350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Repulsion between all pairs (Barnes-Hut approximation skipped for simplicity)
352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (let i = 0; i < nodes.length; i++) {{
353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const a = nodes[i];
354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (let j = i + 1; j < nodes.length; j++) {{
355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = nodes[j];
356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let dx = b.x - a.x, dy = b.y - a.y;
357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dist2 = dx*dx + dy*dy + 0.1;
358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dist = Math.sqrt(dist2);
359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const force = REPEL / dist2;
360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const fx = (dx / dist) * force;
361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const fy = (dy / dist) * force;
362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ a.vx -= fx; a.vy -= fy;
363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.vx += fx; b.vy += fy;
364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Edge spring attraction
368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const e of edges) {{
369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const a = nodeById[e.source_id], b = nodeById[e.target_id];
370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!a || !b) continue;
371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dx = b.x - a.x, dy = b.y - a.y;
372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dist = Math.sqrt(dx*dx + dy*dy) || 1;
373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const force = (dist - EDGE_LEN) * ATTRACT;
374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const fx = (dx / dist) * force;
375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const fy = (dy / dist) * force;
376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ a.vx += fx; a.vy += fy;
377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.vx -= fx; b.vy -= fy;
378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Weak centering pull
381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const n of nodes) {{
382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ n.vx += (cx - n.x) * 0.0005;
383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ n.vy += (cy - n.y) * 0.0005;
384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Integrate
387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const n of nodes) {{
388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (n === dragNode) continue;
389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ n.vx = Math.max(-MAX_VEL, Math.min(MAX_VEL, n.vx * DAMPING));
390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ n.vy = Math.max(-MAX_VEL, Math.min(MAX_VEL, n.vy * DAMPING));
391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ n.x += n.vx;
392 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ n.y += n.vy;
393 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
394 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
395 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
396 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Render ──────────────────────────────────────────────────────────────────
397 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function draw() {{
398 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const w = canvas.width, h = canvas.height;
399 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.clearRect(0, 0, w, h);
400 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.save();
401 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.translate(camX, camY);
402 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.scale(camScale, camScale);
403 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
404 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Edges
405 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.lineWidth = 1 / camScale;
406 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const e of edges) {{
407 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const a = nodeById[e.source_id], b = nodeById[e.target_id];
408 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!a || !b) continue;
409 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const isHighlighted = selectedNode && (a === selectedNode || b === selectedNode);
410 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = isHighlighted ? 0.9 : 0.25;
411 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.strokeStyle = isHighlighted ? '#4e9af1' : '#3a5070';
412 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.beginPath();
413 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.moveTo(a.x, a.y);
414 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.lineTo(b.x, b.y);
415 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.stroke();
416 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
417 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Arrowhead
418 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (isHighlighted) {{
419 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dx = b.x - a.x, dy = b.y - a.y;
420 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dist = Math.sqrt(dx*dx+dy*dy) || 1;
421 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const r = 8;
422 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const tx = b.x - (dx/dist)*r, ty = b.y - (dy/dist)*r;
423 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const angle = Math.atan2(dy, dx);
424 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = 0.7;
425 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fillStyle = '#4e9af1';
426 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.beginPath();
427 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.moveTo(tx, ty);
428 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.lineTo(tx - 8*Math.cos(angle-0.4), ty - 8*Math.sin(angle-0.4));
429 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.lineTo(tx - 8*Math.cos(angle+0.4), ty - 8*Math.sin(angle+0.4));
430 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.closePath();
431 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fill();
432 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
433 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
434 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = 1;
435 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
436 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Edge labels on highlighted edges
437 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (selectedNode) {{
438 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.font = `${{Math.max(9, 10/camScale)}}px sans-serif`;
439 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fillStyle = '#5a8ab8';
440 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const e of edges) {{
441 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const a = nodeById[e.source_id], b = nodeById[e.target_id];
442 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!a || !b) continue;
443 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (a !== selectedNode && b !== selectedNode) continue;
444 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!e.type) continue;
445 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
446 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fillText(e.type, mx, my);
447 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
448 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
449 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
450 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Nodes
451 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nodeR = 8;
452 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const n of nodes) {{
453 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const isSelected = n === selectedNode;
454 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const isHovered = n === hoveredNode;
455 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const color = nodeColor(n.label);
456 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
457 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.beginPath();
458 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.arc(n.x, n.y, nodeR + (isSelected ? 3 : isHovered ? 1 : 0), 0, 2*Math.PI);
459 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fillStyle = color;
460 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = 0.9;
461 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fill();
462 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (isSelected || isHovered) {{
463 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.strokeStyle = '#ffffff';
464 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.lineWidth = 2 / camScale;
465 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.stroke();
466 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
467 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = 1;
468 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
469 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Label
470 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const labelThreshold = 0.4;
471 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (camScale > labelThreshold || isSelected || isHovered) {{
472 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const fontSize = Math.max(8, 11 / camScale);
473 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.font = `${{isSelected ? 'bold ' : ''}}${{fontSize}}px sans-serif`;
474 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fillStyle = '#e0e8ff';
475 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = Math.min(1, (camScale - labelThreshold + 0.1) * 3);
476 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (isSelected || isHovered) ctx.globalAlpha = 1;
477 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.fillText(n.name, n.x + nodeR + 2, n.y + 4);
478 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.globalAlpha = 1;
479 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
480 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
481 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
482 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ctx.restore();
483 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
484 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
485 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function loop() {{
486 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tick();
487 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ draw();
488 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ requestAnimationFrame(loop);
489 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
490 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
491 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Hit testing ─────────────────────────────────────────────────────────────
492 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function screenToWorld(sx, sy) {{
493 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return {{ x: (sx - camX) / camScale, y: (sy - camY) / camScale }};
494 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
495 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
496 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function nodeAtScreen(sx, sy) {{
497 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const w = screenToWorld(sx, sy);
498 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nodeR = 11;
499 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (let i = nodes.length - 1; i >= 0; i--) {{
500 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const n = nodes[i];
501 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const dx = n.x - w.x, dy = n.y - w.y;
502 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (dx*dx + dy*dy <= nodeR*nodeR) return n;
503 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
504 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return null;
505 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
506 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
507 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Mouse / touch events ────────────────────────────────────────────────────
508 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.addEventListener('mousedown', e => {{
509 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const hit = nodeAtScreen(e.offsetX, e.offsetY);
510 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (hit) {{
511 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ isDraggingNode = true;
512 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNode = hit;
513 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ physicsRunning = true;
514 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const w = screenToWorld(e.offsetX, e.offsetY);
515 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNodeOffX = hit.x - w.x;
516 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNodeOffY = hit.y - w.y;
517 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} else {{
518 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ isDragging = true;
519 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragStartX = e.offsetX; dragStartY = e.offsetY;
520 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ camStartX = camX; camStartY = camY;
521 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
522 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }});
523 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
524 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.addEventListener('mousemove', e => {{
525 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (isDraggingNode && dragNode) {{
526 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const w = screenToWorld(e.offsetX, e.offsetY);
527 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNode.x = w.x + dragNodeOffX;
528 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNode.y = w.y + dragNodeOffY;
529 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNode.vx = 0; dragNode.vy = 0;
530 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} else if (isDragging) {{
531 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ camX = camStartX + (e.offsetX - dragStartX);
532 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ camY = camStartY + (e.offsetY - dragStartY);
533 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} else {{
534 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hoveredNode = nodeAtScreen(e.offsetX, e.offsetY);
535 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.style.cursor = hoveredNode ? 'pointer' : 'grab';
536 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
537 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }});
538 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
539 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.addEventListener('mouseup', e => {{
540 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (isDraggingNode && dragNode) {{
541 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const wasDragged = Math.abs(dragNode.vx) < 0.5 && Math.abs(dragNode.vy) < 0.5;
542 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (wasDragged) selectNode(dragNode);
543 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} else if (!isDragging || (Math.abs(e.offsetX - dragStartX) < 4 && Math.abs(e.offsetY - dragStartY) < 4)) {{
544 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const hit = nodeAtScreen(e.offsetX, e.offsetY);
545 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (hit) selectNode(hit);
546 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
547 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ isDragging = false;
548 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ isDraggingNode = false;
549 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dragNode = null;
550 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }});
551 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
552 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas.addEventListener('wheel', e => {{
553 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
554 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const factor = e.deltaY < 0 ? 1.1 : 0.9;
555 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const mx = e.offsetX, my = e.offsetY;
556 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ camX = mx - (mx - camX) * factor;
557 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ camY = my - (my - camY) * factor;
558 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ camScale = Math.max(0.05, Math.min(10, camScale * factor));
559 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}, {{ passive: false }});
560 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
561 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // ── Node selection ──────────────────────────────────────────────────────────
562 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function selectNode(node) {{
563 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ selectedNode = node;
564 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {{
565 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await fetch('/api/node/' + encodeURIComponent(node.name)).then(r => r.json());
566 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderDetail(data);
567 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }} catch(e) {{
568 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderDetail({{ name: node.name, label: node.label, props: node.props, neighbors: [] }});
569 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
570 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
571 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
572 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderDetail(data) {{
573 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const panel = document.getElementById('detail-panel');
574 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const label = data.label || '';
575 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const color = nodeColor(label);
576 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let html = `<h2>${{data.name}}</h2>`;
577 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<div class="detail-label">Type</div>`;
578 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<span class="badge" style="background:${{color}}22;color:${{color}};border:1px solid ${{color}}44">${{label}}</span>`;
579 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
580 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const props = data.props || {{}};
581 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const skip = new Set(['name']);
582 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const order = ['file_path', 'line_start', 'line_end', 'signature', 'docstring',
583 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'description', 'status', 'domain', 'rationale', 'url'];
584 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const shown = new Set();
585 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
586 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const key of order) {{
587 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (props[key] !== undefined && props[key] !== null && props[key] !== '') {{
588 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<div class="detail-label">${{key.replace(/_/g,' ')}}</div>`;
589 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<div class="detail-value">${{escHtml(String(props[key]))}}</div>`;
590 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ shown.add(key);
591 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
592 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
593 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const [key, val] of Object.entries(props)) {{
594 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (skip.has(key) || shown.has(key) || val === null || val === undefined || val === '') continue;
595 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<div class="detail-label">${{key.replace(/_/g,' ')}}</div>`;
596 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<div class="detail-value">${{escHtml(String(val))}}</div>`;
597 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
598 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
599 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const neighbors = data.neighbors || [];
600 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (neighbors.length > 0) {{
601 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ html += `<div class="detail-label">Neighbors (${{neighbors.length}})</div>`;
602 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (con# Copyright CONFLICT LLC 2026 (weareconflict.com)
603 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
604 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ HTML template for the Navegador Graph Explorer.
605 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
606 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ A fully self-contained, single-file HTML page with:
607 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Force-directed graph visualisation via inline canvas + JS physics
608 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Search box that queries /api/search
609 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Node click → detail panel via /api/node/<name>
610 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Stats bar via /api/stats
611 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Zero external dependencies (no CDN, no frameworks)
612 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
613 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
614 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Colour palette per node label ─────────────────────────────────────────────
615 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ NODE_COLORS = {
616 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Function": "#4e9af1",
617 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Method": "#6cb4f5",
618 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Class": "#f4a93b",
619 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "File": "#a8d9a7",
620 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Module": "#82c9a0",
621 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Repository": "#e67e22",
622 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Variable": "#c39bd3",
623 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Import": "#a9cce3",
624 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Decorator": "#f1948a",
625 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Domain": "#f7dc6f",
626 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Concept": "#f9e79f",
627 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Rule": "#f0b27a",
628 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Decision": "#f8c471",
629 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "WikiPage": "#d2b4de",
630 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Person": "#fadbd8",
631 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "default": "#aaaaaa",
632 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
633 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
634 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _COLORS_JS = "\n".join(f" '{label}': '{color}'," for label, color in NODE_COLORS.items())
635 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
636 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ HTML_TEMPLATE = """<!DOCTYPE html>
637 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <html lang="en">
638 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <head>
639 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <meta charset="UTF-8">
640 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
641 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <title>Navegador Graph Explorer</title>
642 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <style>
643 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
644 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ body {{
645 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
646 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #1a1a2e;
647 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ color: #e0e0e0;
648 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ height: 100vh;
649 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
650 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex-direction: column;
651 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ overflow: hidden;
652 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }}
653 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #header {{
654 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ background: #16213e;
655 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ border-bottom: 1px solid #0f3460;
656 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ padding: 10px 16px;
657 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ display: flex;
658 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ align-items: center;
659 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ gap: 12px;
660 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ flex-shrink: