rivet-design 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 (80) hide show
  1. package/LICENSE.md +674 -0
  2. package/README.md +112 -0
  3. package/bin/rivet.js +27 -0
  4. package/dist/index-core.d.ts +15 -0
  5. package/dist/index-core.d.ts.map +1 -0
  6. package/dist/index-core.js +38 -0
  7. package/dist/index-core.js.map +1 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +217 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/routes/components.d.ts +2 -0
  13. package/dist/routes/components.d.ts.map +1 -0
  14. package/dist/routes/components.js +58 -0
  15. package/dist/routes/components.js.map +1 -0
  16. package/dist/routes/git.d.ts +3 -0
  17. package/dist/routes/git.d.ts.map +1 -0
  18. package/dist/routes/git.js +52 -0
  19. package/dist/routes/git.js.map +1 -0
  20. package/dist/routes/modifications.d.ts +3 -0
  21. package/dist/routes/modifications.d.ts.map +1 -0
  22. package/dist/routes/modifications.js +241 -0
  23. package/dist/routes/modifications.js.map +1 -0
  24. package/dist/routes/selection.d.ts +2 -0
  25. package/dist/routes/selection.d.ts.map +1 -0
  26. package/dist/routes/selection.js +38 -0
  27. package/dist/routes/selection.js.map +1 -0
  28. package/dist/scripts/selection-script.js +724 -0
  29. package/dist/server.d.ts +9 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +93 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/services/ComponentSearchService.d.ts +12 -0
  34. package/dist/services/ComponentSearchService.d.ts.map +1 -0
  35. package/dist/services/ComponentSearchService.js +61 -0
  36. package/dist/services/ComponentSearchService.js.map +1 -0
  37. package/dist/services/FileModificationService.d.ts +29 -0
  38. package/dist/services/FileModificationService.d.ts.map +1 -0
  39. package/dist/services/FileModificationService.js +82 -0
  40. package/dist/services/FileModificationService.js.map +1 -0
  41. package/dist/services/LLMService.d.ts +60 -0
  42. package/dist/services/LLMService.d.ts.map +1 -0
  43. package/dist/services/LLMService.js +201 -0
  44. package/dist/services/LLMService.js.map +1 -0
  45. package/dist/services/LocalGitService.d.ts +33 -0
  46. package/dist/services/LocalGitService.d.ts.map +1 -0
  47. package/dist/services/LocalGitService.js +252 -0
  48. package/dist/services/LocalGitService.js.map +1 -0
  49. package/dist/services/ProjectDetectionService.d.ts +26 -0
  50. package/dist/services/ProjectDetectionService.d.ts.map +1 -0
  51. package/dist/services/ProjectDetectionService.js +147 -0
  52. package/dist/services/ProjectDetectionService.js.map +1 -0
  53. package/dist/services/ScriptInjectionService.d.ts +8 -0
  54. package/dist/services/ScriptInjectionService.d.ts.map +1 -0
  55. package/dist/services/ScriptInjectionService.js +178 -0
  56. package/dist/services/ScriptInjectionService.js.map +1 -0
  57. package/dist/services/SessionService.d.ts +26 -0
  58. package/dist/services/SessionService.d.ts.map +1 -0
  59. package/dist/services/SessionService.js +141 -0
  60. package/dist/services/SessionService.js.map +1 -0
  61. package/dist/types/change-types.d.ts +93 -0
  62. package/dist/types/change-types.d.ts.map +1 -0
  63. package/dist/types/change-types.js +4 -0
  64. package/dist/types/change-types.js.map +1 -0
  65. package/dist/types/proxy-types.d.ts +34 -0
  66. package/dist/types/proxy-types.d.ts.map +1 -0
  67. package/dist/types/proxy-types.js +3 -0
  68. package/dist/types/proxy-types.js.map +1 -0
  69. package/dist/types/types.d.ts +15 -0
  70. package/dist/types/types.d.ts.map +1 -0
  71. package/dist/types/types.js +3 -0
  72. package/dist/types/types.js.map +1 -0
  73. package/dist/utils/logger.d.ts +20 -0
  74. package/dist/utils/logger.d.ts.map +1 -0
  75. package/dist/utils/logger.js +51 -0
  76. package/dist/utils/logger.js.map +1 -0
  77. package/package.json +86 -0
  78. package/src/ui/dist/assets/main-DuNgkeFM.js +105 -0
  79. package/src/ui/dist/assets/main-DzZ9GWvo.css +1 -0
  80. package/src/ui/dist/index.html +14 -0
