@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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Ghost Paths -- renders discarded reasoning branches as
|
|
3
|
+
* curved dashed lines on the diagram SVG. Toggle visibility via button.
|
|
4
|
+
*
|
|
5
|
+
* Dependencies:
|
|
6
|
+
* - diagram-dom.js (DiagramDOM)
|
|
7
|
+
* - event-bus.js (SmartCodeEventBus)
|
|
8
|
+
*/
|
|
9
|
+
(function() {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
var SVG_NS = 'http://www.w3.org/2000/svg';
|
|
13
|
+
var LS_KEY = 'smartcode-ghost-paths-visible';
|
|
14
|
+
|
|
15
|
+
// Visual config
|
|
16
|
+
var GHOST_COLOR = '#8b5cf6'; // matches --ghost-path token
|
|
17
|
+
var GHOST_OPACITY = 0.55;
|
|
18
|
+
var GHOST_STROKE_W = 2;
|
|
19
|
+
var GHOST_DASH = '6,4';
|
|
20
|
+
var LABEL_FONT_SIZE = 10;
|
|
21
|
+
var LABEL_BG_PAD_X = 6;
|
|
22
|
+
var LABEL_BG_PAD_Y = 3;
|
|
23
|
+
var LABEL_BG_COLOR = '#18181b'; // matches --surface-1 token
|
|
24
|
+
var LABEL_BG_OPACITY = 0.85;
|
|
25
|
+
var LABEL_MAX_CHARS = 40;
|
|
26
|
+
var CURVE_OFFSET = 60; // Bezier curve offset to avoid straight-line crossings
|
|
27
|
+
|
|
28
|
+
// ── Module State ──
|
|
29
|
+
var ghostPathsByFile = {}; // { filePath: [ghostPath, ...] }
|
|
30
|
+
var visible = false;
|
|
31
|
+
var userExplicitlyHid = false; // Track if user manually toggled off (GHOST-06)
|
|
32
|
+
|
|
33
|
+
// ── Helpers ──
|
|
34
|
+
|
|
35
|
+
function loadVisibility() {
|
|
36
|
+
try {
|
|
37
|
+
var stored = localStorage.getItem(LS_KEY);
|
|
38
|
+
if (stored !== null) visible = stored === 'true';
|
|
39
|
+
} catch (e) { /* localStorage unavailable */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveVisibility() {
|
|
43
|
+
try { localStorage.setItem(LS_KEY, String(visible)); } catch (e) {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Get ghost paths for the currently viewed file */
|
|
47
|
+
function getCurrentPaths() {
|
|
48
|
+
var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
|
|
49
|
+
if (!file) return [];
|
|
50
|
+
return ghostPathsByFile[file] || [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function updateBadge() {
|
|
54
|
+
var badge = document.getElementById('ghostCountBadge');
|
|
55
|
+
if (badge) {
|
|
56
|
+
var count = getCurrentPaths().length;
|
|
57
|
+
badge.textContent = count || '';
|
|
58
|
+
badge.dataset.count = count;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateButtonState() {
|
|
63
|
+
var btn = document.getElementById('btnGhostPaths');
|
|
64
|
+
if (btn) btn.classList.toggle('active', visible);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function truncateLabel(label) {
|
|
68
|
+
if (!label) return '';
|
|
69
|
+
return label.length > LABEL_MAX_CHARS
|
|
70
|
+
? label.substring(0, LABEL_MAX_CHARS - 1) + '\u2026'
|
|
71
|
+
: label;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Dedup: keep only the latest ghost path per from->to pair ──
|
|
75
|
+
function dedupPaths(paths) {
|
|
76
|
+
var seen = {};
|
|
77
|
+
var result = [];
|
|
78
|
+
// Walk backwards so the last recorded wins
|
|
79
|
+
for (var i = paths.length - 1; i >= 0; i--) {
|
|
80
|
+
var key = paths[i].fromNodeId + '->' + paths[i].toNodeId;
|
|
81
|
+
if (!seen[key]) {
|
|
82
|
+
seen[key] = true;
|
|
83
|
+
result.unshift(paths[i]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Ghost Path Rendering ──
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse translate(x,y) from an element's transform attribute.
|
|
93
|
+
* Returns { tx, ty } with offsets (0,0 if no transform).
|
|
94
|
+
*/
|
|
95
|
+
function parseTranslate(el) {
|
|
96
|
+
var tx = 0, ty = 0;
|
|
97
|
+
var transform = el.getAttribute('transform');
|
|
98
|
+
if (transform) {
|
|
99
|
+
var match = transform.match(/translate\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
|
|
100
|
+
if (match) {
|
|
101
|
+
tx = parseFloat(match[1]);
|
|
102
|
+
ty = parseFloat(match[2]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { tx: tx, ty: ty };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getNodeCenter(nodeId) {
|
|
109
|
+
var el = DiagramDOM.findNodeElement(nodeId);
|
|
110
|
+
if (!el || !el.getBBox) return null;
|
|
111
|
+
var bbox = el.getBBox();
|
|
112
|
+
var t = parseTranslate(el);
|
|
113
|
+
return {
|
|
114
|
+
x: t.tx + bbox.x + bbox.width / 2,
|
|
115
|
+
y: t.ty + bbox.y + bbox.height / 2
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getNodeBottom(nodeId) {
|
|
120
|
+
var el = DiagramDOM.findNodeElement(nodeId);
|
|
121
|
+
if (!el || !el.getBBox) return null;
|
|
122
|
+
var bbox = el.getBBox();
|
|
123
|
+
var t = parseTranslate(el);
|
|
124
|
+
return {
|
|
125
|
+
x: t.tx + bbox.x + bbox.width / 2,
|
|
126
|
+
y: t.ty + bbox.y + bbox.height
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getNodeTop(nodeId) {
|
|
131
|
+
var el = DiagramDOM.findNodeElement(nodeId);
|
|
132
|
+
if (!el || !el.getBBox) return null;
|
|
133
|
+
var bbox = el.getBBox();
|
|
134
|
+
var t = parseTranslate(el);
|
|
135
|
+
return {
|
|
136
|
+
x: t.tx + bbox.x + bbox.width / 2,
|
|
137
|
+
y: t.ty + bbox.y
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build a cubic bezier that curves to the right of the straight line,
|
|
143
|
+
* so it doesn't overlap existing edges.
|
|
144
|
+
*/
|
|
145
|
+
function buildCurvedPath(from, to, index) {
|
|
146
|
+
var dx = to.x - from.x;
|
|
147
|
+
var dy = to.y - from.y;
|
|
148
|
+
// Perpendicular offset direction (alternate sides for multiple ghosts)
|
|
149
|
+
var side = (index % 2 === 0) ? 1 : -1;
|
|
150
|
+
var offset = CURVE_OFFSET * side;
|
|
151
|
+
|
|
152
|
+
// Midpoint
|
|
153
|
+
var mx = (from.x + to.x) / 2;
|
|
154
|
+
var my = (from.y + to.y) / 2;
|
|
155
|
+
|
|
156
|
+
// Perpendicular vector (normalized)
|
|
157
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
158
|
+
var px = -dy / len * offset;
|
|
159
|
+
var py = dx / len * offset;
|
|
160
|
+
|
|
161
|
+
// Control point
|
|
162
|
+
var cx = mx + px;
|
|
163
|
+
var cy = my + py;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
d: 'M ' + from.x + ' ' + from.y +
|
|
167
|
+
' Q ' + cx + ' ' + cy +
|
|
168
|
+
' ' + to.x + ' ' + to.y,
|
|
169
|
+
labelX: (from.x + 2 * cx + to.x) / 4,
|
|
170
|
+
labelY: (from.y + 2 * cy + to.y) / 4
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createArrowMarker(svg) {
|
|
175
|
+
// Check if our ghost arrow marker already exists
|
|
176
|
+
if (svg.querySelector('#ghost-arrow')) return;
|
|
177
|
+
|
|
178
|
+
var defs = svg.querySelector('defs');
|
|
179
|
+
if (!defs) {
|
|
180
|
+
defs = document.createElementNS(SVG_NS, 'defs');
|
|
181
|
+
svg.insertBefore(defs, svg.firstChild);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var marker = document.createElementNS(SVG_NS, 'marker');
|
|
185
|
+
marker.setAttribute('id', 'ghost-arrow');
|
|
186
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
187
|
+
marker.setAttribute('refX', '9');
|
|
188
|
+
marker.setAttribute('refY', '5');
|
|
189
|
+
marker.setAttribute('markerWidth', '6');
|
|
190
|
+
marker.setAttribute('markerHeight', '6');
|
|
191
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
192
|
+
|
|
193
|
+
var arrow = document.createElementNS(SVG_NS, 'path');
|
|
194
|
+
arrow.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
195
|
+
arrow.setAttribute('fill', GHOST_COLOR);
|
|
196
|
+
marker.appendChild(arrow);
|
|
197
|
+
defs.appendChild(marker);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderGhostPaths() {
|
|
201
|
+
var svg = DiagramDOM.getSVG();
|
|
202
|
+
if (!svg) return;
|
|
203
|
+
|
|
204
|
+
// Remove existing ghost path elements
|
|
205
|
+
svg.querySelectorAll('.ghost-path').forEach(function(el) { el.remove(); });
|
|
206
|
+
|
|
207
|
+
var paths = getCurrentPaths();
|
|
208
|
+
if (!visible || paths.length === 0) return;
|
|
209
|
+
|
|
210
|
+
createArrowMarker(svg);
|
|
211
|
+
|
|
212
|
+
var unique = dedupPaths(paths);
|
|
213
|
+
var diagramGroup = svg.querySelector('.smartcode-diagram');
|
|
214
|
+
var container = diagramGroup || svg;
|
|
215
|
+
|
|
216
|
+
for (var i = 0; i < unique.length; i++) {
|
|
217
|
+
var gp = unique[i];
|
|
218
|
+
var fromPt = getNodeBottom(gp.fromNodeId) || getNodeCenter(gp.fromNodeId);
|
|
219
|
+
var toPt = getNodeTop(gp.toNodeId) || getNodeCenter(gp.toNodeId);
|
|
220
|
+
if (!fromPt || !toPt) {
|
|
221
|
+
console.warn('[SmartCode GhostPaths] Node not found in SVG:',
|
|
222
|
+
!fromPt ? gp.fromNodeId : gp.toNodeId,
|
|
223
|
+
'— ghost path skipped:', gp.fromNodeId, '->', gp.toNodeId);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var g = document.createElementNS(SVG_NS, 'g');
|
|
228
|
+
g.setAttribute('class', 'ghost-path');
|
|
229
|
+
g.setAttribute('opacity', String(GHOST_OPACITY));
|
|
230
|
+
|
|
231
|
+
// Curved path
|
|
232
|
+
var curve = buildCurvedPath(fromPt, toPt, i);
|
|
233
|
+
var pathEl = document.createElementNS(SVG_NS, 'path');
|
|
234
|
+
pathEl.setAttribute('d', curve.d);
|
|
235
|
+
pathEl.setAttribute('stroke-dasharray', GHOST_DASH);
|
|
236
|
+
pathEl.setAttribute('stroke', GHOST_COLOR);
|
|
237
|
+
pathEl.setAttribute('stroke-width', String(GHOST_STROKE_W));
|
|
238
|
+
pathEl.setAttribute('fill', 'none');
|
|
239
|
+
pathEl.setAttribute('marker-end', 'url(#ghost-arrow)');
|
|
240
|
+
g.appendChild(pathEl);
|
|
241
|
+
|
|
242
|
+
// Label with background pill
|
|
243
|
+
if (gp.label) {
|
|
244
|
+
var labelText = truncateLabel(gp.label);
|
|
245
|
+
|
|
246
|
+
// Create text first to measure it
|
|
247
|
+
var text = document.createElementNS(SVG_NS, 'text');
|
|
248
|
+
text.setAttribute('x', String(curve.labelX));
|
|
249
|
+
text.setAttribute('y', String(curve.labelY));
|
|
250
|
+
text.setAttribute('text-anchor', 'middle');
|
|
251
|
+
text.setAttribute('dominant-baseline', 'central');
|
|
252
|
+
text.setAttribute('fill', GHOST_COLOR);
|
|
253
|
+
text.setAttribute('font-size', String(LABEL_FONT_SIZE));
|
|
254
|
+
text.setAttribute('font-family', "'JetBrains Mono', 'Inter', monospace");
|
|
255
|
+
text.setAttribute('font-weight', '500');
|
|
256
|
+
text.textContent = labelText;
|
|
257
|
+
|
|
258
|
+
// Append text temporarily to measure
|
|
259
|
+
g.appendChild(text);
|
|
260
|
+
container.appendChild(g);
|
|
261
|
+
var tBox = text.getBBox();
|
|
262
|
+
container.removeChild(g);
|
|
263
|
+
g.removeChild(text);
|
|
264
|
+
|
|
265
|
+
// Background pill
|
|
266
|
+
var bg = document.createElementNS(SVG_NS, 'rect');
|
|
267
|
+
bg.setAttribute('x', String(tBox.x - LABEL_BG_PAD_X));
|
|
268
|
+
bg.setAttribute('y', String(tBox.y - LABEL_BG_PAD_Y));
|
|
269
|
+
bg.setAttribute('width', String(tBox.width + LABEL_BG_PAD_X * 2));
|
|
270
|
+
bg.setAttribute('height', String(tBox.height + LABEL_BG_PAD_Y * 2));
|
|
271
|
+
bg.setAttribute('rx', '4');
|
|
272
|
+
bg.setAttribute('fill', LABEL_BG_COLOR);
|
|
273
|
+
bg.setAttribute('opacity', String(LABEL_BG_OPACITY));
|
|
274
|
+
g.appendChild(bg);
|
|
275
|
+
g.appendChild(text);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
container.appendChild(g);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Public API Methods ──
|
|
283
|
+
|
|
284
|
+
function toggle() {
|
|
285
|
+
visible = !visible;
|
|
286
|
+
// Track explicit user choice: if they hide, don't auto-show again
|
|
287
|
+
userExplicitlyHid = !visible;
|
|
288
|
+
saveVisibility();
|
|
289
|
+
updateButtonState();
|
|
290
|
+
renderGhostPaths();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isVisibleFn() {
|
|
294
|
+
return visible;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function updateGhostPathsFn(file, paths) {
|
|
298
|
+
// Support legacy call without file: updateGhostPaths(paths)
|
|
299
|
+
if (Array.isArray(file)) {
|
|
300
|
+
paths = file;
|
|
301
|
+
file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : 'unknown';
|
|
302
|
+
}
|
|
303
|
+
var list = Array.isArray(paths) ? paths : [];
|
|
304
|
+
if (file) {
|
|
305
|
+
ghostPathsByFile[file] = list;
|
|
306
|
+
}
|
|
307
|
+
updateBadge();
|
|
308
|
+
|
|
309
|
+
// Auto-show ghost paths when new ones arrive -- but respect user choice
|
|
310
|
+
var currentFile = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
|
|
311
|
+
if (list.length > 0 && file === currentFile && !visible && !userExplicitlyHid) {
|
|
312
|
+
visible = true;
|
|
313
|
+
saveVisibility();
|
|
314
|
+
updateButtonState();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
renderGhostPaths();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getCount() {
|
|
321
|
+
return getCurrentPaths().length;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Init ──
|
|
325
|
+
|
|
326
|
+
function init() {
|
|
327
|
+
loadVisibility();
|
|
328
|
+
updateButtonState();
|
|
329
|
+
updateBadge();
|
|
330
|
+
|
|
331
|
+
// Re-render ghost paths after each diagram render
|
|
332
|
+
if (window.SmartCodeEventBus) {
|
|
333
|
+
SmartCodeEventBus.on('diagram:rendered', renderGhostPaths);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
renderGhostPaths();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── URL helper ──
|
|
340
|
+
function bUrl(path) { return (window.SmartCodeBaseUrl || '') + path; }
|
|
341
|
+
|
|
342
|
+
/** Create a ghost path via REST API and update UI */
|
|
343
|
+
function createGhostPath(fromNodeId, toNodeId, label) {
|
|
344
|
+
var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
|
|
345
|
+
if (!file) return;
|
|
346
|
+
var encoded = encodeURIComponent(file);
|
|
347
|
+
fetch(bUrl('/api/ghost-paths/' + encoded), {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
350
|
+
body: JSON.stringify({ fromNodeId: fromNodeId, toNodeId: toNodeId, label: label || undefined }),
|
|
351
|
+
}).then(function(r) {
|
|
352
|
+
if (!r.ok) throw new Error('Failed');
|
|
353
|
+
// Refetch to get updated list
|
|
354
|
+
return fetch(bUrl('/api/ghost-paths/' + encoded));
|
|
355
|
+
}).then(function(r) { return r.ok ? r.json() : null; })
|
|
356
|
+
.then(function(d) {
|
|
357
|
+
if (d) updateGhostPathsFn(file, d.ghostPaths || []);
|
|
358
|
+
if (window.toast) toast('Ghost path created');
|
|
359
|
+
}).catch(function() {
|
|
360
|
+
if (window.toast) toast('Error creating ghost path');
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Clear all ghost paths for the current file via REST API */
|
|
365
|
+
function clearAllGhostPaths() {
|
|
366
|
+
var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
|
|
367
|
+
if (!file) return;
|
|
368
|
+
var encoded = encodeURIComponent(file);
|
|
369
|
+
fetch(bUrl('/api/ghost-paths/' + encoded), {
|
|
370
|
+
method: 'DELETE',
|
|
371
|
+
}).then(function(r) {
|
|
372
|
+
if (!r.ok) throw new Error('Failed');
|
|
373
|
+
updateGhostPathsFn(file, []);
|
|
374
|
+
if (window.toast) toast('Ghost paths cleared');
|
|
375
|
+
}).catch(function() {
|
|
376
|
+
if (window.toast) toast('Error clearing ghost paths');
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Reset the explicit-hide flag (called on file switch) */
|
|
381
|
+
function resetUserHide() {
|
|
382
|
+
userExplicitlyHid = false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Public API ──
|
|
386
|
+
window.SmartCodeGhostPaths = {
|
|
387
|
+
init: init,
|
|
388
|
+
toggle: toggle,
|
|
389
|
+
isVisible: isVisibleFn,
|
|
390
|
+
updateGhostPaths: updateGhostPathsFn,
|
|
391
|
+
renderGhostPaths: renderGhostPaths,
|
|
392
|
+
getCount: getCount,
|
|
393
|
+
createGhostPath: createGhostPath,
|
|
394
|
+
clearAll: clearAllGhostPaths,
|
|
395
|
+
resetUserHide: resetUserHide,
|
|
396
|
+
};
|
|
397
|
+
})();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* ===================================================
|
|
2
|
+
SmartCode -- Heatmap Toggle & Legend Styles
|
|
3
|
+
=================================================== */
|
|
4
|
+
|
|
5
|
+
/* Heatmap toggle button active state */
|
|
6
|
+
#btnHeatmap {
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
#btnHeatmap.active {
|
|
10
|
+
background: rgba(239, 68, 68, 0.15) !important;
|
|
11
|
+
color: var(--status-problem) !important;
|
|
12
|
+
border-color: rgba(239, 68, 68, 0.3) !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Legend container -- bottom-right of preview area */
|
|
16
|
+
.heatmap-legend {
|
|
17
|
+
position: absolute;
|
|
18
|
+
bottom: 60px;
|
|
19
|
+
right: 16px;
|
|
20
|
+
z-index: 50;
|
|
21
|
+
background: var(--surface-1);
|
|
22
|
+
border: 1px solid var(--border-default);
|
|
23
|
+
border-radius: 6px;
|
|
24
|
+
padding: 8px 12px;
|
|
25
|
+
font-family: 'Inter', sans-serif;
|
|
26
|
+
font-size: 11px;
|
|
27
|
+
color: var(--text-primary);
|
|
28
|
+
min-width: 80px;
|
|
29
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.heatmap-legend-title {
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
font-size: 10px;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
letter-spacing: 1px;
|
|
37
|
+
color: var(--text-secondary);
|
|
38
|
+
margin-bottom: 6px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Risk mode legend items */
|
|
42
|
+
.heatmap-legend-item {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 6px;
|
|
46
|
+
padding: 2px 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.heatmap-legend-dot {
|
|
50
|
+
width: 10px;
|
|
51
|
+
height: 10px;
|
|
52
|
+
border-radius: 50%;
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Frequency mode gradient bar */
|
|
57
|
+
.heatmap-legend-gradient {
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 10px;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
background: linear-gradient(to right,
|
|
62
|
+
rgb(66, 133, 244),
|
|
63
|
+
rgb(160, 106, 122),
|
|
64
|
+
rgb(255, 80, 0)
|
|
65
|
+
);
|
|
66
|
+
margin: 4px 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.heatmap-legend-labels {
|
|
70
|
+
display: flex;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
font-size: 9px;
|
|
73
|
+
color: var(--text-secondary);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Legend header with title + mode toggle */
|
|
77
|
+
.heatmap-legend-header {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
margin-bottom: 6px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.heatmap-legend-header .heatmap-legend-title {
|
|
86
|
+
margin-bottom: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Mode toggle button */
|
|
90
|
+
.heatmap-mode-toggle {
|
|
91
|
+
font-size: 9px;
|
|
92
|
+
font-family: 'Inter', sans-serif;
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
padding: 2px 6px;
|
|
95
|
+
border-radius: 3px;
|
|
96
|
+
border: 1px solid var(--border-default);
|
|
97
|
+
background: var(--surface-2, #2a2a2a);
|
|
98
|
+
color: var(--text-secondary);
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
transition: background 0.15s, color 0.15s;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.heatmap-mode-toggle:hover {
|
|
105
|
+
background: var(--surface-3, #333);
|
|
106
|
+
color: var(--text-primary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Empty state guidance */
|
|
110
|
+
.heatmap-empty-state {
|
|
111
|
+
font-size: 10px;
|
|
112
|
+
color: var(--text-secondary);
|
|
113
|
+
line-height: 1.4;
|
|
114
|
+
padding: 4px 0;
|
|
115
|
+
max-width: 180px;
|
|
116
|
+
}
|