@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Breakpoints -- visual breakpoint indicators on SVG nodes,
|
|
3
|
+
* notification bar for breakpoint:hit events, REST toggle/continue/remove.
|
|
4
|
+
*
|
|
5
|
+
* Dependencies:
|
|
6
|
+
* - diagram-dom.js (DiagramDOM)
|
|
7
|
+
* - event-bus.js (SmartCodeEventBus)
|
|
8
|
+
* - file-tree.js (SmartCodeFileTree) — for currentFile
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* SmartCodeBreakpoints.init();
|
|
12
|
+
* SmartCodeBreakpoints.updateBreakpoints(breakpointSet);
|
|
13
|
+
* SmartCodeBreakpoints.toggleBreakpoint(nodeId);
|
|
14
|
+
* SmartCodeBreakpoints.showNotification(nodeId);
|
|
15
|
+
* SmartCodeBreakpoints.hideNotification();
|
|
16
|
+
*/
|
|
17
|
+
(function() {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
var SVG_NS = 'http://www.w3.org/2000/svg';
|
|
21
|
+
|
|
22
|
+
// ── Module State ──
|
|
23
|
+
var breakpoints = new Set();
|
|
24
|
+
var activeNotification = null;
|
|
25
|
+
|
|
26
|
+
// ── Helpers ──
|
|
27
|
+
|
|
28
|
+
function getCurrentFile() {
|
|
29
|
+
if (window.SmartCodeFileTree) return SmartCodeFileTree.getCurrentFile();
|
|
30
|
+
return window.currentFile || '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Breakpoint Indicators on SVG ──
|
|
34
|
+
|
|
35
|
+
function applyBreakpointIndicators() {
|
|
36
|
+
var svg = DiagramDOM.getSVG();
|
|
37
|
+
if (!svg) return;
|
|
38
|
+
|
|
39
|
+
// Remove existing indicators
|
|
40
|
+
svg.querySelectorAll('.breakpoint-indicator').forEach(function(el) { el.remove(); });
|
|
41
|
+
|
|
42
|
+
if (breakpoints.size === 0) return;
|
|
43
|
+
|
|
44
|
+
breakpoints.forEach(function(nodeId) {
|
|
45
|
+
var nodeEl = DiagramDOM.findNodeElement(nodeId);
|
|
46
|
+
if (!nodeEl) return;
|
|
47
|
+
var bbox = nodeEl.getBBox ? nodeEl.getBBox() : null;
|
|
48
|
+
if (!bbox) return;
|
|
49
|
+
|
|
50
|
+
// Account for transform="translate(x,y)" on custom renderer nodes
|
|
51
|
+
var tx = 0, ty = 0;
|
|
52
|
+
var transform = nodeEl.getAttribute('transform');
|
|
53
|
+
if (transform) {
|
|
54
|
+
var m = transform.match(/translate\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
|
|
55
|
+
if (m) { tx = parseFloat(m[1]); ty = parseFloat(m[2]); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var circle = document.createElementNS(SVG_NS, 'circle');
|
|
59
|
+
circle.setAttribute('cx', tx + bbox.x - 4);
|
|
60
|
+
circle.setAttribute('cy', ty + bbox.y + bbox.height / 2);
|
|
61
|
+
circle.setAttribute('r', '6');
|
|
62
|
+
circle.setAttribute('fill', '#ef4444');
|
|
63
|
+
circle.setAttribute('class', 'breakpoint-indicator');
|
|
64
|
+
svg.appendChild(circle);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Update breakpoints from annotations ──
|
|
69
|
+
|
|
70
|
+
function updateBreakpoints(breakpointSet) {
|
|
71
|
+
breakpoints = breakpointSet instanceof Set ? breakpointSet : new Set(breakpointSet);
|
|
72
|
+
applyBreakpointIndicators();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Notification Bar ──
|
|
76
|
+
|
|
77
|
+
function showNotification(nodeId) {
|
|
78
|
+
hideNotification();
|
|
79
|
+
|
|
80
|
+
var container = document.getElementById('preview-container');
|
|
81
|
+
if (!container) return;
|
|
82
|
+
|
|
83
|
+
var bar = document.createElement('div');
|
|
84
|
+
bar.className = 'breakpoint-notification';
|
|
85
|
+
|
|
86
|
+
var msgSpan = document.createElement('span');
|
|
87
|
+
msgSpan.textContent = 'Breakpoint hit on ';
|
|
88
|
+
var nameSpan = document.createElement('span');
|
|
89
|
+
nameSpan.className = 'bp-node-name';
|
|
90
|
+
nameSpan.textContent = nodeId;
|
|
91
|
+
msgSpan.appendChild(nameSpan);
|
|
92
|
+
bar.appendChild(msgSpan);
|
|
93
|
+
|
|
94
|
+
var spacer = document.createElement('span');
|
|
95
|
+
spacer.style.flex = '1';
|
|
96
|
+
bar.appendChild(spacer);
|
|
97
|
+
|
|
98
|
+
var btnContinue = document.createElement('button');
|
|
99
|
+
btnContinue.className = 'btn-breakpoint-action primary';
|
|
100
|
+
btnContinue.textContent = 'Continue';
|
|
101
|
+
btnContinue.addEventListener('click', function() { continueBreakpoint(nodeId); });
|
|
102
|
+
bar.appendChild(btnContinue);
|
|
103
|
+
|
|
104
|
+
var btnRemove = document.createElement('button');
|
|
105
|
+
btnRemove.className = 'btn-breakpoint-action';
|
|
106
|
+
btnRemove.textContent = 'Remove Breakpoint';
|
|
107
|
+
btnRemove.addEventListener('click', function() { removeBreakpoint(nodeId); });
|
|
108
|
+
bar.appendChild(btnRemove);
|
|
109
|
+
|
|
110
|
+
container.insertBefore(bar, container.firstChild);
|
|
111
|
+
activeNotification = bar;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hideNotification() {
|
|
115
|
+
if (activeNotification && activeNotification.parentNode) {
|
|
116
|
+
activeNotification.parentNode.removeChild(activeNotification);
|
|
117
|
+
}
|
|
118
|
+
activeNotification = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── REST Integration ──
|
|
122
|
+
|
|
123
|
+
function toggleBreakpoint(nodeId) {
|
|
124
|
+
var file = getCurrentFile();
|
|
125
|
+
if (!file) return;
|
|
126
|
+
var action = breakpoints.has(nodeId) ? 'remove' : 'set';
|
|
127
|
+
fetch((window.SmartCodeBaseUrl || '') + '/api/breakpoints/' + encodeURIComponent(file), {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ nodeId: nodeId, action: action })
|
|
131
|
+
}).then(function(resp) {
|
|
132
|
+
if (!resp.ok) return;
|
|
133
|
+
if (action === 'set') {
|
|
134
|
+
breakpoints.add(nodeId);
|
|
135
|
+
} else {
|
|
136
|
+
breakpoints.delete(nodeId);
|
|
137
|
+
}
|
|
138
|
+
applyBreakpointIndicators();
|
|
139
|
+
}).catch(function(err) {
|
|
140
|
+
console.warn('Failed to toggle breakpoint:', err.message);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function continueBreakpoint(nodeId) {
|
|
145
|
+
var file = getCurrentFile();
|
|
146
|
+
if (!file) return;
|
|
147
|
+
fetch((window.SmartCodeBaseUrl || '') + '/api/breakpoints/' + encodeURIComponent(file) + '/continue', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ nodeId: nodeId })
|
|
151
|
+
}).catch(function(err) {
|
|
152
|
+
console.warn('Failed to continue breakpoint:', err.message);
|
|
153
|
+
});
|
|
154
|
+
hideNotification();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function removeBreakpoint(nodeId) {
|
|
158
|
+
var file = getCurrentFile();
|
|
159
|
+
if (!file) return;
|
|
160
|
+
fetch((window.SmartCodeBaseUrl || '') + '/api/breakpoints/' + encodeURIComponent(file), {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ nodeId: nodeId, action: 'remove' })
|
|
164
|
+
}).then(function(resp) {
|
|
165
|
+
if (!resp.ok) return;
|
|
166
|
+
breakpoints.delete(nodeId);
|
|
167
|
+
applyBreakpointIndicators();
|
|
168
|
+
}).catch(function(err) {
|
|
169
|
+
console.warn('Failed to remove breakpoint:', err.message);
|
|
170
|
+
});
|
|
171
|
+
hideNotification();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Init ──
|
|
175
|
+
|
|
176
|
+
function init() {
|
|
177
|
+
// Re-apply indicators after each diagram render
|
|
178
|
+
if (window.SmartCodeEventBus) {
|
|
179
|
+
SmartCodeEventBus.on('diagram:rendered', applyBreakpointIndicators);
|
|
180
|
+
}
|
|
181
|
+
// Apply indicators if SVG already exists
|
|
182
|
+
applyBreakpointIndicators();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Public API ──
|
|
186
|
+
window.SmartCodeBreakpoints = {
|
|
187
|
+
init: init,
|
|
188
|
+
updateBreakpoints: updateBreakpoints,
|
|
189
|
+
applyBreakpointIndicators: applyBreakpointIndicators,
|
|
190
|
+
toggleBreakpoint: toggleBreakpoint,
|
|
191
|
+
showNotification: showNotification,
|
|
192
|
+
hideNotification: hideNotification,
|
|
193
|
+
continueBreakpoint: continueBreakpoint,
|
|
194
|
+
removeBreakpoint: removeBreakpoint,
|
|
195
|
+
getBreakpoints: function() { return breakpoints; },
|
|
196
|
+
};
|
|
197
|
+
})();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode -- Clipboard (Copy/Paste/Duplicate)
|
|
3
|
+
* Internal clipboard buffer for node copy/paste/duplicate operations.
|
|
4
|
+
* Uses internal JS buffer (NOT browser Clipboard API -- localhost is not HTTPS).
|
|
5
|
+
*
|
|
6
|
+
* Dependencies:
|
|
7
|
+
* - selection.js (SmartCodeSelection)
|
|
8
|
+
* - diagram-editor.js (MmdEditor)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* SmartCodeClipboard.copy(); // copies selected node to buffer
|
|
12
|
+
* SmartCodeClipboard.paste(); // pastes node from buffer with new ID
|
|
13
|
+
* SmartCodeClipboard.duplicate(); // duplicates selected node in place
|
|
14
|
+
* SmartCodeClipboard.hasContent(); // true if buffer has content
|
|
15
|
+
* SmartCodeClipboard.clear(); // clears the buffer
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
var buffer = null; // { nodeId: string, label: string } | null
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copy the currently selected node to the internal buffer.
|
|
24
|
+
* @returns {boolean} true if a node was copied, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
function copy() {
|
|
27
|
+
if (!window.SmartCodeSelection) return false;
|
|
28
|
+
var sel = SmartCodeSelection.getSelected();
|
|
29
|
+
if (!sel || sel.type !== 'node') return false;
|
|
30
|
+
|
|
31
|
+
if (!window.MmdEditor) return false;
|
|
32
|
+
var editor = document.getElementById('editor');
|
|
33
|
+
if (!editor) return false;
|
|
34
|
+
|
|
35
|
+
var label = MmdEditor.getNodeText(editor.value, sel.id);
|
|
36
|
+
buffer = { nodeId: sel.id, label: label };
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Paste the node from the buffer with a new ID and "(copy)" suffix.
|
|
42
|
+
* @returns {boolean} true if paste was performed, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
function paste() {
|
|
45
|
+
if (!buffer) return false;
|
|
46
|
+
if (!window.MmdEditor) return false;
|
|
47
|
+
|
|
48
|
+
var pasteLabel = buffer.label + ' (copy)';
|
|
49
|
+
MmdEditor.applyEdit(function (c) {
|
|
50
|
+
var newId = MmdEditor.generateNodeId(c);
|
|
51
|
+
return MmdEditor.addNode(c, newId, pasteLabel);
|
|
52
|
+
});
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Duplicate the currently selected node in place (new ID, "(copy)" suffix).
|
|
58
|
+
* @returns {boolean} true if duplication was performed, false otherwise
|
|
59
|
+
*/
|
|
60
|
+
function duplicate() {
|
|
61
|
+
if (!window.SmartCodeSelection) return false;
|
|
62
|
+
var sel = SmartCodeSelection.getSelected();
|
|
63
|
+
if (!sel || sel.type !== 'node') return false;
|
|
64
|
+
|
|
65
|
+
if (!window.MmdEditor) return false;
|
|
66
|
+
var nodeId = sel.id;
|
|
67
|
+
MmdEditor.applyEdit(function (c) {
|
|
68
|
+
return MmdEditor.duplicateNode(c, nodeId);
|
|
69
|
+
});
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @returns {boolean} Whether the clipboard buffer has content
|
|
75
|
+
*/
|
|
76
|
+
function hasContent() {
|
|
77
|
+
return buffer !== null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear the clipboard buffer.
|
|
82
|
+
*/
|
|
83
|
+
function clear() {
|
|
84
|
+
buffer = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
window.SmartCodeClipboard = {
|
|
88
|
+
copy: copy,
|
|
89
|
+
paste: paste,
|
|
90
|
+
duplicate: duplicate,
|
|
91
|
+
hasContent: hasContent,
|
|
92
|
+
clear: clear,
|
|
93
|
+
};
|
|
94
|
+
})();
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode — Collapse UI
|
|
3
|
+
* Browser-side collapse/expand interaction handlers.
|
|
4
|
+
* Handles click events on collapsed nodes and subgraph headers.
|
|
5
|
+
* Exposed as window.SmartCodeCollapseUI
|
|
6
|
+
*
|
|
7
|
+
* Dependencies: diagram-dom.js (DiagramDOM), event-bus.js (SmartCodeEventBus)
|
|
8
|
+
*/
|
|
9
|
+
(function() {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// Private state -- not exposed on the public object
|
|
13
|
+
var _state = { collapsed: new Set() };
|
|
14
|
+
|
|
15
|
+
var SmartCodeCollapseUI = {
|
|
16
|
+
autoCollapsed: [],
|
|
17
|
+
breadcrumbs: [],
|
|
18
|
+
focusedSubgraph: null,
|
|
19
|
+
config: { maxVisibleNodes: 50 },
|
|
20
|
+
onToggle: null,
|
|
21
|
+
onFocusChange: null,
|
|
22
|
+
|
|
23
|
+
// ─── Initialization ──────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
init: function(options) {
|
|
26
|
+
options = options || {};
|
|
27
|
+
this.onToggle = options.onToggle || (function() {});
|
|
28
|
+
this.attachClickHandlers();
|
|
29
|
+
|
|
30
|
+
// Subscribe to event bus: re-apply overlays after diagram render
|
|
31
|
+
if (window.SmartCodeEventBus) {
|
|
32
|
+
var self = this;
|
|
33
|
+
SmartCodeEventBus.on('diagram:rendered', function() {
|
|
34
|
+
if (typeof self.applyOverlays === 'function') {
|
|
35
|
+
self.applyOverlays();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
initFocusMode: function(options) {
|
|
42
|
+
options = options || {};
|
|
43
|
+
this.onFocusChange = options.onFocusChange || (function() {});
|
|
44
|
+
this.attachFocusHandlers();
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// ─── State Management ────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
setCollapsed: function(ids) {
|
|
50
|
+
_state.collapsed = new Set(ids);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
getCollapsed: function() {
|
|
54
|
+
return Array.from(_state.collapsed);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
setAutoCollapsed: function(ids) {
|
|
58
|
+
this.autoCollapsed = ids || [];
|
|
59
|
+
this.renderAutoCollapseNotice();
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
setConfig: function(config) {
|
|
63
|
+
this.config = Object.assign({}, this.config, config);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
setBreadcrumbs: function(crumbs, focusedSubgraph) {
|
|
67
|
+
this.breadcrumbs = crumbs || [];
|
|
68
|
+
this.focusedSubgraph = focusedSubgraph;
|
|
69
|
+
this.renderBreadcrumbs();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// ─── Click Handlers ──────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
attachClickHandlers: function() {
|
|
75
|
+
var diagram = document.getElementById('preview');
|
|
76
|
+
if (!diagram) return;
|
|
77
|
+
var self = this;
|
|
78
|
+
|
|
79
|
+
diagram.addEventListener('click', function(e) {
|
|
80
|
+
// Respect FSM states — don't interfere with editing or context menu
|
|
81
|
+
if (window.SmartCodeInteraction) {
|
|
82
|
+
var st = SmartCodeInteraction.getState();
|
|
83
|
+
if (st === 'editing' || st === 'context-menu') return;
|
|
84
|
+
}
|
|
85
|
+
// Don't interfere with zoom controls
|
|
86
|
+
if (e.target.closest('.zoom-controls')) return;
|
|
87
|
+
if (e.target.closest('.flag-popover')) return;
|
|
88
|
+
|
|
89
|
+
var target = e.target.closest('.node') || e.target.closest('.smartcode-node');
|
|
90
|
+
if (target) {
|
|
91
|
+
var nodeId = self.extractNodeId(target);
|
|
92
|
+
if (nodeId && nodeId.startsWith('__collapsed__')) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
e.stopPropagation();
|
|
95
|
+
var subgraphId = nodeId.replace('__collapsed__', '');
|
|
96
|
+
self.expand(subgraphId);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Click on cluster label to collapse (Mermaid .cluster or custom .smartcode-subgraph)
|
|
102
|
+
var clusterLabel = e.target.closest('.cluster-label');
|
|
103
|
+
if (clusterLabel) {
|
|
104
|
+
var cluster = clusterLabel.closest('.cluster') || clusterLabel.closest('.smartcode-subgraph');
|
|
105
|
+
if (cluster) {
|
|
106
|
+
var clusterId = self.extractClusterId(cluster);
|
|
107
|
+
if (clusterId) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
self.collapse(clusterId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
attachFocusHandlers: function() {
|
|
118
|
+
var diagram = document.getElementById('preview');
|
|
119
|
+
if (!diagram) return;
|
|
120
|
+
var self = this;
|
|
121
|
+
|
|
122
|
+
// Double-click to enter focus mode
|
|
123
|
+
diagram.addEventListener('dblclick', function(e) {
|
|
124
|
+
// Respect FSM states — don't enter focus mode when editing or a node is selected
|
|
125
|
+
if (window.SmartCodeInteraction) {
|
|
126
|
+
var st = SmartCodeInteraction.getState();
|
|
127
|
+
if (st === 'editing' || st === 'selected') return;
|
|
128
|
+
}
|
|
129
|
+
var node = e.target.closest('.node') || e.target.closest('.smartcode-node');
|
|
130
|
+
if (!node) return;
|
|
131
|
+
|
|
132
|
+
var nodeId = self.extractNodeId(node);
|
|
133
|
+
if (!nodeId) return;
|
|
134
|
+
|
|
135
|
+
// Don't focus on collapsed summary nodes
|
|
136
|
+
if (nodeId.startsWith('__collapsed__')) return;
|
|
137
|
+
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
self.enterFocusMode(nodeId);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Escape to exit focus mode
|
|
143
|
+
document.addEventListener('keydown', function(e) {
|
|
144
|
+
if (e.key === 'Escape' && self.focusedSubgraph) {
|
|
145
|
+
self.exitFocusMode();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Use DiagramDOM.extractNodeId for node identification
|
|
151
|
+
extractNodeId: function(nodeElement) {
|
|
152
|
+
var info = DiagramDOM.extractNodeId(nodeElement);
|
|
153
|
+
return info ? info.id : null;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// Use DiagramDOM for cluster/subgraph identification
|
|
157
|
+
extractClusterId: function(clusterElement) {
|
|
158
|
+
var info = DiagramDOM.extractNodeId(clusterElement);
|
|
159
|
+
if (info && info.type === 'subgraph') return info.id;
|
|
160
|
+
// Fallback: try the element's own ID for plain cluster IDs
|
|
161
|
+
var id = clusterElement.id || '';
|
|
162
|
+
if (id) return id.replace(/^subGraph\d*-?/, '') || null;
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// ─── Collapse/Expand Operations ──────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
expand: function(subgraphId) {
|
|
169
|
+
_state.collapsed.delete(subgraphId);
|
|
170
|
+
// Remove from auto-collapsed if present
|
|
171
|
+
var idx = this.autoCollapsed.indexOf(subgraphId);
|
|
172
|
+
if (idx !== -1) this.autoCollapsed.splice(idx, 1);
|
|
173
|
+
this.renderAutoCollapseNotice();
|
|
174
|
+
this.onToggle(this.getCollapsed());
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
collapse: function(subgraphId) {
|
|
178
|
+
_state.collapsed.add(subgraphId);
|
|
179
|
+
this.onToggle(this.getCollapsed());
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
toggle: function(subgraphId) {
|
|
183
|
+
if (_state.collapsed.has(subgraphId)) {
|
|
184
|
+
this.expand(subgraphId);
|
|
185
|
+
} else {
|
|
186
|
+
this.collapse(subgraphId);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
expandAll: function() {
|
|
191
|
+
for (var i = 0; i < this.autoCollapsed.length; i++) {
|
|
192
|
+
_state.collapsed.delete(this.autoCollapsed[i]);
|
|
193
|
+
}
|
|
194
|
+
this.autoCollapsed = [];
|
|
195
|
+
this.renderAutoCollapseNotice();
|
|
196
|
+
this.onToggle(this.getCollapsed());
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
// ─── Focus Mode ──────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
enterFocusMode: function(nodeId) {
|
|
202
|
+
if (this.onFocusChange) {
|
|
203
|
+
this.onFocusChange({ action: 'focus', nodeId: nodeId });
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
exitFocusMode: function() {
|
|
208
|
+
this.focusedSubgraph = null;
|
|
209
|
+
this.breadcrumbs = [];
|
|
210
|
+
this.renderBreadcrumbs();
|
|
211
|
+
if (this.onFocusChange) {
|
|
212
|
+
this.onFocusChange({ action: 'exit' });
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
navigateTo: function(breadcrumbId) {
|
|
217
|
+
if (this.onFocusChange) {
|
|
218
|
+
this.onFocusChange({ action: 'navigate', breadcrumbId: breadcrumbId });
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// ─── UI Rendering ────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
renderAutoCollapseNotice: function() {
|
|
225
|
+
var existing = document.getElementById('auto-collapse-notice');
|
|
226
|
+
if (existing) existing.remove();
|
|
227
|
+
|
|
228
|
+
if (this.autoCollapsed.length === 0) return;
|
|
229
|
+
|
|
230
|
+
var notice = document.createElement('div');
|
|
231
|
+
notice.id = 'auto-collapse-notice';
|
|
232
|
+
notice.className = 'auto-collapse-notice';
|
|
233
|
+
|
|
234
|
+
var icon = document.createElement('span');
|
|
235
|
+
icon.className = 'notice-icon';
|
|
236
|
+
// Safe: SmartCodeIcons.chart is a static trusted SVG string
|
|
237
|
+
icon.innerHTML = SmartCodeIcons.chart;
|
|
238
|
+
notice.appendChild(icon);
|
|
239
|
+
|
|
240
|
+
var text = document.createElement('span');
|
|
241
|
+
text.className = 'notice-text';
|
|
242
|
+
var count = this.autoCollapsed.length;
|
|
243
|
+
var limit = this.config.maxVisibleNodes || 50;
|
|
244
|
+
text.textContent = count + ' subgraph' + (count > 1 ? 's' : '') + ' auto-collapsed to fit ' + limit + ' node limit';
|
|
245
|
+
notice.appendChild(text);
|
|
246
|
+
|
|
247
|
+
var self = this;
|
|
248
|
+
var expandBtn = document.createElement('button');
|
|
249
|
+
expandBtn.className = 'notice-expand-all';
|
|
250
|
+
expandBtn.title = 'Expand all';
|
|
251
|
+
expandBtn.textContent = 'Expand All';
|
|
252
|
+
expandBtn.addEventListener('click', function() {
|
|
253
|
+
self.expandAll();
|
|
254
|
+
});
|
|
255
|
+
notice.appendChild(expandBtn);
|
|
256
|
+
|
|
257
|
+
var dismissBtn = document.createElement('button');
|
|
258
|
+
dismissBtn.className = 'notice-dismiss';
|
|
259
|
+
dismissBtn.title = 'Dismiss';
|
|
260
|
+
// Safe: SmartCodeIcons.close is a static trusted SVG string
|
|
261
|
+
dismissBtn.innerHTML = SmartCodeIcons.close;
|
|
262
|
+
dismissBtn.addEventListener('click', function() {
|
|
263
|
+
notice.remove();
|
|
264
|
+
});
|
|
265
|
+
notice.appendChild(dismissBtn);
|
|
266
|
+
|
|
267
|
+
var container = document.getElementById('preview-container');
|
|
268
|
+
if (container) container.insertBefore(notice, container.firstChild);
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
renderBreadcrumbs: function() {
|
|
272
|
+
var existing = document.getElementById('breadcrumb-bar');
|
|
273
|
+
if (existing) existing.remove();
|
|
274
|
+
|
|
275
|
+
// Don't render if no breadcrumbs or only "Overview" without focus
|
|
276
|
+
if (this.breadcrumbs.length <= 1 && !this.focusedSubgraph) return;
|
|
277
|
+
|
|
278
|
+
var bar = document.createElement('div');
|
|
279
|
+
bar.id = 'breadcrumb-bar';
|
|
280
|
+
bar.className = 'breadcrumb-bar';
|
|
281
|
+
|
|
282
|
+
var self = this;
|
|
283
|
+
for (var i = 0; i < this.breadcrumbs.length; i++) {
|
|
284
|
+
var crumb = this.breadcrumbs[i];
|
|
285
|
+
var isLast = i === this.breadcrumbs.length - 1;
|
|
286
|
+
|
|
287
|
+
var item = document.createElement('span');
|
|
288
|
+
item.className = 'breadcrumb-item' + (isLast ? ' current' : '');
|
|
289
|
+
item.textContent = crumb.label;
|
|
290
|
+
item.dataset.id = crumb.id;
|
|
291
|
+
|
|
292
|
+
if (!isLast) {
|
|
293
|
+
item.addEventListener('click', (function(c) {
|
|
294
|
+
return function() { self.navigateTo(c.id); };
|
|
295
|
+
})(crumb));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
bar.appendChild(item);
|
|
299
|
+
|
|
300
|
+
if (!isLast) {
|
|
301
|
+
var sep = document.createElement('span');
|
|
302
|
+
sep.className = 'breadcrumb-separator';
|
|
303
|
+
sep.textContent = ' \u203A ';
|
|
304
|
+
bar.appendChild(sep);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Add exit button if in focus mode
|
|
309
|
+
if (this.focusedSubgraph) {
|
|
310
|
+
var exitBtn = document.createElement('button');
|
|
311
|
+
exitBtn.className = 'breadcrumb-exit';
|
|
312
|
+
// Safe: SmartCodeIcons.close is a static trusted SVG string
|
|
313
|
+
exitBtn.innerHTML = SmartCodeIcons.close + ' Exit Focus';
|
|
314
|
+
exitBtn.title = 'Exit focus mode (Esc)';
|
|
315
|
+
exitBtn.addEventListener('click', function() { self.exitFocusMode(); });
|
|
316
|
+
bar.appendChild(exitBtn);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
var container = document.getElementById('preview-container');
|
|
320
|
+
if (container) container.insertBefore(bar, container.firstChild);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
window.SmartCodeCollapseUI = SmartCodeCollapseUI;
|
|
325
|
+
})();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode -- Command History (Undo/Redo)
|
|
3
|
+
* Implements the Command pattern with capped undo/redo stacks.
|
|
4
|
+
* Each command is { before: string, after: string, description: string }.
|
|
5
|
+
* Dependencies: event-bus.js (SmartCodeEventBus, optional)
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
var undoStack = [];
|
|
11
|
+
var redoStack = [];
|
|
12
|
+
var MAX_HISTORY = 100;
|
|
13
|
+
|
|
14
|
+
function emitChanged() {
|
|
15
|
+
if (window.SmartCodeEventBus) {
|
|
16
|
+
SmartCodeEventBus.emit('history:changed', {
|
|
17
|
+
canUndo: undoStack.length > 0,
|
|
18
|
+
canRedo: redoStack.length > 0,
|
|
19
|
+
undoCount: undoStack.length,
|
|
20
|
+
redoCount: redoStack.length,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Push a new command to the undo stack. Clears redo stack (standard editor behavior).
|
|
27
|
+
* @param {{ before: string, after: string, description: string }} command
|
|
28
|
+
*/
|
|
29
|
+
function execute(command) {
|
|
30
|
+
undoStack.push(command);
|
|
31
|
+
if (undoStack.length > MAX_HISTORY) undoStack.shift();
|
|
32
|
+
redoStack.length = 0;
|
|
33
|
+
emitChanged();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pop the last command from the undo stack and push to redo stack.
|
|
38
|
+
* @returns {string|null} The before-state content, or null if nothing to undo.
|
|
39
|
+
*/
|
|
40
|
+
function undo() {
|
|
41
|
+
if (undoStack.length === 0) return null;
|
|
42
|
+
var cmd = undoStack.pop();
|
|
43
|
+
redoStack.push(cmd);
|
|
44
|
+
emitChanged();
|
|
45
|
+
return cmd.before;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pop the last command from the redo stack and push to undo stack.
|
|
50
|
+
* @returns {string|null} The after-state content, or null if nothing to redo.
|
|
51
|
+
*/
|
|
52
|
+
function redo() {
|
|
53
|
+
if (redoStack.length === 0) return null;
|
|
54
|
+
var cmd = redoStack.pop();
|
|
55
|
+
undoStack.push(cmd);
|
|
56
|
+
emitChanged();
|
|
57
|
+
return cmd.after;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @returns {boolean} Whether there are commands to undo */
|
|
61
|
+
function canUndo() { return undoStack.length > 0; }
|
|
62
|
+
|
|
63
|
+
/** @returns {boolean} Whether there are commands to redo */
|
|
64
|
+
function canRedo() { return redoStack.length > 0; }
|
|
65
|
+
|
|
66
|
+
/** Clear both stacks. Called on file switch. */
|
|
67
|
+
function clear() {
|
|
68
|
+
undoStack.length = 0;
|
|
69
|
+
redoStack.length = 0;
|
|
70
|
+
emitChanged();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @returns {number} Number of commands in the undo stack */
|
|
74
|
+
function getUndoCount() { return undoStack.length; }
|
|
75
|
+
|
|
76
|
+
/** @returns {number} Number of commands in the redo stack */
|
|
77
|
+
function getRedoCount() { return redoStack.length; }
|
|
78
|
+
|
|
79
|
+
window.SmartCodeCommandHistory = {
|
|
80
|
+
execute: execute,
|
|
81
|
+
undo: undo,
|
|
82
|
+
redo: redo,
|
|
83
|
+
canUndo: canUndo,
|
|
84
|
+
canRedo: canRedo,
|
|
85
|
+
clear: clear,
|
|
86
|
+
getUndoCount: getUndoCount,
|
|
87
|
+
getRedoCount: getRedoCount,
|
|
88
|
+
};
|
|
89
|
+
})();
|