@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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Renderer -- Mermaid rendering pipeline, error panel, status injection.
|
|
3
|
+
* Extracted from live.html (Phase 9 Plan 02).
|
|
4
|
+
*
|
|
5
|
+
* Dependencies: mermaid (CDN), event-bus.js (SmartCodeEventBus)
|
|
6
|
+
* Dependents: pan-zoom.js, export.js, annotations.js, collapse-ui.js
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* SmartCodeRenderer.render(code); // async
|
|
10
|
+
* SmartCodeRenderer.escapeHtml(text);
|
|
11
|
+
* SmartCodeRenderer.injectStatusStyles(cleanCode);
|
|
12
|
+
* SmartCodeRenderer.MERMAID_CONFIG; // shared config for export.js
|
|
13
|
+
*/
|
|
14
|
+
(function() {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// ── Shared Mermaid Config ──
|
|
18
|
+
var MERMAID_CONFIG = {
|
|
19
|
+
startOnLoad: false,
|
|
20
|
+
theme: 'base',
|
|
21
|
+
themeVariables: {
|
|
22
|
+
darkMode: false,
|
|
23
|
+
background: '#ffffff',
|
|
24
|
+
fontFamily: 'Inter, sans-serif',
|
|
25
|
+
fontSize: '16px',
|
|
26
|
+
primaryColor: '#3b82f6',
|
|
27
|
+
primaryTextColor: '#18181b',
|
|
28
|
+
primaryBorderColor: '#2563eb',
|
|
29
|
+
secondaryColor: '#dbeafe',
|
|
30
|
+
tertiaryColor: '#f0fdf4',
|
|
31
|
+
lineColor: '#52525b',
|
|
32
|
+
mainBkg: '#eff6ff',
|
|
33
|
+
nodeBorder: '#2563eb',
|
|
34
|
+
clusterBkg: '#f4f4f5',
|
|
35
|
+
clusterBorder: '#a1a1aa',
|
|
36
|
+
titleColor: '#18181b',
|
|
37
|
+
edgeLabelBackground: '#ffffff',
|
|
38
|
+
},
|
|
39
|
+
flowchart: {
|
|
40
|
+
curve: 'basis',
|
|
41
|
+
padding: 48,
|
|
42
|
+
nodeSpacing: 100,
|
|
43
|
+
rankSpacing: 120,
|
|
44
|
+
htmlLabels: false,
|
|
45
|
+
useMaxWidth: false,
|
|
46
|
+
},
|
|
47
|
+
securityLevel: 'loose',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
mermaid.initialize(MERMAID_CONFIG);
|
|
51
|
+
|
|
52
|
+
// ── Render State ──
|
|
53
|
+
var isInitialRender = true;
|
|
54
|
+
|
|
55
|
+
// ── HTML Escape Helper ──
|
|
56
|
+
function escapeHtml(text) {
|
|
57
|
+
var div = document.createElement('div');
|
|
58
|
+
div.textContent = String(text);
|
|
59
|
+
return div.innerHTML;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Status Style Injection ──
|
|
63
|
+
var STATUS_CLASS_MAP = {
|
|
64
|
+
'ok': 'ok',
|
|
65
|
+
'problem': 'problem',
|
|
66
|
+
'in-progress': 'inProgress',
|
|
67
|
+
'discarded': 'discarded',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function injectStatusStyles(cleanCode) {
|
|
71
|
+
if (!window.SmartCodeAnnotations) return cleanCode;
|
|
72
|
+
var statusMap = SmartCodeAnnotations.getStatusMap();
|
|
73
|
+
if (!statusMap || statusMap.size === 0) return cleanCode;
|
|
74
|
+
|
|
75
|
+
var classDefs = [
|
|
76
|
+
'classDef ok fill:#22c55e,stroke:#16a34a,color:#fff;',
|
|
77
|
+
'classDef problem fill:#ef4444,stroke:#dc2626,color:#fff;',
|
|
78
|
+
'classDef inProgress fill:#eab308,stroke:#ca8a04,color:#000;',
|
|
79
|
+
'classDef discarded fill:#71717a,stroke:#52525b,color:#fff;',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
var classAssignments = [];
|
|
83
|
+
for (var entry of statusMap) {
|
|
84
|
+
var nodeId = entry[0];
|
|
85
|
+
var statusValue = entry[1];
|
|
86
|
+
var className = STATUS_CLASS_MAP[statusValue];
|
|
87
|
+
if (className) {
|
|
88
|
+
classAssignments.push('class ' + nodeId + ' ' + className);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (classAssignments.length === 0) return cleanCode;
|
|
93
|
+
|
|
94
|
+
return cleanCode.trimEnd() + '\n' + classDefs.join('\n') + '\n' + classAssignments.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Error Icon (SVG via DOM) ──
|
|
98
|
+
function createErrorIcon() {
|
|
99
|
+
var ns = 'http://www.w3.org/2000/svg';
|
|
100
|
+
var svg = document.createElementNS(ns, 'svg');
|
|
101
|
+
svg.setAttribute('width', '20');
|
|
102
|
+
svg.setAttribute('height', '20');
|
|
103
|
+
svg.setAttribute('viewBox', '0 0 20 20');
|
|
104
|
+
svg.setAttribute('fill', 'none');
|
|
105
|
+
|
|
106
|
+
var circle = document.createElementNS(ns, 'circle');
|
|
107
|
+
circle.setAttribute('cx', '10');
|
|
108
|
+
circle.setAttribute('cy', '10');
|
|
109
|
+
circle.setAttribute('r', '9');
|
|
110
|
+
circle.setAttribute('stroke', '#ef4444');
|
|
111
|
+
circle.setAttribute('stroke-width', '2');
|
|
112
|
+
svg.appendChild(circle);
|
|
113
|
+
|
|
114
|
+
var line = document.createElementNS(ns, 'path');
|
|
115
|
+
line.setAttribute('d', 'M10 6v5');
|
|
116
|
+
line.setAttribute('stroke', '#ef4444');
|
|
117
|
+
line.setAttribute('stroke-width', '2');
|
|
118
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
119
|
+
svg.appendChild(line);
|
|
120
|
+
|
|
121
|
+
var dot = document.createElementNS(ns, 'circle');
|
|
122
|
+
dot.setAttribute('cx', '10');
|
|
123
|
+
dot.setAttribute('cy', '14');
|
|
124
|
+
dot.setAttribute('r', '1');
|
|
125
|
+
dot.setAttribute('fill', '#ef4444');
|
|
126
|
+
svg.appendChild(dot);
|
|
127
|
+
|
|
128
|
+
return svg;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Error Panel Builder ──
|
|
132
|
+
function buildErrorPanel(error, sourceCode) {
|
|
133
|
+
var container = document.createElement('div');
|
|
134
|
+
container.style.cssText = 'padding:40px;max-width:700px;font-family:Inter,sans-serif;';
|
|
135
|
+
|
|
136
|
+
// Main panel
|
|
137
|
+
var panel = document.createElement('div');
|
|
138
|
+
panel.style.cssText = 'background:#fff;border:1px solid #fecaca;border-left:4px solid #ef4444;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.1);';
|
|
139
|
+
|
|
140
|
+
// Header
|
|
141
|
+
var header = document.createElement('div');
|
|
142
|
+
header.style.cssText = 'padding:16px 20px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #fef2f2;';
|
|
143
|
+
|
|
144
|
+
header.appendChild(createErrorIcon());
|
|
145
|
+
|
|
146
|
+
var title = document.createElement('span');
|
|
147
|
+
title.style.cssText = 'font-size:15px;font-weight:700;color:#991b1b;';
|
|
148
|
+
title.textContent = 'Mermaid Syntax Error';
|
|
149
|
+
header.appendChild(title);
|
|
150
|
+
|
|
151
|
+
// Parse line number from error message
|
|
152
|
+
var errorMsg = String(error.message || error);
|
|
153
|
+
var lineMatch = errorMsg.match(/line\s+(\d+)/i);
|
|
154
|
+
var errorLine = lineMatch ? parseInt(lineMatch[1], 10) : null;
|
|
155
|
+
|
|
156
|
+
if (errorLine !== null) {
|
|
157
|
+
var badge = document.createElement('span');
|
|
158
|
+
badge.style.cssText = 'background:#fef2f2;color:#ef4444;font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;border:1px solid #fecaca;';
|
|
159
|
+
badge.textContent = 'Line ' + errorLine;
|
|
160
|
+
header.appendChild(badge);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
panel.appendChild(header);
|
|
164
|
+
|
|
165
|
+
// Error message
|
|
166
|
+
var msgBlock = document.createElement('div');
|
|
167
|
+
msgBlock.style.cssText = 'padding:16px 20px;';
|
|
168
|
+
|
|
169
|
+
var pre = document.createElement('pre');
|
|
170
|
+
pre.style.cssText = 'white-space:pre-wrap;word-break:break-word;color:#6b7280;font-size:13px;font-family:"JetBrains Mono",monospace;margin:0;line-height:1.6;background:#fef2f2;padding:12px;border-radius:8px;';
|
|
171
|
+
pre.textContent = errorMsg;
|
|
172
|
+
msgBlock.appendChild(pre);
|
|
173
|
+
panel.appendChild(msgBlock);
|
|
174
|
+
|
|
175
|
+
// Code snippet with context (if line number available)
|
|
176
|
+
if (errorLine !== null && sourceCode) {
|
|
177
|
+
var lines = sourceCode.split('\n');
|
|
178
|
+
var start = Math.max(0, errorLine - 4); // 3 lines before
|
|
179
|
+
var end = Math.min(lines.length, errorLine + 2); // 2 lines after
|
|
180
|
+
|
|
181
|
+
var snippetBlock = document.createElement('div');
|
|
182
|
+
snippetBlock.style.cssText = 'padding:0 20px 16px;';
|
|
183
|
+
|
|
184
|
+
var snippetLabel = document.createElement('div');
|
|
185
|
+
snippetLabel.style.cssText = 'font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;';
|
|
186
|
+
snippetLabel.textContent = 'Codigo fonte';
|
|
187
|
+
snippetBlock.appendChild(snippetLabel);
|
|
188
|
+
|
|
189
|
+
var codeContainer = document.createElement('div');
|
|
190
|
+
codeContainer.style.cssText = 'background:#18181b;border-radius:8px;overflow:hidden;font-family:"JetBrains Mono",monospace;font-size:12px;line-height:1.7;';
|
|
191
|
+
|
|
192
|
+
for (var i = start; i < end; i++) {
|
|
193
|
+
var lineNum = i + 1;
|
|
194
|
+
var isError = lineNum === errorLine;
|
|
195
|
+
var lineDiv = document.createElement('div');
|
|
196
|
+
lineDiv.style.cssText = isError
|
|
197
|
+
? 'display:flex;background:rgba(239,68,68,0.2);'
|
|
198
|
+
: 'display:flex;';
|
|
199
|
+
|
|
200
|
+
var numSpan = document.createElement('span');
|
|
201
|
+
numSpan.style.cssText = 'display:inline-block;width:40px;text-align:right;padding-right:12px;color:' + (isError ? '#ef4444' : '#6b7280') + ';user-select:none;flex-shrink:0;';
|
|
202
|
+
numSpan.textContent = String(lineNum);
|
|
203
|
+
|
|
204
|
+
var codeSpan = document.createElement('span');
|
|
205
|
+
codeSpan.style.cssText = 'color:' + (isError ? '#fca5a5' : '#e4e4e7') + ';padding-right:12px;';
|
|
206
|
+
codeSpan.textContent = lines[i] || '';
|
|
207
|
+
|
|
208
|
+
lineDiv.appendChild(numSpan);
|
|
209
|
+
lineDiv.appendChild(codeSpan);
|
|
210
|
+
codeContainer.appendChild(lineDiv);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
snippetBlock.appendChild(codeContainer);
|
|
214
|
+
panel.appendChild(snippetBlock);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
container.appendChild(panel);
|
|
218
|
+
return container;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Render ──
|
|
222
|
+
async function render(code) {
|
|
223
|
+
if (!code || !code.trim()) return;
|
|
224
|
+
// Strip annotations before rendering (Mermaid doesn't understand %% @flag / @status)
|
|
225
|
+
var cleanCode = window.SmartCodeAnnotations
|
|
226
|
+
? SmartCodeAnnotations.getCleanContent(code)
|
|
227
|
+
: code;
|
|
228
|
+
// Inject status classDef styles before rendering
|
|
229
|
+
var styledCode = injectStatusStyles(cleanCode);
|
|
230
|
+
var preview = document.getElementById('preview');
|
|
231
|
+
try {
|
|
232
|
+
var result = await mermaid.render('mermaid-' + Date.now(), styledCode.trim());
|
|
233
|
+
// Safe: SVG generated by mermaid.render(), not user input
|
|
234
|
+
preview.textContent = '';
|
|
235
|
+
preview.insertAdjacentHTML('afterbegin', result.svg);
|
|
236
|
+
// Apply transform (from pan-zoom module, loaded later but called after all scripts)
|
|
237
|
+
if (window.applyTransform) window.applyTransform();
|
|
238
|
+
// Apply flag indicators after SVG is in the DOM
|
|
239
|
+
if (window.SmartCodeAnnotations) SmartCodeAnnotations.applyFlagsToSVG();
|
|
240
|
+
// Apply collapse overlays if available
|
|
241
|
+
if (window.SmartCodeCollapseUI && SmartCodeCollapseUI.applyOverlays) SmartCodeCollapseUI.applyOverlays();
|
|
242
|
+
// Only auto-fit on initial render or file navigation; preserve zoom on live updates
|
|
243
|
+
if (isInitialRender) {
|
|
244
|
+
requestAnimationFrame(function() {
|
|
245
|
+
if (window.zoomFit) window.zoomFit();
|
|
246
|
+
});
|
|
247
|
+
isInitialRender = false;
|
|
248
|
+
} else {
|
|
249
|
+
if (window.applyTransform) window.applyTransform();
|
|
250
|
+
}
|
|
251
|
+
// Emit rendered event
|
|
252
|
+
if (window.SmartCodeEventBus) {
|
|
253
|
+
SmartCodeEventBus.emit('diagram:rendered', { svg: result.svg });
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
// Show structured error panel with line numbers using cleanCode (user's source)
|
|
257
|
+
preview.textContent = '';
|
|
258
|
+
preview.appendChild(buildErrorPanel(e, cleanCode));
|
|
259
|
+
// Emit error event
|
|
260
|
+
if (window.SmartCodeEventBus) {
|
|
261
|
+
SmartCodeEventBus.emit('diagram:error', { error: e });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Public API ──
|
|
267
|
+
window.SmartCodeRenderer = {
|
|
268
|
+
render: render,
|
|
269
|
+
escapeHtml: escapeHtml,
|
|
270
|
+
injectStatusStyles: injectStatusStyles,
|
|
271
|
+
MERMAID_CONFIG: MERMAID_CONFIG,
|
|
272
|
+
setInitialRender: function(v) { isInitialRender = v; },
|
|
273
|
+
getInitialRender: function() { return isInitialRender; },
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Backward compat -- other modules and inline code call these directly
|
|
277
|
+
window.render = render;
|
|
278
|
+
window.escapeHtml = escapeHtml;
|
|
279
|
+
window.injectStatusStyles = injectStatusStyles;
|
|
280
|
+
})();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════
|
|
2
|
+
SmartCode — Node Search (Ctrl+F)
|
|
3
|
+
═══════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
/* ── Search Bar ── */
|
|
6
|
+
.search-bar {
|
|
7
|
+
position: absolute;
|
|
8
|
+
top: 12px;
|
|
9
|
+
left: 50%;
|
|
10
|
+
transform: translateX(-50%);
|
|
11
|
+
z-index: 50;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 6px;
|
|
15
|
+
background: var(--surface-2);
|
|
16
|
+
border: 1px solid var(--border-default);
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
padding: 6px 10px;
|
|
19
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.search-bar input {
|
|
23
|
+
background: var(--surface-3);
|
|
24
|
+
border: 1px solid var(--border-subtle);
|
|
25
|
+
border-radius: 8px;
|
|
26
|
+
color: var(--text-primary);
|
|
27
|
+
font-family: inherit;
|
|
28
|
+
font-size: 13px;
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
padding: 6px 12px;
|
|
31
|
+
outline: none;
|
|
32
|
+
width: 220px;
|
|
33
|
+
transition: border-color 0.15s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.search-bar input:focus {
|
|
37
|
+
border-color: var(--accent);
|
|
38
|
+
box-shadow: 0 0 0 2px var(--accent-muted);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.search-bar input::placeholder {
|
|
42
|
+
color: var(--text-tertiary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── Match Count ── */
|
|
46
|
+
.search-bar .match-count {
|
|
47
|
+
font-size: 11px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
color: var(--text-secondary);
|
|
50
|
+
min-width: 50px;
|
|
51
|
+
text-align: center;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
user-select: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.search-bar .match-count.no-match {
|
|
57
|
+
color: var(--status-problem);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── Navigation Buttons ── */
|
|
61
|
+
.search-bar .search-nav-btn,
|
|
62
|
+
.search-bar .search-close-btn {
|
|
63
|
+
width: 28px;
|
|
64
|
+
height: 28px;
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
border: none;
|
|
69
|
+
background: none;
|
|
70
|
+
color: var(--text-secondary);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
transition: background 0.15s, color 0.15s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.search-bar .search-nav-btn:hover,
|
|
78
|
+
.search-bar .search-close-btn:hover {
|
|
79
|
+
background: var(--surface-4);
|
|
80
|
+
color: var(--text-primary);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.search-bar .search-close-btn {
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
margin-left: 2px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── Match Highlight (amber outline) ── */
|
|
89
|
+
.search-match rect,
|
|
90
|
+
.search-match circle,
|
|
91
|
+
.search-match polygon {
|
|
92
|
+
stroke: var(--search-match) !important;
|
|
93
|
+
stroke-width: 3px !important;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── Active Match (accent glow) ── */
|
|
97
|
+
.search-match-active rect,
|
|
98
|
+
.search-match-active circle,
|
|
99
|
+
.search-match-active polygon {
|
|
100
|
+
stroke: var(--accent) !important;
|
|
101
|
+
stroke-width: 4px !important;
|
|
102
|
+
filter: drop-shadow(0 0 6px var(--accent-muted));
|
|
103
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode — Node Search (Ctrl+F)
|
|
3
|
+
* Find and highlight matching nodes in the current SVG diagram.
|
|
4
|
+
* Exposed as window.SmartCodeSearch
|
|
5
|
+
*
|
|
6
|
+
* Dependencies: diagram-dom.js (DiagramDOM), event-bus.js (SmartCodeEventBus)
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
var state = {
|
|
12
|
+
isOpen: false,
|
|
13
|
+
query: '',
|
|
14
|
+
matches: [],
|
|
15
|
+
currentIndex: -1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
var hooks = {
|
|
19
|
+
getEditor: function () { return document.getElementById('editor'); },
|
|
20
|
+
getPan: function () { return { panX: 0, panY: 0, zoom: 1 }; },
|
|
21
|
+
setPan: function () {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
var barEl = null;
|
|
25
|
+
var inputEl = null;
|
|
26
|
+
var countEl = null;
|
|
27
|
+
var debounceId = null;
|
|
28
|
+
|
|
29
|
+
// ── DOM-safe Search Bar Builder ──
|
|
30
|
+
|
|
31
|
+
function buildSearchBar() {
|
|
32
|
+
var bar = document.createElement('div');
|
|
33
|
+
bar.className = 'search-bar';
|
|
34
|
+
|
|
35
|
+
var input = document.createElement('input');
|
|
36
|
+
input.type = 'text';
|
|
37
|
+
input.placeholder = 'Search node...';
|
|
38
|
+
input.setAttribute('autocomplete', 'off');
|
|
39
|
+
input.setAttribute('spellcheck', 'false');
|
|
40
|
+
|
|
41
|
+
var count = document.createElement('span');
|
|
42
|
+
count.className = 'match-count';
|
|
43
|
+
count.textContent = '';
|
|
44
|
+
|
|
45
|
+
var btnPrev = document.createElement('button');
|
|
46
|
+
btnPrev.className = 'search-nav-btn';
|
|
47
|
+
btnPrev.innerHTML = SmartCodeIcons.arrowUp; /* safe: static SVG */
|
|
48
|
+
btnPrev.title = 'Previous (Shift+Enter)';
|
|
49
|
+
btnPrev.addEventListener('click', function () { navigatePrev(); });
|
|
50
|
+
|
|
51
|
+
var btnNext = document.createElement('button');
|
|
52
|
+
btnNext.className = 'search-nav-btn';
|
|
53
|
+
btnNext.innerHTML = SmartCodeIcons.arrowDown; /* safe: static SVG */
|
|
54
|
+
btnNext.title = 'Next (Enter)';
|
|
55
|
+
btnNext.addEventListener('click', function () { navigateNext(); });
|
|
56
|
+
|
|
57
|
+
var btnClose = document.createElement('button');
|
|
58
|
+
btnClose.className = 'search-close-btn';
|
|
59
|
+
btnClose.innerHTML = SmartCodeIcons.close; /* safe: static SVG */
|
|
60
|
+
btnClose.title = 'Close (Esc)';
|
|
61
|
+
btnClose.addEventListener('click', function () { close(); });
|
|
62
|
+
|
|
63
|
+
bar.appendChild(input);
|
|
64
|
+
bar.appendChild(count);
|
|
65
|
+
bar.appendChild(btnPrev);
|
|
66
|
+
bar.appendChild(btnNext);
|
|
67
|
+
bar.appendChild(btnClose);
|
|
68
|
+
|
|
69
|
+
input.addEventListener('input', function () {
|
|
70
|
+
clearTimeout(debounceId);
|
|
71
|
+
var val = input.value;
|
|
72
|
+
debounceId = setTimeout(function () { search(val); }, 150);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
input.addEventListener('keydown', function (e) {
|
|
76
|
+
if (e.key === 'Enter' && e.shiftKey) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
navigatePrev();
|
|
79
|
+
} else if (e.key === 'Enter') {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
navigateNext();
|
|
82
|
+
} else if (e.key === 'Escape') {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
close();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
barEl = bar;
|
|
89
|
+
inputEl = input;
|
|
90
|
+
countEl = count;
|
|
91
|
+
|
|
92
|
+
return bar;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Open / Close ──
|
|
96
|
+
|
|
97
|
+
function open() {
|
|
98
|
+
if (state.isOpen) {
|
|
99
|
+
// Already open — just refocus
|
|
100
|
+
if (inputEl) { inputEl.focus(); inputEl.select(); }
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
state.isOpen = true;
|
|
104
|
+
|
|
105
|
+
var container = document.getElementById('preview-container');
|
|
106
|
+
if (!container) return;
|
|
107
|
+
|
|
108
|
+
if (!barEl) buildSearchBar();
|
|
109
|
+
container.appendChild(barEl);
|
|
110
|
+
inputEl.value = state.query || '';
|
|
111
|
+
inputEl.focus();
|
|
112
|
+
if (state.query) inputEl.select();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function close() {
|
|
116
|
+
if (!state.isOpen) return;
|
|
117
|
+
state.isOpen = false;
|
|
118
|
+
state.query = '';
|
|
119
|
+
state.matches = [];
|
|
120
|
+
state.currentIndex = -1;
|
|
121
|
+
|
|
122
|
+
clearHighlights();
|
|
123
|
+
if (barEl && barEl.parentNode) barEl.parentNode.removeChild(barEl);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Search Logic (uses DiagramDOM) ──
|
|
127
|
+
|
|
128
|
+
function search(query) {
|
|
129
|
+
state.query = query;
|
|
130
|
+
state.matches = [];
|
|
131
|
+
state.currentIndex = -1;
|
|
132
|
+
clearHighlights();
|
|
133
|
+
|
|
134
|
+
if (!query || !query.trim()) {
|
|
135
|
+
updateCount();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
var lowerQuery = query.toLowerCase();
|
|
140
|
+
|
|
141
|
+
// Use DiagramDOM.getAllNodeLabels() instead of direct SVG query
|
|
142
|
+
var labels = DiagramDOM.getAllNodeLabels();
|
|
143
|
+
if (labels.length === 0) { updateCount(); return; }
|
|
144
|
+
|
|
145
|
+
var seen = new Set();
|
|
146
|
+
|
|
147
|
+
for (var i = 0; i < labels.length; i++) {
|
|
148
|
+
var label = labels[i];
|
|
149
|
+
var text = (label.textContent || '').toLowerCase();
|
|
150
|
+
if (text.indexOf(lowerQuery) === -1) continue;
|
|
151
|
+
|
|
152
|
+
// Use DiagramDOM.findMatchParent() instead of inline walk-up loop
|
|
153
|
+
var parent = DiagramDOM.findMatchParent(label);
|
|
154
|
+
if (!parent) continue;
|
|
155
|
+
|
|
156
|
+
// Deduplicate by element reference
|
|
157
|
+
var parentId = parent.getAttribute('id') || ('__match_' + i);
|
|
158
|
+
if (seen.has(parentId)) continue;
|
|
159
|
+
seen.add(parentId);
|
|
160
|
+
|
|
161
|
+
state.matches.push(parent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
highlightMatches();
|
|
165
|
+
updateCount();
|
|
166
|
+
|
|
167
|
+
// Auto-navigate to first match
|
|
168
|
+
if (state.matches.length > 0) {
|
|
169
|
+
state.currentIndex = 0;
|
|
170
|
+
setActiveMatch(0);
|
|
171
|
+
scrollToMatch(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Emit search event via event bus
|
|
175
|
+
if (window.SmartCodeEventBus) {
|
|
176
|
+
SmartCodeEventBus.emit('search:results', { query: query, matchCount: state.matches.length });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Highlighting ──
|
|
181
|
+
|
|
182
|
+
function highlightMatches() {
|
|
183
|
+
for (var i = 0; i < state.matches.length; i++) {
|
|
184
|
+
state.matches[i].classList.add('search-match');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function clearHighlights() {
|
|
189
|
+
var svg = DiagramDOM.getSVG();
|
|
190
|
+
if (!svg) return;
|
|
191
|
+
var matched = svg.querySelectorAll('.search-match, .search-match-active');
|
|
192
|
+
for (var i = 0; i < matched.length; i++) {
|
|
193
|
+
matched[i].classList.remove('search-match', 'search-match-active');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function setActiveMatch(index) {
|
|
198
|
+
// Clear previous active
|
|
199
|
+
var svg = DiagramDOM.getSVG();
|
|
200
|
+
if (!svg) return;
|
|
201
|
+
var prev = svg.querySelectorAll('.search-match-active');
|
|
202
|
+
for (var i = 0; i < prev.length; i++) {
|
|
203
|
+
prev[i].classList.remove('search-match-active');
|
|
204
|
+
}
|
|
205
|
+
// Set new active
|
|
206
|
+
if (index >= 0 && index < state.matches.length) {
|
|
207
|
+
state.matches[index].classList.add('search-match-active');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Navigation ──
|
|
212
|
+
|
|
213
|
+
function navigateNext() {
|
|
214
|
+
if (state.matches.length === 0) return;
|
|
215
|
+
state.currentIndex = (state.currentIndex + 1) % state.matches.length;
|
|
216
|
+
setActiveMatch(state.currentIndex);
|
|
217
|
+
scrollToMatch(state.currentIndex);
|
|
218
|
+
updateCount();
|
|
219
|
+
// Emit match navigation event
|
|
220
|
+
if (window.SmartCodeEventBus) {
|
|
221
|
+
SmartCodeEventBus.emit('search:match-selected', { index: state.currentIndex });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function navigatePrev() {
|
|
226
|
+
if (state.matches.length === 0) return;
|
|
227
|
+
state.currentIndex = (state.currentIndex - 1 + state.matches.length) % state.matches.length;
|
|
228
|
+
setActiveMatch(state.currentIndex);
|
|
229
|
+
scrollToMatch(state.currentIndex);
|
|
230
|
+
updateCount();
|
|
231
|
+
// Emit match navigation event
|
|
232
|
+
if (window.SmartCodeEventBus) {
|
|
233
|
+
SmartCodeEventBus.emit('search:match-selected', { index: state.currentIndex });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Pan to Match ──
|
|
238
|
+
|
|
239
|
+
function scrollToMatch(index) {
|
|
240
|
+
if (index < 0 || index >= state.matches.length) return;
|
|
241
|
+
var matchEl = state.matches[index];
|
|
242
|
+
if (!matchEl) return;
|
|
243
|
+
|
|
244
|
+
var container = document.getElementById('preview-container');
|
|
245
|
+
if (!container) return;
|
|
246
|
+
|
|
247
|
+
var panState = hooks.getPan();
|
|
248
|
+
var matchRect = matchEl.getBoundingClientRect();
|
|
249
|
+
var containerRect = container.getBoundingClientRect();
|
|
250
|
+
|
|
251
|
+
// Calculate the center of the match element relative to the container
|
|
252
|
+
var matchCenterX = matchRect.left + matchRect.width / 2 - containerRect.left;
|
|
253
|
+
var matchCenterY = matchRect.top + matchRect.height / 2 - containerRect.top;
|
|
254
|
+
|
|
255
|
+
// Calculate new pan to center the match in the container
|
|
256
|
+
var containerCenterX = containerRect.width / 2;
|
|
257
|
+
var containerCenterY = containerRect.height / 2;
|
|
258
|
+
|
|
259
|
+
var newPanX = panState.panX + (containerCenterX - matchCenterX);
|
|
260
|
+
var newPanY = panState.panY + (containerCenterY - matchCenterY);
|
|
261
|
+
|
|
262
|
+
hooks.setPan(newPanX, newPanY);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Count Display ──
|
|
266
|
+
|
|
267
|
+
function updateCount() {
|
|
268
|
+
if (!countEl) return;
|
|
269
|
+
if (state.matches.length === 0) {
|
|
270
|
+
countEl.textContent = state.query ? 'No matches' : '';
|
|
271
|
+
countEl.classList.toggle('no-match', !!state.query);
|
|
272
|
+
} else {
|
|
273
|
+
countEl.textContent = (state.currentIndex + 1) + ' of ' + state.matches.length;
|
|
274
|
+
countEl.classList.remove('no-match');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Init ──
|
|
279
|
+
|
|
280
|
+
function init(options) {
|
|
281
|
+
if (options) {
|
|
282
|
+
if (options.getPan) hooks.getPan = options.getPan;
|
|
283
|
+
if (options.setPan) hooks.setPan = options.setPan;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Subscribe to event bus: refresh search results after diagram re-render
|
|
287
|
+
if (window.SmartCodeEventBus) {
|
|
288
|
+
SmartCodeEventBus.on('diagram:rendered', function() {
|
|
289
|
+
if (state.isOpen && state.query) {
|
|
290
|
+
search(state.query);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Public API ──
|
|
297
|
+
|
|
298
|
+
window.SmartCodeSearch = {
|
|
299
|
+
init: init,
|
|
300
|
+
open: open,
|
|
301
|
+
close: close,
|
|
302
|
+
getState: function () { return state; },
|
|
303
|
+
};
|
|
304
|
+
})();
|