@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,291 @@
1
+ /**
2
+ * SmartCode Dagre Layout -- graph layout computation using dagre.
3
+ * Converts a GraphModel JSON (from /api/graph/:file) into positioned nodes,
4
+ * edges with routed points, and subgraph bounding boxes.
5
+ *
6
+ * Dependencies: dagre (loaded via CDN in live.html)
7
+ * Dependents: canvas-renderer.js (Plan 02)
8
+ *
9
+ * Usage:
10
+ * var result = SmartCodeDagreLayout.computeLayout(graphModel);
11
+ * // result => { width, height, nodes[], edges[], subgraphs[] }
12
+ */
13
+ (function() {
14
+ 'use strict';
15
+
16
+ // ── Text measurement (lazy canvas init) ──
17
+
18
+ var _measureCanvas = null;
19
+
20
+ function getMeasureContext() {
21
+ if (!_measureCanvas) {
22
+ _measureCanvas = document.createElement('canvas');
23
+ }
24
+ return _measureCanvas.getContext('2d');
25
+ }
26
+
27
+ /**
28
+ * Measure the pixel width of a text string using an offscreen canvas.
29
+ * @param {string} text - The text to measure.
30
+ * @param {string} [font] - CSS font string. Defaults to '600 15px Inter, sans-serif'.
31
+ * @returns {number} Width in pixels.
32
+ */
33
+ function measureTextWidth(text, font) {
34
+ var ctx = getMeasureContext();
35
+ ctx.font = font || '600 15px Inter, sans-serif';
36
+ return ctx.measureText(text).width;
37
+ }
38
+
39
+ /**
40
+ * Calculate node dimensions based on label text and shape type.
41
+ * Returns { width, height } with shape-specific adjustments.
42
+ * @param {string} label - Node label text.
43
+ * @param {string} shape - Node shape (rect, circle, diamond, etc.).
44
+ * @returns {{ width: number, height: number }}
45
+ */
46
+ function measureNodeDimensions(label, shape) {
47
+ // Handle multiline labels: measure widest line, stack lines vertically
48
+ var labelStr = (label || '').replace(/\\n/g, '\n');
49
+ var lines = labelStr.split('\n');
50
+ var maxLineW = 0;
51
+ for (var li = 0; li < lines.length; li++) {
52
+ var lw = measureTextWidth(lines[li]);
53
+ if (lw > maxLineW) maxLineW = lw;
54
+ }
55
+ var textW = maxLineW;
56
+ var hPad = 32;
57
+ var lineH = 24;
58
+ var vPad = 24;
59
+ var w = textW + hPad;
60
+ var h = lineH * lines.length + vPad;
61
+
62
+ switch (shape) {
63
+ case 'circle':
64
+ var diameter = Math.max(w, h) + 8;
65
+ w = diameter;
66
+ h = diameter;
67
+ break;
68
+ case 'diamond':
69
+ w = w * 1.4;
70
+ h = h * 1.4;
71
+ break;
72
+ case 'hexagon':
73
+ w = w + h / 2;
74
+ break;
75
+ case 'cylinder':
76
+ h = h + 16;
77
+ break;
78
+ default:
79
+ break;
80
+ }
81
+
82
+ return { width: Math.ceil(w), height: Math.ceil(h) };
83
+ }
84
+
85
+ // ── Subgraph membership lookup ──
86
+
87
+ /**
88
+ * Build a Set of all subgraph IDs for quick lookup.
89
+ * @param {Object} graphModel - The graph model from /api/graph/:file.
90
+ * @returns {Set<string>}
91
+ */
92
+ function buildSubgraphIdSet(graphModel) {
93
+ var ids = {};
94
+ var entries = Object.keys(graphModel.subgraphs || {});
95
+ for (var i = 0; i < entries.length; i++) {
96
+ ids[entries[i]] = true;
97
+ }
98
+ return ids;
99
+ }
100
+
101
+ /**
102
+ * Resolve a subgraph endpoint to its first child node (dagre bug #238 workaround).
103
+ * When an edge references a subgraph ID, dagre cannot route it properly in
104
+ * compound mode. We redirect the edge to the first node inside the subgraph.
105
+ * @param {string} endpoint - Node or subgraph ID.
106
+ * @param {Object} subgraphIds - Lookup of subgraph IDs.
107
+ * @param {Object} subgraphs - The subgraphs map from graphModel.
108
+ * @returns {string} The resolved node ID.
109
+ */
110
+ function resolveEndpoint(endpoint, subgraphIds, subgraphs) {
111
+ if (subgraphIds[endpoint] && subgraphs[endpoint]) {
112
+ var sg = subgraphs[endpoint];
113
+ if (sg.nodeIds && sg.nodeIds.length > 0) {
114
+ return sg.nodeIds[0];
115
+ }
116
+ }
117
+ return endpoint;
118
+ }
119
+
120
+ // ── Layout computation ──
121
+
122
+ /**
123
+ * Compute a full layout for a GraphModel using dagre.
124
+ * @param {Object} graphModel - The graph model JSON from /api/graph/:file.
125
+ * @returns {{ width: number, height: number, nodes: Array, edges: Array, subgraphs: Array }}
126
+ */
127
+ function computeLayout(graphModel) {
128
+ /* global dagre */
129
+ var g = new dagre.graphlib.Graph({ compound: true });
130
+
131
+ g.setGraph({
132
+ rankdir: graphModel.direction || 'TB',
133
+ nodesep: 80,
134
+ ranksep: 100,
135
+ edgesep: 20,
136
+ marginx: 20,
137
+ marginy: 20,
138
+ });
139
+
140
+ g.setDefaultEdgeLabel(function() { return {}; });
141
+
142
+ // -- Add nodes --
143
+ var nodeEntries = Object.entries(graphModel.nodes || {});
144
+ for (var ni = 0; ni < nodeEntries.length; ni++) {
145
+ var nodeId = nodeEntries[ni][0];
146
+ var node = nodeEntries[ni][1];
147
+ var dims = measureNodeDimensions(node.label || nodeId, node.shape || 'rect');
148
+ g.setNode(nodeId, {
149
+ label: node.label || nodeId,
150
+ width: dims.width,
151
+ height: dims.height,
152
+ shape: node.shape || 'rect',
153
+ });
154
+ }
155
+
156
+ // -- Add subgraphs --
157
+ var sgEntries = Object.entries(graphModel.subgraphs || {});
158
+ var subgraphIds = buildSubgraphIdSet(graphModel);
159
+
160
+ for (var si = 0; si < sgEntries.length; si++) {
161
+ var sgId = sgEntries[si][0];
162
+ var sg = sgEntries[si][1];
163
+ g.setNode(sgId, {
164
+ label: sg.label || sgId,
165
+ clusterLabelPos: 'top',
166
+ style: 'subgraph',
167
+ });
168
+
169
+ // Parent child nodes inside this subgraph
170
+ var sgNodeIds = sg.nodeIds || [];
171
+ for (var sni = 0; sni < sgNodeIds.length; sni++) {
172
+ g.setParent(sgNodeIds[sni], sgId);
173
+ }
174
+
175
+ // Nested subgraphs
176
+ if (sg.parentId) {
177
+ g.setParent(sgId, sg.parentId);
178
+ }
179
+ }
180
+
181
+ // -- Add edges --
182
+ var edges = graphModel.edges || [];
183
+ var subgraphs = graphModel.subgraphs || {};
184
+
185
+ for (var ei = 0; ei < edges.length; ei++) {
186
+ var edge = edges[ei];
187
+ var from = resolveEndpoint(edge.from, subgraphIds, subgraphs);
188
+ var to = resolveEndpoint(edge.to, subgraphIds, subgraphs);
189
+
190
+ var edgeLabel = edge.label || '';
191
+ var edgeLabelW = edgeLabel ? measureTextWidth(edgeLabel) + 16 : 0;
192
+ var edgeLabelH = edgeLabel ? 20 : 0;
193
+
194
+ g.setEdge(from, to, {
195
+ label: edgeLabel,
196
+ width: edgeLabelW,
197
+ height: edgeLabelH,
198
+ labelpos: 'c',
199
+ });
200
+ }
201
+
202
+ // -- Run dagre layout --
203
+ dagre.layout(g);
204
+
205
+ // -- Extract results --
206
+ return extractLayoutResult(g, graphModel);
207
+ }
208
+
209
+ /**
210
+ * Extract positioned layout data from a dagre graph after layout().
211
+ * @param {Object} g - The dagre graph after layout.
212
+ * @param {Object} graphModel - The original graph model (for metadata).
213
+ * @returns {{ width: number, height: number, nodes: Array, edges: Array, subgraphs: Array }}
214
+ */
215
+ function extractLayoutResult(g, graphModel) {
216
+ var graphInfo = g.graph();
217
+ var subgraphIds = buildSubgraphIdSet(graphModel);
218
+
219
+ var layoutNodes = [];
220
+ var layoutSubgraphs = [];
221
+
222
+ var allNodeIds = g.nodes();
223
+ for (var i = 0; i < allNodeIds.length; i++) {
224
+ var nid = allNodeIds[i];
225
+ var ndata = g.node(nid);
226
+ if (!ndata) continue;
227
+
228
+ if (subgraphIds[nid]) {
229
+ layoutSubgraphs.push({
230
+ id: nid,
231
+ label: ndata.label || nid,
232
+ x: ndata.x,
233
+ y: ndata.y,
234
+ width: ndata.width,
235
+ height: ndata.height,
236
+ });
237
+ } else {
238
+ layoutNodes.push({
239
+ id: nid,
240
+ label: ndata.label || nid,
241
+ shape: ndata.shape || 'rect',
242
+ x: ndata.x,
243
+ y: ndata.y,
244
+ width: ndata.width,
245
+ height: ndata.height,
246
+ });
247
+ }
248
+ }
249
+
250
+ var layoutEdges = [];
251
+ var dagreEdges = g.edges();
252
+ for (var j = 0; j < dagreEdges.length; j++) {
253
+ var de = dagreEdges[j];
254
+ var edata = g.edge(de);
255
+ if (!edata) continue;
256
+
257
+ // Find the original edge data for type/id
258
+ var origEdges = graphModel.edges || [];
259
+ var origEdge = null;
260
+ for (var k = 0; k < origEdges.length; k++) {
261
+ if (origEdges[k].from === de.v && origEdges[k].to === de.w) {
262
+ origEdge = origEdges[k];
263
+ break;
264
+ }
265
+ }
266
+
267
+ layoutEdges.push({
268
+ id: origEdge ? origEdge.id : (de.v + '->' + de.w),
269
+ from: de.v,
270
+ to: de.w,
271
+ label: edata.label || '',
272
+ type: origEdge ? origEdge.type : 'arrow',
273
+ points: edata.points || [],
274
+ });
275
+ }
276
+
277
+ return {
278
+ width: graphInfo.width || 0,
279
+ height: graphInfo.height || 0,
280
+ nodes: layoutNodes,
281
+ edges: layoutEdges,
282
+ subgraphs: layoutSubgraphs,
283
+ };
284
+ }
285
+
286
+ // ── Public API ──
287
+ window.SmartCodeDagreLayout = {
288
+ computeLayout: computeLayout,
289
+ };
290
+
291
+ })();
@@ -0,0 +1,241 @@
1
+ /**
2
+ * DiagramDOM — abstraction layer for SVG DOM queries.
3
+ * Supports both Mermaid-rendered SVGs and custom SmartCode SVGs.
4
+ * Consolidates SVG element lookups duplicated across annotations.js,
5
+ * collapse-ui.js, search.js, and diagram-editor.js.
6
+ *
7
+ * IMPORTANT: Never cache SVG element references — render()
8
+ * replaces the entire SVG via innerHTML, invalidating all references.
9
+ *
10
+ * Usage:
11
+ * DiagramDOM.getSVG();
12
+ * DiagramDOM.getRendererType(); // 'custom' | 'mermaid'
13
+ * DiagramDOM.findNodeElement('myNode');
14
+ * DiagramDOM.extractNodeId(clickedElement);
15
+ */
16
+ (function() {
17
+ 'use strict';
18
+
19
+ var NODE_RE = /^flowchart-(.+)-\d+$/;
20
+ var SUBGRAPH_RE = /^subGraph\d+-(.+)-\d+$/;
21
+ var EDGE_RE = /^L-(.+)$/;
22
+
23
+ var DiagramDOM = {
24
+ /**
25
+ * Returns the current SVG element, or null.
26
+ */
27
+ getSVG: function() {
28
+ return document.querySelector('#preview svg');
29
+ },
30
+
31
+ /**
32
+ * Detects whether the current SVG is from the custom renderer.
33
+ * Checks for the `.smartcode-diagram` class on the root <g> element.
34
+ * @returns {'custom'|'mermaid'}
35
+ */
36
+ getRendererType: function() {
37
+ var svg = this.getSVG();
38
+ if (!svg) return 'mermaid';
39
+ return svg.querySelector('.smartcode-diagram') ? 'custom' : 'mermaid';
40
+ },
41
+
42
+ /**
43
+ * Finds the SVG element for a given node ID.
44
+ * Tries data-node-id attribute first (custom), then Mermaid regex.
45
+ */
46
+ findNodeElement: function(nodeId) {
47
+ var svg = this.getSVG();
48
+ if (!svg) return null;
49
+ // Custom renderer: data-node-id attribute
50
+ var custom = svg.querySelector('[data-node-id="' + nodeId + '"]');
51
+ if (custom) return custom;
52
+ // Mermaid: regex on element id attributes
53
+ var elements = svg.querySelectorAll('[id]');
54
+ for (var i = 0; i < elements.length; i++) {
55
+ var el = elements[i];
56
+ var id = el.getAttribute('id');
57
+ var match = id ? id.match(NODE_RE) : null;
58
+ if (match && match[1] === nodeId) return el;
59
+ }
60
+ return null;
61
+ },
62
+
63
+ /**
64
+ * Finds the SVG element for a given subgraph ID.
65
+ * Matches /^subGraph\d+-(.+)-\d+$/.
66
+ */
67
+ findSubgraphElement: function(subgraphId) {
68
+ var svg = this.getSVG();
69
+ if (!svg) return null;
70
+ // Custom renderer: data-subgraph-id attribute
71
+ var custom = svg.querySelector('[data-subgraph-id="' + subgraphId + '"]');
72
+ if (custom) return custom;
73
+ // Mermaid: regex on element id attributes
74
+ var elements = svg.querySelectorAll('[id]');
75
+ for (var i = 0; i < elements.length; i++) {
76
+ var el = elements[i];
77
+ var id = el.getAttribute('id');
78
+ var match = id ? id.match(SUBGRAPH_RE) : null;
79
+ if (match && match[1] === subgraphId) return el;
80
+ }
81
+ return null;
82
+ },
83
+
84
+ /**
85
+ * Walks up the DOM from an element to find node/edge/subgraph identity.
86
+ * Consolidates duplicated logic from annotations.js, collapse-ui.js,
87
+ * search.js, and diagram-editor.js.
88
+ *
89
+ * Returns: { type: 'node'|'edge'|'subgraph', id: string } or null.
90
+ */
91
+ extractNodeId: function(element) {
92
+ var el = element;
93
+ while (el && el !== document.body) {
94
+ // Custom renderer: check data attributes first
95
+ if (el.getAttribute) {
96
+ var dataNodeId = el.getAttribute('data-node-id');
97
+ if (dataNodeId) return { type: 'node', id: dataNodeId };
98
+ var dataEdgeId = el.getAttribute('data-edge-id');
99
+ if (dataEdgeId) return { type: 'edge', id: dataEdgeId };
100
+ var dataSubgraphId = el.getAttribute('data-subgraph-id');
101
+ if (dataSubgraphId) return { type: 'subgraph', id: dataSubgraphId };
102
+ }
103
+ // Mermaid: regex patterns on id attribute
104
+ var id = el.getAttribute ? el.getAttribute('id') : null;
105
+ if (id) {
106
+ var nodeMatch = id.match(NODE_RE);
107
+ if (nodeMatch) return { type: 'node', id: nodeMatch[1] };
108
+ var edgeMatch = id.match(EDGE_RE);
109
+ if (edgeMatch) return { type: 'edge', id: 'L-' + edgeMatch[1] };
110
+ var subMatch = id.match(SUBGRAPH_RE);
111
+ if (subMatch) return { type: 'subgraph', id: subMatch[1] };
112
+ }
113
+ el = el.parentElement;
114
+ }
115
+ return null;
116
+ },
117
+
118
+ /**
119
+ * Returns getBBox() of the found node element, or null.
120
+ */
121
+ getNodeBBox: function(nodeId) {
122
+ var el = this.findNodeElement(nodeId);
123
+ if (!el || !el.getBBox) return null;
124
+ return el.getBBox();
125
+ },
126
+
127
+ /**
128
+ * Returns the .nodeLabel textContent within a node, or null.
129
+ */
130
+ getNodeLabel: function(nodeId) {
131
+ var el = this.findNodeElement(nodeId);
132
+ if (!el) return null;
133
+ // Custom renderer: direct child <text> element
134
+ if (el.getAttribute('data-node-id')) {
135
+ var textEl = el.querySelector('text');
136
+ return textEl ? textEl.textContent : null;
137
+ }
138
+ // Mermaid: .nodeLabel span
139
+ var label = el.querySelector('.nodeLabel');
140
+ return label ? label.textContent : null;
141
+ },
142
+
143
+ /**
144
+ * Returns all node label elements from the SVG.
145
+ * Custom renderer: <text> children of .smartcode-node
146
+ * Mermaid: .nodeLabel elements
147
+ */
148
+ getAllNodeLabels: function() {
149
+ var svg = this.getSVG();
150
+ if (!svg) return [];
151
+ if (this.getRendererType() === 'custom') {
152
+ return Array.from(svg.querySelectorAll('.smartcode-node > text'));
153
+ }
154
+ return Array.from(svg.querySelectorAll('.nodeLabel'));
155
+ },
156
+
157
+ /**
158
+ * Walks up to find .node, .cluster, or .smartcode-edge parent element.
159
+ */
160
+ findMatchParent: function(element) {
161
+ var current = element;
162
+ while (current && current.tagName !== 'svg') {
163
+ if (current.classList &&
164
+ (current.classList.contains('node') ||
165
+ current.classList.contains('cluster') ||
166
+ current.classList.contains('smartcode-node') ||
167
+ current.classList.contains('smartcode-subgraph') ||
168
+ current.classList.contains('smartcode-edge'))) {
169
+ return current;
170
+ }
171
+ current = current.parentElement;
172
+ }
173
+ return null;
174
+ },
175
+
176
+ /**
177
+ * Finds the SVG element for a given edge ID.
178
+ * Custom: data-edge-id attribute
179
+ * Mermaid: id="L-{edgeId}"
180
+ */
181
+ findEdgeElement: function(edgeId) {
182
+ var svg = this.getSVG();
183
+ if (!svg) return null;
184
+ // Custom renderer: data-edge-id attribute
185
+ var custom = svg.querySelector('[data-edge-id="' + edgeId + '"]');
186
+ if (custom) return custom;
187
+ // Mermaid: id="L-{edgeId}"
188
+ var mermaid = svg.querySelector('[id="L-' + edgeId + '"]');
189
+ return mermaid || null;
190
+ },
191
+
192
+ /**
193
+ * Adds/removes outline styling on a node.
194
+ */
195
+ highlightNode: function(nodeId, on) {
196
+ var el = this.findNodeElement(nodeId);
197
+ if (!el) return;
198
+ // SVG elements don't support CSS outline — modify shape stroke instead
199
+ var shape = el.querySelector('rect, circle, polygon, path, ellipse');
200
+ if (!shape) {
201
+ var childG = el.querySelector('g');
202
+ if (childG) shape = childG.querySelector('rect, circle, polygon, path, ellipse');
203
+ }
204
+ if (shape) {
205
+ if (on) {
206
+ shape.setAttribute('data-prev-stroke', shape.getAttribute('stroke') || '');
207
+ shape.setAttribute('data-prev-stroke-width', shape.getAttribute('stroke-width') || '');
208
+ shape.setAttribute('stroke', '#3b82f6');
209
+ shape.setAttribute('stroke-width', '3');
210
+ } else {
211
+ shape.setAttribute('stroke', shape.getAttribute('data-prev-stroke') || '');
212
+ shape.setAttribute('stroke-width', shape.getAttribute('data-prev-stroke-width') || '');
213
+ }
214
+ }
215
+ },
216
+
217
+ /**
218
+ * Returns all node elements from the SVG.
219
+ * Custom: .smartcode-node elements; Mermaid: .node elements
220
+ */
221
+ getAllNodeElements: function() {
222
+ var svg = this.getSVG();
223
+ if (!svg) return [];
224
+ if (this.getRendererType() === 'custom') {
225
+ return Array.from(svg.querySelectorAll('.smartcode-node'));
226
+ }
227
+ return Array.from(svg.querySelectorAll('.node'));
228
+ },
229
+
230
+ /**
231
+ * Returns SVG viewBox baseVal, or null.
232
+ */
233
+ getViewBox: function() {
234
+ var svg = this.getSVG();
235
+ if (!svg) return null;
236
+ return (svg.viewBox && svg.viewBox.baseVal) ? svg.viewBox.baseVal : null;
237
+ }
238
+ };
239
+
240
+ window.DiagramDOM = DiagramDOM;
241
+ })();