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.
- package/LICENSE.md +674 -0
- package/README.md +112 -0
- package/bin/rivet.js +27 -0
- package/dist/index-core.d.ts +15 -0
- package/dist/index-core.d.ts.map +1 -0
- package/dist/index-core.js +38 -0
- package/dist/index-core.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/components.d.ts +2 -0
- package/dist/routes/components.d.ts.map +1 -0
- package/dist/routes/components.js +58 -0
- package/dist/routes/components.js.map +1 -0
- package/dist/routes/git.d.ts +3 -0
- package/dist/routes/git.d.ts.map +1 -0
- package/dist/routes/git.js +52 -0
- package/dist/routes/git.js.map +1 -0
- package/dist/routes/modifications.d.ts +3 -0
- package/dist/routes/modifications.d.ts.map +1 -0
- package/dist/routes/modifications.js +241 -0
- package/dist/routes/modifications.js.map +1 -0
- package/dist/routes/selection.d.ts +2 -0
- package/dist/routes/selection.d.ts.map +1 -0
- package/dist/routes/selection.js +38 -0
- package/dist/routes/selection.js.map +1 -0
- package/dist/scripts/selection-script.js +724 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/dist/services/ComponentSearchService.d.ts +12 -0
- package/dist/services/ComponentSearchService.d.ts.map +1 -0
- package/dist/services/ComponentSearchService.js +61 -0
- package/dist/services/ComponentSearchService.js.map +1 -0
- package/dist/services/FileModificationService.d.ts +29 -0
- package/dist/services/FileModificationService.d.ts.map +1 -0
- package/dist/services/FileModificationService.js +82 -0
- package/dist/services/FileModificationService.js.map +1 -0
- package/dist/services/LLMService.d.ts +60 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +201 -0
- package/dist/services/LLMService.js.map +1 -0
- package/dist/services/LocalGitService.d.ts +33 -0
- package/dist/services/LocalGitService.d.ts.map +1 -0
- package/dist/services/LocalGitService.js +252 -0
- package/dist/services/LocalGitService.js.map +1 -0
- package/dist/services/ProjectDetectionService.d.ts +26 -0
- package/dist/services/ProjectDetectionService.d.ts.map +1 -0
- package/dist/services/ProjectDetectionService.js +147 -0
- package/dist/services/ProjectDetectionService.js.map +1 -0
- package/dist/services/ScriptInjectionService.d.ts +8 -0
- package/dist/services/ScriptInjectionService.d.ts.map +1 -0
- package/dist/services/ScriptInjectionService.js +178 -0
- package/dist/services/ScriptInjectionService.js.map +1 -0
- package/dist/services/SessionService.d.ts +26 -0
- package/dist/services/SessionService.d.ts.map +1 -0
- package/dist/services/SessionService.js +141 -0
- package/dist/services/SessionService.js.map +1 -0
- package/dist/types/change-types.d.ts +93 -0
- package/dist/types/change-types.d.ts.map +1 -0
- package/dist/types/change-types.js +4 -0
- package/dist/types/change-types.js.map +1 -0
- package/dist/types/proxy-types.d.ts +34 -0
- package/dist/types/proxy-types.d.ts.map +1 -0
- package/dist/types/proxy-types.js +3 -0
- package/dist/types/proxy-types.js.map +1 -0
- package/dist/types/types.d.ts +15 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/types.js +3 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/logger.d.ts +20 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +51 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +86 -0
- package/src/ui/dist/assets/main-DuNgkeFM.js +105 -0
- package/src/ui/dist/assets/main-DzZ9GWvo.css +1 -0
- 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
|
+
})();
|