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