@hugobatist/smartcode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.js +4324 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.js +1167 -0
- package/dist/index.js.map +1 -0
- package/dist/static/annotations-panel.js +133 -0
- package/dist/static/annotations-svg.js +108 -0
- package/dist/static/annotations.css +367 -0
- package/dist/static/annotations.js +367 -0
- package/dist/static/app-init.js +497 -0
- package/dist/static/breakpoints.css +69 -0
- package/dist/static/breakpoints.js +197 -0
- package/dist/static/clipboard.js +94 -0
- package/dist/static/collapse-ui.js +325 -0
- package/dist/static/command-history.js +89 -0
- package/dist/static/context-menu.js +334 -0
- package/dist/static/custom-renderer.js +201 -0
- package/dist/static/dagre-layout.js +291 -0
- package/dist/static/diagram-dom.js +241 -0
- package/dist/static/diagram-editor.js +368 -0
- package/dist/static/editor-panel.js +107 -0
- package/dist/static/editor-popovers.js +187 -0
- package/dist/static/event-bus.js +57 -0
- package/dist/static/export.js +181 -0
- package/dist/static/file-tree.js +470 -0
- package/dist/static/ghost-paths.js +397 -0
- package/dist/static/heatmap.css +116 -0
- package/dist/static/heatmap.js +308 -0
- package/dist/static/icons.js +66 -0
- package/dist/static/inline-edit.js +294 -0
- package/dist/static/interaction-state.js +155 -0
- package/dist/static/interaction-tracker.js +93 -0
- package/dist/static/live.html +239 -0
- package/dist/static/main-layout.css +220 -0
- package/dist/static/main.css +334 -0
- package/dist/static/mcp-sessions.js +202 -0
- package/dist/static/modal.css +109 -0
- package/dist/static/modal.js +171 -0
- package/dist/static/node-drag.js +293 -0
- package/dist/static/pan-zoom.js +199 -0
- package/dist/static/renderer.js +280 -0
- package/dist/static/search.css +103 -0
- package/dist/static/search.js +304 -0
- package/dist/static/selection.js +353 -0
- package/dist/static/session-player.css +137 -0
- package/dist/static/session-player.js +411 -0
- package/dist/static/sidebar.css +248 -0
- package/dist/static/svg-renderer.js +313 -0
- package/dist/static/svg-shapes.js +218 -0
- package/dist/static/tokens.css +76 -0
- package/dist/static/vendor/dagre-bundle.js +43 -0
- package/dist/static/vendor/dagre.min.js +3 -0
- package/dist/static/vendor/graphlib.min.js +2 -0
- package/dist/static/viewport-transform.js +107 -0
- package/dist/static/workspace-switcher.js +202 -0
- package/dist/static/ws-client.js +71 -0
- package/dist/static/ws-handler.js +125 -0
- package/package.json +74 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Context Menu -- right-click context menu for diagram nodes and edges.
|
|
3
|
+
* Shows action items: Edit, Delete, Duplicate, Flag, Connect (nodes) or Delete, Flag (edges).
|
|
4
|
+
*
|
|
5
|
+
* Dependencies:
|
|
6
|
+
* - interaction-state.js (SmartCodeInteraction)
|
|
7
|
+
* - diagram-dom.js (DiagramDOM)
|
|
8
|
+
* - diagram-editor.js (MmdEditor)
|
|
9
|
+
* - annotations.js (SmartCodeAnnotations)
|
|
10
|
+
* - selection.js (SmartCodeSelection)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* SmartCodeContextMenu.init();
|
|
14
|
+
* SmartCodeContextMenu.show(x, y, nodeInfo);
|
|
15
|
+
* SmartCodeContextMenu.close();
|
|
16
|
+
*/
|
|
17
|
+
(function() {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
var menuEl = null;
|
|
21
|
+
var outsideHandler = null;
|
|
22
|
+
|
|
23
|
+
// ── Helpers ──
|
|
24
|
+
|
|
25
|
+
function createMenuItem(label, className, handler) {
|
|
26
|
+
var item = document.createElement('div');
|
|
27
|
+
item.className = 'context-menu-item' + (className ? ' ' + className : '');
|
|
28
|
+
item.textContent = label;
|
|
29
|
+
item.addEventListener('click', function(e) {
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
closeContextMenu();
|
|
32
|
+
handler();
|
|
33
|
+
});
|
|
34
|
+
return item;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createSeparator() {
|
|
38
|
+
var sep = document.createElement('div');
|
|
39
|
+
sep.className = 'context-menu-separator';
|
|
40
|
+
return sep;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Duplicate Helper ──
|
|
44
|
+
|
|
45
|
+
function doDuplicate(nodeId) {
|
|
46
|
+
if (window.MmdEditor && MmdEditor.applyEdit && MmdEditor.duplicateNode) {
|
|
47
|
+
MmdEditor.applyEdit(function(c) { return MmdEditor.duplicateNode(c, nodeId); });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Flag Helper ──
|
|
52
|
+
|
|
53
|
+
function doFlag() {
|
|
54
|
+
if (window.SmartCodeInteraction) {
|
|
55
|
+
SmartCodeInteraction.forceState('flagging');
|
|
56
|
+
}
|
|
57
|
+
if (window.SmartCodeAnnotations) {
|
|
58
|
+
var s = SmartCodeAnnotations.getState();
|
|
59
|
+
if (!s.flagMode) SmartCodeAnnotations.toggleFlagMode();
|
|
60
|
+
}
|
|
61
|
+
if (window.toast) toast('Flag Mode enabled -- click on a node to flag');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Ghost Path Helper ──
|
|
65
|
+
|
|
66
|
+
var ghostPathSource = null;
|
|
67
|
+
|
|
68
|
+
function startGhostPathFlow(sourceNodeId) {
|
|
69
|
+
ghostPathSource = sourceNodeId;
|
|
70
|
+
if (window.toast) toast('Click destination node for ghost path');
|
|
71
|
+
var container = document.getElementById('preview-container');
|
|
72
|
+
if (container) container.addEventListener('click', ghostPathDestinationHandler, { once: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ghostPathDestinationHandler(e) {
|
|
76
|
+
var nodeInfo = DiagramDOM.extractNodeId(e.target);
|
|
77
|
+
if (!nodeInfo || nodeInfo.type === 'edge' || !ghostPathSource) {
|
|
78
|
+
ghostPathSource = null;
|
|
79
|
+
if (window.toast) toast('Ghost path cancelled');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (nodeInfo.id === ghostPathSource) {
|
|
83
|
+
ghostPathSource = null;
|
|
84
|
+
if (window.toast) toast('Cannot create ghost path to same node');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
var from = ghostPathSource;
|
|
90
|
+
var to = nodeInfo.id;
|
|
91
|
+
ghostPathSource = null;
|
|
92
|
+
SmartCodeModal.prompt({
|
|
93
|
+
title: 'Ghost Path: ' + from + ' → ' + to,
|
|
94
|
+
placeholder: 'Reason (optional)',
|
|
95
|
+
allowEmpty: true,
|
|
96
|
+
onConfirm: function(label) {
|
|
97
|
+
if (window.SmartCodeGhostPaths) SmartCodeGhostPaths.createGhostPath(from, to, label || undefined);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Risk Level Helper ──
|
|
103
|
+
|
|
104
|
+
function bUrl(path) { return (window.SmartCodeBaseUrl || '') + path; }
|
|
105
|
+
|
|
106
|
+
function setRisk(nodeId, level) {
|
|
107
|
+
SmartCodeModal.prompt({
|
|
108
|
+
title: 'Risk: ' + level.charAt(0).toUpperCase() + level.slice(1),
|
|
109
|
+
placeholder: 'Reason for ' + level + ' risk...',
|
|
110
|
+
onConfirm: function(reason) {
|
|
111
|
+
var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
|
|
112
|
+
if (!file) return;
|
|
113
|
+
fetch(bUrl('/api/annotations/' + encodeURIComponent(file) + '/risk'), {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ nodeId: nodeId, level: level, reason: reason }),
|
|
117
|
+
}).then(function(r) {
|
|
118
|
+
if (r.ok && window.toast) toast('Risk set: ' + level);
|
|
119
|
+
else if (!r.ok && window.toast) toast('Error setting risk');
|
|
120
|
+
}).catch(function() {
|
|
121
|
+
if (window.toast) toast('Error setting risk');
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function removeRisk(nodeId) {
|
|
128
|
+
var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
|
|
129
|
+
if (!file) return;
|
|
130
|
+
fetch(bUrl('/api/annotations/' + encodeURIComponent(file) + '/risk'), {
|
|
131
|
+
method: 'DELETE',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({ nodeId: nodeId }),
|
|
134
|
+
}).then(function(r) {
|
|
135
|
+
if (r.ok && window.toast) toast('Risk removed');
|
|
136
|
+
else if (!r.ok && window.toast) toast('Error removing risk');
|
|
137
|
+
}).catch(function() {
|
|
138
|
+
if (window.toast) toast('Error removing risk');
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Submenu Helper ──
|
|
143
|
+
|
|
144
|
+
function createSubmenuItem(label, className, handler) {
|
|
145
|
+
var item = document.createElement('div');
|
|
146
|
+
item.className = 'context-menu-item context-menu-submenu' + (className ? ' ' + className : '');
|
|
147
|
+
item.textContent = label;
|
|
148
|
+
item.addEventListener('click', function(e) {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
closeContextMenu();
|
|
151
|
+
handler();
|
|
152
|
+
});
|
|
153
|
+
return item;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Show Context Menu ──
|
|
157
|
+
|
|
158
|
+
function showContextMenu(x, y, nodeInfo) {
|
|
159
|
+
closeContextMenu();
|
|
160
|
+
|
|
161
|
+
var menu = document.createElement('div');
|
|
162
|
+
menu.className = 'context-menu';
|
|
163
|
+
|
|
164
|
+
// Clamp position to stay on-screen
|
|
165
|
+
menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
|
|
166
|
+
menu.style.top = Math.min(y, window.innerHeight - 250) + 'px';
|
|
167
|
+
|
|
168
|
+
if (nodeInfo.type === 'node' || nodeInfo.type === 'subgraph') {
|
|
169
|
+
// Node context menu: 5 items
|
|
170
|
+
menu.appendChild(createMenuItem('Edit Text', '', function() {
|
|
171
|
+
if (window.SmartCodeInlineEdit) {
|
|
172
|
+
if (window.SmartCodeInteraction) SmartCodeInteraction.transition('edit_action');
|
|
173
|
+
SmartCodeInlineEdit.open(nodeInfo.id);
|
|
174
|
+
} else if (window.MmdEditor) {
|
|
175
|
+
MmdEditor.doEditNodeText(nodeInfo.id);
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
menu.appendChild(createMenuItem('Delete', 'danger', function() {
|
|
180
|
+
if (window.MmdEditor) MmdEditor.doRemoveNode(nodeInfo.id);
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
menu.appendChild(createMenuItem('Duplicate', '', function() {
|
|
184
|
+
doDuplicate(nodeInfo.id);
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
menu.appendChild(createSeparator());
|
|
188
|
+
|
|
189
|
+
menu.appendChild(createMenuItem('Flag', '', function() {
|
|
190
|
+
doFlag();
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
menu.appendChild(createMenuItem('Toggle Breakpoint', '', function() {
|
|
194
|
+
if (window.SmartCodeBreakpoints) SmartCodeBreakpoints.toggleBreakpoint(nodeInfo.id);
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
menu.appendChild(createMenuItem('New Edge', '', function() {
|
|
198
|
+
if (window.MmdEditor) MmdEditor.startConnectFrom(nodeInfo.id);
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
menu.appendChild(createSeparator());
|
|
202
|
+
|
|
203
|
+
menu.appendChild(createMenuItem('Ghost Path to...', '', function() {
|
|
204
|
+
startGhostPathFlow(nodeInfo.id);
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
// Risk level submenu items
|
|
208
|
+
menu.appendChild(createSubmenuItem('Risk: High', 'risk-high', function() {
|
|
209
|
+
setRisk(nodeInfo.id, 'high');
|
|
210
|
+
}));
|
|
211
|
+
menu.appendChild(createSubmenuItem('Risk: Medium', 'risk-medium', function() {
|
|
212
|
+
setRisk(nodeInfo.id, 'medium');
|
|
213
|
+
}));
|
|
214
|
+
menu.appendChild(createSubmenuItem('Risk: Low', 'risk-low', function() {
|
|
215
|
+
setRisk(nodeInfo.id, 'low');
|
|
216
|
+
}));
|
|
217
|
+
menu.appendChild(createSubmenuItem('Remove Risk', '', function() {
|
|
218
|
+
removeRisk(nodeInfo.id);
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
} else if (nodeInfo.type === 'edge') {
|
|
222
|
+
// Edge context menu: 2 items
|
|
223
|
+
menu.appendChild(createMenuItem('Delete', 'danger', function() {
|
|
224
|
+
if (window.MmdEditor) {
|
|
225
|
+
// Parse edge endpoints from content
|
|
226
|
+
var editor = document.getElementById('editor');
|
|
227
|
+
if (editor) {
|
|
228
|
+
var patterns = MmdEditor.findEdgeEndpoints(nodeInfo.id, editor.value);
|
|
229
|
+
if (patterns.length > 0) {
|
|
230
|
+
MmdEditor.doRemoveEdge(patterns[0].from, patterns[0].to);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
menu.appendChild(createMenuItem('Flag', '', function() {
|
|
237
|
+
doFlag();
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
document.body.appendChild(menu);
|
|
242
|
+
menuEl = menu;
|
|
243
|
+
|
|
244
|
+
// Outside click handler (delay to avoid immediate close)
|
|
245
|
+
setTimeout(function() {
|
|
246
|
+
outsideHandler = function(e) {
|
|
247
|
+
if (menu.contains(e.target)) return;
|
|
248
|
+
closeContextMenu();
|
|
249
|
+
};
|
|
250
|
+
document.addEventListener('mousedown', outsideHandler);
|
|
251
|
+
}, 50);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Close Context Menu ──
|
|
255
|
+
|
|
256
|
+
function closeContextMenu() {
|
|
257
|
+
if (menuEl) {
|
|
258
|
+
menuEl.remove();
|
|
259
|
+
menuEl = null;
|
|
260
|
+
}
|
|
261
|
+
if (outsideHandler) {
|
|
262
|
+
document.removeEventListener('mousedown', outsideHandler);
|
|
263
|
+
outsideHandler = null;
|
|
264
|
+
}
|
|
265
|
+
// Transition FSM back to idle if in context-menu state
|
|
266
|
+
if (window.SmartCodeInteraction && SmartCodeInteraction.getState() === 'context-menu') {
|
|
267
|
+
SmartCodeInteraction.transition('close');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Right-click Handler ──
|
|
272
|
+
|
|
273
|
+
function handleContextMenu(e) {
|
|
274
|
+
// Check FSM blocking states
|
|
275
|
+
if (window.SmartCodeInteraction && SmartCodeInteraction.isBlocking()) return;
|
|
276
|
+
|
|
277
|
+
// Don't show custom menu if in special modes
|
|
278
|
+
var fsmState = window.SmartCodeInteraction ? SmartCodeInteraction.getState() : 'idle';
|
|
279
|
+
if (fsmState === 'flagging' || fsmState === 'add-node' || fsmState === 'add-edge') return;
|
|
280
|
+
|
|
281
|
+
// Skip UI controls
|
|
282
|
+
if (e.target.closest('.zoom-controls') ||
|
|
283
|
+
e.target.closest('.flag-popover') ||
|
|
284
|
+
e.target.closest('.editor-popover')) return;
|
|
285
|
+
|
|
286
|
+
// Detect what was right-clicked
|
|
287
|
+
var nodeInfo = DiagramDOM.extractNodeId(e.target);
|
|
288
|
+
if (!nodeInfo) return; // Let browser default context menu show
|
|
289
|
+
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
|
|
292
|
+
// Select the node/edge first
|
|
293
|
+
if (window.SmartCodeSelection) {
|
|
294
|
+
if (nodeInfo.type === 'node' || nodeInfo.type === 'subgraph') {
|
|
295
|
+
SmartCodeSelection.selectNode(nodeInfo.id);
|
|
296
|
+
} else if (nodeInfo.type === 'edge') {
|
|
297
|
+
SmartCodeSelection.selectEdge(nodeInfo.id);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Transition FSM to context-menu state
|
|
302
|
+
if (window.SmartCodeInteraction) {
|
|
303
|
+
SmartCodeInteraction.transition('right_click', nodeInfo);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
showContextMenu(e.clientX, e.clientY, nodeInfo);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Escape Handler ──
|
|
310
|
+
|
|
311
|
+
function handleEscapeKey(e) {
|
|
312
|
+
if (e.key === 'Escape' && window.SmartCodeInteraction &&
|
|
313
|
+
SmartCodeInteraction.getState() === 'context-menu') {
|
|
314
|
+
closeContextMenu();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Init ──
|
|
319
|
+
|
|
320
|
+
function init() {
|
|
321
|
+
var container = document.getElementById('preview-container');
|
|
322
|
+
if (container) {
|
|
323
|
+
container.addEventListener('contextmenu', handleContextMenu);
|
|
324
|
+
}
|
|
325
|
+
document.addEventListener('keydown', handleEscapeKey);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Public API ──
|
|
329
|
+
window.SmartCodeContextMenu = {
|
|
330
|
+
init: init,
|
|
331
|
+
close: closeContextMenu,
|
|
332
|
+
show: showContextMenu,
|
|
333
|
+
};
|
|
334
|
+
})();
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Custom Renderer -- orchestrates the custom SVG rendering pipeline.
|
|
3
|
+
* Fetches graph data from /api/graph/, runs dagre layout, builds SVG,
|
|
4
|
+
* and inserts into the preview container.
|
|
5
|
+
*
|
|
6
|
+
* Dependencies: dagre-layout.js (SmartCodeDagreLayout), svg-renderer.js (SmartCodeSvgRenderer),
|
|
7
|
+
* renderer.js (SmartCodeRenderer), event-bus.js (SmartCodeEventBus),
|
|
8
|
+
* pan-zoom.js (applyTransform, zoomFit)
|
|
9
|
+
* Dependents: app-init.js (called via renderWithType)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* SmartCodeCustomRenderer.render(graphModel);
|
|
13
|
+
* SmartCodeCustomRenderer.fetchAndRender(filePath);
|
|
14
|
+
*/
|
|
15
|
+
(function() {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// ── Status color palette (matches Mermaid classDef in renderer.js) ──
|
|
19
|
+
var STATUS_COLORS = {
|
|
20
|
+
'ok': { fill: '#22c55e', stroke: '#16a34a', text: '#fff' },
|
|
21
|
+
'problem': { fill: '#ef4444', stroke: '#dc2626', text: '#fff' },
|
|
22
|
+
'in-progress': { fill: '#eab308', stroke: '#ca8a04', text: '#000' },
|
|
23
|
+
'discarded': { fill: '#71717a', stroke: '#52525b', text: '#fff' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ── Last rendered graph model (for re-application) ──
|
|
27
|
+
var lastGraphModel = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Apply status colors to SVG nodes based on the graph model's statuses map.
|
|
31
|
+
* @param {Object} graphModel - Graph model containing a .statuses map.
|
|
32
|
+
*/
|
|
33
|
+
function applyStatusColors(graphModel) {
|
|
34
|
+
if (!graphModel || !graphModel.statuses) return;
|
|
35
|
+
var svg = document.querySelector('#preview svg');
|
|
36
|
+
if (!svg) return;
|
|
37
|
+
var statuses = graphModel.statuses;
|
|
38
|
+
for (var nodeId in statuses) {
|
|
39
|
+
if (!statuses.hasOwnProperty(nodeId)) continue;
|
|
40
|
+
var status = statuses[nodeId];
|
|
41
|
+
var colors = STATUS_COLORS[status];
|
|
42
|
+
if (!colors) continue;
|
|
43
|
+
var nodeEl = svg.querySelector('[data-node-id="' + nodeId + '"]');
|
|
44
|
+
if (!nodeEl) continue;
|
|
45
|
+
// Find shape element (rect, circle, polygon, etc.)
|
|
46
|
+
var shape = nodeEl.querySelector('rect, circle, polygon, path, ellipse');
|
|
47
|
+
if (!shape) {
|
|
48
|
+
var childG = nodeEl.querySelector('g');
|
|
49
|
+
if (childG) shape = childG.querySelector('rect, circle, polygon, path, ellipse');
|
|
50
|
+
}
|
|
51
|
+
if (shape) {
|
|
52
|
+
shape.setAttribute('fill', colors.fill);
|
|
53
|
+
shape.setAttribute('stroke', colors.stroke);
|
|
54
|
+
}
|
|
55
|
+
var textEl = nodeEl.querySelector('text');
|
|
56
|
+
if (textEl) textEl.setAttribute('fill', colors.text);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a Mermaid-style inline style string into a key-value map.
|
|
62
|
+
* E.g. "fill:#e3f2fd,stroke:#1565c0" → { fill: '#e3f2fd', stroke: '#1565c0' }
|
|
63
|
+
*/
|
|
64
|
+
function parseStyleString(styleStr) {
|
|
65
|
+
var result = {};
|
|
66
|
+
if (!styleStr) return result;
|
|
67
|
+
var parts = styleStr.split(',');
|
|
68
|
+
for (var i = 0; i < parts.length; i++) {
|
|
69
|
+
var kv = parts[i].split(':');
|
|
70
|
+
if (kv.length >= 2) {
|
|
71
|
+
result[kv[0].trim()] = kv.slice(1).join(':').trim();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Apply nodeStyles from the graph model to matching SVG elements.
|
|
79
|
+
* Targets both nodes (data-node-id) and subgraphs (data-subgraph-id).
|
|
80
|
+
*/
|
|
81
|
+
function applyNodeStyles(graphModel) {
|
|
82
|
+
if (!graphModel || !graphModel.nodeStyles) return;
|
|
83
|
+
var svg = document.querySelector('#preview svg');
|
|
84
|
+
if (!svg) return;
|
|
85
|
+
var styles = graphModel.nodeStyles;
|
|
86
|
+
for (var targetId in styles) {
|
|
87
|
+
if (!styles.hasOwnProperty(targetId)) continue;
|
|
88
|
+
var parsed = parseStyleString(styles[targetId]);
|
|
89
|
+
// Try subgraph first (more common for style directives)
|
|
90
|
+
var el = svg.querySelector('[data-subgraph-id="' + targetId + '"]')
|
|
91
|
+
|| svg.querySelector('[data-node-id="' + targetId + '"]');
|
|
92
|
+
if (!el) continue;
|
|
93
|
+
var shape = el.querySelector('rect, circle, polygon, path, ellipse');
|
|
94
|
+
if (!shape) {
|
|
95
|
+
var childG = el.querySelector('g');
|
|
96
|
+
if (childG) shape = childG.querySelector('rect, circle, polygon, path, ellipse');
|
|
97
|
+
}
|
|
98
|
+
if (shape) {
|
|
99
|
+
if (parsed.fill) shape.setAttribute('fill', parsed.fill);
|
|
100
|
+
if (parsed.stroke) shape.setAttribute('stroke', parsed.stroke);
|
|
101
|
+
if (parsed['stroke-width']) shape.setAttribute('stroke-width', parsed['stroke-width']);
|
|
102
|
+
if (parsed['stroke-dasharray']) shape.setAttribute('stroke-dasharray', parsed['stroke-dasharray']);
|
|
103
|
+
}
|
|
104
|
+
// Apply text color if specified
|
|
105
|
+
if (parsed.color) {
|
|
106
|
+
var textEl = el.querySelector('text');
|
|
107
|
+
if (textEl) textEl.setAttribute('fill', parsed.color);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Render a graph model into the preview container.
|
|
114
|
+
* Runs dagre layout, builds SVG, inserts into DOM, applies pan-zoom.
|
|
115
|
+
* @param {Object} graphModel - Graph model JSON (nodes, edges, subgraphs).
|
|
116
|
+
*/
|
|
117
|
+
async function render(graphModel) {
|
|
118
|
+
if (!graphModel || !graphModel.nodes) return;
|
|
119
|
+
|
|
120
|
+
// Store for re-application
|
|
121
|
+
lastGraphModel = graphModel;
|
|
122
|
+
|
|
123
|
+
// Wait for fonts so text measurement is accurate
|
|
124
|
+
await document.fonts.ready;
|
|
125
|
+
|
|
126
|
+
// Compute layout via dagre — throws if NaN detected
|
|
127
|
+
var layout = SmartCodeDagreLayout.computeLayout(graphModel);
|
|
128
|
+
|
|
129
|
+
// Build SVG DOM
|
|
130
|
+
var svg = SmartCodeSvgRenderer.createSVG(layout);
|
|
131
|
+
|
|
132
|
+
// Insert into preview, clearing previous content
|
|
133
|
+
var preview = document.getElementById('preview');
|
|
134
|
+
preview.textContent = '';
|
|
135
|
+
preview.appendChild(svg);
|
|
136
|
+
|
|
137
|
+
// Apply inline styles from .mmd source (fill, stroke, etc.)
|
|
138
|
+
applyNodeStyles(graphModel);
|
|
139
|
+
|
|
140
|
+
// Apply current pan-zoom transform
|
|
141
|
+
if (window.applyTransform) window.applyTransform();
|
|
142
|
+
|
|
143
|
+
// Auto-fit on initial render
|
|
144
|
+
if (window.SmartCodeRenderer && SmartCodeRenderer.getInitialRender()) {
|
|
145
|
+
requestAnimationFrame(function() {
|
|
146
|
+
if (window.zoomFit) window.zoomFit();
|
|
147
|
+
});
|
|
148
|
+
SmartCodeRenderer.setInitialRender(false);
|
|
149
|
+
} else {
|
|
150
|
+
if (window.applyTransform) window.applyTransform();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Apply flag indicators after SVG is in the DOM
|
|
154
|
+
if (window.SmartCodeAnnotations) SmartCodeAnnotations.applyFlagsToSVG();
|
|
155
|
+
|
|
156
|
+
// Apply status colors from graph model (overrides nodeStyles for flagged nodes)
|
|
157
|
+
applyStatusColors(graphModel);
|
|
158
|
+
|
|
159
|
+
// Re-apply heatmap risk overlay if active (heatmap overrides status colors)
|
|
160
|
+
if (window.SmartCodeHeatmap && SmartCodeHeatmap.isActive()) {
|
|
161
|
+
SmartCodeHeatmap.applyRiskOverlay();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Apply collapse overlays if available
|
|
165
|
+
if (window.SmartCodeCollapseUI && SmartCodeCollapseUI.applyOverlays) {
|
|
166
|
+
SmartCodeCollapseUI.applyOverlays();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Emit rendered event
|
|
170
|
+
if (window.SmartCodeEventBus) {
|
|
171
|
+
SmartCodeEventBus.emit('diagram:rendered', {
|
|
172
|
+
svg: svg.outerHTML,
|
|
173
|
+
renderer: 'custom'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fetch graph model from /api/graph/ endpoint and render it.
|
|
180
|
+
* @param {string} filePath - The diagram file path to fetch.
|
|
181
|
+
*/
|
|
182
|
+
async function fetchAndRender(filePath) {
|
|
183
|
+
if (!filePath) return;
|
|
184
|
+
|
|
185
|
+
var resp = await fetch((window.SmartCodeBaseUrl || '') + '/api/graph/' + encodeURIComponent(filePath));
|
|
186
|
+
if (!resp.ok) {
|
|
187
|
+
throw new Error('Failed to fetch graph model: ' + resp.status + ' ' + resp.statusText);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var graphModel = await resp.json();
|
|
191
|
+
await render(graphModel);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Public API ──
|
|
195
|
+
window.SmartCodeCustomRenderer = {
|
|
196
|
+
render: render,
|
|
197
|
+
fetchAndRender: fetchAndRender,
|
|
198
|
+
getLastGraphModel: function() { return lastGraphModel; },
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
})();
|