@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.js +4324 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index.d.ts +374 -0
  6. package/dist/index.js +1167 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/static/annotations-panel.js +133 -0
  9. package/dist/static/annotations-svg.js +108 -0
  10. package/dist/static/annotations.css +367 -0
  11. package/dist/static/annotations.js +367 -0
  12. package/dist/static/app-init.js +497 -0
  13. package/dist/static/breakpoints.css +69 -0
  14. package/dist/static/breakpoints.js +197 -0
  15. package/dist/static/clipboard.js +94 -0
  16. package/dist/static/collapse-ui.js +325 -0
  17. package/dist/static/command-history.js +89 -0
  18. package/dist/static/context-menu.js +334 -0
  19. package/dist/static/custom-renderer.js +201 -0
  20. package/dist/static/dagre-layout.js +291 -0
  21. package/dist/static/diagram-dom.js +241 -0
  22. package/dist/static/diagram-editor.js +368 -0
  23. package/dist/static/editor-panel.js +107 -0
  24. package/dist/static/editor-popovers.js +187 -0
  25. package/dist/static/event-bus.js +57 -0
  26. package/dist/static/export.js +181 -0
  27. package/dist/static/file-tree.js +470 -0
  28. package/dist/static/ghost-paths.js +397 -0
  29. package/dist/static/heatmap.css +116 -0
  30. package/dist/static/heatmap.js +308 -0
  31. package/dist/static/icons.js +66 -0
  32. package/dist/static/inline-edit.js +294 -0
  33. package/dist/static/interaction-state.js +155 -0
  34. package/dist/static/interaction-tracker.js +93 -0
  35. package/dist/static/live.html +239 -0
  36. package/dist/static/main-layout.css +220 -0
  37. package/dist/static/main.css +334 -0
  38. package/dist/static/mcp-sessions.js +202 -0
  39. package/dist/static/modal.css +109 -0
  40. package/dist/static/modal.js +171 -0
  41. package/dist/static/node-drag.js +293 -0
  42. package/dist/static/pan-zoom.js +199 -0
  43. package/dist/static/renderer.js +280 -0
  44. package/dist/static/search.css +103 -0
  45. package/dist/static/search.js +304 -0
  46. package/dist/static/selection.js +353 -0
  47. package/dist/static/session-player.css +137 -0
  48. package/dist/static/session-player.js +411 -0
  49. package/dist/static/sidebar.css +248 -0
  50. package/dist/static/svg-renderer.js +313 -0
  51. package/dist/static/svg-shapes.js +218 -0
  52. package/dist/static/tokens.css +76 -0
  53. package/dist/static/vendor/dagre-bundle.js +43 -0
  54. package/dist/static/vendor/dagre.min.js +3 -0
  55. package/dist/static/vendor/graphlib.min.js +2 -0
  56. package/dist/static/viewport-transform.js +107 -0
  57. package/dist/static/workspace-switcher.js +202 -0
  58. package/dist/static/ws-client.js +71 -0
  59. package/dist/static/ws-handler.js +125 -0
  60. package/package.json +74 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * SmartCode Heatmap -- risk overlay coloring and execution frequency heatmap.