@@ -0,0 +1,724 @@
1
+ (function() {
2
+ // Prevent multiple injections
3
+ if (window.rivetSelectionInjected) return;
4
+ window.rivetSelectionInjected = true;
5
+
6
+ console.log('🔧 Rivet selection script loading...');
7
+
8
+ let isSelectionMode = false;
9
+ let hoveredElement = null;
10
+
11
+ // Selection box variables
12
+ let isBoxSelection = false;
13
+ let boxSelectionStart = null;
14
+ let selectionBox = null;
15
+ let scrollAccumulation = 0;
16
+ let currentMousePos = { x: 0, y: 0 };
17
+
18
+ const transformComputedStyles = (computedStyles) =>
19
+ Object.fromEntries(
20
+ Array.from(computedStyles).map(prop => [prop, computedStyles.getPropertyValue(prop)])
21
+ );
22
+ ;
23
+
24
+ // Add selection styles
25
+ const style = document.createElement('style');
26
+ style.id = 'rivet-selection-styles';
27
+ style.textContent = `
28
+ .rivet-highlight {
29
+ outline: 2px solid #007acc !important;
30
+ outline-offset: 2px !important;
31
+ cursor: crosshair !important;
32
+ }
33
+ .rivet-selected {
34
+ outline: 2px solid #ff6b35 !important;
35
+ outline-offset: 2px !important;
36
+ }
37
+ .rivet-selection-box {
38
+ position: fixed !important;
39
+ border: 3px solid #ff6b35 !important;
40
+ background-color: rgba(255, 107, 53, 0.5) !important;
41
+ pointer-events: none !important;
42
+ z-index: 999999 !important;
43
+ display: none !important;
44
+ box-sizing: border-box !important;
45
+ border-radius: 4px !important;
46
+ /* Debug styles to make it very obvious */
47
+ box-shadow: 0 0 10px rgba(255, 107, 53, 0.7) !important;
48
+ opacity: 1 !important;
49
+ }
50
+ `;
51
+ document.head.appendChild(style);
52
+
53
+
54
+ const removeHighlight = (element) => {
55
+ element.classList.remove('rivet-highlight');
56
+ };
57
+
58
+ const getXPath = (element) => {
59
+ if (element.id) return `//*[@id="${element.id}"]`;
60
+ if (element === document.body) return '/html/body';
61
+
62
+ let ix = 0;
63
+ const siblings = element.parentNode?.childNodes || [];
64
+ for (let i = 0; i < siblings.length; i++) {
65
+ const sibling = siblings[i];
66
+ if (sibling === element) {
67
+ const parent = element.parentNode;
68
+ return getXPath(parent) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
69
+ }
70
+ if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
71
+ ix++;
72
+ }
73
+ }
74
+ return '';
75
+ };
76
+
77
+ const getSourceFile = (element) => {
78
+ console.log('DEBUG: getSourceFile invoked', element);
79
+ try {
80
+ // Strategy 1: React Dev Tools fiber with debug source
81
+ console.log('DEBUG: Attempting strategy 1: React dev tools');
82
+
83
+ const reactFiberKeys = Object.keys(element).filter(key =>
84
+ key.startsWith('__reactInternalInstance$') ||
85
+ key.startsWith('__reactFiber$')
86
+ );
87
+
88
+ for (const key of reactFiberKeys) {
89
+ const fiber = element[key];
90
+ let current = fiber;
91
+ let depth = 0;
92
+
93
+ // Walk up the fiber tree looking for _debugSource
94
+ while (current && depth < 10) {
95
+ if (current._debugSource && current._debugSource.fileName) {
96
+ const fileName = current._debugSource.fileName;
97
+ // Convert absolute path to relative from project root
98
+ const srcMatch = fileName.match(/(src\/.+)$/);
99
+ if (srcMatch) {
100
+ console.log('🗂️ Found source file via React fiber:', srcMatch[1]);
101
+ return srcMatch[1];
102
+ }
103
+ return fileName;
104
+ }
105
+
106
+ // Try different fiber navigation patterns
107
+ current = current.return || current.parent || current._owner;
108
+ depth++;
109
+ }
110
+ }
111
+
112
+ // Strategy 2: Check for Next.js development data attributes
113
+ console.log('DEBUG: Attempting strategy 2: Next.js data attributes');
114
+ let currentEl = element;
115
+ let attempts = 0;
116
+ while (currentEl && attempts < 5) {
117
+ const nextjsSource = currentEl.getAttribute?.('data-nextjs-source');
118
+ if (nextjsSource) {
119
+ console.log('🗂️ Found source file via Next.js data attribute:', nextjsSource);
120
+ return nextjsSource;
121
+ }
122
+
123
+ const reactSource = currentEl.getAttribute?.('data-reactroot') ||
124
+ currentEl.getAttribute?.('data-react-component');
125
+ if (reactSource) {
126
+ console.log('🗂️ Found React component data attribute:', reactSource);
127
+ return reactSource;
128
+ }
129
+
130
+ currentEl = currentEl.parentElement;
131
+ attempts++;
132
+ }
133
+
134
+ // Strategy 3: Check for webpack/dev server source mapping comments
135
+ console.log('DEBUG: Attempting strategy 3: webpack/dev server source mapping comments');
136
+ const scripts = document.querySelectorAll('script[src*="webpack"], script[src*="vite"], script[src*="next"]');
137
+ if (scripts.length > 0) {
138
+ // In dev mode, try to extract from current script stack
139
+ const error = new Error();
140
+ const stack = error.stack || '';
141
+
142
+ // Look for webpack module paths in stack trace
143
+ const webpackMatch = stack.match(/webpack:\/\/.*\/(src\/[^?\\s:]+\.(tsx?|jsx?))/);
144
+ if (webpackMatch) {
145
+ console.log('🗂️ Found source file via webpack stack:', webpackMatch[1]);
146
+ return webpackMatch[1];
147
+ }
148
+
149
+ // Look for localhost dev server paths
150
+ const devMatch = stack.match(/localhost:\d+.*\/(src\/[^?\\s:]+\.(tsx?|jsx?))/);
151
+ if (devMatch) {
152
+ console.log('🗂️ Found source file via dev server:', devMatch[1]);
153
+ return devMatch[1];
154
+ }
155
+ }
156
+
157
+ console.log('⚠️ No source file detected for element', element);
158
+ return null;
159
+ } catch (error) {
160
+ console.warn('Error detecting source file:', error);
161
+ return null;
162
+ }
163
+ };
164
+
165
+ // Selection box functions
166
+ const createSelectionBox = () => {
167
+ if (selectionBox) {
168
+ console.log('🔍 Reusing existing selection box:', selectionBox);
169
+ return selectionBox;
170
+ }
171
+
172
+ selectionBox = document.createElement('div');
173
+ selectionBox.id = 'rivet-debug-selection-box';
174
+ // Don't use the CSS class - apply styles directly to avoid display:none conflict
175
+
176
+ // Force all styles directly to avoid any CSS conflicts
177
+ // TODO: Standardize this with the element data above
178
+ selectionBox.style.position = 'fixed';
179
+ selectionBox.style.pointerEvents = 'none';
180
+ selectionBox.style.zIndex = '999999';
181
+ selectionBox.style.boxSizing = 'border-box';
182
+ selectionBox.style.border = '3px solid #ff6b35';
183
+ selectionBox.style.backgroundColor = 'rgba(255, 107, 53, 0.5)';
184
+ selectionBox.style.borderRadius = '4px';
185
+ selectionBox.style.boxShadow = '0 0 10px rgba(255, 107, 53, 0.7)';
186
+ selectionBox.style.display = 'none'; // Will be set to block when shown
187
+
188
+ document.body.appendChild(selectionBox);
189
+ console.log('🔍 Created new selection box:', selectionBox, 'Parent:', document.body);
190
+ return selectionBox;
191
+ };
192
+
193
+ const showSelectionBox = (startX, startY, endX, endY) => {
194
+ const box = createSelectionBox();
195
+
196
+ // Use viewport coordinates directly since we're using fixed positioning
197
+ const left = Math.min(startX, endX);
198
+ const top = Math.min(startY, endY);
199
+ const width = Math.abs(endX - startX) + scrollAccumulation;
200
+ const height = Math.abs(endY - startY) + scrollAccumulation;
201
+
202
+ // Figma-style: start from exact size (even 0px) for precise feedback
203
+ box.style.left = left + 'px';
204
+ box.style.top = top + 'px';
205
+ box.style.width = width + 'px';
206
+ box.style.height = height + 'px';
207
+ box.style.display = 'block';
208
+
209
+ // Debug: Force visible styles with display:block override
210
+ box.style.backgroundColor = 'rgba(255, 107, 53, 0.5)';
211
+ box.style.border = '3px solid #ff6b35';
212
+ box.style.zIndex = '999999';
213
+ box.style.display = 'block';
214
+ box.style.visibility = 'visible';
215
+
216
+ console.log('🔍 Selection box updated:', {
217
+ left, top, width, height,
218
+ scrollAccumulation,
219
+ element: box,
220
+ computedStyle: window.getComputedStyle(box).display,
221
+ visible: box.offsetWidth > 0 && box.offsetHeight > 0
222
+ });
223
+ };
224
+
225
+ const hideSelectionBox = () => {
226
+ if (selectionBox) {
227
+ selectionBox.style.display = 'none';
228
+ }
229
+ };
230
+
231
+ // Box selection event handlers
232
+ const handleMouseDown = (e) => {
233
+ console.log('🔍 MouseDown event triggered:', {
234
+ isSelectionMode,
235
+ clientX: e.clientX,
236
+ clientY: e.clientY,
237
+ target: e.target
238
+ });
239
+
240
+ // Only enable bounding box interactions in edit mode
241
+ if (isSelectionMode) {
242
+ e.preventDefault();
243
+ e.stopPropagation();
244
+
245
+ isBoxSelection = true;
246
+ scrollAccumulation = 0;
247
+ boxSelectionStart = {
248
+ x: e.clientX,
249
+ y: e.clientY
250
+ };
251
+
252
+ // Update current mouse position
253
+ currentMousePos = { x: e.clientX, y: e.clientY };
254
+
255
+ // Disable individual element highlighting during box selection
256
+ if (hoveredElement) {
257
+ removeHighlight(hoveredElement);
258
+ hoveredElement = null;
259
+ }
260
+
261
+ // Show initial selection box immediately (even with 0 size)
262
+ console.log('🔍 About to show selection box...');
263
+ showSelectionBox(
264
+ boxSelectionStart.x,
265
+ boxSelectionStart.y,
266
+ boxSelectionStart.x,
267
+ boxSelectionStart.y
268
+ );
269
+
270
+ console.log('🔍 Box selection started at:', boxSelectionStart);
271
+ }
272
+ };
273
+
274
+ const handleMouseMove = (e) => {
275
+ // Always track mouse position for wheel events
276
+ currentMousePos = { x: e.clientX, y: e.clientY };
277
+
278
+ // Figma-style: Immediate and smooth bounding box updates during drag
279
+ if (isBoxSelection && boxSelectionStart && isSelectionMode) {
280
+ e.preventDefault();
281
+ e.stopPropagation();
282
+
283
+ console.log('🔍 MouseMove during drag:', {
284
+ startX: boxSelectionStart.x,
285
+ startY: boxSelectionStart.y,
286
+ currentX: e.clientX,
287
+ currentY: e.clientY
288
+ });
289
+
290
+ showSelectionBox(
291
+ boxSelectionStart.x,
292
+ boxSelectionStart.y,
293
+ e.clientX,
294
+ e.clientY
295
+ );
296
+ }
297
+ };
298
+
299
+ const handleMouseUp = (e) => {
300
+ if (!isBoxSelection || !isSelectionMode) return;
301
+
302
+ if (boxSelectionStart) {
303
+ e.preventDefault();
304
+ e.stopPropagation();
305
+
306
+ // Store coordinates before cleanup for element selection
307
+ const selectionCoords = {
308
+ left: Math.min(boxSelectionStart.x, e.clientX),
309
+ top: Math.min(boxSelectionStart.y, e.clientY),
310
+ width: Math.abs(e.clientX - boxSelectionStart.x) + scrollAccumulation,
311
+ height: Math.abs(e.clientY - boxSelectionStart.y) + scrollAccumulation
312
+ };
313
+
314
+ // Always clean up the visual bounding box
315
+ hideSelectionBox();
316
+ isBoxSelection = false;
317
+ boxSelectionStart = null;
318
+ scrollAccumulation = 0;
319
+
320
+ // Process element selection and highlighting
321
+ if (isSelectionMode) {
322
+ // Use stored coordinates for element selection
323
+ const { left, top, width, height } = selectionCoords;
324
+
325
+ // Convert to document coordinates for element intersection checks
326
+ const docLeft = left + window.scrollX;
327
+ const docTop = top + window.scrollY;
328
+
329
+ // Find elements within selection box
330
+ const rawSelectedElements = [];
331
+ const allElements = document.querySelectorAll('*');
332
+
333
+ allElements.forEach(element => {
334
+ const rect = element.getBoundingClientRect();
335
+ const elemLeft = rect.left + window.scrollX;
336
+ const elemTop = rect.top + window.scrollY;
337
+ const elemRight = elemLeft + rect.width;
338
+ const elemBottom = elemTop + rect.height;
339
+
340
+ // Check if element intersects with selection box
341
+ if (elemLeft < docLeft + width &&
342
+ elemRight > docLeft &&
343
+ elemTop < docTop + height &&
344
+ elemBottom > docTop &&
345
+ rect.width > 0 && rect.height > 0) { // Only include visible elements
346
+ rawSelectedElements.push(element);
347
+ }
348
+ });
349
+
350
+ // Find meaningful elements for multi-selection
351
+ const findMeaningfulElements = (elements) => {
352
+ const selectionArea = width * height;
353
+
354
+ // Step 1: Score by fit to selection box
355
+ const scored = elements.map(el => {
356
+ const rect = el.getBoundingClientRect();
357
+ const elementArea = rect.width * rect.height;
358
+ const ratio = elementArea / selectionArea;
359
+ const tagName = el.tagName.toLowerCase();
360
+
361
+ // Good fit: similar size to selection (0.2x to 3x)
362
+ let score = (ratio >= 0.2 && ratio <= 3) ? 100 : 20;
363
+
364
+ // Boost interactive elements
365
+ if (['button', 'a', 'input', 'select', 'textarea'].includes(tagName)) score += 50;
366
+
367
+ // Penalty for tiny noise or huge containers
368
+ if (ratio < 0.05 || ratio > 10) score -= 50;
369
+
370
+ return { element: el, score, ratio };
371
+ });
372
+
373
+ // Step 2: Remove noise and sort by score
374
+ const filtered = scored
375
+ .filter(item => item.score > 30)
376
+ .filter(item => !isDecorative(item.element))
377
+ .sort((a, b) => b.score - a.score);
378
+
379
+ // Step 3: Remove children if parent selected
380
+ const final = [];
381
+ for (const candidate of filtered) {
382
+ const hasParentInList = final.some(chosen =>
383
+ chosen.element.contains(candidate.element)
384
+ );
385
+ if (!hasParentInList) {
386
+ final.push(candidate);
387
+ }
388
+ }
389
+
390
+ console.log(`🎯 Multi-selection: ${final.length} elements selected`);
391
+ return final.slice(0, 6).map(item => item.element);
392
+ };
393
+
394
+ // Helper: Check if element is decorative noise
395
+ const isDecorative = (element) => {
396
+ const tagName = element.tagName.toLowerCase();
397
+
398
+ // SVG children
399
+ if (['path', 'g', 'circle', 'rect', 'line', 'polygon'].includes(tagName)) return true;
400
+
401
+ // Icon fonts
402
+ if (tagName === 'i' && element.className && /\b(fa|icon|material)\b/.test(element.className)) return true;
403
+
404
+ return false;
405
+ };
406
+
407
+ // Find the most specific single element using a scoring system
408
+ const findMostSpecificElement = (elements) => {
409
+ // Calculate selection center for proximity scoring
410
+ const selectionCenterX = left + width / 2;
411
+ const selectionCenterY = top + height / 2;
412
+
413
+ // Score each element based on specificity and relevance
414
+ const scoredElements = elements.map(element => {
415
+ const tagName = element.tagName.toLowerCase();
416
+ const rect = element.getBoundingClientRect();
417
+ const elementCenterX = rect.left + rect.width / 2;
418
+ const elementCenterY = rect.top + rect.height / 2;
419
+
420
+ let score = 0;
421
+
422
+ // 1. Semantic element type scoring (higher = more specific)
423
+ const semanticScores = {
424
+ 'button': 100,
425
+ 'input': 95,
426
+ 'select': 95,
427
+ 'textarea': 95,
428
+ 'a': 90,
429
+ 'img': 85,
430
+ 'h1': 80, 'h2': 80, 'h3': 80, 'h4': 80, 'h5': 80, 'h6': 80,
431
+ 'p': 70,
432
+ 'span': 60,
433
+ 'strong': 60, 'em': 60, 'b': 60, 'i': 60,
434
+ 'li': 55,
435
+ 'div': 40,
436
+ 'section': 30, 'article': 30, 'header': 30, 'footer': 30, 'nav': 30,
437
+ 'main': 20,
438
+ 'html': 0, 'body': 0, 'head': 0
439
+ };
440
+ score += semanticScores[tagName] || 50;
441
+
442
+ // 2. Meaningful attributes boost score
443
+ try {
444
+ const className = element.className && typeof element.className === 'string'
445
+ ? element.className
446
+ : (element.className && element.className.baseVal) || '';
447
+ const id = element.id || '';
448
+
449
+ if (id.trim()) score += 30;
450
+ if (className.trim()) score += 20;
451
+
452
+ // Specific class patterns that indicate interactive elements
453
+ const interactiveClasses = ['btn', 'button', 'link', 'card', 'item', 'component'];
454
+ if (interactiveClasses.some(cls => className.toLowerCase().includes(cls))) {
455
+ score += 40;
456
+ }
457
+ } catch (e) {
458
+ // Handle SVG or other special elements
459
+ }
460
+
461
+ // 3. Content specificity
462
+ const textContent = element.textContent ? element.textContent.trim() : '';
463
+ if (textContent && textContent.length < 200) { // Meaningful but not too verbose
464
+ score += Math.min(textContent.length, 50); // Up to 50 points for content
465
+ }
466
+
467
+ // 4. Penalty for overly large elements (likely containers)
468
+ const elementArea = rect.width * rect.height;
469
+ const screenArea = window.innerWidth * window.innerHeight;
470
+ if (elementArea > screenArea * 0.5) {
471
+ score -= 100; // Major penalty for very large elements
472
+ } else if (elementArea > screenArea * 0.2) {
473
+ score -= 50; // Moderate penalty for large elements
474
+ }
475
+
476
+ // 5. Proximity to selection center (closer = better)
477
+ const distance = Math.sqrt(
478
+ Math.pow(elementCenterX - selectionCenterX, 2) +
479
+ Math.pow(elementCenterY - selectionCenterY, 2)
480
+ );
481
+ const maxDistance = Math.sqrt(width * width + height * height);
482
+ const proximityScore = Math.max(0, (maxDistance - distance) / maxDistance * 30);
483
+ score += proximityScore;
484
+
485
+ // 6. Size appropriateness (elements that closely match selection size get bonus)
486
+ const selectionArea = width * height;
487
+ const sizeRatio = Math.min(elementArea, selectionArea) / Math.max(elementArea, selectionArea);
488
+ score += sizeRatio * 25; // Up to 25 points for size match
489
+
490
+ // 7. Depth in DOM (deeper = more specific)
491
+ let depth = 0;
492
+ let current = element;
493
+ while (current.parentElement && depth < 50) {
494
+ depth++;
495
+ current = current.parentElement;
496
+ }
497
+ score += Math.min(depth * 2, 30); // Up to 30 points for depth
498
+
499
+ // 8. Penalty for structural elements
500
+ const structuralElements = ['html', 'body', 'head', 'meta', 'script', 'style', 'link'];
501
+ if (structuralElements.includes(tagName)) {
502
+ score -= 1000; // Eliminate structural elements
503
+ }
504
+
505
+ return { element, score, tagName, rect };
506
+ });
507
+
508
+ // Filter out elements with negative scores and structural elements
509
+ const validElements = scoredElements.filter(item => item.score > 0);
510
+
511
+ if (validElements.length === 0) {
512
+ console.log('🔍 No valid elements found in selection');
513
+ return [];
514
+ }
515
+
516
+ // Sort by score (highest first) and return the best match
517
+ validElements.sort((a, b) => b.score - a.score);
518
+
519
+ console.log('🔍 Element scoring results:', validElements.slice(0, 5).map(item => ({
520
+ tag: item.tagName,
521
+ score: Math.round(item.score),
522
+ className: item.element.className,
523
+ id: item.element.id,
524
+ text: item.element.textContent?.substring(0, 50)
525
+ })));
526
+
527
+ // Return only the single best element
528
+ const bestElement = validElements[0].element;
529
+ console.log(`🎯 Selected best element: <${bestElement.tagName.toLowerCase()}> with score ${Math.round(validElements[0].score)}`);
530
+ return [bestElement];
531
+ };
532
+
533
+ // Apply filtering to determine meaningful elements BEFORE highlighting
534
+ const selectionArea = width * height;
535
+ const isSmallSelection = selectionArea < 2000;
536
+ const hasMultipleRelevantElements = rawSelectedElements.length > 3;
537
+
538
+ let selectedElements; // These will be both highlighted AND sent to design panel
539
+ if (isSmallSelection || !hasMultipleRelevantElements) {
540
+ selectedElements = findMostSpecificElement(rawSelectedElements);
541
+ console.log('🎯 Using single-element selection logic');
542
+ } else {
543
+ selectedElements = findMeaningfulElements(rawSelectedElements);
544
+ console.log('🎯 Using multi-element selection logic');
545
+ }
546
+
547
+ // Send selection data to parent
548
+ const selectionData = selectedElements.map(element => {
549
+ const rect = element.getBoundingClientRect();
550
+ const computedStyles = window.getComputedStyle(element);
551
+
552
+ // Safely extract className (handle SVG elements)
553
+ let className = '';
554
+ try {
555
+ className = element.className && typeof element.className === 'string'
556
+ ? element.className
557
+ : (element.className && element.className.baseVal) || '';
558
+ } catch (e) {
559
+ className = '';
560
+ }
561
+
562
+ // Safely extract id
563
+ let id = '';
564
+ try {
565
+ id = element.id || '';
566
+ } catch (e) {
567
+ id = '';
568
+ }
569
+
570
+ const filePath = getSourceFile(element);
571
+ console.log('🧭 Element source mapping:', {
572
+ tag: element.tagName.toLowerCase(),
573
+ id,
574
+ className,
575
+ filePath
576
+ });
577
+ return {
578
+ tagName: element.tagName.toLowerCase(),
579
+ className: className,
580
+ id: id,
581
+ textContent: element.textContent?.trim().substring(0, 100) || '',
582
+ innerHTML: element.innerHTML?.substring(0, 200) || '',
583
+ attributes: Array.from(element.attributes).reduce((acc, attr) => {
584
+ acc[attr.name] = attr.value;
585
+ return acc;
586
+ }, {}),
587
+ computedStyles: transformComputedStyles(computedStyles),
588
+ boundingRect: {
589
+ x: rect.x,
590
+ y: rect.y,
591
+ width: rect.width,
592
+ height: rect.height,
593
+ top: rect.top,
594
+ left: rect.left,
595
+ right: rect.right,
596
+ bottom: rect.bottom,
597
+ },
598
+ xpath: getXPath(element),
599
+ filePath
600
+ };
601
+ });
602
+
603
+ // Apply visual feedback to selected elements
604
+ document.querySelectorAll('.rivet-selected').forEach(el => {
605
+ el.classList.remove('rivet-selected');
606
+ });
607
+
608
+ selectedElements.forEach(element => {
609
+ element.classList.add('rivet-selected');
610
+ });
611
+
612
+ console.log('Box selection completed:', {
613
+ selectionBox: { left, top, width, height },
614
+ elementsFound: selectedElements.length,
615
+ selectedElement: selectedElements.length > 0 ? selectedElements[0].tagName : 'none',
616
+ selectionArea: selectionCoords.width * selectionCoords.height
617
+ });
618
+
619
+ // Send different message types based on selection count
620
+ if (selectedElements.length === 1) {
621
+ // Single element selected - treat as individual element selection
622
+ console.log('🎯 Single element selected via box, sending as ELEMENT_SELECTED');
623
+ window.parent.postMessage({
624
+ type: 'ELEMENT_SELECTED',
625
+ element: selectionData[0]
626
+ }, '*');
627
+ } else if (selectedElements.length > 1) {
628
+ // Multiple elements - send as box selection
629
+ console.log('📦 Multiple elements selected, sending as BOX_SELECTION_COMPLETE');
630
+ window.parent.postMessage({
631
+ type: 'BOX_SELECTION_COMPLETE',
632
+ elements: selectionData,
633
+ selectionBox: { left, top, width, height }
634
+ }, '*');
635
+ } else {
636
+ console.log('⚠️ No elements selected in bounding box');
637
+ }
638
+ }
639
+ }
640
+ };
641
+
642
+ const handleWheel = (e) => {
643
+ if (!isBoxSelection || !boxSelectionStart || !isSelectionMode) return;
644
+
645
+ e.preventDefault();
646
+ e.stopPropagation();
647
+
648
+ // Accumulate scroll for growing the box
649
+ const scrollDelta = Math.abs(e.deltaY) * 1.0; // Increased scale factor for better visibility
650
+ scrollAccumulation += scrollDelta;
651
+
652
+ console.log('Wheel event detected, scroll accumulation:', scrollAccumulation);
653
+
654
+ // Update the selection box with current mouse position
655
+ showSelectionBox(
656
+ boxSelectionStart.x,
657
+ boxSelectionStart.y,
658
+ currentMousePos.x,
659
+ currentMousePos.y
660
+ );
661
+ };
662
+
663
+ // Event handlers
664
+ const handleMouseOver = (e) => {
665
+ // Disable individual element highlighting since box selection is default
666
+ return;
667
+ };
668
+
669
+ const handleMouseOut = (e) => {
670
+ // Disable individual element highlighting since box selection is default
671
+ return;
672
+ };
673
+
674
+ const handleClick = (e) => {
675
+ // Disable individual element selection since box selection is default
676
+ return;
677
+ };
678
+
679
+ // Add event listeners
680
+ document.addEventListener('mouseover', handleMouseOver);
681
+ document.addEventListener('mouseout', handleMouseOut);
682
+ document.addEventListener('click', handleClick);
683
+ document.addEventListener('mousedown', handleMouseDown);
684
+ document.addEventListener('mousemove', handleMouseMove);
685
+ document.addEventListener('mouseup', handleMouseUp);
686
+ document.addEventListener('wheel', handleWheel, { passive: false });
687
+
688
+ // Listen for commands from parent
689
+ window.addEventListener('message', (event) => {
690
+ console.log('📨 Message received:', event.data, 'from origin:', event.origin);
691
+
692
+ if (event.data.type === 'TOGGLE_SELECTION') {
693
+ isSelectionMode = event.data.enabled;
694
+ console.log('🔄 Selection mode changed:', isSelectionMode);
695
+ document.body.style.cursor = isSelectionMode ? 'crosshair' : 'auto';
696
+
697
+ // Clear highlights when disabling selection mode (but keep visual box enabled)
698
+ if (!isSelectionMode) {
699
+ document.querySelectorAll('.rivet-highlight, .rivet-selected').forEach(el => {
700
+ el.classList.remove('rivet-highlight', 'rivet-selected');
701
+ });
702
+ hoveredElement = null;
703
+
704
+ // Only clean up box selection if not currently dragging
705
+ if (!isBoxSelection) {
706
+ hideSelectionBox();
707
+ }
708
+
709
+ // Notify parent to clear selection state and close RHS panel
710
+ console.log('🔄 Clearing selection state in parent');
711
+ window.parent.postMessage({
712
+ type: 'CLEAR_SELECTION'
713
+ }, '*');
714
+ }
715
+ }
716
+ });
717
+
718
+ // Notify parent that script is loaded
719
+ window.parent.postMessage({
720
+ type: 'SCRIPT_INJECTED'
721
+ }, '*');
722
+
723
+ console.log('✅ Rivet selection script loaded');
724
+ })();