@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,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
|
+
})();
|