@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,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode -- Visual Diagram Editor
|
|
3
|
+
* Manipulates .mmd content: add/remove/edit nodes and edges.
|
|
4
|
+
* Dependencies: diagram-dom.js (DiagramDOM), event-bus.js (SmartCodeEventBus),
|
|
5
|
+
* command-history.js (SmartCodeCommandHistory), editor-popovers.js (SmartCodeEditorPopovers)
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
/** Find insertion point: before first `style` line, or before annotations, or at end */
|
|
11
|
+
function findInsertionLine(lines) {
|
|
12
|
+
for (var i = 0; i < lines.length; i++) {
|
|
13
|
+
var t = lines[i].trim();
|
|
14
|
+
if (t.startsWith('style ') || t.startsWith('%% ---')) return i;
|
|
15
|
+
}
|
|
16
|
+
return lines.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Add a node definition to .mmd content */
|
|
20
|
+
function addNode(content, nodeId, label) {
|
|
21
|
+
var lines = content.split('\n');
|
|
22
|
+
var idx = findInsertionLine(lines);
|
|
23
|
+
var newLine = ' ' + nodeId + '["' + label + '"]';
|
|
24
|
+
lines.splice(idx, 0, '', newLine);
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Add an edge between two nodes */
|
|
29
|
+
function addEdge(content, fromId, toId, label) {
|
|
30
|
+
var lines = content.split('\n');
|
|
31
|
+
var idx = findInsertionLine(lines);
|
|
32
|
+
var edgeLine = label
|
|
33
|
+
? ' ' + fromId + ' -->|"' + label + '"| ' + toId
|
|
34
|
+
: ' ' + fromId + ' --> ' + toId;
|
|
35
|
+
lines.splice(idx, 0, edgeLine);
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Remove a node and all its edges/styles from .mmd content */
|
|
40
|
+
function removeNode(content, nodeId) {
|
|
41
|
+
var lines = content.split('\n');
|
|
42
|
+
var escaped = nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
// Match: node definitions, edges referencing it, style directives
|
|
44
|
+
var nodeDefRe = new RegExp('^\\s*' + escaped + '[\\s\\[\\(\\{\\>"]');
|
|
45
|
+
var edgeFromRe = new RegExp('\\b' + escaped + '\\s*(-->|---|-.->|==>)');
|
|
46
|
+
var edgeToRe = new RegExp('(-->|---|-.->|==>)\\s*(\\|[^|]*\\|\\s*)?' + escaped + '\\b');
|
|
47
|
+
var styleRe = new RegExp('^\\s*style\\s+' + escaped + '\\b');
|
|
48
|
+
|
|
49
|
+
var result = lines.filter(function(line) {
|
|
50
|
+
var t = line.trim();
|
|
51
|
+
if (!t) return true;
|
|
52
|
+
if (nodeDefRe.test(t)) return false;
|
|
53
|
+
if (edgeFromRe.test(t)) return false;
|
|
54
|
+
if (edgeToRe.test(t)) return false;
|
|
55
|
+
if (styleRe.test(t)) return false;
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
return result.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Remove a specific edge line (from --> to) */
|
|
62
|
+
function removeEdge(content, fromId, toId) {
|
|
63
|
+
var lines = content.split('\n');
|
|
64
|
+
var escFrom = fromId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
65
|
+
var escTo = toId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
66
|
+
var re = new RegExp('\\b' + escFrom + '\\s*(-->|---|-.->|==>)(\\s*\\|[^|]*\\|)?\\s*' + escTo + '\\b');
|
|
67
|
+
var result = lines.filter(function(line) { return !re.test(line.trim()); });
|
|
68
|
+
return result.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Edit a node's label text */
|
|
72
|
+
function editNodeText(content, nodeId, newText) {
|
|
73
|
+
var lines = content.split('\n');
|
|
74
|
+
var escaped = nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
// Match patterns: ID["text"], ID[text], ID("text"), ID(text), ID{"text"}, etc
|
|
76
|
+
var re = new RegExp('(' + escaped + '\\s*[\\[\\(\\{\\>]+\\"?)([^"\\]\\)\\}]*?)(\\"?[\\]\\)\\}]+)');
|
|
77
|
+
for (var i = 0; i < lines.length; i++) {
|
|
78
|
+
if (re.test(lines[i])) {
|
|
79
|
+
lines[i] = lines[i].replace(re, '$1' + newText + '$3');
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Extract current label text for a node */
|
|
87
|
+
function getNodeText(content, nodeId) {
|
|
88
|
+
var escaped = nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
+
var re = new RegExp(escaped + '\\s*[\\[\\(\\{\\>]+"?([^"\\]\\)\\}]*)"?[\\]\\)\\}]+');
|
|
90
|
+
var m = content.match(re);
|
|
91
|
+
return m ? m[1] : nodeId;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Duplicate a node with a new ID and "(copy)" appended to label */
|
|
95
|
+
function duplicateNode(content, nodeId) {
|
|
96
|
+
var newId = generateNodeId(content);
|
|
97
|
+
var label = getNodeText(content, nodeId);
|
|
98
|
+
return addNode(content, newId, label + ' (copy)');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find edge source and target from content.
|
|
103
|
+
* If edgeId contains "-->", "---", "-.->", or "==>", it encodes from/to.
|
|
104
|
+
* Otherwise returns all edges found in content (for iteration).
|
|
105
|
+
*/
|
|
106
|
+
function findEdgeEndpoints(edgeId, content) {
|
|
107
|
+
var lines = content.split('\n');
|
|
108
|
+
var edgePatterns = [];
|
|
109
|
+
for (var i = 0; i < lines.length; i++) {
|
|
110
|
+
var m = lines[i].trim().match(/^(\S+)\s*(-->|---|-.->|==>)(\s*\|[^|]*\|)?\s*(\S+)/);
|
|
111
|
+
if (m) {
|
|
112
|
+
var from = m[1].replace(/[\["'\(\{].*$/, '');
|
|
113
|
+
var to = m[4].replace(/[\["'\(\{].*$/, '');
|
|
114
|
+
edgePatterns.push({ from: from, to: to, line: lines[i].trim() });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Filter by edgeId if it encodes a specific from-to pair (e.g., "A-B" or "L-A-B")
|
|
118
|
+
if (edgeId) {
|
|
119
|
+
var cleanId = edgeId.replace(/^L-/, '');
|
|
120
|
+
var filtered = edgePatterns.filter(function(ep) {
|
|
121
|
+
return cleanId === ep.from + '-' + ep.to || cleanId === ep.to + '-' + ep.from;
|
|
122
|
+
});
|
|
123
|
+
if (filtered.length > 0) return filtered;
|
|
124
|
+
}
|
|
125
|
+
return edgePatterns;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Get all node IDs from content */
|
|
129
|
+
function getAllNodeIds(content) {
|
|
130
|
+
var ids = new Set();
|
|
131
|
+
var lines = content.split('\n');
|
|
132
|
+
var reserved = ['subgraph', 'style', 'class', 'click', 'flowchart', 'graph', 'end'];
|
|
133
|
+
for (var i = 0; i < lines.length; i++) {
|
|
134
|
+
var t = lines[i].trim();
|
|
135
|
+
// Node definitions: ID["text"] or ID[text] etc
|
|
136
|
+
var defMatch = t.match(/^\s*([A-Za-z_]\w*)\s*[\[\(\{>]/);
|
|
137
|
+
if (defMatch && reserved.indexOf(defMatch[1]) === -1) {
|
|
138
|
+
ids.add(defMatch[1]);
|
|
139
|
+
}
|
|
140
|
+
// Nodes in edges
|
|
141
|
+
var edgeMatch = t.match(/^\s*([A-Za-z_]\w*)\s*(-->|---|-.->|==>)/);
|
|
142
|
+
if (edgeMatch) ids.add(edgeMatch[1]);
|
|
143
|
+
var edgeTo = t.match(/(-->|---|-.->|==>)\s*(?:\|[^|]*\|\s*)?([A-Za-z_]\w*)/);
|
|
144
|
+
if (edgeTo) ids.add(edgeTo[2]);
|
|
145
|
+
}
|
|
146
|
+
return Array.from(ids);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Generate a unique node ID */
|
|
150
|
+
function generateNodeId(content) {
|
|
151
|
+
var existing = getAllNodeIds(content);
|
|
152
|
+
var i = 1;
|
|
153
|
+
while (existing.indexOf('N' + i) !== -1) i++;
|
|
154
|
+
return 'N' + i;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var editorState = {
|
|
158
|
+
mode: null, // null | 'addNode' | 'addEdge'
|
|
159
|
+
edgeSource: null, // node ID when in addEdge and source is selected
|
|
160
|
+
pendingAction: null, // { type: 'connectFrom'|'connectTo', nodeId }
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
var editorHooks = {
|
|
164
|
+
getEditor: function() { return document.getElementById('editor'); },
|
|
165
|
+
getLastContent: function() { return window.lastContent || ''; },
|
|
166
|
+
setLastContent: function(v) { window.lastContent = v; },
|
|
167
|
+
saveFile: null,
|
|
168
|
+
renderDiagram: null,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function setMode(mode) {
|
|
172
|
+
editorState.mode = mode;
|
|
173
|
+
editorState.edgeSource = null;
|
|
174
|
+
document.body.classList.remove('mode-addNode', 'mode-addEdge');
|
|
175
|
+
if (mode) document.body.classList.add('mode-' + mode);
|
|
176
|
+
|
|
177
|
+
// Update button states
|
|
178
|
+
var btnNode = document.getElementById('btnAddNode');
|
|
179
|
+
var btnEdge = document.getElementById('btnAddEdge');
|
|
180
|
+
if (btnNode) btnNode.classList.toggle('active', mode === 'addNode');
|
|
181
|
+
if (btnEdge) btnEdge.classList.toggle('active', mode === 'addEdge');
|
|
182
|
+
|
|
183
|
+
// Disable flag mode if entering edit mode
|
|
184
|
+
if (mode && window.SmartCodeAnnotations) {
|
|
185
|
+
var s = SmartCodeAnnotations.getState();
|
|
186
|
+
if (s.flagMode) SmartCodeAnnotations.toggleFlagMode();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (window.toast) {
|
|
190
|
+
var msgs = {
|
|
191
|
+
addNode: 'Node Mode -- click on empty space in the diagram',
|
|
192
|
+
addEdge: 'Edge Mode -- click on the SOURCE node',
|
|
193
|
+
};
|
|
194
|
+
window.toast(msgs[mode] || 'Edit mode disabled');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toggleAddNode() { setMode(editorState.mode === 'addNode' ? null : 'addNode'); }
|
|
199
|
+
function toggleAddEdge() { setMode(editorState.mode === 'addEdge' ? null : 'addEdge'); }
|
|
200
|
+
|
|
201
|
+
function handleClick(e) {
|
|
202
|
+
if (!editorState.mode) return;
|
|
203
|
+
if (e.target.closest('.zoom-controls') || e.target.closest('.flag-popover') || e.target.closest('.editor-popover')) return;
|
|
204
|
+
|
|
205
|
+
// Use DiagramDOM.extractNodeId instead of SmartCodeAnnotations.extractNodeId
|
|
206
|
+
var nodeInfo = DiagramDOM.extractNodeId(e.target);
|
|
207
|
+
|
|
208
|
+
if (editorState.mode === 'addNode') {
|
|
209
|
+
if (nodeInfo) return; // Clicked an existing node, ignore
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
if (window.SmartCodeEditorPopovers) {
|
|
213
|
+
SmartCodeEditorPopovers.showAddNodePopover(e.clientX, e.clientY);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (editorState.mode === 'addEdge') {
|
|
218
|
+
if (!nodeInfo || nodeInfo.type === 'edge') return;
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
if (!editorState.edgeSource) {
|
|
222
|
+
editorState.edgeSource = nodeInfo.id;
|
|
223
|
+
DiagramDOM.highlightNode(nodeInfo.id, true);
|
|
224
|
+
if (window.toast) window.toast('Source: ' + nodeInfo.id + ' -- now click the TARGET');
|
|
225
|
+
} else {
|
|
226
|
+
var from = editorState.edgeSource;
|
|
227
|
+
var to = nodeInfo.id;
|
|
228
|
+
if (from === to) return;
|
|
229
|
+
if (window.SmartCodeEditorPopovers) {
|
|
230
|
+
SmartCodeEditorPopovers.showAddEdgePopover(e.clientX, e.clientY, from, to);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function doRemoveNode(nodeId) {
|
|
237
|
+
applyEdit(function(c) { return removeNode(c, nodeId); });
|
|
238
|
+
if (window.SmartCodeAnnotations) {
|
|
239
|
+
SmartCodeAnnotations.getState().flags.delete(nodeId);
|
|
240
|
+
SmartCodeAnnotations.renderPanel();
|
|
241
|
+
SmartCodeAnnotations.updateBadge();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function doRemoveEdge(fromId, toId) {
|
|
246
|
+
applyEdit(function(c) { return removeEdge(c, fromId, toId); });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function doEditNodeText(nodeId) {
|
|
250
|
+
var editor = editorHooks.getEditor();
|
|
251
|
+
var currentText = getNodeText(editor.value, nodeId);
|
|
252
|
+
SmartCodeModal.prompt({
|
|
253
|
+
title: 'Edit Node: ' + nodeId,
|
|
254
|
+
placeholder: 'Node text',
|
|
255
|
+
defaultValue: currentText,
|
|
256
|
+
onConfirm: function(newText) {
|
|
257
|
+
if (newText === currentText) return;
|
|
258
|
+
applyEdit(function(c) { return editNodeText(c, nodeId, newText); });
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function startConnectFrom(nodeId) {
|
|
264
|
+
if (window.SmartCodeAnnotations) SmartCodeAnnotations.closePopover();
|
|
265
|
+
setMode('addEdge');
|
|
266
|
+
editorState.edgeSource = nodeId;
|
|
267
|
+
DiagramDOM.highlightNode(nodeId, true);
|
|
268
|
+
if (window.toast) window.toast('Source: ' + nodeId + ' -- click the TARGET');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Undo the last edit via SmartCodeCommandHistory */
|
|
272
|
+
async function undo() {
|
|
273
|
+
if (!window.SmartCodeCommandHistory || !SmartCodeCommandHistory.canUndo()) {
|
|
274
|
+
if (window.toast) window.toast('Nothing to undo');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
var content = SmartCodeCommandHistory.undo();
|
|
278
|
+
var editor = editorHooks.getEditor();
|
|
279
|
+
if (!editor || content === null) return;
|
|
280
|
+
editor.value = content;
|
|
281
|
+
editorHooks.setLastContent(content);
|
|
282
|
+
if (editorHooks.saveFile) await editorHooks.saveFile();
|
|
283
|
+
if (editorHooks.renderDiagram) await editorHooks.renderDiagram(content);
|
|
284
|
+
if (window.toast) window.toast('Undone (' + SmartCodeCommandHistory.getUndoCount() + ' remaining)');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Redo the last undone edit via SmartCodeCommandHistory */
|
|
288
|
+
async function redo() {
|
|
289
|
+
if (!window.SmartCodeCommandHistory || !SmartCodeCommandHistory.canRedo()) {
|
|
290
|
+
if (window.toast) window.toast('Nothing to redo');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
var content = SmartCodeCommandHistory.redo();
|
|
294
|
+
var editor = editorHooks.getEditor();
|
|
295
|
+
if (!editor || content === null) return;
|
|
296
|
+
editor.value = content;
|
|
297
|
+
editorHooks.setLastContent(content);
|
|
298
|
+
if (editorHooks.saveFile) await editorHooks.saveFile();
|
|
299
|
+
if (editorHooks.renderDiagram) await editorHooks.renderDiagram(content);
|
|
300
|
+
if (window.toast) window.toast('Redone (' + SmartCodeCommandHistory.getRedoCount() + ' remaining)');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function applyEdit(editFn) {
|
|
304
|
+
var editor = editorHooks.getEditor();
|
|
305
|
+
if (!editor) return;
|
|
306
|
+
|
|
307
|
+
// Capture BEFORE state for command history
|
|
308
|
+
var beforeContent = editor.value;
|
|
309
|
+
|
|
310
|
+
// Strip annotations, apply edit, re-inject annotations
|
|
311
|
+
var annotations = window.SmartCodeAnnotations;
|
|
312
|
+
var content = editor.value;
|
|
313
|
+
var flags = new Map();
|
|
314
|
+
if (annotations) {
|
|
315
|
+
flags = annotations.getState().flags;
|
|
316
|
+
content = annotations.stripAnnotations(content);
|
|
317
|
+
}
|
|
318
|
+
content = editFn(content);
|
|
319
|
+
if (annotations) content = annotations.injectAnnotations(content, flags);
|
|
320
|
+
editor.value = content;
|
|
321
|
+
editorHooks.setLastContent(content);
|
|
322
|
+
|
|
323
|
+
// Push command to history AFTER edit is applied
|
|
324
|
+
if (window.SmartCodeCommandHistory) {
|
|
325
|
+
SmartCodeCommandHistory.execute({
|
|
326
|
+
before: beforeContent,
|
|
327
|
+
after: content,
|
|
328
|
+
description: 'edit',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (editorHooks.saveFile) await editorHooks.saveFile();
|
|
333
|
+
if (editorHooks.renderDiagram) await editorHooks.renderDiagram(content);
|
|
334
|
+
|
|
335
|
+
// Emit diagram:edited event via event bus
|
|
336
|
+
if (window.SmartCodeEventBus) {
|
|
337
|
+
SmartCodeEventBus.emit('diagram:edited', { source: 'diagram-editor' });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function init(options) {
|
|
342
|
+
if (options) Object.assign(editorHooks, options);
|
|
343
|
+
var container = document.getElementById('preview-container');
|
|
344
|
+
if (container) container.addEventListener('click', handleClick);
|
|
345
|
+
|
|
346
|
+
// Subscribe to event bus: re-init after diagram render if needed
|
|
347
|
+
if (window.SmartCodeEventBus) {
|
|
348
|
+
SmartCodeEventBus.on('diagram:rendered', function() {
|
|
349
|
+
// Clear edge source highlight after re-render (SVG is replaced)
|
|
350
|
+
editorState.edgeSource = null;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
window.MmdEditor = {
|
|
356
|
+
init: init, setMode: setMode, undo: undo, redo: redo,
|
|
357
|
+
toggleAddNode: toggleAddNode, toggleAddEdge: toggleAddEdge,
|
|
358
|
+
addNode: addNode, addEdge: addEdge, removeNode: removeNode,
|
|
359
|
+
removeEdge: removeEdge, editNodeText: editNodeText, getNodeText: getNodeText,
|
|
360
|
+
getAllNodeIds: getAllNodeIds, generateNodeId: generateNodeId,
|
|
361
|
+
findEdgeEndpoints: findEdgeEndpoints, duplicateNode: duplicateNode,
|
|
362
|
+
doRemoveNode: doRemoveNode, doRemoveEdge: doRemoveEdge,
|
|
363
|
+
doEditNodeText: doEditNodeText, startConnectFrom: startConnectFrom,
|
|
364
|
+
applyEdit: applyEdit,
|
|
365
|
+
getState: function() { return editorState; },
|
|
366
|
+
closeEditorPopover: function() { if (window.SmartCodeEditorPopovers) SmartCodeEditorPopovers.closePopover(); },
|
|
367
|
+
};
|
|
368
|
+
})();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Editor Panel -- editor textarea events, panel toggles, resize handle.
|
|
3
|
+
* Extracted from live.html (Phase 9 Plan 03).
|
|
4
|
+
*
|
|
5
|
+
* Dependencies: renderer.js (render), pan-zoom.js (zoomFit)
|
|
6
|
+
* Dependents: app-init.js
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* SmartCodeEditorPanel.isAutoSync();
|
|
10
|
+
* SmartCodeEditorPanel.setAutoSync(v);
|
|
11
|
+
* SmartCodeEditorPanel.toggleEditor();
|
|
12
|
+
* SmartCodeEditorPanel.toggleSidebar();
|
|
13
|
+
*/
|
|
14
|
+
(function() {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// ── State ──
|
|
18
|
+
var autoSync = true;
|
|
19
|
+
var debounceTimer = null;
|
|
20
|
+
|
|
21
|
+
// Keep window.autoSync in sync for cross-module access
|
|
22
|
+
window.autoSync = autoSync;
|
|
23
|
+
|
|
24
|
+
// ── Editor textarea events ──
|
|
25
|
+
var editor = document.getElementById('editor');
|
|
26
|
+
|
|
27
|
+
editor.addEventListener('input', function() {
|
|
28
|
+
clearTimeout(debounceTimer);
|
|
29
|
+
debounceTimer = setTimeout(function() { render(editor.value); }, 500);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
editor.addEventListener('keydown', function(e) {
|
|
33
|
+
if (e.key === 'Tab') {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
var start = editor.selectionStart;
|
|
36
|
+
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(editor.selectionEnd);
|
|
37
|
+
editor.selectionStart = editor.selectionEnd = start + 4;
|
|
38
|
+
}
|
|
39
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
render(editor.value);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── Auto-sync toggle ──
|
|
46
|
+
var syncBtn = document.getElementById('toggleAutoSync');
|
|
47
|
+
|
|
48
|
+
function updateSyncUI() {
|
|
49
|
+
syncBtn.classList.toggle('active', autoSync);
|
|
50
|
+
syncBtn.textContent = autoSync ? 'Auto-Sync ON' : 'Auto-Sync OFF';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
syncBtn.addEventListener('click', function() {
|
|
54
|
+
autoSync = !autoSync;
|
|
55
|
+
window.autoSync = autoSync;
|
|
56
|
+
updateSyncUI();
|
|
57
|
+
});
|
|
58
|
+
updateSyncUI();
|
|
59
|
+
|
|
60
|
+
// ── Toggle panels ──
|
|
61
|
+
function toggleEditor() {
|
|
62
|
+
var panel = document.getElementById('editorPanel');
|
|
63
|
+
var handle = document.getElementById('resizeHandle');
|
|
64
|
+
panel.classList.toggle('hidden');
|
|
65
|
+
handle.style.display = panel.classList.contains('hidden') ? 'none' : '';
|
|
66
|
+
setTimeout(zoomFit, 100);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toggleSidebar() {
|
|
70
|
+
document.getElementById('sidebar').classList.toggle('hidden');
|
|
71
|
+
setTimeout(zoomFit, 100);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
document.getElementById('toggleEditor').addEventListener('click', toggleEditor);
|
|
75
|
+
document.getElementById('toggleSidebar').addEventListener('click', toggleSidebar);
|
|
76
|
+
|
|
77
|
+
// ── Resize handle ──
|
|
78
|
+
var resizeHandle = document.getElementById('resizeHandle');
|
|
79
|
+
var isResizing = false;
|
|
80
|
+
|
|
81
|
+
resizeHandle.addEventListener('mousedown', function(e) {
|
|
82
|
+
isResizing = true;
|
|
83
|
+
resizeHandle.classList.add('active');
|
|
84
|
+
document.addEventListener('mousemove', onResize);
|
|
85
|
+
document.addEventListener('mouseup', function() {
|
|
86
|
+
isResizing = false;
|
|
87
|
+
resizeHandle.classList.remove('active');
|
|
88
|
+
document.removeEventListener('mousemove', onResize);
|
|
89
|
+
}, { once: true });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function onResize(e) {
|
|
93
|
+
if (!isResizing) return;
|
|
94
|
+
var sidebar = document.getElementById('sidebar');
|
|
95
|
+
var sidebarWidth = sidebar.classList.contains('hidden') ? 0 : sidebar.offsetWidth;
|
|
96
|
+
var newWidth = e.clientX - sidebarWidth;
|
|
97
|
+
document.getElementById('editorPanel').style.width = Math.max(200, Math.min(newWidth, window.innerWidth * 0.7)) + 'px';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Public API ──
|
|
101
|
+
window.SmartCodeEditorPanel = {
|
|
102
|
+
isAutoSync: function() { return autoSync; },
|
|
103
|
+
setAutoSync: function(v) { autoSync = v; window.autoSync = v; updateSyncUI(); },
|
|
104
|
+
toggleEditor: toggleEditor,
|
|
105
|
+
toggleSidebar: toggleSidebar,
|
|
106
|
+
};
|
|
107
|
+
})();
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode -- Editor Popovers
|
|
3
|
+
* Add-node and add-edge popover UI, extracted from diagram-editor.js.
|
|
4
|
+
* Dependencies: diagram-editor.js (MmdEditor), diagram-dom.js (DiagramDOM)
|
|
5
|
+
*/
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var _outsideHandler = null;
|
|
10
|
+
|
|
11
|
+
function closeEditorPopover() {
|
|
12
|
+
if (_outsideHandler) {
|
|
13
|
+
document.removeEventListener('mousedown', _outsideHandler);
|
|
14
|
+
_outsideHandler = null;
|
|
15
|
+
}
|
|
16
|
+
var existing = document.querySelector('.editor-popover');
|
|
17
|
+
if (existing) existing.remove();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createPopover(x, y) {
|
|
21
|
+
closeEditorPopover();
|
|
22
|
+
var pop = document.createElement('div');
|
|
23
|
+
pop.className = 'flag-popover editor-popover';
|
|
24
|
+
pop.style.left = Math.min(x + 12, window.innerWidth - 360) + 'px';
|
|
25
|
+
pop.style.top = Math.min(y - 20, window.innerHeight - 280) + 'px';
|
|
26
|
+
document.body.appendChild(pop);
|
|
27
|
+
|
|
28
|
+
setTimeout(function () {
|
|
29
|
+
function outside(e) {
|
|
30
|
+
if (pop.contains(e.target)) return;
|
|
31
|
+
closeEditorPopover();
|
|
32
|
+
}
|
|
33
|
+
_outsideHandler = outside;
|
|
34
|
+
document.addEventListener('mousedown', outside);
|
|
35
|
+
}, 50);
|
|
36
|
+
|
|
37
|
+
return pop;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showAddNodePopover(clientX, clientY) {
|
|
41
|
+
var editor = document.getElementById('editor');
|
|
42
|
+
if (!editor) return;
|
|
43
|
+
var suggestedId = MmdEditor.generateNodeId(editor.value);
|
|
44
|
+
var pop = createPopover(clientX, clientY);
|
|
45
|
+
|
|
46
|
+
// Build popover content using DOM methods
|
|
47
|
+
var titleDiv = document.createElement('div');
|
|
48
|
+
titleDiv.className = 'flag-popover-title';
|
|
49
|
+
var titleSpan = document.createElement('span');
|
|
50
|
+
titleSpan.textContent = 'New Node';
|
|
51
|
+
titleDiv.appendChild(titleSpan);
|
|
52
|
+
pop.appendChild(titleDiv);
|
|
53
|
+
|
|
54
|
+
var idLabel = document.createElement('label');
|
|
55
|
+
idLabel.style.cssText = 'font-size:11px;color:var(--text-secondary);margin-bottom:2px;display:block';
|
|
56
|
+
idLabel.textContent = 'ID (no spaces)';
|
|
57
|
+
pop.appendChild(idLabel);
|
|
58
|
+
|
|
59
|
+
var idInput = document.createElement('input');
|
|
60
|
+
idInput.className = 'ep-input';
|
|
61
|
+
idInput.type = 'text';
|
|
62
|
+
idInput.value = suggestedId;
|
|
63
|
+
idInput.style.marginBottom = '8px';
|
|
64
|
+
pop.appendChild(idInput);
|
|
65
|
+
|
|
66
|
+
var labelLabel = document.createElement('label');
|
|
67
|
+
labelLabel.style.cssText = 'font-size:11px;color:var(--text-secondary);margin-bottom:2px;display:block';
|
|
68
|
+
labelLabel.textContent = 'Label';
|
|
69
|
+
pop.appendChild(labelLabel);
|
|
70
|
+
|
|
71
|
+
var labelInput = document.createElement('input');
|
|
72
|
+
labelInput.className = 'ep-input ep-label';
|
|
73
|
+
labelInput.type = 'text';
|
|
74
|
+
labelInput.placeholder = 'Node label...';
|
|
75
|
+
pop.appendChild(labelInput);
|
|
76
|
+
|
|
77
|
+
var actionsDiv = document.createElement('div');
|
|
78
|
+
actionsDiv.className = 'flag-popover-actions';
|
|
79
|
+
actionsDiv.style.marginTop = '10px';
|
|
80
|
+
|
|
81
|
+
var btnCancel = document.createElement('button');
|
|
82
|
+
btnCancel.className = 'btn-flag secondary';
|
|
83
|
+
btnCancel.textContent = 'Cancel';
|
|
84
|
+
btnCancel.addEventListener('click', closeEditorPopover);
|
|
85
|
+
actionsDiv.appendChild(btnCancel);
|
|
86
|
+
|
|
87
|
+
var btnCreate = document.createElement('button');
|
|
88
|
+
btnCreate.className = 'btn-flag primary';
|
|
89
|
+
btnCreate.style.background = 'var(--accent)';
|
|
90
|
+
btnCreate.textContent = 'Create Node';
|
|
91
|
+
actionsDiv.appendChild(btnCreate);
|
|
92
|
+
pop.appendChild(actionsDiv);
|
|
93
|
+
|
|
94
|
+
labelInput.focus();
|
|
95
|
+
|
|
96
|
+
function doCreate() {
|
|
97
|
+
var id = idInput.value.trim().replace(/\s+/g, '_');
|
|
98
|
+
var label = labelInput.value.trim();
|
|
99
|
+
if (!id || !label) return;
|
|
100
|
+
MmdEditor.applyEdit(function (c) { return MmdEditor.addNode(c, id, label); });
|
|
101
|
+
closeEditorPopover();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
labelInput.addEventListener('keydown', function (e) {
|
|
105
|
+
if (e.key === 'Enter') doCreate();
|
|
106
|
+
if (e.key === 'Escape') closeEditorPopover();
|
|
107
|
+
});
|
|
108
|
+
btnCreate.addEventListener('click', doCreate);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function showAddEdgePopover(clientX, clientY, fromId, toId) {
|
|
112
|
+
var pop = createPopover(clientX, clientY);
|
|
113
|
+
|
|
114
|
+
// Build popover content using DOM methods
|
|
115
|
+
var titleDiv = document.createElement('div');
|
|
116
|
+
titleDiv.className = 'flag-popover-title';
|
|
117
|
+
var titleSpan = document.createElement('span');
|
|
118
|
+
titleSpan.textContent = 'New Edge';
|
|
119
|
+
titleDiv.appendChild(titleSpan);
|
|
120
|
+
var edgeIdSpan = document.createElement('span');
|
|
121
|
+
edgeIdSpan.className = 'node-id';
|
|
122
|
+
edgeIdSpan.textContent = fromId + ' \u2192 ' + toId;
|
|
123
|
+
titleDiv.appendChild(edgeIdSpan);
|
|
124
|
+
pop.appendChild(titleDiv);
|
|
125
|
+
|
|
126
|
+
var labelLabel = document.createElement('label');
|
|
127
|
+
labelLabel.style.cssText = 'font-size:11px;color:var(--text-secondary);margin-bottom:2px;display:block';
|
|
128
|
+
labelLabel.textContent = 'Label (optional)';
|
|
129
|
+
pop.appendChild(labelLabel);
|
|
130
|
+
|
|
131
|
+
var labelInput = document.createElement('input');
|
|
132
|
+
labelInput.className = 'ep-input ep-label';
|
|
133
|
+
labelInput.type = 'text';
|
|
134
|
+
labelInput.placeholder = 'Edge label...';
|
|
135
|
+
pop.appendChild(labelInput);
|
|
136
|
+
|
|
137
|
+
var actionsDiv = document.createElement('div');
|
|
138
|
+
actionsDiv.className = 'flag-popover-actions';
|
|
139
|
+
actionsDiv.style.marginTop = '10px';
|
|
140
|
+
|
|
141
|
+
var btnCancel = document.createElement('button');
|
|
142
|
+
btnCancel.className = 'btn-flag secondary';
|
|
143
|
+
btnCancel.textContent = 'Cancel';
|
|
144
|
+
btnCancel.addEventListener('click', function () {
|
|
145
|
+
closeEditorPopover();
|
|
146
|
+
DiagramDOM.highlightNode(fromId, false);
|
|
147
|
+
var state = MmdEditor.getState();
|
|
148
|
+
state.edgeSource = null;
|
|
149
|
+
});
|
|
150
|
+
actionsDiv.appendChild(btnCancel);
|
|
151
|
+
|
|
152
|
+
var btnCreate = document.createElement('button');
|
|
153
|
+
btnCreate.className = 'btn-flag primary';
|
|
154
|
+
btnCreate.style.background = 'var(--accent)';
|
|
155
|
+
btnCreate.textContent = 'Create Edge';
|
|
156
|
+
actionsDiv.appendChild(btnCreate);
|
|
157
|
+
pop.appendChild(actionsDiv);
|
|
158
|
+
|
|
159
|
+
labelInput.focus();
|
|
160
|
+
|
|
161
|
+
function doCreate() {
|
|
162
|
+
var label = labelInput.value.trim();
|
|
163
|
+
MmdEditor.applyEdit(function (c) { return MmdEditor.addEdge(c, fromId, toId, label || null); });
|
|
164
|
+
closeEditorPopover();
|
|
165
|
+
DiagramDOM.highlightNode(fromId, false);
|
|
166
|
+
var state = MmdEditor.getState();
|
|
167
|
+
state.edgeSource = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
labelInput.addEventListener('keydown', function (e) {
|
|
171
|
+
if (e.key === 'Enter') doCreate();
|
|
172
|
+
if (e.key === 'Escape') {
|
|
173
|
+
closeEditorPopover();
|
|
174
|
+
DiagramDOM.highlightNode(fromId, false);
|
|
175
|
+
var state = MmdEditor.getState();
|
|
176
|
+
state.edgeSource = null;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
btnCreate.addEventListener('click', doCreate);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
window.SmartCodeEditorPopovers = {
|
|
183
|
+
showAddNodePopover: showAddNodePopover,
|
|
184
|
+
showAddEdgePopover: showAddEdgePopover,
|
|
185
|
+
closePopover: closeEditorPopover,
|
|
186
|
+
};
|
|
187
|
+
})();
|