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