3
+ * Colors nodes by risk level (red/yellow/green) or frequency (cold blue to hot red).
4
+ * Supports mode toggle (risk/frequency), incremental merges, and empty state guidance.
5
+ * Dependencies: diagram-dom.js, event-bus.js, file-tree.js
6
+ */
7
+ var SmartCodeHeatmap = (function() {
8
+ 'use strict';
9
+
10
+ var RISK_COLORS = {
11
+ high: { fill: '#ef4444', opacity: 0.75 },
12
+ medium: { fill: '#eab308', opacity: 0.60 },
13
+ low: { fill: '#22c55e', opacity: 0.50 },
14
+ };
15
+
16
+ var state = {
17
+ active: false,
18
+ mode: 'risk', // 'risk' or 'frequency'
19
+ risks: new Map(),
20
+ visitCounts: {},
21
+ savedFills: new Map(),
22
+ };
23
+
24
+ function intensityToColor(t) {
25
+ var r = Math.round(66 + 189 * t);
26
+ var g = Math.round(133 - 53 * t);
27
+ var b = Math.round(244 - 244 * t);
28
+ return 'rgb(' + r + ',' + g + ',' + b + ')';
29
+ }
30
+
31
+ function findShape(nodeEl) {
32
+ if (!nodeEl) return null;
33
+ var shape = nodeEl.querySelector('rect, polygon, circle, path, ellipse');
34
+ if (!shape) {
35
+ var childG = nodeEl.querySelector('g');
36
+ if (childG) shape = childG.querySelector('rect, polygon, circle, path, ellipse');
37
+ }
38
+ return shape;
39
+ }
40
+
41
+ function getNodeId(nodeEl, idx) {
42
+ return nodeEl.getAttribute('data-node-id') || nodeEl.getAttribute('id') || String(idx);
43
+ }
44
+
45
+ function saveFills() {
46
+ state.savedFills.clear();
47
+ var nodes = DiagramDOM.getAllNodeElements();
48
+ for (var i = 0; i < nodes.length; i++) {
49
+ var shape = findShape(nodes[i]);
50
+ if (!shape) continue;
51
+ state.savedFills.set(getNodeId(nodes[i], i), {
52
+ fill: shape.getAttribute('fill') || '',
53
+ fillOpacity: shape.getAttribute('fill-opacity') || '',
54
+ });
55
+ }
56
+ }
57
+
58
+ function restoreFills() {
59
+ var nodes = DiagramDOM.getAllNodeElements();
60
+ for (var i = 0; i < nodes.length; i++) {
61
+ var shape = findShape(nodes[i]);
62
+ if (!shape) continue;
63
+ var saved = state.savedFills.get(getNodeId(nodes[i], i));
64
+ if (saved) {
65
+ if (saved.fill) shape.setAttribute('fill', saved.fill);
66
+ else shape.removeAttribute('fill');
67
+ if (saved.fillOpacity) shape.setAttribute('fill-opacity', saved.fillOpacity);
68
+ else shape.removeAttribute('fill-opacity');
69
+ }
70
+ }
71
+ state.savedFills.clear();
72
+ }
73
+
74
+ function applyRiskOverlay() {
75
+ if (state.risks.size === 0) return;
76
+ state.risks.forEach(function(risk, nodeId) {
77
+ var shape = findShape(DiagramDOM.findNodeElement(nodeId));
78
+ if (!shape) return;
79
+ var colors = RISK_COLORS[risk.level || risk];
80
+ if (!colors) return;
81
+ shape.setAttribute('fill', colors.fill);
82
+ shape.setAttribute('fill-opacity', String(colors.opacity));
83
+ });
84
+ }
85
+
86
+ function applyFrequencyHeatmap() {
87
+ var counts = state.visitCounts;
88
+ var keys = Object.keys(counts);
89
+ if (keys.length === 0) return;
90
+ var max = 0;
91
+ for (var k = 0; k < keys.length; k++) {
92
+ if (counts[keys[k]] > max) max = counts[keys[k]];
93
+ }
94
+ if (max === 0) return;
95
+ for (var j = 0; j < keys.length; j++) {
96
+ var shape = findShape(DiagramDOM.findNodeElement(keys[j]));
97
+ if (!shape) continue;
98
+ var intensity = counts[keys[j]] / max;
99
+ shape.setAttribute('fill', intensityToColor(intensity));
100
+ shape.setAttribute('fill-opacity', String(0.4 + intensity * 0.5));
101
+ }
102
+ }
103
+
104
+ /** Check if there is any data for the current mode */
105
+ function hasDataForMode(mode) {
106
+ if (mode === 'frequency') return Object.keys(state.visitCounts).length > 0;
107
+ if (mode === 'risk') return state.risks.size > 0;
108
+ return false;
109
+ }
110
+
111
+ /** Check if there is any data at all (either mode) */
112
+ function hasAnyData() {
113
+ return Object.keys(state.visitCounts).length > 0 || state.risks.size > 0;
114
+ }
115
+
116
+ function applyCurrent() {
117
+ if (state.mode === 'frequency' && Object.keys(state.visitCounts).length > 0) {
118
+ applyFrequencyHeatmap();
119
+ } else if (state.mode === 'risk') {
120
+ applyRiskOverlay();
121
+ }
122
+ updateLegend();
123
+ }
124
+
125
+ function createEl(tag, cls, text) {
126
+ var el = document.createElement(tag);
127
+ if (cls) el.className = cls;
128
+ if (text) el.textContent = text;
129
+ return el;
130
+ }
131
+
132
+ /** Create the mode toggle button */
133
+ function createModeToggle() {
134
+ var btn = createEl('button', 'heatmap-mode-toggle');
135
+ var otherMode = state.mode === 'risk' ? 'frequency' : 'risk';
136
+ btn.textContent = otherMode === 'frequency' ? 'Frequency' : 'Risk';
137
+ btn.title = 'Switch to ' + otherMode + ' mode';
138
+ btn.addEventListener('click', function() {
139
+ setMode(otherMode);
140
+ });
141
+ return btn;
142
+ }
143
+
144
+ /** Build the empty state message */
145
+ function buildEmptyState(legend) {
146
+ var msg = createEl('div', 'heatmap-empty-state');
147
+ if (state.mode === 'frequency') {
148
+ msg.textContent = 'No frequency data yet. Click on diagram nodes to build a frequency map.';
149
+ } else {
150
+ msg.textContent = 'No risk annotations. Use MCP set_risk_level to annotate node risk levels.';
151
+ }
152
+ legend.appendChild(msg);
153
+ }
154
+
155
+ function updateLegend() {
156
+ var existing = document.querySelector('.heatmap-legend');
157
+ if (existing) existing.remove();
158
+ if (!state.active) return;
159
+ var container = document.getElementById('preview-container');
160
+ if (!container) return;
161
+
162
+ var legend = createEl('div', 'heatmap-legend');
163
+
164
+ // Header with title and mode toggle
165
+ var header = createEl('div', 'heatmap-legend-header');
166
+ var title = createEl('div', 'heatmap-legend-title',
167
+ state.mode === 'frequency' ? 'Frequency' : 'Risk');
168
+ header.appendChild(title);
169
+
170
+ // Only show toggle if there is data in the other mode too
171
+ if (hasAnyData()) {
172
+ header.appendChild(createModeToggle());
173
+ }
174
+ legend.appendChild(header);
175
+
176
+ // Show empty state if no data for current mode
177
+ if (!hasDataForMode(state.mode)) {
178
+ buildEmptyState(legend);
179
+ container.appendChild(legend);
180
+ return;
181
+ }
182
+
183
+ // Frequency legend content
184
+ if (state.mode === 'frequency') {
185
+ legend.appendChild(createEl('div', 'heatmap-legend-gradient'));
186
+ var labels = createEl('div', 'heatmap-legend-labels');
187
+ labels.appendChild(createEl('span', null, 'Low'));
188
+ labels.appendChild(createEl('span', null, 'High'));
189
+ legend.appendChild(labels);
190
+ } else {
191
+ // Risk legend content
192
+ var levels = [['high','High'],['medium','Medium'],['low','Low']];
193
+ for (var i = 0; i < levels.length; i++) {
194
+ var item = createEl('div', 'heatmap-legend-item');
195
+ var dot = createEl('span', 'heatmap-legend-dot');
196
+ dot.style.background = RISK_COLORS[levels[i][0]].fill;
197
+ item.appendChild(dot);
198
+ item.appendChild(createEl('span', null, levels[i][1]));
199
+ legend.appendChild(item);
200
+ }
201
+ }
202
+ container.appendChild(legend);
203
+ }
204
+
205
+ function toggle() {
206
+ var btn = document.getElementById('btnHeatmap');
207
+ if (state.active) {
208
+ restoreFills();
209
+ state.active = false;
210
+ if (btn) btn.classList.remove('active');
211
+ updateLegend();
212
+ } else {
213
+ // Auto-select mode based on available data
214
+ if (Object.keys(state.visitCounts).length > 0) {
215
+ state.mode = 'frequency';
216
+ } else if (state.risks.size > 0) {
217
+ state.mode = 'risk';
218
+ }
219
+ // If no data in either mode, still activate to show empty state
220
+ saveFills();
221
+ state.active = true;
222
+ if (btn) btn.classList.add('active');
223
+ applyCurrent();
224
+ }
225
+ }
226
+
227
+ function updateRisks(risksMap) {
228
+ if (risksMap instanceof Map) {
229
+ state.risks = risksMap;
230
+ } else if (risksMap && typeof risksMap === 'object') {
231
+ state.risks = new Map();
232
+ var keys = Object.keys(risksMap);
233
+ for (var i = 0; i < keys.length; i++) state.risks.set(keys[i], risksMap[keys[i]]);
234
+ }
235
+ if (state.active && state.mode === 'risk') {
236
+ restoreFills(); saveFills(); applyRiskOverlay(); updateLegend();
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Replace all visit counts (used for full refresh, e.g., file switch or initial load).
242
+ */
243
+ function updateVisitCounts(counts) {
244
+ state.visitCounts = counts || {};
245
+ if (state.active) {
246
+ if (Object.keys(state.visitCounts).length > 0) {
247
+ state.mode = 'frequency';
248
+ }
249
+ restoreFills(); saveFills(); applyCurrent();
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Merge incremental visit counts (used for real-time WebSocket updates).
255
+ * Adds delta counts to existing counts rather than replacing.
256
+ */
257
+ function mergeVisitCounts(delta) {
258
+ if (!delta || typeof delta !== 'object') return;
259
+ var keys = Object.keys(delta);
260
+ for (var i = 0; i < keys.length; i++) {
261
+ var nodeId = keys[i];
262
+ var count = delta[nodeId];
263
+ if (typeof count !== 'number') continue;
264
+ state.visitCounts[nodeId] = (state.visitCounts[nodeId] || 0) + count;
265
+ }
266
+ if (state.active && state.mode === 'frequency') {
267
+ restoreFills(); saveFills(); applyFrequencyHeatmap(); updateLegend();
268
+ }
269
+ }
270
+
271
+ function setMode(newMode) {
272
+ if (newMode !== 'risk' && newMode !== 'frequency') return;
273
+ state.mode = newMode;
274
+ if (state.active) { restoreFills(); saveFills(); applyCurrent(); }
275
+ }
276
+
277
+ function onDiagramRendered() {
278
+ if (!state.active) return;
279
+ state.savedFills.clear();
280
+ saveFills();
281
+ applyCurrent();
282
+ }
283
+
284
+ function init() {
285
+ if (window.SmartCodeEventBus) {
286
+ SmartCodeEventBus.on('diagram:rendered', onDiagramRendered);
287
+ }
288
+ var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : '';
289
+ if (file) {
290
+ fetch((window.SmartCodeBaseUrl || '') + '/api/heatmap/' + encodeURIComponent(file))
291
+ .then(function(r) { return r.ok ? r.json() : null; })
292
+ .then(function(data) { if (data) updateVisitCounts(data); })
293
+ .catch(function() {});
294
+ }
295
+ }
296
+
297
+ return {
298
+ init: init,
299
+ toggle: toggle,
300
+ updateRisks: updateRisks,
301
+ updateVisitCounts: updateVisitCounts,
302
+ mergeVisitCounts: mergeVisitCounts,
303
+ setMode: setMode,
304
+ isActive: function() { return state.active; },
305
+ applyRiskOverlay: applyRiskOverlay,
306
+ getState: function() { return state; },
307
+ };
308
+ })();
@@ -0,0 +1,66 @@
1
+ /* SmartCode — SVG Icon System */
2
+ /* Lucide-based icons (MIT), stroke=currentColor, 24x24 viewBox at 16x16 */
3
+
4
+ (function () {
5
+ 'use strict';
6
+
7
+ var SIZE = 16;
8
+ var ATTRS = 'xmlns="http://www.w3.org/2000/svg" width="' + SIZE + '" height="' + SIZE +
9
+ '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
10
+
11
+ window.SmartCodeIcons = {
12
+ folder: '<svg ' + ATTRS + '><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
13
+
14
+ folderOpen: '<svg ' + ATTRS + '><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>',
15
+
16
+ file: '<svg ' + ATTRS + '><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>',
17
+
18
+ save: '<svg ' + ATTRS + '><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>',
19
+
20
+ plus: '<svg ' + ATTRS + '><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
21
+
22
+ chevronRight: '<svg ' + ATTRS + '><path d="m9 18 6-6-6-6"/></svg>',
23
+
24
+ chevronDown: '<svg ' + ATTRS + '><path d="m6 9 6 6 6-6"/></svg>',
25
+
26
+ edit: '<svg ' + ATTRS + '><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>',
27
+
28
+ trash: '<svg ' + ATTRS + '><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>',
29
+
30
+ close: '<svg ' + ATTRS + '><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
31
+
32
+ arrowUp: '<svg ' + ATTRS + '><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>',
33
+
34
+ arrowDown: '<svg ' + ATTRS + '><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>',
35
+
36
+ arrowRight: '<svg ' + ATTRS + '><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
37
+
38
+ chart: '<svg ' + ATTRS + '><line x1="18" x2="18" y1="20" y2="10"/><line x1="12" x2="12" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="14"/></svg>',
39
+
40
+ diamond: '<svg ' + ATTRS + '><path d="M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z"/></svg>',
41
+
42
+ help: '<svg ' + ATTRS + '><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',
43
+
44
+ flag: '<svg ' + ATTRS + '><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/></svg>',
45
+
46
+ sidebar: '<svg ' + ATTRS + '><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>',
47
+
48
+ editor: '<svg ' + ATTRS + '><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
49
+
50
+ eye: '<svg ' + ATTRS + '><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>',
51
+
52
+ download: '<svg ' + ATTRS + '><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>',
53
+
54
+ image: '<svg ' + ATTRS + '><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
55
+
56
+ sync: '<svg ' + ATTRS + '><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>',
57
+
58
+ ghost: '<svg ' + ATTRS + ' style="opacity:0.7"><path d="M9 10h.01"/><path d="M15 10h.01"/><path d="M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z"/></svg>',
59
+
60
+ heatmap: '<svg ' + ATTRS + '><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 3v18"/><path d="M15 3v18"/></svg>',
61
+
62
+ play: '<svg ' + ATTRS + '><polygon points="6 3 20 12 6 21 6 3"/></svg>',
63
+
64
+ node: '<svg ' + ATTRS + '><rect width="16" height="10" x="4" y="7" rx="2"/><circle cx="12" cy="12" r="1"/></svg>',
65
+ };
66
+ })();
@@ -0,0 +1,294 @@
1
+ /**
2
+ * SmartCode Inline Edit -- double-click contenteditable overlay for label editing.
3
+ * Opens an HTML overlay positioned over the SVG text element for in-place editing.
4
+ *
5
+ * Dependencies:
6
+ * - interaction-state.js (SmartCodeInteraction)
7
+ * - diagram-dom.js (DiagramDOM)
8
+ * - diagram-editor.js (MmdEditor)
9
+ * - event-bus.js (SmartCodeEventBus)
10
+ *
11
+ * Usage:
12
+ * SmartCodeInlineEdit.init();
13
+ * SmartCodeInlineEdit.open('nodeId');
14
+ * SmartCodeInlineEdit.confirm();
15
+ * SmartCodeInlineEdit.cancel();
16
+ * SmartCodeInlineEdit.isActive();
17
+ */
18
+ (function() {
19
+ 'use strict';
20
+
21
+ var activeOverlay = null;
22
+ var activeNodeId = null;
23
+ var originalLabel = null;
24
+ var hiddenTextEl = null;
25
+ var isCommitting = false; // guard against blur firing after confirm
26
+
27
+ // ── Open Inline Edit ──
28
+
29
+ function open(nodeId) {
30
+ // Close any existing edit first
31
+ if (activeOverlay) confirm();
32
+
33
+ // Find the node's text element
34
+ var nodeEl = DiagramDOM.findNodeElement(nodeId);
35
+ if (!nodeEl) return;
36
+
37
+ var textEl = nodeEl.querySelector('text');
38
+ // Mermaid uses .nodeLabel span instead of <text>
39
+ var mermaidLabel = nodeEl.querySelector('.nodeLabel');
40
+ var targetEl = textEl || mermaidLabel;
41
+ if (!targetEl) return;
42
+
43
+ // Get current label
44
+ var currentLabel = '';
45
+ if (window.MmdEditor) {
46
+ var editor = document.getElementById('editor');
47
+ if (editor) currentLabel = MmdEditor.getNodeText(editor.value, nodeId);
48
+ }
49
+ if (!currentLabel) {
50
+ currentLabel = DiagramDOM.getNodeLabel(nodeId) || nodeId;
51
+ }
52
+
53
+ // Get positions
54
+ var textRect = targetEl.getBoundingClientRect();
55
+ var container = document.getElementById('preview-container');
56
+ if (!container) return;
57
+ var containerRect = container.getBoundingClientRect();
58
+
59
+ // Create the overlay
60
+ var overlay = document.createElement('div');
61
+ overlay.className = 'inline-edit-overlay';
62
+ overlay.contentEditable = 'true';
63
+ overlay.spellcheck = false;
64
+ overlay.textContent = currentLabel;
65
+
66
+ // Position relative to preview-container
67
+ overlay.style.position = 'absolute';
68
+ overlay.style.left = (textRect.left - containerRect.left) + 'px';
69
+ overlay.style.top = (textRect.top - containerRect.top) + 'px';
70
+ overlay.style.minWidth = Math.max(textRect.width, 60) + 'px';
71
+ overlay.style.minHeight = textRect.height + 'px';
72
+
73
+ // Style to match SVG text appearance
74
+ var computed = window.getComputedStyle(targetEl);
75
+ overlay.style.fontFamily = computed.fontFamily || "'Inter', sans-serif";
76
+ overlay.style.fontSize = computed.fontSize || '14px';
77
+ overlay.style.fontWeight = computed.fontWeight || '400';
78
+ overlay.style.color = '#e4e4e7';
79
+ overlay.style.background = 'var(--surface-2)';
80
+ overlay.style.border = '2px solid #3b82f6';
81
+ overlay.style.borderRadius = '4px';
82
+ overlay.style.padding = '2px 6px';
83
+ overlay.style.outline = 'none';
84
+ overlay.style.zIndex = '10001';
85
+ overlay.style.whiteSpace = 'nowrap';
86
+ overlay.style.lineHeight = textRect.height + 'px';
87
+ overlay.style.boxSizing = 'border-box';
88
+
89
+ container.appendChild(overlay);
90
+
91
+ // Focus and select all text
92
+ overlay.focus();
93
+ var range = document.createRange();
94
+ range.selectNodeContents(overlay);
95
+ var sel = window.getSelection();
96
+ sel.removeAllRanges();
97
+ sel.addRange(range);
98
+
99
+ // Store references
100
+ activeOverlay = overlay;
101
+ activeNodeId = nodeId;
102
+ originalLabel = currentLabel;
103
+
104
+ // Hide original SVG text element
105
+ hiddenTextEl = targetEl;
106
+ targetEl.style.visibility = 'hidden';
107
+
108
+ // Attach event handlers
109
+ overlay.addEventListener('keydown', handleOverlayKeydown);
110
+ overlay.addEventListener('blur', handleOverlayBlur);
111
+
112
+ // Emit event
113
+ if (window.SmartCodeEventBus) {
114
+ SmartCodeEventBus.emit('edit:started', { nodeId: nodeId });
115
+ }
116
+ }
117
+
118
+ // ── Confirm Edit ──
119
+
120
+ function confirm() {
121
+ if (!activeOverlay) return;
122
+ isCommitting = true;
123
+
124
+ var newText = activeOverlay.textContent.trim();
125
+ var nodeId = activeNodeId;
126
+ var oldLabel = originalLabel;
127
+
128
+ // Restore SVG text visibility before close
129
+ if (hiddenTextEl) {
130
+ hiddenTextEl.style.visibility = '';
131
+ }
132
+
133
+ // Save if changed and not empty
134
+ if (newText && newText !== originalLabel && window.MmdEditor && MmdEditor.applyEdit) {
135
+ MmdEditor.applyEdit(function(c) {
136
+ return MmdEditor.editNodeText(c, nodeId, newText);
137
+ });
138
+ }
139
+
140
+ // Close the overlay
141
+ closeOverlay();
142
+ isCommitting = false;
143
+
144
+ // Transition FSM
145
+ if (window.SmartCodeInteraction) {
146
+ SmartCodeInteraction.transition('confirm');
147
+ }
148
+
149
+ // Emit event
150
+ if (window.SmartCodeEventBus) {
151
+ SmartCodeEventBus.emit('edit:completed', {
152
+ nodeId: nodeId,
153
+ oldLabel: oldLabel,
154
+ newLabel: newText,
155
+ });
156
+ }
157
+ }
158
+
159
+ // ── Cancel Edit ──
160
+
161
+ function cancel() {
162
+ if (!activeOverlay) return;
163
+
164
+ var nodeId = activeNodeId;
165
+
166
+ // Restore SVG text visibility
167
+ if (hiddenTextEl) {
168
+ hiddenTextEl.style.visibility = '';
169
+ }
170
+
171
+ // Close the overlay
172
+ closeOverlay();
173
+
174
+ // Transition FSM
175
+ if (window.SmartCodeInteraction) {
176
+ SmartCodeInteraction.transition('cancel');
177
+ }
178
+
179
+ // Emit event
180
+ if (window.SmartCodeEventBus) {
181
+ SmartCodeEventBus.emit('edit:cancelled', { nodeId: nodeId });
182
+ }
183
+ }
184
+
185
+ // ── Close Overlay ──
186
+
187
+ function closeOverlay() {
188
+ if (activeOverlay) {
189
+ activeOverlay.removeEventListener('keydown', handleOverlayKeydown);
190
+ activeOverlay.removeEventListener('blur', handleOverlayBlur);
191
+ if (activeOverlay.parentNode) {
192
+ activeOverlay.parentNode.removeChild(activeOverlay);
193
+ }
194
+ }
195
+ activeOverlay = null;
196
+ activeNodeId = null;
197
+ originalLabel = null;
198
+ hiddenTextEl = null;
199
+ }
200
+
201
+ // ── Event Handlers ──
202
+
203
+ function handleOverlayKeydown(e) {
204
+ if (e.key === 'Enter' && !e.shiftKey) {
205
+ e.preventDefault();
206
+ confirm();
207
+ } else if (e.key === 'Escape') {
208
+ cancel();
209
+ }
210
+ }
211
+
212
+ function handleOverlayBlur(e) {
213
+ // If focus moved to another interactive element (context menu, etc.), cancel
214
+ // Use a timeout to let the relatedTarget resolve
215
+ setTimeout(function() {
216
+ if (!activeOverlay) return;
217
+ // Guard: if confirm() already ran, don't cancel
218
+ if (isCommitting) return;
219
+ // Check if focus went to a valid target that should keep the edit open
220
+ var active = document.activeElement;
221
+ if (active && (active.closest('.context-menu') || active.closest('.flag-popover'))) return;
222
+ // Default: cancel on blur to prevent accidental data loss
223
+ cancel();
224
+ }, 150);
225
+ }
226
+
227
+ // ── Double-click Handler (delegated on #preview-container) ──
228
+
229
+ function handleDblClick(e) {
230
+ // Check FSM blocking states
231
+ if (window.SmartCodeInteraction && SmartCodeInteraction.isBlocking()) return;
232
+
233
+ // Don't handle in special modes
234
+ var fsmState = window.SmartCodeInteraction ? SmartCodeInteraction.getState() : 'idle';
235
+ if (fsmState === 'flagging' || fsmState === 'add-node' || fsmState === 'add-edge') return;
236
+
237
+ // Skip UI controls
238
+ if (e.target.closest('.zoom-controls') ||
239
+ e.target.closest('.flag-popover') ||
240
+ e.target.closest('.editor-popover') ||
241
+ e.target.closest('.context-menu') ||
242
+ e.target.closest('.inline-edit-overlay')) return;
243
+
244
+ // Detect what was double-clicked
245
+ var nodeInfo = DiagramDOM.extractNodeId(e.target);
246
+ if (!nodeInfo) return;
247
+
248
+ // Only handle nodes and subgraphs (not edges)
249
+ if (nodeInfo.type !== 'node' && nodeInfo.type !== 'subgraph') return;
250
+
251
+ // Don't open inline edit on collapsed nodes (let collapse-ui handle them)
252
+ if (nodeInfo.id.startsWith('__collapsed__')) return;
253
+
254
+ // Transition FSM to editing
255
+ if (window.SmartCodeInteraction) {
256
+ SmartCodeInteraction.transition('dbl_click', nodeInfo);
257
+ }
258
+
259
+ open(nodeInfo.id);
260
+ }
261
+
262
+ // ── EventBus Subscription: auto-commit on re-render ──
263
+
264
+ function handleDiagramRendered() {
265
+ // If inline edit is active, commit before SVG replacement
266
+ if (activeOverlay) {
267
+ confirm();
268
+ }
269
+ }
270
+
271
+ // ── Init ──
272
+
273
+ function init() {
274
+ var container = document.getElementById('preview-container');
275
+ if (container) {
276
+ container.addEventListener('dblclick', handleDblClick);
277
+ }
278
+
279
+ // Subscribe to diagram:rendered to auto-commit active edits
280
+ if (window.SmartCodeEventBus) {
281
+ SmartCodeEventBus.on('diagram:rendered', handleDiagramRendered);
282
+ }
283
+ }
284
+
285
+ // ── Public API ──
286
+ window.SmartCodeInlineEdit = {
287
+ init: init,
288
+ open: open,
289
+ close: closeOverlay,
290
+ confirm: confirm,
291
+ cancel: cancel,
292
+ isActive: function() { return !!activeOverlay; },
293
+ };
294
+ })();