@kentwynn/kgraph 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,293 @@
1
+ export function renderHtml(graphData, rootPath) {
2
+ const repoName = escAttr(rootPath.split('/').pop() ?? 'Repository');
3
+ const { meta } = graphData;
4
+ // Prevent </script> tag injection from embedded JSON
5
+ const safeData = JSON.stringify(graphData).replace(/<\/script>/gi, '<\\/script>');
6
+ return `<!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width,initial-scale=1">
11
+ <title>KGraph \u2014 ${repoName}</title>
12
+ <script src="https://unpkg.com/cytoscape@3.30.2/dist/cytoscape.min.js"></script>
13
+ <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
14
+ <script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
15
+ <style>
16
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
17
+ body{background:#0f172a;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:13px;height:100vh;display:flex;flex-direction:column;overflow:hidden}
18
+ #toolbar{background:#1e293b;border-bottom:1px solid #334155;padding:10px 16px;display:flex;align-items:center;gap:16px;flex-shrink:0;min-width:0}
19
+ #t-title{font-weight:700;font-size:14px;color:#7dd3fc;white-space:nowrap;flex-shrink:0}
20
+ #t-stats{color:#64748b;font-size:12px;white-space:nowrap;flex-shrink:0}
21
+ #t-controls{display:flex;align-items:center;gap:8px;margin-left:auto;flex-shrink:0}
22
+ .clabel{display:flex;align-items:center;gap:5px;cursor:pointer;color:#cbd5e1;font-size:12px;white-space:nowrap;user-select:none}
23
+ .clabel input{accent-color:#7dd3fc;cursor:pointer}
24
+ select,button{background:#334155;border:1px solid #475569;color:#e2e8f0;border-radius:5px;padding:5px 10px;font-size:12px;cursor:pointer;font-family:inherit;transition:background .15s}
25
+ select:hover,button:hover{background:#475569}
26
+ #main{display:flex;flex:1;overflow:hidden;min-height:0}
27
+ #cy{flex:1;min-width:0}
28
+ #sidebar{width:290px;background:#1e293b;border-left:1px solid #334155;display:none;flex-direction:column;overflow:hidden;flex-shrink:0}
29
+ #sidebar.open{display:flex}
30
+ #sb-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid #334155;flex-shrink:0}
31
+ #sb-type{font-weight:600;color:#7dd3fc;font-size:11px;text-transform:uppercase;letter-spacing:.06em}
32
+ #sb-close{background:none;border:none;color:#64748b;font-size:17px;line-height:1;cursor:pointer;padding:0 2px}
33
+ #sb-close:hover{color:#e2e8f0;background:none}
34
+ #sb-body{padding:14px;overflow-y:auto;flex:1}
35
+ .sb-badge{display:inline-block;padding:2px 9px;border-radius:10px;font-size:11px;font-weight:600;margin-bottom:10px}
36
+ .sb-title{font-size:13px;font-weight:700;color:#f1f5f9;word-break:break-all;line-height:1.45;margin-bottom:10px}
37
+ .sb-sect{margin-top:14px}
38
+ .sb-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#475569;margin-bottom:5px}
39
+ .sb-val{color:#94a3b8;font-size:12px}
40
+ .sb-code{font-family:'JetBrains Mono','Fira Code',ui-monospace,monospace;font-size:11px;color:#7dd3fc;background:#0f172a;padding:1px 4px;border-radius:3px}
41
+ .sb-list{list-style:none;display:flex;flex-direction:column;gap:3px}
42
+ .sb-list li{font-family:'JetBrains Mono','Fira Code',ui-monospace,monospace;font-size:11px;color:#7dd3fc;padding:2px 0}
43
+ #legend{background:#1e293b;border-top:1px solid #334155;padding:7px 16px;display:flex;align-items:center;gap:14px;flex-shrink:0;flex-wrap:wrap}
44
+ .li{display:flex;align-items:center;gap:5px;font-size:11px;color:#64748b}
45
+ .li-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;display:inline-block}
46
+ .li-dia{width:10px;height:10px;transform:rotate(45deg);flex-shrink:0;display:inline-block}
47
+ .li-sep{width:1px;height:14px;background:#334155;flex-shrink:0}
48
+ .li-head{font-size:11px;color:#475569;font-weight:700;letter-spacing:.04em}
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <div id="toolbar">
53
+ <span id="t-title">\u29e1 KGraph \u00b7 ${repoName}</span>
54
+ <span id="t-stats">${meta.fileCount} files &middot; ${meta.symbolCount} symbols &middot; ${meta.cognitionCount} notes</span>
55
+ <div id="t-controls">
56
+ <label class="clabel"><input type="checkbox" id="tog-cog" checked> Cognition</label>
57
+ <select id="sel-layout" title="Graph layout algorithm">
58
+ <option value="dagre">Hierarchical</option>
59
+ <option value="cose">Force-directed</option>
60
+ <option value="grid">Grid</option>
61
+ <option value="concentric">Concentric</option>
62
+ </select>
63
+ <button id="btn-fit" title="Fit graph to viewport">\u229f Fit</button>
64
+ <button id="btn-png" title="Download as PNG">\u2193 PNG</button>
65
+ </div>
66
+ </div>
67
+ <div id="main">
68
+ <div id="cy"></div>
69
+ <div id="sidebar">
70
+ <div id="sb-head">
71
+ <span id="sb-type">Details</span>
72
+ <button id="sb-close" title="Close panel">\u00d7</button>
73
+ </div>
74
+ <div id="sb-body"></div>
75
+ </div>
76
+ </div>
77
+ <div id="legend">
78
+ <span class="li-head">Files</span>
79
+ <span class="li"><span class="li-dot" style="background:#3b82f6"></span>TypeScript</span>
80
+ <span class="li"><span class="li-dot" style="background:#f59e0b"></span>JavaScript</span>
81
+ <span class="li"><span class="li-dot" style="background:#10b981"></span>Markdown</span>
82
+ <span class="li"><span class="li-dot" style="background:#8b5cf6"></span>YAML</span>
83
+ <span class="li"><span class="li-dot" style="background:#94a3b8"></span>Other</span>
84
+ <span class="li-sep"></span>
85
+ <span class="li-head">Cognition</span>
86
+ <span class="li"><span class="li-dia" style="background:#10b981"></span>Current</span>
87
+ <span class="li"><span class="li-dia" style="background:#f59e0b"></span>Mixed</span>
88
+ <span class="li"><span class="li-dia" style="background:#ef4444"></span>Stale</span>
89
+ <span class="li-sep"></span>
90
+ <span class="li" style="margin-left:auto;color:#334155;font-size:10px">KGraph v${meta.generatedAt.slice(0, 10)}</span>
91
+ </div>
92
+ <script>
93
+ (function () {
94
+ var GRAPH_DATA = ${safeData};
95
+
96
+ if (!GRAPH_DATA.elements.length) {
97
+ document.getElementById('cy').innerHTML =
98
+ '<div style="display:flex;height:100%;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:#475569">' +
99
+ '<span style="font-size:36px">\u29e1</span>' +
100
+ '<span>No graph data found. Run <code style="color:#7dd3fc">kgraph scan</code> first.</span>' +
101
+ '</div>';
102
+ return;
103
+ }
104
+
105
+ if (typeof cytoscapeDagre !== 'undefined') {
106
+ cytoscape.use(cytoscapeDagre);
107
+ }
108
+
109
+ function esc(v) {
110
+ return String(v == null ? '' : v)
111
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
112
+ }
113
+
114
+ function bytes(b) {
115
+ if (!b) return '0 B';
116
+ if (b < 1024) return b + ' B';
117
+ if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
118
+ return (b / 1048576).toFixed(1) + ' MB';
119
+ }
120
+
121
+ var cy = cytoscape({
122
+ container: document.getElementById('cy'),
123
+ elements: GRAPH_DATA.elements,
124
+ style: [
125
+ {
126
+ selector: 'node',
127
+ style: {
128
+ 'background-color': 'data(color)',
129
+ label: 'data(label)',
130
+ color: '#94a3b8',
131
+ 'font-size': '10px',
132
+ 'text-valign': 'bottom',
133
+ 'text-halign': 'center',
134
+ 'text-margin-y': '5px',
135
+ 'border-width': 1,
136
+ 'border-color': '#1e293b',
137
+ width: 30,
138
+ height: 30,
139
+ 'text-wrap': 'ellipsis',
140
+ 'text-max-width': '80px',
141
+ 'overlay-opacity': 0
142
+ }
143
+ },
144
+ {
145
+ selector: 'node.cognition',
146
+ style: {
147
+ shape: 'diamond',
148
+ width: 40,
149
+ height: 40,
150
+ 'background-color': '#0f172a',
151
+ 'border-color': 'data(color)',
152
+ 'border-width': 2,
153
+ color: '#e2e8f0'
154
+ }
155
+ },
156
+ {
157
+ selector: 'node:selected',
158
+ style: {
159
+ 'border-color': '#7dd3fc',
160
+ 'border-width': 3,
161
+ 'overlay-opacity': 0.06,
162
+ 'overlay-color': '#7dd3fc'
163
+ }
164
+ },
165
+ {
166
+ selector: 'edge.import',
167
+ style: {
168
+ width: 1,
169
+ 'line-color': '#2d3f55',
170
+ 'target-arrow-color': '#2d3f55',
171
+ 'target-arrow-shape': 'triangle',
172
+ 'curve-style': 'bezier',
173
+ opacity: 0.7
174
+ }
175
+ },
176
+ {
177
+ selector: 'edge.cognition-ref',
178
+ style: {
179
+ width: 1.5,
180
+ 'line-color': '#7dd3fc',
181
+ 'target-arrow-color': '#7dd3fc',
182
+ 'target-arrow-shape': 'triangle',
183
+ 'line-style': 'dashed',
184
+ 'line-dash-pattern': [5, 3],
185
+ 'curve-style': 'bezier',
186
+ opacity: 0.5
187
+ }
188
+ },
189
+ { selector: '.hidden', style: { display: 'none' } }
190
+ ],
191
+ layout: {
192
+ name: 'dagre',
193
+ rankDir: 'LR',
194
+ nodeSep: 60,
195
+ rankSep: 120,
196
+ padding: 40,
197
+ animate: true,
198
+ animationDuration: 400
199
+ }
200
+ });
201
+
202
+ var LAYOUTS = {
203
+ dagre: { name: 'dagre', rankDir: 'LR', nodeSep: 60, rankSep: 120, animate: true, animationDuration: 400, padding: 40 },
204
+ cose: { name: 'cose', animate: true, animationDuration: 600, padding: 40 },
205
+ grid: { name: 'grid', animate: true, animationDuration: 400, padding: 40 },
206
+ concentric: {
207
+ name: 'concentric',
208
+ concentric: function (n) { return n.degree(); },
209
+ levelWidth: function () { return 2; },
210
+ animate: true,
211
+ animationDuration: 400,
212
+ padding: 40
213
+ }
214
+ };
215
+
216
+ function renderFilePanel(d) {
217
+ return '<div class="sb-badge" style="background:' + esc(d.color) + '22;color:' + esc(d.color) + ';border:1px solid ' + esc(d.color) + '44">' + esc(d.language) + '</div>' +
218
+ '<div class="sb-title">' + esc(d.path) + '</div>' +
219
+ '<div class="sb-sect"><div class="sb-lbl">Scan Status</div><div class="sb-val">' + esc(d.scanStatus) + '</div></div>' +
220
+ '<div class="sb-sect"><div class="sb-lbl">File Size</div><div class="sb-val">' + bytes(d.size) + '</div></div>';
221
+ }
222
+
223
+ function renderCognitionPanel(d) {
224
+ var sc = { current: '#10b981', mixed: '#f59e0b', stale: '#ef4444', unresolved: '#6b7280' }[d.referencesStatus] || '#6b7280';
225
+ var files = d.relatedFiles && d.relatedFiles.length
226
+ ? '<ul class="sb-list">' + d.relatedFiles.map(function (f) { return '<li>' + esc(f) + '</li>'; }).join('') + '</ul>'
227
+ : '<span class="sb-val">none</span>';
228
+ var syms = d.relatedSymbols && d.relatedSymbols.length
229
+ ? d.relatedSymbols.slice(0, 15).map(function (s) { return '<span class="sb-code">' + esc(s) + '</span>'; }).join(' ')
230
+ : '<span class="sb-val">none</span>';
231
+ return '<div class="sb-badge" style="background:' + sc + '22;color:' + sc + ';border:1px solid ' + sc + '44">' + esc(d.referencesStatus) + '</div>' +
232
+ '<div class="sb-title">' + esc(d.label) + '</div>' +
233
+ (d.domain ? '<div class="sb-sect"><div class="sb-lbl">Domain</div><div class="sb-val">' + esc(d.domain) + '</div></div>' : '') +
234
+ '<div class="sb-sect"><div class="sb-lbl">Related Files</div>' + files + '</div>' +
235
+ '<div class="sb-sect"><div class="sb-lbl">Symbols</div><div class="sb-val">' + syms + '</div></div>';
236
+ }
237
+
238
+ cy.on('tap', 'node', function (evt) {
239
+ var d = evt.target.data();
240
+ document.getElementById('sb-type').textContent = d.type === 'cognition' ? 'Cognition Note' : 'File';
241
+ document.getElementById('sb-body').innerHTML = d.type === 'cognition' ? renderCognitionPanel(d) : renderFilePanel(d);
242
+ document.getElementById('sidebar').classList.add('open');
243
+ });
244
+
245
+ cy.on('tap', function (evt) {
246
+ if (evt.target === cy) {
247
+ document.getElementById('sidebar').classList.remove('open');
248
+ }
249
+ });
250
+
251
+ document.getElementById('sb-close').addEventListener('click', function () {
252
+ document.getElementById('sidebar').classList.remove('open');
253
+ });
254
+
255
+ document.getElementById('tog-cog').addEventListener('change', function (e) {
256
+ if (e.target.checked) {
257
+ cy.nodes('.cognition').removeClass('hidden');
258
+ cy.edges('.cognition-ref').removeClass('hidden');
259
+ } else {
260
+ cy.nodes('.cognition').addClass('hidden');
261
+ cy.edges('.cognition-ref').addClass('hidden');
262
+ }
263
+ });
264
+
265
+ document.getElementById('sel-layout').addEventListener('change', function (e) {
266
+ cy.layout(LAYOUTS[e.target.value] || LAYOUTS.dagre).run();
267
+ });
268
+
269
+ document.getElementById('btn-fit').addEventListener('click', function () {
270
+ cy.fit(undefined, 40);
271
+ });
272
+
273
+ document.getElementById('btn-png').addEventListener('click', function () {
274
+ var png = cy.png({ output: 'base64uri', bg: '#0f172a', scale: 2, full: true });
275
+ var a = document.createElement('a');
276
+ a.href = png;
277
+ a.download = 'kgraph-${repoName}.png';
278
+ document.body.appendChild(a);
279
+ a.click();
280
+ document.body.removeChild(a);
281
+ });
282
+ })();
283
+ </script>
284
+ </body>
285
+ </html>`;
286
+ }
287
+ function escAttr(str) {
288
+ return str
289
+ .replace(/&/g, '&amp;')
290
+ .replace(/</g, '&lt;')
291
+ .replace(/>/g, '&gt;')
292
+ .replace(/"/g, '&quot;');
293
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {