visual-ai-staging 0.0.1
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/.github/workflows/publish.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/app.js +2612 -0
- package/cli.js +193 -0
- package/index.html +430 -0
- package/package.json +10 -0
- package/styles.css +1642 -0
package/app.js
ADDED
|
@@ -0,0 +1,2612 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
GLOBAL STATE VARIABLES (Defined in PROJECT.md)
|
|
3
|
+
========================================================================== */
|
|
4
|
+
const state = {
|
|
5
|
+
activeElement: null,
|
|
6
|
+
focusRoot: null, // Root elements focused inside Mock Page
|
|
7
|
+
floatingWindow: null, // Detached pop-out window object reference
|
|
8
|
+
inspectionMode: false,
|
|
9
|
+
drawingMode: false,
|
|
10
|
+
recordingMode: false,
|
|
11
|
+
stagedChanges: new Map() // Maps selector -> { element, originalStyles, currentStyles }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isLocalServer = typeof window !== 'undefined' && window.location && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/* ==========================================================================
|
|
18
|
+
DESIGN TOKENS MAPPING DICTIONARIES
|
|
19
|
+
========================================================================== */
|
|
20
|
+
const spacingTokens = {
|
|
21
|
+
4: 'var(--spacing-xs)',
|
|
22
|
+
8: 'var(--spacing-sm)',
|
|
23
|
+
16: 'var(--spacing-md)',
|
|
24
|
+
24: 'var(--spacing-lg)',
|
|
25
|
+
32: 'var(--spacing-xl)'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const borderRadiusTokens = {
|
|
29
|
+
4: 'var(--border-radius-sm)',
|
|
30
|
+
8: 'var(--border-radius-md)',
|
|
31
|
+
12: 'var(--border-radius-lg)'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/* ==========================================================================
|
|
35
|
+
UTILITY FUNCTIONS
|
|
36
|
+
========================================================================== */
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates a unique CSS selector for a DOM element within #mock-page context
|
|
40
|
+
*/
|
|
41
|
+
function getUniqueSelector(el) {
|
|
42
|
+
if (!(el instanceof Element)) return '';
|
|
43
|
+
const path = [];
|
|
44
|
+
let current = el;
|
|
45
|
+
|
|
46
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
47
|
+
let selector = current.nodeName.toLowerCase();
|
|
48
|
+
|
|
49
|
+
if (current.id) {
|
|
50
|
+
selector += '#' + current.id;
|
|
51
|
+
path.unshift(selector);
|
|
52
|
+
break;
|
|
53
|
+
} else {
|
|
54
|
+
let sibling = current;
|
|
55
|
+
let nth = 1;
|
|
56
|
+
while (sibling = sibling.previousElementSibling) {
|
|
57
|
+
if (sibling.nodeName.toLowerCase() === current.nodeName.toLowerCase()) {
|
|
58
|
+
nth++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (nth > 1) {
|
|
62
|
+
selector += `:nth-of-type(${nth})`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
path.unshift(selector);
|
|
66
|
+
current = current.parentElement;
|
|
67
|
+
if (current && current.id === 'mock-page') {
|
|
68
|
+
path.unshift('#mock-page');
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return path.join(' > ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classifies an element into the standardized UI Catalog types for rich semantics
|
|
77
|
+
*/
|
|
78
|
+
function classifyUIElement(el) {
|
|
79
|
+
if (!(el instanceof Element)) return { category: 'Other', label: 'Generic Element', icon: '⚙️' };
|
|
80
|
+
|
|
81
|
+
const tagName = el.tagName.toLowerCase();
|
|
82
|
+
const classListStr = Array.from(el.classList).join(' ').toLowerCase();
|
|
83
|
+
|
|
84
|
+
// 1. Layout & Navbar
|
|
85
|
+
if (tagName === 'nav' || classListStr.includes('nav') || classListStr.includes('menu')) {
|
|
86
|
+
return { category: 'Layout', label: 'Navbar / Navigation Bar', icon: '🧭' };
|
|
87
|
+
}
|
|
88
|
+
if (tagName === 'header' || classListStr.includes('hero') || classListStr.includes('banner')) {
|
|
89
|
+
return { category: 'Layout', label: 'Hero / Header Section', icon: '🚀' };
|
|
90
|
+
}
|
|
91
|
+
if (tagName === 'footer' || classListStr.includes('footer')) {
|
|
92
|
+
return { category: 'Layout', label: 'Footer Section', icon: '🏁' };
|
|
93
|
+
}
|
|
94
|
+
if (classListStr.includes('grid') || classListStr.includes('flex')) {
|
|
95
|
+
return { category: 'Layout', label: 'Grid / Flex Container', icon: '🎛️' };
|
|
96
|
+
}
|
|
97
|
+
if (classListStr.includes('card')) {
|
|
98
|
+
return { category: 'Layout', label: 'Container / Card Component', icon: '📦' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Interactive & Forms
|
|
102
|
+
if (tagName === 'button' || classListStr.includes('btn') || classListStr.includes('button')) {
|
|
103
|
+
return { category: 'Interactive', label: 'Interactive Button', icon: '🔘' };
|
|
104
|
+
}
|
|
105
|
+
if (tagName === 'form' || classListStr.includes('form')) {
|
|
106
|
+
return { category: 'Interactive', label: 'Form Container', icon: '📝' };
|
|
107
|
+
}
|
|
108
|
+
if (tagName === 'input' && el.type === 'search' || classListStr.includes('search')) {
|
|
109
|
+
return { category: 'Interactive', label: 'Search Bar', icon: '🔍' };
|
|
110
|
+
}
|
|
111
|
+
if (tagName === 'input' || tagName === 'textarea') {
|
|
112
|
+
return { category: 'Interactive', label: 'Form Input / Area', icon: '✏️' };
|
|
113
|
+
}
|
|
114
|
+
if (tagName === 'select' || classListStr.includes('dropdown')) {
|
|
115
|
+
return { category: 'Interactive', label: 'Dropdown / Select', icon: '⌥' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3. Media & Content
|
|
119
|
+
if (tagName === 'img' || classListStr.includes('image') || classListStr.includes('img') || classListStr.includes('placeholder')) {
|
|
120
|
+
return { category: 'Media', label: 'Image Placeholder', icon: '🖼️' };
|
|
121
|
+
}
|
|
122
|
+
if (classListStr.includes('carousel') || classListStr.includes('slider')) {
|
|
123
|
+
return { category: 'Media', label: 'Carousel Slider', icon: '🎡' };
|
|
124
|
+
}
|
|
125
|
+
if (tagName === 'video' || classListStr.includes('video') || classListStr.includes('player')) {
|
|
126
|
+
return { category: 'Media', label: 'Video Player', icon: '🎥' };
|
|
127
|
+
}
|
|
128
|
+
if (classListStr.includes('avatar') || classListStr.includes('profile')) {
|
|
129
|
+
return { category: 'Media', label: 'User Avatar / Badge', icon: '👤' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 4. WebApp Advanced
|
|
133
|
+
if (classListStr.includes('modal') || classListStr.includes('dialog')) {
|
|
134
|
+
return { category: 'WebApp', label: 'Modal / Dialog Window', icon: '💬' };
|
|
135
|
+
}
|
|
136
|
+
if (classListStr.includes('tab')) {
|
|
137
|
+
return { category: 'WebApp', label: 'Tabs Layout', icon: '📑' };
|
|
138
|
+
}
|
|
139
|
+
if (classListStr.includes('accordion') || classListStr.includes('faq')) {
|
|
140
|
+
return { category: 'WebApp', label: 'Accordion / FAQ List', icon: '📂' };
|
|
141
|
+
}
|
|
142
|
+
if (classListStr.includes('chart') || classListStr.includes('graph') || classListStr.includes('analytics')) {
|
|
143
|
+
return { category: 'WebApp', label: 'Analytics Chart Card', icon: '📊' };
|
|
144
|
+
}
|
|
145
|
+
if (tagName === 'table' || classListStr.includes('table') || classListStr.includes('grid-data')) {
|
|
146
|
+
return { category: 'WebApp', label: 'Data Table Grid', icon: '📅' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 5. Basic Text Elements
|
|
150
|
+
const textTypes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
151
|
+
if (textTypes.includes(tagName)) {
|
|
152
|
+
return { category: 'Text', label: `Heading ${tagName.toUpperCase()}`, icon: '🏷️' };
|
|
153
|
+
}
|
|
154
|
+
if (tagName === 'p') {
|
|
155
|
+
return { category: 'Text', label: 'Paragraph Block', icon: '📇' };
|
|
156
|
+
}
|
|
157
|
+
if (tagName === 'span' || tagName === 'em' || tagName === 'strong') {
|
|
158
|
+
return { category: 'Text', label: 'Inline Text Element', icon: '🔤' };
|
|
159
|
+
}
|
|
160
|
+
if (tagName === 'a') {
|
|
161
|
+
return { category: 'Text', label: 'Interactive Link', icon: '🔗' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Default Generic
|
|
165
|
+
const isContainer = ['div', 'section', 'article', 'aside', 'main', 'body'].includes(tagName);
|
|
166
|
+
return {
|
|
167
|
+
category: isContainer ? 'Container' : 'Other',
|
|
168
|
+
label: isContainer ? 'Generic Layout Container' : `Element <${tagName}>`,
|
|
169
|
+
icon: isContainer ? '🗂️' : '⚙️'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Maps numeric style values to nearest Design Token variables if applicable
|
|
175
|
+
*/
|
|
176
|
+
function mapToToken(property, valueNum) {
|
|
177
|
+
const rounded = Math.round(valueNum);
|
|
178
|
+
if (property === 'padding' || property === 'margin') {
|
|
179
|
+
for (const size of [4, 8, 16, 24, 32]) {
|
|
180
|
+
if (Math.abs(rounded - size) <= 1.5) {
|
|
181
|
+
return spacingTokens[size];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (property === 'borderRadius') {
|
|
186
|
+
for (const size of [4, 8, 12]) {
|
|
187
|
+
if (Math.abs(rounded - size) <= 1.5) {
|
|
188
|
+
return borderRadiusTokens[size];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parses RGB or RGBA string into separate numeric color channels
|
|
197
|
+
*/
|
|
198
|
+
function parseRgb(rgbStr) {
|
|
199
|
+
if (!rgbStr || rgbStr === 'transparent') {
|
|
200
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
201
|
+
}
|
|
202
|
+
const matches = rgbStr.match(/\d+(\.\d+)?/g);
|
|
203
|
+
if (!matches) {
|
|
204
|
+
return { r: 0, g: 0, b: 0, a: 1 };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
r: parseInt(matches[0], 10),
|
|
208
|
+
g: parseInt(matches[1], 10),
|
|
209
|
+
b: parseInt(matches[2], 10),
|
|
210
|
+
a: matches[3] !== undefined ? parseFloat(matches[3]) : 1
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Converts RGB components to HSL values
|
|
216
|
+
*/
|
|
217
|
+
function rgbToHsl(r, g, b) {
|
|
218
|
+
r /= 255;
|
|
219
|
+
g /= 255;
|
|
220
|
+
b /= 255;
|
|
221
|
+
const max = Math.max(r, g, b);
|
|
222
|
+
const min = Math.min(r, g, b);
|
|
223
|
+
let h = 0;
|
|
224
|
+
let s = 0;
|
|
225
|
+
let l = (max + min) / 2;
|
|
226
|
+
|
|
227
|
+
if (max !== min) {
|
|
228
|
+
const d = max - min;
|
|
229
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
230
|
+
switch (max) {
|
|
231
|
+
case r:
|
|
232
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
233
|
+
break;
|
|
234
|
+
case g:
|
|
235
|
+
h = (b - r) / d + 2;
|
|
236
|
+
break;
|
|
237
|
+
case b:
|
|
238
|
+
h = (r - g) / d + 4;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
h /= 6;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
h: Math.round(h * 360),
|
|
246
|
+
s: Math.round(s * 100),
|
|
247
|
+
l: Math.round(l * 100)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ==========================================================================
|
|
252
|
+
INSPECTOR ENGINE
|
|
253
|
+
========================================================================== */
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Initializes mouse event listeners inside mock page for style inspection
|
|
257
|
+
*/
|
|
258
|
+
function initInspector() {
|
|
259
|
+
const mockPage = document.getElementById('mock-page');
|
|
260
|
+
if (!mockPage) return;
|
|
261
|
+
|
|
262
|
+
mockPage.addEventListener('mouseover', (e) => {
|
|
263
|
+
if (!state.inspectionMode) return;
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
|
|
266
|
+
// Clean prior hover highlights
|
|
267
|
+
const priorHovered = mockPage.querySelectorAll('.inspect-hovered');
|
|
268
|
+
priorHovered.forEach(el => el.classList.remove('inspect-hovered'));
|
|
269
|
+
|
|
270
|
+
// Do not highlight the root mock page itself
|
|
271
|
+
if (e.target !== mockPage) {
|
|
272
|
+
e.target.classList.add('inspect-hovered');
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
mockPage.addEventListener('mouseout', (e) => {
|
|
277
|
+
if (!state.inspectionMode) return;
|
|
278
|
+
e.stopPropagation();
|
|
279
|
+
if (e.target !== mockPage) {
|
|
280
|
+
e.target.classList.remove('inspect-hovered');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
mockPage.addEventListener('click', (e) => {
|
|
285
|
+
if (!state.inspectionMode) return;
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
|
|
289
|
+
if (e.target !== mockPage) {
|
|
290
|
+
e.target.classList.remove('inspect-hovered');
|
|
291
|
+
selectElement(e.target);
|
|
292
|
+
// Keep inspection mode active so developer can continuously inspect
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Enables or disables the hover listener state in DOM mock container
|
|
299
|
+
*/
|
|
300
|
+
function toggleInspectionMode(forceState) {
|
|
301
|
+
const targetState = forceState !== undefined ? forceState : !state.inspectionMode;
|
|
302
|
+
state.inspectionMode = targetState;
|
|
303
|
+
|
|
304
|
+
const btnInspect = document.getElementById('btn-inspect');
|
|
305
|
+
const fab = document.getElementById('fab-trigger');
|
|
306
|
+
|
|
307
|
+
if (targetState) {
|
|
308
|
+
// If drawing mode is active, turn it off
|
|
309
|
+
if (state.drawingMode) {
|
|
310
|
+
toggleDrawingMode(false);
|
|
311
|
+
}
|
|
312
|
+
if (btnInspect) btnInspect.classList.add('active');
|
|
313
|
+
if (fab) fab.classList.add('inspect-active');
|
|
314
|
+
} else {
|
|
315
|
+
if (btnInspect) btnInspect.classList.remove('active');
|
|
316
|
+
if (fab) fab.classList.remove('inspect-active');
|
|
317
|
+
|
|
318
|
+
// Cleanup any lingering hover outlines
|
|
319
|
+
const mockPage = document.getElementById('mock-page');
|
|
320
|
+
if (mockPage) {
|
|
321
|
+
const priorHovered = mockPage.querySelectorAll('.inspect-hovered');
|
|
322
|
+
priorHovered.forEach(el => el.classList.remove('inspect-hovered'));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Filters visible Visual Sandbox slider controls based on the selected element's DOM type
|
|
329
|
+
*/
|
|
330
|
+
function filterSlidersByElementType(element) {
|
|
331
|
+
if (!element) return;
|
|
332
|
+
const tagName = element.tagName.toLowerCase();
|
|
333
|
+
|
|
334
|
+
const textTypes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'a', 'label', 'li', 'em', 'strong'];
|
|
335
|
+
const containerTypes = ['div', 'section', 'header', 'footer', 'nav', 'article', 'aside', 'main', 'form', 'ul', 'ol', 'body'];
|
|
336
|
+
const interactiveTypes = ['button', 'input', 'select', 'textarea'];
|
|
337
|
+
const imageTypes = ['img', 'svg'];
|
|
338
|
+
|
|
339
|
+
const getWrapper = (id, selector) => {
|
|
340
|
+
const el = document.getElementById(id);
|
|
341
|
+
if (!el) return null;
|
|
342
|
+
if (typeof el.closest === 'function') {
|
|
343
|
+
return el.closest(selector);
|
|
344
|
+
}
|
|
345
|
+
// Fallback safe representation for custom Node testing mocks
|
|
346
|
+
return el.parentElement || el;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const sliderWrappers = {
|
|
350
|
+
padding: getWrapper('slider-padding', '.control-group'),
|
|
351
|
+
margin: getWrapper('slider-margin', '.control-group'),
|
|
352
|
+
width: getWrapper('slider-width', '.control-group'),
|
|
353
|
+
height: getWrapper('slider-height', '.control-group'),
|
|
354
|
+
borderRadius: getWrapper('slider-borderRadius', '.control-group'),
|
|
355
|
+
fontSize: getWrapper('slider-fontSize', '.control-group'),
|
|
356
|
+
textContent: getWrapper('input-textContent', '.control-group'),
|
|
357
|
+
background: getWrapper('bg-h', '.color-control-box'),
|
|
358
|
+
text: getWrapper('text-h', '.color-control-box')
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
let activeProps = [];
|
|
362
|
+
if (textTypes.includes(tagName)) {
|
|
363
|
+
activeProps = ['margin', 'fontSize', 'text', 'textContent'];
|
|
364
|
+
} else if (containerTypes.includes(tagName)) {
|
|
365
|
+
activeProps = ['padding', 'margin', 'width', 'height', 'borderRadius', 'background', 'textContent'];
|
|
366
|
+
} else if (interactiveTypes.includes(tagName)) {
|
|
367
|
+
activeProps = ['padding', 'borderRadius', 'fontSize', 'background', 'text', 'width', 'height', 'textContent'];
|
|
368
|
+
} else if (imageTypes.includes(tagName)) {
|
|
369
|
+
activeProps = ['margin', 'width', 'height', 'borderRadius'];
|
|
370
|
+
} else {
|
|
371
|
+
activeProps = ['padding', 'margin', 'width', 'height', 'borderRadius', 'fontSize', 'background', 'text', 'textContent'];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const [prop, el] of Object.entries(sliderWrappers)) {
|
|
375
|
+
if (el) {
|
|
376
|
+
if (activeProps.includes(prop)) {
|
|
377
|
+
el.classList.remove('hidden');
|
|
378
|
+
} else {
|
|
379
|
+
el.classList.add('hidden');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Renders a recursive expandable hierarchy tree of children elements within state.focusRoot
|
|
387
|
+
*/
|
|
388
|
+
function renderHierarchyTree(rootElement) {
|
|
389
|
+
const container = document.getElementById('hierarchy-tree-container');
|
|
390
|
+
if (!container) return;
|
|
391
|
+
|
|
392
|
+
container.innerHTML = '';
|
|
393
|
+
|
|
394
|
+
if (!rootElement) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Recursively traverse and build nodes programmatically for strict XSS mitigation
|
|
399
|
+
function buildTreeNode(el, depth = 0) {
|
|
400
|
+
if (!(el instanceof Element)) return;
|
|
401
|
+
if (el.id === 'canvas-overlay' || el.classList.contains('voice-badge')) return;
|
|
402
|
+
|
|
403
|
+
const nodeRow = document.createElement('div');
|
|
404
|
+
nodeRow.className = 'tree-node';
|
|
405
|
+
nodeRow.style.paddingLeft = `${depth * 12 + 8}px`;
|
|
406
|
+
|
|
407
|
+
if (el === state.focusRoot) {
|
|
408
|
+
nodeRow.classList.add('focus-root');
|
|
409
|
+
}
|
|
410
|
+
if (el === state.activeElement) {
|
|
411
|
+
nodeRow.classList.add('active-leaf');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Expand/collapse decorator icon representation
|
|
415
|
+
const hasChildren = Array.from(el.children).filter(c => c.id !== 'canvas-overlay' && !c.classList.contains('voice-badge')).length > 0;
|
|
416
|
+
const arrow = document.createElement('span');
|
|
417
|
+
arrow.className = 'tree-arrow';
|
|
418
|
+
arrow.textContent = hasChildren ? '▼' : '•';
|
|
419
|
+
nodeRow.appendChild(arrow);
|
|
420
|
+
|
|
421
|
+
// Tag and Classes
|
|
422
|
+
const tagSpan = document.createElement('span');
|
|
423
|
+
tagSpan.className = 'tree-tag';
|
|
424
|
+
tagSpan.textContent = el.tagName.toLowerCase();
|
|
425
|
+
nodeRow.appendChild(tagSpan);
|
|
426
|
+
|
|
427
|
+
const classList = Array.from(el.classList)
|
|
428
|
+
.filter(c => c !== 'inspect-selected' && c !== 'inspect-hovered' && c !== 'inspect-focus-root')
|
|
429
|
+
.map(c => '.' + c)
|
|
430
|
+
.join('');
|
|
431
|
+
|
|
432
|
+
if (classList) {
|
|
433
|
+
const classSpan = document.createElement('span');
|
|
434
|
+
classSpan.className = 'tree-class';
|
|
435
|
+
classSpan.textContent = classList.slice(0, 18) + (classList.length > 18 ? '...' : '');
|
|
436
|
+
classSpan.title = classList;
|
|
437
|
+
nodeRow.appendChild(classSpan);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Badge indicator if child has modifications staged or audios
|
|
441
|
+
const elSelector = getUniqueSelector(el);
|
|
442
|
+
if (state.stagedChanges.has(elSelector)) {
|
|
443
|
+
const entry = state.stagedChanges.get(elSelector);
|
|
444
|
+
const hasStyleChanges = Object.keys(entry.currentStyles).some(p => entry.currentStyles[p] !== entry.originalStyles[p]);
|
|
445
|
+
if (entry.voiceNote || hasStyleChanges) {
|
|
446
|
+
const badge = document.createElement('span');
|
|
447
|
+
badge.className = 'tree-badge';
|
|
448
|
+
badge.textContent = entry.voiceNote ? '🎤 +' : '+';
|
|
449
|
+
nodeRow.appendChild(badge);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Click selection handles dynamic overlays
|
|
454
|
+
nodeRow.addEventListener('click', (e) => {
|
|
455
|
+
e.stopPropagation();
|
|
456
|
+
selectElement(el, true);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
container.appendChild(nodeRow);
|
|
460
|
+
|
|
461
|
+
// Process nested layers
|
|
462
|
+
Array.from(el.children).forEach(child => {
|
|
463
|
+
buildTreeNode(child, depth + 1);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
buildTreeNode(rootElement, 0);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Handles docking/undocking pop-out window actions
|
|
472
|
+
*/
|
|
473
|
+
function toggleUndockPanel() {
|
|
474
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
475
|
+
dockPanel();
|
|
476
|
+
} else {
|
|
477
|
+
undockPanel();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Detaches the staging panel into a secondary window for multi-monitor setups
|
|
483
|
+
*/
|
|
484
|
+
function undockPanel() {
|
|
485
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
486
|
+
state.floatingWindow.focus();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const w = 465;
|
|
491
|
+
const h = 850;
|
|
492
|
+
const left = window.screenX + (window.innerWidth - w) / 2;
|
|
493
|
+
const top = window.screenY + (window.innerHeight - h) / 2;
|
|
494
|
+
|
|
495
|
+
state.floatingWindow = window.open('', 'vais_staging_panel', `width=${w},height=${h},left=${left},top=${top},resizable=yes,scrollbars=yes`);
|
|
496
|
+
|
|
497
|
+
if (!state.floatingWindow) {
|
|
498
|
+
alert("Popup blocked! Please allow popups to undock the staging panel.");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const doc = state.floatingWindow.document;
|
|
503
|
+
doc.open();
|
|
504
|
+
doc.write(`
|
|
505
|
+
<!DOCTYPE html>
|
|
506
|
+
<html lang="en">
|
|
507
|
+
<head>
|
|
508
|
+
<meta charset="UTF-8">
|
|
509
|
+
<title>Visual AI Staging - Floating Panel</title>
|
|
510
|
+
<link rel="stylesheet" href="styles.css">
|
|
511
|
+
<style>
|
|
512
|
+
body {
|
|
513
|
+
padding: var(--spacing-sm);
|
|
514
|
+
background: hsl(220, 15%, 10%);
|
|
515
|
+
color: hsl(220, 10%, 95%);
|
|
516
|
+
overflow-y: auto;
|
|
517
|
+
height: auto;
|
|
518
|
+
}
|
|
519
|
+
.staging-panel {
|
|
520
|
+
width: 100% !important;
|
|
521
|
+
height: 100% !important;
|
|
522
|
+
position: static !important;
|
|
523
|
+
box-shadow: none !important;
|
|
524
|
+
border: none !important;
|
|
525
|
+
backdrop-filter: none !important;
|
|
526
|
+
display: block !important;
|
|
527
|
+
}
|
|
528
|
+
</style>
|
|
529
|
+
</head>
|
|
530
|
+
<body>
|
|
531
|
+
<div id="floating-panel-target"></div>
|
|
532
|
+
</body>
|
|
533
|
+
</html>
|
|
534
|
+
`);
|
|
535
|
+
doc.close();
|
|
536
|
+
|
|
537
|
+
// Hide the panel in the original document and expand viewport
|
|
538
|
+
const mainPanel = document.getElementById('main-staging-panel');
|
|
539
|
+
const mockContainer = document.querySelector('.mock-page-container');
|
|
540
|
+
const appLayout = document.querySelector('.app-layout');
|
|
541
|
+
if (mainPanel) mainPanel.classList.add('docked-hidden');
|
|
542
|
+
if (mockContainer) mockContainer.classList.add('full-width');
|
|
543
|
+
if (appLayout) appLayout.classList.add('undocked');
|
|
544
|
+
|
|
545
|
+
// Clone HTML structure to floating window target
|
|
546
|
+
const panelContent = document.getElementById('main-staging-panel').innerHTML;
|
|
547
|
+
const target = doc.getElementById('floating-panel-target');
|
|
548
|
+
target.innerHTML = panelContent;
|
|
549
|
+
|
|
550
|
+
// Toggle undock action label inside floating panel
|
|
551
|
+
const undockBtn = doc.getElementById('btn-undock-panel');
|
|
552
|
+
if (undockBtn) {
|
|
553
|
+
undockBtn.innerHTML = '<span>📥</span> Dock';
|
|
554
|
+
undockBtn.title = 'Dock panel back';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Setup dynamic interactive event bindings inside popup context
|
|
558
|
+
setupFloatingWindowListeners(doc);
|
|
559
|
+
|
|
560
|
+
// Synchronize state attributes
|
|
561
|
+
syncFloatingWindowDOM();
|
|
562
|
+
|
|
563
|
+
// Floating window close synchronization
|
|
564
|
+
state.floatingWindow.addEventListener('beforeunload', () => {
|
|
565
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
566
|
+
setTimeout(dockPanel, 50);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Update parent button state indicator
|
|
571
|
+
const parentUndockBtn = document.getElementById('btn-undock-panel');
|
|
572
|
+
if (parentUndockBtn) {
|
|
573
|
+
parentUndockBtn.innerHTML = '<span>📥</span> Docked';
|
|
574
|
+
parentUndockBtn.disabled = true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Re-docks the staging panel cleanly to the main develop viewport
|
|
580
|
+
*/
|
|
581
|
+
function dockPanel() {
|
|
582
|
+
if (state.floatingWindow) {
|
|
583
|
+
if (!state.floatingWindow.closed) {
|
|
584
|
+
state.floatingWindow.close();
|
|
585
|
+
}
|
|
586
|
+
state.floatingWindow = null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Restore parent layouts
|
|
590
|
+
const mainPanel = document.getElementById('main-staging-panel');
|
|
591
|
+
const mockContainer = document.querySelector('.mock-page-container');
|
|
592
|
+
const appLayout = document.querySelector('.app-layout');
|
|
593
|
+
if (mainPanel) mainPanel.classList.remove('docked-hidden');
|
|
594
|
+
if (mockContainer) mockContainer.classList.remove('full-width');
|
|
595
|
+
if (appLayout) appLayout.classList.remove('undocked');
|
|
596
|
+
|
|
597
|
+
const parentUndockBtn = document.getElementById('btn-undock-panel');
|
|
598
|
+
if (parentUndockBtn) {
|
|
599
|
+
parentUndockBtn.innerHTML = '<span>↗️</span> Undock';
|
|
600
|
+
parentUndockBtn.disabled = false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Re-sync visual sandbox states
|
|
604
|
+
if (state.activeElement) {
|
|
605
|
+
selectElement(state.activeElement, state.activeElement !== state.focusRoot);
|
|
606
|
+
} else {
|
|
607
|
+
const emptyContainer = document.getElementById('meta-container');
|
|
608
|
+
if (emptyContainer) emptyContainer.classList.remove('hidden');
|
|
609
|
+
const details = document.getElementById('meta-details');
|
|
610
|
+
if (details) details.classList.add('hidden');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Sets up slider and voice note event actions inside popup window
|
|
616
|
+
*/
|
|
617
|
+
function setupFloatingWindowListeners(doc) {
|
|
618
|
+
const undockBtn = doc.getElementById('btn-undock-panel');
|
|
619
|
+
if (undockBtn) {
|
|
620
|
+
undockBtn.addEventListener('click', () => {
|
|
621
|
+
dockPanel();
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Visual Sandbox Sliders
|
|
626
|
+
const sliders = [
|
|
627
|
+
{ id: 'slider-padding', property: 'padding' },
|
|
628
|
+
{ id: 'slider-margin', property: 'margin' },
|
|
629
|
+
{ id: 'slider-width', property: 'width' },
|
|
630
|
+
{ id: 'slider-height', property: 'height' },
|
|
631
|
+
{ id: 'slider-borderRadius', property: 'borderRadius' },
|
|
632
|
+
{ id: 'slider-fontSize', property: 'fontSize' }
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
sliders.forEach(s => {
|
|
636
|
+
const el = doc.getElementById(s.id);
|
|
637
|
+
if (el) {
|
|
638
|
+
el.addEventListener('input', (e) => {
|
|
639
|
+
handleSliderChange(s.property, e.target.value);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// Text Content Input
|
|
645
|
+
const textInput = doc.getElementById('input-textContent');
|
|
646
|
+
if (textInput) {
|
|
647
|
+
textInput.addEventListener('input', (e) => {
|
|
648
|
+
updateElementTextContent(e.target.value);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Colors Background Sliders
|
|
653
|
+
['bg-h', 'bg-s', 'bg-l'].forEach(id => {
|
|
654
|
+
const el = doc.getElementById(id);
|
|
655
|
+
if (el) {
|
|
656
|
+
el.addEventListener('input', () => {
|
|
657
|
+
handleColorChange('background', doc);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// Colors Text Sliders
|
|
663
|
+
['text-h', 'text-s', 'text-l'].forEach(id => {
|
|
664
|
+
const el = doc.getElementById(id);
|
|
665
|
+
if (el) {
|
|
666
|
+
el.addEventListener('input', () => {
|
|
667
|
+
handleColorChange('text', doc);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Voice Note actions
|
|
673
|
+
const btnVoiceRecord = doc.getElementById('btn-voice-record');
|
|
674
|
+
if (btnVoiceRecord) {
|
|
675
|
+
btnVoiceRecord.addEventListener('click', () => {
|
|
676
|
+
if (state.recordingMode) {
|
|
677
|
+
stopAudioRecording();
|
|
678
|
+
} else {
|
|
679
|
+
startAudioRecording();
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const btnVoiceDelete = doc.getElementById('btn-voice-delete');
|
|
685
|
+
if (btnVoiceDelete) {
|
|
686
|
+
btnVoiceDelete.addEventListener('click', () => {
|
|
687
|
+
deleteVoiceNote();
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Synchronizes variable adjustments from parent context directly into popup document DOM
|
|
694
|
+
*/
|
|
695
|
+
function syncFloatingWindowDOM() {
|
|
696
|
+
if (!state.floatingWindow || state.floatingWindow.closed) return;
|
|
697
|
+
|
|
698
|
+
const fDoc = state.floatingWindow.document;
|
|
699
|
+
|
|
700
|
+
// Sync element metadata details
|
|
701
|
+
const metaContainer = fDoc.getElementById('meta-container');
|
|
702
|
+
const details = fDoc.getElementById('meta-details');
|
|
703
|
+
if (metaContainer && details) {
|
|
704
|
+
if (!state.activeElement) {
|
|
705
|
+
metaContainer.classList.remove('hidden');
|
|
706
|
+
details.classList.add('hidden');
|
|
707
|
+
} else {
|
|
708
|
+
metaContainer.classList.add('hidden');
|
|
709
|
+
details.classList.remove('hidden');
|
|
710
|
+
|
|
711
|
+
const metaTag = fDoc.getElementById('meta-tag');
|
|
712
|
+
const metaClasses = fDoc.getElementById('meta-classes');
|
|
713
|
+
const metaSelector = fDoc.getElementById('meta-selector');
|
|
714
|
+
const metaUiType = fDoc.getElementById('meta-ui-type');
|
|
715
|
+
|
|
716
|
+
if (metaTag) metaTag.textContent = state.activeElement.tagName.toLowerCase();
|
|
717
|
+
if (metaClasses) {
|
|
718
|
+
const classList = Array.from(state.activeElement.classList)
|
|
719
|
+
.filter(c => c !== 'inspect-selected' && c !== 'inspect-hovered' && c !== 'inspect-focus-root')
|
|
720
|
+
.map(c => '.' + c)
|
|
721
|
+
.join(' ');
|
|
722
|
+
metaClasses.textContent = classList || '(none)';
|
|
723
|
+
}
|
|
724
|
+
if (metaSelector) metaSelector.textContent = getUniqueSelector(state.activeElement);
|
|
725
|
+
|
|
726
|
+
if (metaUiType) {
|
|
727
|
+
const classification = classifyUIElement(state.activeElement);
|
|
728
|
+
metaUiType.textContent = `${classification.icon} ${classification.label}`;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Sync numeric sliders and spacing tokens
|
|
734
|
+
const sliders = ['padding', 'margin', 'width', 'height', 'borderRadius', 'fontSize'];
|
|
735
|
+
sliders.forEach(prop => {
|
|
736
|
+
const parentSlider = document.getElementById(`slider-${prop}`);
|
|
737
|
+
const fSlider = fDoc.getElementById(`slider-${prop}`);
|
|
738
|
+
const fVal = fDoc.getElementById(`val-${prop}`);
|
|
739
|
+
const fToken = fDoc.getElementById(`token-${prop}`);
|
|
740
|
+
|
|
741
|
+
if (parentSlider && fSlider) {
|
|
742
|
+
fSlider.min = parentSlider.min;
|
|
743
|
+
fSlider.max = parentSlider.max;
|
|
744
|
+
fSlider.value = parentSlider.value;
|
|
745
|
+
}
|
|
746
|
+
if (fVal) {
|
|
747
|
+
fVal.textContent = document.getElementById(`val-${prop}`).textContent;
|
|
748
|
+
}
|
|
749
|
+
if (fToken) {
|
|
750
|
+
const parentToken = document.getElementById(`token-${prop}`);
|
|
751
|
+
if (parentToken.classList.contains('hidden')) {
|
|
752
|
+
fToken.classList.add('hidden');
|
|
753
|
+
} else {
|
|
754
|
+
fToken.classList.remove('hidden');
|
|
755
|
+
fToken.textContent = parentToken.textContent;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Sync text content input value
|
|
761
|
+
const parentTextInput = document.getElementById('input-textContent');
|
|
762
|
+
const fTextInput = fDoc.getElementById('input-textContent');
|
|
763
|
+
if (parentTextInput && fTextInput) {
|
|
764
|
+
fTextInput.value = parentTextInput.value;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Sync color sliders and previews
|
|
768
|
+
const syncColors = (type) => {
|
|
769
|
+
['h', 's', 'l'].forEach(chan => {
|
|
770
|
+
const pInput = document.getElementById(`${type}-${chan}`);
|
|
771
|
+
const fInput = fDoc.getElementById(`${type}-${chan}`);
|
|
772
|
+
const fVal = fDoc.getElementById(`${type}-${chan}-val`);
|
|
773
|
+
if (pInput && fInput) {
|
|
774
|
+
fInput.value = pInput.value;
|
|
775
|
+
}
|
|
776
|
+
if (fVal) {
|
|
777
|
+
fVal.textContent = document.getElementById(`${type}-${chan}-val`).textContent;
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
const fPreview = fDoc.getElementById(`${type}-preview`);
|
|
781
|
+
const fHslStr = fDoc.getElementById(`${type}-hsl-string`);
|
|
782
|
+
if (fPreview) fPreview.style.backgroundColor = document.getElementById(`${type}-preview`).style.backgroundColor;
|
|
783
|
+
if (fHslStr) fHslStr.textContent = document.getElementById(`${type}-hsl-string`).textContent;
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
syncColors('bg');
|
|
787
|
+
syncColors('text');
|
|
788
|
+
|
|
789
|
+
// Sync voice panel recorder state
|
|
790
|
+
const btnVoiceRecord = fDoc.getElementById('btn-voice-record');
|
|
791
|
+
const btnVoiceDelete = fDoc.getElementById('btn-voice-delete');
|
|
792
|
+
const voiceStatusContainer = fDoc.getElementById('voice-status-container');
|
|
793
|
+
const voiceAudioPlayerContainer = fDoc.getElementById('voice-audio-player-container');
|
|
794
|
+
const voiceAudioPlayer = fDoc.getElementById('voice-audio-player');
|
|
795
|
+
const recordBtnText = fDoc.getElementById('record-btn-text');
|
|
796
|
+
|
|
797
|
+
if (btnVoiceRecord) {
|
|
798
|
+
if (!state.activeElement) {
|
|
799
|
+
btnVoiceRecord.disabled = true;
|
|
800
|
+
btnVoiceRecord.classList.remove('recording');
|
|
801
|
+
if (recordBtnText) recordBtnText.textContent = 'Record Voice Note';
|
|
802
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.add('hidden');
|
|
803
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.add('hidden');
|
|
804
|
+
if (voiceAudioPlayerContainer) voiceAudioPlayerContainer.classList.add('hidden');
|
|
805
|
+
} else {
|
|
806
|
+
btnVoiceRecord.disabled = false;
|
|
807
|
+
const selector = getUniqueSelector(state.activeElement);
|
|
808
|
+
const entry = state.stagedChanges.get(selector);
|
|
809
|
+
|
|
810
|
+
if (state.recordingMode) {
|
|
811
|
+
btnVoiceRecord.classList.add('recording');
|
|
812
|
+
if (recordBtnText) recordBtnText.textContent = 'Stop Recording';
|
|
813
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.add('hidden');
|
|
814
|
+
if (voiceStatusContainer) {
|
|
815
|
+
voiceStatusContainer.classList.remove('hidden');
|
|
816
|
+
fDoc.getElementById('voice-timer').textContent = document.getElementById('voice-timer').textContent;
|
|
817
|
+
}
|
|
818
|
+
if (voiceAudioPlayerContainer) voiceAudioPlayerContainer.classList.add('hidden');
|
|
819
|
+
} else if (entry && entry.voiceNote) {
|
|
820
|
+
btnVoiceRecord.classList.remove('recording');
|
|
821
|
+
if (recordBtnText) recordBtnText.textContent = 'Re-record Voice Note';
|
|
822
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.remove('hidden');
|
|
823
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.add('hidden');
|
|
824
|
+
if (voiceAudioPlayerContainer) {
|
|
825
|
+
voiceAudioPlayerContainer.classList.remove('hidden');
|
|
826
|
+
if (voiceAudioPlayer && voiceAudioPlayer.src !== entry.voiceNote.url) {
|
|
827
|
+
voiceAudioPlayer.src = entry.voiceNote.url;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
btnVoiceRecord.classList.remove('recording');
|
|
832
|
+
if (recordBtnText) recordBtnText.textContent = 'Record Voice Note';
|
|
833
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.add('hidden');
|
|
834
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.add('hidden');
|
|
835
|
+
if (voiceAudioPlayerContainer) voiceAudioPlayerContainer.classList.add('hidden');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Apply slider category filters in popup document
|
|
841
|
+
if (state.activeElement) {
|
|
842
|
+
const tagName = state.activeElement.tagName.toLowerCase();
|
|
843
|
+
const textTypes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'a', 'label', 'li', 'em', 'strong'];
|
|
844
|
+
const containerTypes = ['div', 'section', 'header', 'footer', 'nav', 'article', 'aside', 'main', 'form', 'ul', 'ol', 'body'];
|
|
845
|
+
const interactiveTypes = ['button', 'input', 'select', 'textarea'];
|
|
846
|
+
const imageTypes = ['img', 'svg'];
|
|
847
|
+
|
|
848
|
+
const fSliderWrappers = {
|
|
849
|
+
padding: fDoc.getElementById('slider-padding').closest('.control-group'),
|
|
850
|
+
margin: fDoc.getElementById('slider-margin').closest('.control-group'),
|
|
851
|
+
width: fDoc.getElementById('slider-width').closest('.control-group'),
|
|
852
|
+
height: fDoc.getElementById('slider-height').closest('.control-group'),
|
|
853
|
+
borderRadius: fDoc.getElementById('slider-borderRadius').closest('.control-group'),
|
|
854
|
+
fontSize: fDoc.getElementById('slider-fontSize').closest('.control-group'),
|
|
855
|
+
textContent: fDoc.getElementById('input-textContent').closest('.control-group'),
|
|
856
|
+
background: fDoc.getElementById('bg-h').closest('.color-control-box'),
|
|
857
|
+
text: fDoc.getElementById('text-h').closest('.color-control-box')
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
let activeProps = [];
|
|
861
|
+
if (textTypes.includes(tagName)) {
|
|
862
|
+
activeProps = ['margin', 'fontSize', 'text', 'textContent'];
|
|
863
|
+
} else if (containerTypes.includes(tagName)) {
|
|
864
|
+
activeProps = ['padding', 'margin', 'width', 'height', 'borderRadius', 'background', 'textContent'];
|
|
865
|
+
} else if (interactiveTypes.includes(tagName)) {
|
|
866
|
+
activeProps = ['padding', 'borderRadius', 'fontSize', 'background', 'text', 'width', 'height', 'textContent'];
|
|
867
|
+
} else if (imageTypes.includes(tagName)) {
|
|
868
|
+
activeProps = ['margin', 'width', 'height', 'borderRadius'];
|
|
869
|
+
} else {
|
|
870
|
+
activeProps = ['padding', 'margin', 'width', 'height', 'borderRadius', 'fontSize', 'background', 'text', 'textContent'];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
for (const [prop, el] of Object.entries(fSliderWrappers)) {
|
|
874
|
+
if (el) {
|
|
875
|
+
if (activeProps.includes(prop)) {
|
|
876
|
+
el.classList.remove('hidden');
|
|
877
|
+
} else {
|
|
878
|
+
el.classList.add('hidden');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Copy changes list markup
|
|
885
|
+
const fStagedList = fDoc.getElementById('staged-changes-list');
|
|
886
|
+
if (fStagedList) {
|
|
887
|
+
fStagedList.innerHTML = document.getElementById('staged-changes-list').innerHTML;
|
|
888
|
+
// Re-bind revert click handlers inside popup document context
|
|
889
|
+
fStagedList.querySelectorAll('.revert-btn').forEach(btn => {
|
|
890
|
+
const item = btn.closest('.change-item');
|
|
891
|
+
if (item) {
|
|
892
|
+
const selectorSpan = item.querySelector('.change-selector');
|
|
893
|
+
if (selectorSpan) {
|
|
894
|
+
const title = selectorSpan.title;
|
|
895
|
+
const label = selectorSpan.textContent;
|
|
896
|
+
btn.addEventListener('click', () => {
|
|
897
|
+
if (label.includes('[Insert:')) {
|
|
898
|
+
const boxIdStr = label.match(/#(\d+)/)[1];
|
|
899
|
+
deleteBoundingBox(parseInt(boxIdStr, 10));
|
|
900
|
+
} else {
|
|
901
|
+
revertAllChangesFor(title);
|
|
902
|
+
}
|
|
903
|
+
syncFloatingWindowDOM();
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Copy DOM hierarchy tree markup and bind select actions
|
|
911
|
+
const fTreeContainer = fDoc.getElementById('hierarchy-tree-container');
|
|
912
|
+
if (fTreeContainer) {
|
|
913
|
+
fTreeContainer.innerHTML = document.getElementById('hierarchy-tree-container').innerHTML;
|
|
914
|
+
fTreeContainer.querySelectorAll('.tree-node').forEach((node, idx) => {
|
|
915
|
+
node.addEventListener('click', (e) => {
|
|
916
|
+
e.stopPropagation();
|
|
917
|
+
const parentNodes = document.querySelectorAll('#hierarchy-tree-container .tree-node');
|
|
918
|
+
if (parentNodes[idx]) {
|
|
919
|
+
parentNodes[idx].click();
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Sets slider visual state and initializes computed styles
|
|
928
|
+
*/
|
|
929
|
+
function setupSlider(property, value, min, max) {
|
|
930
|
+
const slider = document.getElementById(`slider-${property}`);
|
|
931
|
+
const display = document.getElementById(`val-${property}`);
|
|
932
|
+
const tokenBadge = document.getElementById(`token-${property}`);
|
|
933
|
+
|
|
934
|
+
if (slider) {
|
|
935
|
+
slider.min = min;
|
|
936
|
+
slider.max = max;
|
|
937
|
+
slider.value = value;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (display) {
|
|
941
|
+
display.textContent = Math.round(value) + 'px';
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const token = mapToToken(property, Math.round(value));
|
|
945
|
+
if (token && tokenBadge) {
|
|
946
|
+
tokenBadge.textContent = token.replace('var(', '').replace(')', '');
|
|
947
|
+
tokenBadge.classList.remove('hidden');
|
|
948
|
+
} else if (tokenBadge) {
|
|
949
|
+
tokenBadge.classList.add('hidden');
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Extracts and maps color elements into HSL inputs
|
|
955
|
+
*/
|
|
956
|
+
function setupColorSliders(type, colorRgb) {
|
|
957
|
+
const parsed = parseRgb(colorRgb);
|
|
958
|
+
const hsl = rgbToHsl(parsed.r, parsed.g, parsed.b);
|
|
959
|
+
|
|
960
|
+
const idPrefix = type === 'background' ? 'bg' : type;
|
|
961
|
+
|
|
962
|
+
const hInput = document.getElementById(`${idPrefix}-h`);
|
|
963
|
+
const sInput = document.getElementById(`${idPrefix}-s`);
|
|
964
|
+
const lInput = document.getElementById(`${idPrefix}-l`);
|
|
965
|
+
|
|
966
|
+
if (hInput) hInput.value = hsl.h;
|
|
967
|
+
if (sInput) sInput.value = hsl.s;
|
|
968
|
+
if (lInput) lInput.value = hsl.l;
|
|
969
|
+
|
|
970
|
+
const hVal = document.getElementById(`${idPrefix}-h-val`);
|
|
971
|
+
const sVal = document.getElementById(`${idPrefix}-s-val`);
|
|
972
|
+
const lVal = document.getElementById(`${idPrefix}-l-val`);
|
|
973
|
+
|
|
974
|
+
if (hVal) hVal.textContent = hsl.h;
|
|
975
|
+
if (sVal) sVal.textContent = hsl.s + '%';
|
|
976
|
+
if (lVal) lVal.textContent = hsl.l + '%';
|
|
977
|
+
|
|
978
|
+
const preview = document.getElementById(`${idPrefix}-preview`);
|
|
979
|
+
const textStr = document.getElementById(`${idPrefix}-hsl-string`);
|
|
980
|
+
const hslStr = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
|
|
981
|
+
|
|
982
|
+
if (preview) preview.style.backgroundColor = hslStr;
|
|
983
|
+
if (textStr) textStr.textContent = hslStr;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Executes getComputedStyle on clicked mock element and registers properties
|
|
988
|
+
*/
|
|
989
|
+
function selectElement(element, isChildSelection = false) {
|
|
990
|
+
const mockPage = document.getElementById('mock-page');
|
|
991
|
+
|
|
992
|
+
// Clear previous outlines
|
|
993
|
+
if (state.activeElement) {
|
|
994
|
+
state.activeElement.classList.remove('inspect-selected');
|
|
995
|
+
}
|
|
996
|
+
if (state.focusRoot) {
|
|
997
|
+
state.focusRoot.classList.remove('inspect-focus-root');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (!isChildSelection) {
|
|
1001
|
+
state.focusRoot = element;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
state.activeElement = element;
|
|
1005
|
+
|
|
1006
|
+
// Apply visual outline overlays
|
|
1007
|
+
if (state.focusRoot && state.activeElement && state.focusRoot !== state.activeElement) {
|
|
1008
|
+
state.focusRoot.classList.add('inspect-focus-root');
|
|
1009
|
+
state.activeElement.classList.add('inspect-selected');
|
|
1010
|
+
} else {
|
|
1011
|
+
element.classList.add('inspect-selected');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const computed = window.getComputedStyle(element);
|
|
1015
|
+
|
|
1016
|
+
// Open details display
|
|
1017
|
+
const emptyContainer = document.getElementById('meta-container');
|
|
1018
|
+
if (emptyContainer) emptyContainer.classList.add('hidden');
|
|
1019
|
+
|
|
1020
|
+
const details = document.getElementById('meta-details');
|
|
1021
|
+
if (details) details.classList.remove('hidden');
|
|
1022
|
+
|
|
1023
|
+
const metaTag = document.getElementById('meta-tag');
|
|
1024
|
+
if (metaTag) metaTag.textContent = element.tagName.toLowerCase();
|
|
1025
|
+
|
|
1026
|
+
// Gather class lists cleanly
|
|
1027
|
+
const classList = Array.from(element.classList)
|
|
1028
|
+
.filter(c => c !== 'inspect-selected' && c !== 'inspect-hovered' && c !== 'inspect-focus-root')
|
|
1029
|
+
.map(c => '.' + c)
|
|
1030
|
+
.join(' ');
|
|
1031
|
+
const metaClasses = document.getElementById('meta-classes');
|
|
1032
|
+
if (metaClasses) metaClasses.textContent = classList || '(none)';
|
|
1033
|
+
|
|
1034
|
+
const selector = getUniqueSelector(element);
|
|
1035
|
+
const metaSelector = document.getElementById('meta-selector');
|
|
1036
|
+
if (metaSelector) metaSelector.textContent = selector;
|
|
1037
|
+
|
|
1038
|
+
// Classify element semantically
|
|
1039
|
+
const classification = classifyUIElement(element);
|
|
1040
|
+
const metaUiType = document.getElementById('meta-ui-type');
|
|
1041
|
+
if (metaUiType) {
|
|
1042
|
+
metaUiType.textContent = `${classification.icon} ${classification.label}`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Register entry inside staged changes dictionary
|
|
1046
|
+
if (!state.stagedChanges.has(selector)) {
|
|
1047
|
+
state.stagedChanges.set(selector, {
|
|
1048
|
+
element: element,
|
|
1049
|
+
originalStyles: {},
|
|
1050
|
+
currentStyles: {}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const getNumericValue = (styleVal) => {
|
|
1055
|
+
const val = parseFloat(styleVal);
|
|
1056
|
+
return isNaN(val) ? 0 : val;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// Populate sliders with baseline dimensions
|
|
1060
|
+
setupSlider('padding', getNumericValue(computed.paddingTop || computed.padding), 0, 100);
|
|
1061
|
+
setupSlider('margin', getNumericValue(computed.marginTop || computed.margin), 0, 100);
|
|
1062
|
+
setupSlider('width', getNumericValue(computed.width), 50, 1000);
|
|
1063
|
+
setupSlider('height', getNumericValue(computed.height), 10, 800);
|
|
1064
|
+
setupSlider('borderRadius', getNumericValue(computed.borderRadius), 0, 100);
|
|
1065
|
+
setupSlider('fontSize', getNumericValue(computed.fontSize), 8, 72);
|
|
1066
|
+
|
|
1067
|
+
// Populate Color parameters
|
|
1068
|
+
setupColorSliders('background', computed.backgroundColor);
|
|
1069
|
+
setupColorSliders('text', computed.color);
|
|
1070
|
+
|
|
1071
|
+
// Populate Text Content input
|
|
1072
|
+
const textInput = document.getElementById('input-textContent');
|
|
1073
|
+
if (textInput) {
|
|
1074
|
+
textInput.value = element.textContent.trim();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Filter sliders by selected element type
|
|
1078
|
+
filterSlidersByElementType(element);
|
|
1079
|
+
|
|
1080
|
+
// Render dynamic expandable DOM tree hierarchy
|
|
1081
|
+
renderHierarchyTree(state.focusRoot);
|
|
1082
|
+
|
|
1083
|
+
// Update lateral panel voice controls
|
|
1084
|
+
updateVoicePanel();
|
|
1085
|
+
|
|
1086
|
+
// Sync pop-out window state if undocked
|
|
1087
|
+
if (state.floatingWindow) {
|
|
1088
|
+
syncFloatingWindowDOM();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/* ==========================================================================
|
|
1093
|
+
SANDBOX MUTATION CONTROLLER
|
|
1094
|
+
========================================================================== */
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Performs inline overrides on dynamic selected element property
|
|
1098
|
+
*/
|
|
1099
|
+
function updateElementStyle(property, value) {
|
|
1100
|
+
if (!state.activeElement) return;
|
|
1101
|
+
|
|
1102
|
+
const selector = getUniqueSelector(state.activeElement);
|
|
1103
|
+
const entry = state.stagedChanges.get(selector);
|
|
1104
|
+
|
|
1105
|
+
// Cache original element state style before updates
|
|
1106
|
+
if (!(property in entry.originalStyles)) {
|
|
1107
|
+
entry.originalStyles[property] = state.activeElement.style[property] || window.getComputedStyle(state.activeElement)[property];
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Update current overrides
|
|
1111
|
+
entry.currentStyles[property] = value;
|
|
1112
|
+
|
|
1113
|
+
// Write actual rule directly to target element style in hot memory
|
|
1114
|
+
state.activeElement.style[property] = value;
|
|
1115
|
+
|
|
1116
|
+
// Update lateral staging changes list
|
|
1117
|
+
renderStagedChanges();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Performs inline overrides on dynamic selected element textContent
|
|
1122
|
+
*/
|
|
1123
|
+
function updateElementTextContent(value) {
|
|
1124
|
+
if (!state.activeElement) return;
|
|
1125
|
+
|
|
1126
|
+
const selector = getUniqueSelector(state.activeElement);
|
|
1127
|
+
const entry = state.stagedChanges.get(selector);
|
|
1128
|
+
|
|
1129
|
+
// Cache original element text value before updates
|
|
1130
|
+
if (!('textContent' in entry.originalStyles)) {
|
|
1131
|
+
entry.originalStyles['textContent'] = entry.element.textContent;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Update overrides
|
|
1135
|
+
entry.currentStyles['textContent'] = value;
|
|
1136
|
+
|
|
1137
|
+
// Write directly to target element textContent in hot memory
|
|
1138
|
+
state.activeElement.textContent = value;
|
|
1139
|
+
|
|
1140
|
+
// Update input text value in both documents
|
|
1141
|
+
const docs = [document];
|
|
1142
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
1143
|
+
docs.push(state.floatingWindow.document);
|
|
1144
|
+
}
|
|
1145
|
+
docs.forEach(doc => {
|
|
1146
|
+
const textInput = doc.getElementById('input-textContent');
|
|
1147
|
+
if (textInput && textInput.value !== value) {
|
|
1148
|
+
textInput.value = value;
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Update lateral changes panel and floating pop-out window
|
|
1153
|
+
renderStagedChanges();
|
|
1154
|
+
if (state.floatingWindow) {
|
|
1155
|
+
syncFloatingWindowDOM();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Receives input updates from visual sliders and maps tokens
|
|
1161
|
+
*/
|
|
1162
|
+
function handleSliderChange(property, value) {
|
|
1163
|
+
const valueNum = parseFloat(value);
|
|
1164
|
+
let displayValue = value + 'px';
|
|
1165
|
+
let applyValue = displayValue;
|
|
1166
|
+
|
|
1167
|
+
const token = mapToToken(property, valueNum);
|
|
1168
|
+
if (token) {
|
|
1169
|
+
applyValue = token;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// List of active documents to update in real-time
|
|
1173
|
+
const docs = [document];
|
|
1174
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
1175
|
+
docs.push(state.floatingWindow.document);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
docs.forEach(doc => {
|
|
1179
|
+
const slider = doc.getElementById(`slider-${property}`);
|
|
1180
|
+
if (slider) {
|
|
1181
|
+
slider.value = value;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const tokenBadge = doc.getElementById(`token-${property}`);
|
|
1185
|
+
if (tokenBadge) {
|
|
1186
|
+
if (token) {
|
|
1187
|
+
tokenBadge.textContent = token.replace('var(', '').replace(')', '');
|
|
1188
|
+
tokenBadge.classList.remove('hidden');
|
|
1189
|
+
} else {
|
|
1190
|
+
tokenBadge.classList.add('hidden');
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const numDisplay = doc.getElementById(`val-${property}`);
|
|
1195
|
+
if (numDisplay) {
|
|
1196
|
+
numDisplay.textContent = displayValue;
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
if (property === 'width' || property === 'height' || property === 'fontSize') {
|
|
1201
|
+
applyValue = value + 'px';
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
updateElementStyle(property, applyValue);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Handles color sliders change events
|
|
1209
|
+
*/
|
|
1210
|
+
function handleColorChange(type, sourceDoc) {
|
|
1211
|
+
if (!state.activeElement) return;
|
|
1212
|
+
|
|
1213
|
+
// Resolve source document to read from (default to active window if not specified)
|
|
1214
|
+
const readDoc = sourceDoc || ((state.floatingWindow && !state.floatingWindow.closed) ? state.floatingWindow.document : document);
|
|
1215
|
+
|
|
1216
|
+
// Map 'background' to 'bg' to match index.html IDs
|
|
1217
|
+
const idPrefix = type === 'background' ? 'bg' : type;
|
|
1218
|
+
|
|
1219
|
+
const hInput = readDoc.getElementById(`${idPrefix}-h`);
|
|
1220
|
+
const sInput = readDoc.getElementById(`${idPrefix}-s`);
|
|
1221
|
+
const lInput = readDoc.getElementById(`${idPrefix}-l`);
|
|
1222
|
+
|
|
1223
|
+
if (!hInput || !sInput || !lInput) return;
|
|
1224
|
+
|
|
1225
|
+
const h = hInput.value;
|
|
1226
|
+
const s = sInput.value;
|
|
1227
|
+
const l = lInput.value;
|
|
1228
|
+
|
|
1229
|
+
const hslStr = `hsl(${h}, ${s}%, ${l}%)`;
|
|
1230
|
+
|
|
1231
|
+
// List of active documents to update in real-time
|
|
1232
|
+
const docs = [document];
|
|
1233
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
1234
|
+
docs.push(state.floatingWindow.document);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
docs.forEach(doc => {
|
|
1238
|
+
const hi = doc.getElementById(`${idPrefix}-h`);
|
|
1239
|
+
const si = doc.getElementById(`${idPrefix}-s`);
|
|
1240
|
+
const li = doc.getElementById(`${idPrefix}-l`);
|
|
1241
|
+
if (hi) hi.value = h;
|
|
1242
|
+
if (si) si.value = s;
|
|
1243
|
+
if (li) li.value = l;
|
|
1244
|
+
|
|
1245
|
+
const hVal = doc.getElementById(`${idPrefix}-h-val`);
|
|
1246
|
+
const sVal = doc.getElementById(`${idPrefix}-s-val`);
|
|
1247
|
+
const lVal = doc.getElementById(`${idPrefix}-l-val`);
|
|
1248
|
+
if (hVal) hVal.textContent = h;
|
|
1249
|
+
if (sVal) sVal.textContent = s + '%';
|
|
1250
|
+
if (lVal) lVal.textContent = l + '%';
|
|
1251
|
+
|
|
1252
|
+
const preview = doc.getElementById(`${idPrefix}-preview`);
|
|
1253
|
+
const textStr = doc.getElementById(`${idPrefix}-hsl-string`);
|
|
1254
|
+
if (preview) preview.style.backgroundColor = hslStr;
|
|
1255
|
+
if (textStr) textStr.textContent = hslStr;
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
const styleProp = type === 'background' ? 'backgroundColor' : 'color';
|
|
1259
|
+
updateElementStyle(styleProp, hslStr);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/* ==========================================================================
|
|
1263
|
+
LOCAL AUDIO RECORDER & MICROPHONE BADGES ENGINE (Milestone 4)
|
|
1264
|
+
========================================================================== */
|
|
1265
|
+
|
|
1266
|
+
let mediaStream = null;
|
|
1267
|
+
let mediaRecorder = null;
|
|
1268
|
+
let audioChunks = [];
|
|
1269
|
+
let recordingSeconds = 0;
|
|
1270
|
+
let timerInterval = null;
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Updates the Lateral Staging Panel controls based on state
|
|
1274
|
+
*/
|
|
1275
|
+
function updateVoicePanel() {
|
|
1276
|
+
const btnVoiceRecord = document.getElementById('btn-voice-record');
|
|
1277
|
+
const btnVoiceDelete = document.getElementById('btn-voice-delete');
|
|
1278
|
+
const voiceStatusContainer = document.getElementById('voice-status-container');
|
|
1279
|
+
const voiceAudioPlayerContainer = document.getElementById('voice-audio-player-container');
|
|
1280
|
+
const voiceAudioPlayer = document.getElementById('voice-audio-player');
|
|
1281
|
+
const recordBtnText = document.getElementById('record-btn-text');
|
|
1282
|
+
|
|
1283
|
+
if (!btnVoiceRecord) return;
|
|
1284
|
+
|
|
1285
|
+
if (!state.activeElement) {
|
|
1286
|
+
btnVoiceRecord.disabled = true;
|
|
1287
|
+
btnVoiceRecord.classList.remove('recording');
|
|
1288
|
+
if (recordBtnText) recordBtnText.textContent = 'Record Voice Note';
|
|
1289
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.add('hidden');
|
|
1290
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.add('hidden');
|
|
1291
|
+
if (voiceAudioPlayerContainer) voiceAudioPlayerContainer.classList.add('hidden');
|
|
1292
|
+
if (voiceAudioPlayer) voiceAudioPlayer.src = '';
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
btnVoiceRecord.disabled = false;
|
|
1297
|
+
const selector = getUniqueSelector(state.activeElement);
|
|
1298
|
+
const entry = state.stagedChanges.get(selector);
|
|
1299
|
+
|
|
1300
|
+
if (state.recordingMode) {
|
|
1301
|
+
btnVoiceRecord.classList.add('recording');
|
|
1302
|
+
if (recordBtnText) recordBtnText.textContent = 'Stop Recording';
|
|
1303
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.add('hidden');
|
|
1304
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.remove('hidden');
|
|
1305
|
+
if (voiceAudioPlayerContainer) voiceAudioPlayerContainer.classList.add('hidden');
|
|
1306
|
+
} else if (entry && entry.voiceNote) {
|
|
1307
|
+
btnVoiceRecord.classList.remove('recording');
|
|
1308
|
+
if (recordBtnText) recordBtnText.textContent = 'Re-record Voice Note';
|
|
1309
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.remove('hidden');
|
|
1310
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.add('hidden');
|
|
1311
|
+
if (voiceAudioPlayerContainer) {
|
|
1312
|
+
voiceAudioPlayerContainer.classList.remove('hidden');
|
|
1313
|
+
if (voiceAudioPlayer && voiceAudioPlayer.src !== entry.voiceNote.url) {
|
|
1314
|
+
voiceAudioPlayer.src = entry.voiceNote.url;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
} else {
|
|
1318
|
+
btnVoiceRecord.classList.remove('recording');
|
|
1319
|
+
if (recordBtnText) recordBtnText.textContent = 'Record Voice Note';
|
|
1320
|
+
if (btnVoiceDelete) btnVoiceDelete.classList.add('hidden');
|
|
1321
|
+
if (voiceStatusContainer) voiceStatusContainer.classList.add('hidden');
|
|
1322
|
+
if (voiceAudioPlayerContainer) voiceAudioPlayerContainer.classList.add('hidden');
|
|
1323
|
+
if (voiceAudioPlayer) voiceAudioPlayer.src = '';
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Starts audio recording via navigator.mediaDevices.getUserMedia
|
|
1329
|
+
*/
|
|
1330
|
+
function startAudioRecording() {
|
|
1331
|
+
if (!state.activeElement || state.recordingMode) return;
|
|
1332
|
+
|
|
1333
|
+
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
1334
|
+
.then(stream => {
|
|
1335
|
+
mediaStream = stream;
|
|
1336
|
+
state.recordingMode = true;
|
|
1337
|
+
audioChunks = [];
|
|
1338
|
+
|
|
1339
|
+
// Detect support and use appropriate format
|
|
1340
|
+
let options = { mimeType: 'audio/webm' };
|
|
1341
|
+
if (!MediaRecorder.isTypeSupported('audio/webm')) {
|
|
1342
|
+
options = { mimeType: 'audio/ogg' };
|
|
1343
|
+
}
|
|
1344
|
+
if (!MediaRecorder.isTypeSupported('audio/ogg')) {
|
|
1345
|
+
options = {}; // Fallback
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
mediaRecorder = new MediaRecorder(stream, options);
|
|
1349
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
1350
|
+
if (event.data.size > 0) {
|
|
1351
|
+
audioChunks.push(event.data);
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
mediaRecorder.onstop = () => {
|
|
1356
|
+
const mime = mediaRecorder.mimeType || 'audio/wav';
|
|
1357
|
+
const blob = new Blob(audioChunks, { type: mime });
|
|
1358
|
+
saveVoiceNote(blob);
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
recordingSeconds = 0;
|
|
1362
|
+
const timerEl = document.getElementById('voice-timer');
|
|
1363
|
+
if (timerEl) timerEl.textContent = '00:00';
|
|
1364
|
+
|
|
1365
|
+
timerInterval = setInterval(() => {
|
|
1366
|
+
recordingSeconds++;
|
|
1367
|
+
const mins = String(Math.floor(recordingSeconds / 60)).padStart(2, '0');
|
|
1368
|
+
const secs = String(recordingSeconds % 60).padStart(2, '0');
|
|
1369
|
+
if (timerEl) timerEl.textContent = `${mins}:${secs}`;
|
|
1370
|
+
}, 1000);
|
|
1371
|
+
|
|
1372
|
+
mediaRecorder.start();
|
|
1373
|
+
updateVoicePanel();
|
|
1374
|
+
})
|
|
1375
|
+
.catch(err => {
|
|
1376
|
+
console.error('Microphone permissions or API error:', err);
|
|
1377
|
+
alert('Could not access microphone. Please verify site permissions.');
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Stops ongoing voice recording session
|
|
1383
|
+
*/
|
|
1384
|
+
function stopAudioRecording() {
|
|
1385
|
+
if (!state.recordingMode || !mediaRecorder) return;
|
|
1386
|
+
|
|
1387
|
+
state.recordingMode = false;
|
|
1388
|
+
mediaRecorder.stop();
|
|
1389
|
+
|
|
1390
|
+
if (mediaStream) {
|
|
1391
|
+
mediaStream.getTracks().forEach(track => track.stop());
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (timerInterval) {
|
|
1395
|
+
clearInterval(timerInterval);
|
|
1396
|
+
timerInterval = null;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
updateVoicePanel();
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Creates Object URL and local files references, registering them under target elements
|
|
1404
|
+
*/
|
|
1405
|
+
function saveVoiceNote(blob) {
|
|
1406
|
+
if (!state.activeElement) return;
|
|
1407
|
+
|
|
1408
|
+
const selector = getUniqueSelector(state.activeElement);
|
|
1409
|
+
const entry = state.stagedChanges.get(selector);
|
|
1410
|
+
if (!entry) return;
|
|
1411
|
+
|
|
1412
|
+
const now = new Date();
|
|
1413
|
+
const year = now.getFullYear();
|
|
1414
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
1415
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
1416
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
1417
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
1418
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
1419
|
+
const timestamp = `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
|
|
1420
|
+
const filename = `${timestamp}_feedback.wav`;
|
|
1421
|
+
|
|
1422
|
+
// Local absolute save simulation path
|
|
1423
|
+
const absolutePath = `d:\\Github Repos\\Extensiones_Ideas\\visual_ai_staging\\.ai-staging\\audio\\${filename}`;
|
|
1424
|
+
const urlPath = `file:///d:/Github%20Repos/Extensiones_Ideas/visual_ai_staging/.ai-staging/audio/${filename}`;
|
|
1425
|
+
|
|
1426
|
+
const audioUrl = URL.createObjectURL(blob);
|
|
1427
|
+
|
|
1428
|
+
// Register voice note inside element state
|
|
1429
|
+
entry.voiceNote = {
|
|
1430
|
+
url: audioUrl,
|
|
1431
|
+
absolutePath: absolutePath,
|
|
1432
|
+
urlPath: urlPath,
|
|
1433
|
+
filename: filename
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
const fallbackDownload = () => {
|
|
1437
|
+
// User-downloadable workspace fallback saving via programmatic click
|
|
1438
|
+
const downloadAnchor = document.createElement('a');
|
|
1439
|
+
downloadAnchor.href = audioUrl;
|
|
1440
|
+
downloadAnchor.download = filename;
|
|
1441
|
+
document.body.appendChild(downloadAnchor);
|
|
1442
|
+
downloadAnchor.click();
|
|
1443
|
+
downloadAnchor.remove();
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
if (isLocalServer) {
|
|
1447
|
+
fetch(`/api/save-audio?filename=${encodeURIComponent(filename)}`, {
|
|
1448
|
+
method: 'POST',
|
|
1449
|
+
body: blob
|
|
1450
|
+
})
|
|
1451
|
+
.then(res => {
|
|
1452
|
+
if (!res.ok) {
|
|
1453
|
+
fallbackDownload();
|
|
1454
|
+
}
|
|
1455
|
+
})
|
|
1456
|
+
.catch(err => {
|
|
1457
|
+
fallbackDownload();
|
|
1458
|
+
});
|
|
1459
|
+
} else {
|
|
1460
|
+
fallbackDownload();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
renderStagedChanges();
|
|
1464
|
+
updateVoicePanel();
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Deletes voice note for the active selector element
|
|
1469
|
+
*/
|
|
1470
|
+
function deleteVoiceNote() {
|
|
1471
|
+
if (!state.activeElement) return;
|
|
1472
|
+
|
|
1473
|
+
const selector = getUniqueSelector(state.activeElement);
|
|
1474
|
+
const entry = state.stagedChanges.get(selector);
|
|
1475
|
+
if (!entry) return;
|
|
1476
|
+
|
|
1477
|
+
if (entry.voiceNote) {
|
|
1478
|
+
URL.revokeObjectURL(entry.voiceNote.url);
|
|
1479
|
+
delete entry.voiceNote;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// If there are no other visual modifications, clean selector
|
|
1483
|
+
const hasStyleChanges = Object.keys(entry.currentStyles).some(
|
|
1484
|
+
prop => entry.currentStyles[prop] !== entry.originalStyles[prop]
|
|
1485
|
+
);
|
|
1486
|
+
if (!hasStyleChanges) {
|
|
1487
|
+
state.stagedChanges.delete(selector);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
renderStagedChanges();
|
|
1491
|
+
updateVoicePanel();
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Reactively draws/erases microphone floating DOM badges above targeted elements
|
|
1496
|
+
*/
|
|
1497
|
+
function updateVoiceBadges() {
|
|
1498
|
+
// Clear all previous badges and restore static positioning if necessary
|
|
1499
|
+
const existingBadges = document.querySelectorAll('.voice-badge');
|
|
1500
|
+
existingBadges.forEach(badge => {
|
|
1501
|
+
const parent = badge.parentElement;
|
|
1502
|
+
if (parent) {
|
|
1503
|
+
if (parent.dataset.originalPosition === 'static') {
|
|
1504
|
+
parent.style.position = '';
|
|
1505
|
+
delete parent.dataset.originalPosition;
|
|
1506
|
+
}
|
|
1507
|
+
badge.remove();
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// Re-render voice badges on any active element with a staged voice note
|
|
1512
|
+
state.stagedChanges.forEach((changeData, selector) => {
|
|
1513
|
+
if (changeData.type !== 'insertion' && changeData.voiceNote && changeData.element) {
|
|
1514
|
+
const el = changeData.element;
|
|
1515
|
+
const computedStyle = window.getComputedStyle(el);
|
|
1516
|
+
|
|
1517
|
+
if (computedStyle.position === 'static') {
|
|
1518
|
+
el.dataset.originalPosition = 'static';
|
|
1519
|
+
el.style.position = 'relative';
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const badge = document.createElement('div');
|
|
1523
|
+
badge.className = 'voice-badge';
|
|
1524
|
+
badge.textContent = '🎤';
|
|
1525
|
+
badge.title = 'Voice note annotation staged';
|
|
1526
|
+
|
|
1527
|
+
el.appendChild(badge);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Renders staged variations live in the right Lateral Panel
|
|
1534
|
+
*/
|
|
1535
|
+
function renderStagedChanges() {
|
|
1536
|
+
const container = document.getElementById('staged-changes-list');
|
|
1537
|
+
if (!container) return;
|
|
1538
|
+
|
|
1539
|
+
container.innerHTML = '';
|
|
1540
|
+
let count = 0;
|
|
1541
|
+
|
|
1542
|
+
state.stagedChanges.forEach((changeData, selector) => {
|
|
1543
|
+
// If it's a Free-Zone Bounding Box Insertion
|
|
1544
|
+
if (changeData.type === 'insertion') {
|
|
1545
|
+
count++;
|
|
1546
|
+
const changeEl = document.createElement('div');
|
|
1547
|
+
changeEl.className = 'change-item change-item-insertion';
|
|
1548
|
+
|
|
1549
|
+
const headerEl = document.createElement('div');
|
|
1550
|
+
headerEl.className = 'change-header';
|
|
1551
|
+
|
|
1552
|
+
const selectorSpan = document.createElement('span');
|
|
1553
|
+
selectorSpan.className = 'change-selector';
|
|
1554
|
+
selectorSpan.textContent = `[Insert: ${changeData.template}]`;
|
|
1555
|
+
selectorSpan.title = `Inserted inside: ${changeData.parentSelector}`;
|
|
1556
|
+
|
|
1557
|
+
const deleteBtn = document.createElement('button');
|
|
1558
|
+
deleteBtn.className = 'revert-btn';
|
|
1559
|
+
deleteBtn.textContent = 'Delete';
|
|
1560
|
+
deleteBtn.addEventListener('click', () => {
|
|
1561
|
+
deleteBoundingBox(changeData.boxId);
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
headerEl.appendChild(selectorSpan);
|
|
1565
|
+
headerEl.appendChild(deleteBtn);
|
|
1566
|
+
|
|
1567
|
+
const bodyEl = document.createElement('div');
|
|
1568
|
+
bodyEl.className = 'change-body';
|
|
1569
|
+
|
|
1570
|
+
// Parent Container
|
|
1571
|
+
const parentEl = document.createElement('div');
|
|
1572
|
+
parentEl.className = 'change-prop';
|
|
1573
|
+
|
|
1574
|
+
const parentNameSpan = document.createElement('span');
|
|
1575
|
+
parentNameSpan.className = 'prop-name';
|
|
1576
|
+
parentNameSpan.textContent = 'Parent: ';
|
|
1577
|
+
|
|
1578
|
+
const parentValSpan = document.createElement('span');
|
|
1579
|
+
parentValSpan.className = 'prop-val-new';
|
|
1580
|
+
parentValSpan.style.fontFamily = 'monospace';
|
|
1581
|
+
parentValSpan.style.fontSize = '11px';
|
|
1582
|
+
parentValSpan.textContent = changeData.parentSelector.split(' > ').pop();
|
|
1583
|
+
|
|
1584
|
+
parentEl.appendChild(parentNameSpan);
|
|
1585
|
+
parentEl.appendChild(parentValSpan);
|
|
1586
|
+
bodyEl.appendChild(parentEl);
|
|
1587
|
+
|
|
1588
|
+
// Size
|
|
1589
|
+
const dimsEl = document.createElement('div');
|
|
1590
|
+
dimsEl.className = 'change-prop';
|
|
1591
|
+
|
|
1592
|
+
const dimsNameSpan = document.createElement('span');
|
|
1593
|
+
dimsNameSpan.className = 'prop-name';
|
|
1594
|
+
dimsNameSpan.textContent = 'Dimensions: ';
|
|
1595
|
+
|
|
1596
|
+
const dimsValSpan = document.createElement('span');
|
|
1597
|
+
dimsValSpan.className = 'prop-val-new';
|
|
1598
|
+
dimsValSpan.style.color = 'var(--color-violet)';
|
|
1599
|
+
dimsValSpan.style.fontFamily = 'monospace';
|
|
1600
|
+
dimsValSpan.textContent = `${Math.round(changeData.width)}px × ${Math.round(changeData.height)}px`;
|
|
1601
|
+
|
|
1602
|
+
dimsEl.appendChild(dimsNameSpan);
|
|
1603
|
+
dimsEl.appendChild(dimsValSpan);
|
|
1604
|
+
bodyEl.appendChild(dimsEl);
|
|
1605
|
+
|
|
1606
|
+
// Notes
|
|
1607
|
+
if (changeData.notes) {
|
|
1608
|
+
const notesEl = document.createElement('div');
|
|
1609
|
+
notesEl.className = 'change-prop';
|
|
1610
|
+
notesEl.style.flexDirection = 'column';
|
|
1611
|
+
notesEl.style.alignItems = 'flex-start';
|
|
1612
|
+
|
|
1613
|
+
const notesNameSpan = document.createElement('span');
|
|
1614
|
+
notesNameSpan.className = 'prop-name';
|
|
1615
|
+
notesNameSpan.textContent = 'Notes:';
|
|
1616
|
+
|
|
1617
|
+
const notesValSpan = document.createElement('span');
|
|
1618
|
+
notesValSpan.className = 'prop-val-new';
|
|
1619
|
+
notesValSpan.style.color = 'var(--color-text-secondary)';
|
|
1620
|
+
notesValSpan.style.fontFamily = 'inherit';
|
|
1621
|
+
notesValSpan.style.fontWeight = 'normal';
|
|
1622
|
+
notesValSpan.style.marginTop = '2px';
|
|
1623
|
+
notesValSpan.textContent = changeData.notes;
|
|
1624
|
+
|
|
1625
|
+
notesEl.appendChild(notesNameSpan);
|
|
1626
|
+
notesEl.appendChild(notesValSpan);
|
|
1627
|
+
bodyEl.appendChild(notesEl);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
changeEl.appendChild(headerEl);
|
|
1631
|
+
changeEl.appendChild(bodyEl);
|
|
1632
|
+
container.appendChild(changeEl);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const changePropElements = [];
|
|
1637
|
+
let hasVisibleChanges = false;
|
|
1638
|
+
|
|
1639
|
+
for (const [prop, val] of Object.entries(changeData.currentStyles)) {
|
|
1640
|
+
const origVal = changeData.originalStyles[prop];
|
|
1641
|
+
if (origVal === val) continue;
|
|
1642
|
+
|
|
1643
|
+
hasVisibleChanges = true;
|
|
1644
|
+
|
|
1645
|
+
const propEl = document.createElement('div');
|
|
1646
|
+
propEl.className = 'change-prop';
|
|
1647
|
+
|
|
1648
|
+
const nameSpan = document.createElement('span');
|
|
1649
|
+
nameSpan.className = 'prop-name';
|
|
1650
|
+
nameSpan.textContent = `${prop}: `;
|
|
1651
|
+
|
|
1652
|
+
const oldSpan = document.createElement('span');
|
|
1653
|
+
oldSpan.className = 'prop-val-old';
|
|
1654
|
+
oldSpan.textContent = origVal;
|
|
1655
|
+
|
|
1656
|
+
const arrowSpan = document.createElement('span');
|
|
1657
|
+
arrowSpan.className = 'prop-arrow';
|
|
1658
|
+
arrowSpan.textContent = ' → ';
|
|
1659
|
+
|
|
1660
|
+
const newSpan = document.createElement('span');
|
|
1661
|
+
newSpan.className = 'prop-val-new';
|
|
1662
|
+
newSpan.textContent = val;
|
|
1663
|
+
|
|
1664
|
+
propEl.appendChild(nameSpan);
|
|
1665
|
+
propEl.appendChild(oldSpan);
|
|
1666
|
+
propEl.appendChild(arrowSpan);
|
|
1667
|
+
propEl.appendChild(newSpan);
|
|
1668
|
+
|
|
1669
|
+
changePropElements.push(propEl);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Add voice note indicator if present in lateral changes panel
|
|
1673
|
+
if (changeData.voiceNote) {
|
|
1674
|
+
hasVisibleChanges = true;
|
|
1675
|
+
|
|
1676
|
+
const voicePropEl = document.createElement('div');
|
|
1677
|
+
voicePropEl.className = 'change-prop voice-note-indicator';
|
|
1678
|
+
|
|
1679
|
+
const voiceIconSpan = document.createElement('span');
|
|
1680
|
+
voiceIconSpan.textContent = '🎤';
|
|
1681
|
+
|
|
1682
|
+
const voiceTextSpan = document.createElement('span');
|
|
1683
|
+
voiceTextSpan.className = 'prop-val-new';
|
|
1684
|
+
voiceTextSpan.style.color = 'var(--color-accent)';
|
|
1685
|
+
voiceTextSpan.style.fontFamily = 'sans-serif';
|
|
1686
|
+
voiceTextSpan.style.fontSize = '12px';
|
|
1687
|
+
voiceTextSpan.textContent = 'Voice Note Staged';
|
|
1688
|
+
voiceTextSpan.title = changeData.voiceNote.filename;
|
|
1689
|
+
|
|
1690
|
+
voicePropEl.appendChild(voiceIconSpan);
|
|
1691
|
+
voicePropEl.appendChild(voiceTextSpan);
|
|
1692
|
+
changePropElements.push(voicePropEl);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (!hasVisibleChanges) return;
|
|
1696
|
+
count++;
|
|
1697
|
+
|
|
1698
|
+
const changeEl = document.createElement('div');
|
|
1699
|
+
changeEl.className = 'change-item';
|
|
1700
|
+
|
|
1701
|
+
const headerEl = document.createElement('div');
|
|
1702
|
+
headerEl.className = 'change-header';
|
|
1703
|
+
|
|
1704
|
+
const selectorSpan = document.createElement('span');
|
|
1705
|
+
selectorSpan.className = 'change-selector';
|
|
1706
|
+
selectorSpan.title = selector;
|
|
1707
|
+
selectorSpan.textContent = selector.split(' > ').pop();
|
|
1708
|
+
|
|
1709
|
+
const revertBtn = document.createElement('button');
|
|
1710
|
+
revertBtn.className = 'revert-btn';
|
|
1711
|
+
revertBtn.textContent = 'Revert';
|
|
1712
|
+
revertBtn.addEventListener('click', () => {
|
|
1713
|
+
revertAllChangesFor(selector);
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
headerEl.appendChild(selectorSpan);
|
|
1717
|
+
headerEl.appendChild(revertBtn);
|
|
1718
|
+
|
|
1719
|
+
const bodyEl = document.createElement('div');
|
|
1720
|
+
bodyEl.className = 'change-body';
|
|
1721
|
+
|
|
1722
|
+
changePropElements.forEach(propEl => {
|
|
1723
|
+
bodyEl.appendChild(propEl);
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
changeEl.appendChild(headerEl);
|
|
1727
|
+
changeEl.appendChild(bodyEl);
|
|
1728
|
+
|
|
1729
|
+
container.appendChild(changeEl);
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
if (count === 0) {
|
|
1733
|
+
const noChangesEl = document.createElement('div');
|
|
1734
|
+
noChangesEl.className = 'no-changes';
|
|
1735
|
+
noChangesEl.textContent = 'No staged changes yet. Select an element and adjust properties.';
|
|
1736
|
+
container.appendChild(noChangesEl);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Reactively sync floating microphone DOM badges
|
|
1740
|
+
updateVoiceBadges();
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* Restores original style characteristics on target element
|
|
1745
|
+
*/
|
|
1746
|
+
function revertAllChangesFor(selector) {
|
|
1747
|
+
const changeData = state.stagedChanges.get(selector);
|
|
1748
|
+
if (!changeData) return;
|
|
1749
|
+
|
|
1750
|
+
if (changeData.type === 'insertion') {
|
|
1751
|
+
deleteBoundingBox(changeData.boxId);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Reapply cached rules directly
|
|
1756
|
+
for (const [prop, val] of Object.entries(changeData.originalStyles)) {
|
|
1757
|
+
changeData.element.style[prop] = val;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (changeData.voiceNote) {
|
|
1761
|
+
URL.revokeObjectURL(changeData.voiceNote.url);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
state.stagedChanges.delete(selector);
|
|
1765
|
+
renderStagedChanges();
|
|
1766
|
+
|
|
1767
|
+
// If reverted element is the active one, reload sliders
|
|
1768
|
+
if (state.activeElement === changeData.element) {
|
|
1769
|
+
selectElement(changeData.element);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Resets the entire visual workspace session back to pristine base
|
|
1775
|
+
*/
|
|
1776
|
+
function clearAllStagedChanges() {
|
|
1777
|
+
state.stagedChanges.forEach((changeData) => {
|
|
1778
|
+
if (changeData.type !== 'insertion' && changeData.element) {
|
|
1779
|
+
for (const [prop, val] of Object.entries(changeData.originalStyles)) {
|
|
1780
|
+
changeData.element.style[prop] = val;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (changeData.voiceNote) {
|
|
1784
|
+
URL.revokeObjectURL(changeData.voiceNote.url);
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
state.stagedChanges.clear();
|
|
1789
|
+
renderBoundingBoxes(); // Redraw overlay
|
|
1790
|
+
renderStagedChanges();
|
|
1791
|
+
|
|
1792
|
+
if (state.activeElement) {
|
|
1793
|
+
state.activeElement.classList.remove('inspect-selected');
|
|
1794
|
+
state.activeElement = null;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
updateVoicePanel();
|
|
1798
|
+
|
|
1799
|
+
const emptyContainer = document.getElementById('meta-container');
|
|
1800
|
+
if (emptyContainer) emptyContainer.classList.remove('hidden');
|
|
1801
|
+
|
|
1802
|
+
const details = document.getElementById('meta-details');
|
|
1803
|
+
if (details) details.classList.add('hidden');
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/* ==========================================================================
|
|
1807
|
+
PROMPT COMPILER & EXPORT WORKFLOWS
|
|
1808
|
+
========================================================================== */
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Builds structured recipe instructions text
|
|
1812
|
+
*/
|
|
1813
|
+
/**
|
|
1814
|
+
* Helper to format current date/time to YYYY-MM-DD_HHMMSS
|
|
1815
|
+
*/
|
|
1816
|
+
function getFormattedTimestamp() {
|
|
1817
|
+
const now = new Date();
|
|
1818
|
+
const year = now.getFullYear();
|
|
1819
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
1820
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
1821
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
1822
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
1823
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
1824
|
+
return `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Triggers a programmatic browser download of the compiled Markdown recipe
|
|
1829
|
+
*/
|
|
1830
|
+
function triggerFeedbackDownload(recipe) {
|
|
1831
|
+
const timestamp = getFormattedTimestamp();
|
|
1832
|
+
const filename = `${timestamp}_feedback.md`;
|
|
1833
|
+
|
|
1834
|
+
const fallbackDownload = () => {
|
|
1835
|
+
const blob = new Blob([recipe], { type: 'text/markdown;charset=utf-8' });
|
|
1836
|
+
const url = URL.createObjectURL(blob);
|
|
1837
|
+
|
|
1838
|
+
const anchor = document.createElement('a');
|
|
1839
|
+
anchor.href = url;
|
|
1840
|
+
anchor.download = filename;
|
|
1841
|
+
|
|
1842
|
+
// CSP/XSS: hide and append to body off-screen programmatically
|
|
1843
|
+
anchor.style.position = 'absolute';
|
|
1844
|
+
anchor.style.left = '-9999px';
|
|
1845
|
+
anchor.style.top = '-9999px';
|
|
1846
|
+
|
|
1847
|
+
document.body.appendChild(anchor);
|
|
1848
|
+
anchor.click();
|
|
1849
|
+
|
|
1850
|
+
// Clean up
|
|
1851
|
+
document.body.removeChild(anchor);
|
|
1852
|
+
URL.revokeObjectURL(url);
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
if (isLocalServer) {
|
|
1856
|
+
fetch('/api/save-feedback', {
|
|
1857
|
+
method: 'POST',
|
|
1858
|
+
headers: {
|
|
1859
|
+
'Content-Type': 'application/json'
|
|
1860
|
+
},
|
|
1861
|
+
body: JSON.stringify({ filename: filename, content: recipe })
|
|
1862
|
+
})
|
|
1863
|
+
.then(res => {
|
|
1864
|
+
if (!res.ok) {
|
|
1865
|
+
fallbackDownload();
|
|
1866
|
+
}
|
|
1867
|
+
})
|
|
1868
|
+
.catch(err => {
|
|
1869
|
+
fallbackDownload();
|
|
1870
|
+
});
|
|
1871
|
+
} else {
|
|
1872
|
+
fallbackDownload();
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
return filename;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* Displays a premium visual toast notification in the UI
|
|
1880
|
+
*/
|
|
1881
|
+
function showToastNotification(filename, isError = false) {
|
|
1882
|
+
// Check if a toast is already on screen and remove it
|
|
1883
|
+
const existingToast = document.querySelector('.toast-notification');
|
|
1884
|
+
if (existingToast) {
|
|
1885
|
+
existingToast.remove();
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const toast = document.createElement('div');
|
|
1889
|
+
toast.className = 'toast-notification';
|
|
1890
|
+
|
|
1891
|
+
const header = document.createElement('div');
|
|
1892
|
+
header.className = 'toast-header';
|
|
1893
|
+
|
|
1894
|
+
const iconSpan = document.createElement('span');
|
|
1895
|
+
iconSpan.textContent = isError ? '❌' : '✨';
|
|
1896
|
+
header.appendChild(iconSpan);
|
|
1897
|
+
|
|
1898
|
+
const titleSpan = document.createElement('span');
|
|
1899
|
+
titleSpan.textContent = isError
|
|
1900
|
+
? 'Clipboard Access Error'
|
|
1901
|
+
: 'Recipe Prompt Exported!';
|
|
1902
|
+
header.appendChild(titleSpan);
|
|
1903
|
+
|
|
1904
|
+
toast.appendChild(header);
|
|
1905
|
+
|
|
1906
|
+
const body = document.createElement('div');
|
|
1907
|
+
body.className = 'toast-body';
|
|
1908
|
+
|
|
1909
|
+
const p1 = document.createElement('p');
|
|
1910
|
+
p1.textContent = isError
|
|
1911
|
+
? 'Could not copy the prompt automatically to the clipboard, but the recipe file is ready.'
|
|
1912
|
+
: 'The compiled recipe prompt was successfully copied to your clipboard.';
|
|
1913
|
+
body.appendChild(p1);
|
|
1914
|
+
|
|
1915
|
+
if (filename) {
|
|
1916
|
+
const p2 = document.createElement('div');
|
|
1917
|
+
p2.className = 'toast-meta';
|
|
1918
|
+
|
|
1919
|
+
// We want the text to say it is auto-saved inside the workspace feedback folder
|
|
1920
|
+
p2.textContent = `Auto-saved feedback file: .ai-staging/feedback/${filename}`;
|
|
1921
|
+
body.appendChild(p2);
|
|
1922
|
+
} else {
|
|
1923
|
+
const p2 = document.createElement('div');
|
|
1924
|
+
p2.className = 'toast-meta';
|
|
1925
|
+
p2.textContent = 'Note: No staged changes to save to .ai-staging/feedback/';
|
|
1926
|
+
body.appendChild(p2);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
toast.appendChild(body);
|
|
1930
|
+
document.body.appendChild(toast);
|
|
1931
|
+
|
|
1932
|
+
// Auto-dim and remove toast after 5 seconds
|
|
1933
|
+
setTimeout(() => {
|
|
1934
|
+
toast.classList.add('hide');
|
|
1935
|
+
setTimeout(() => {
|
|
1936
|
+
toast.remove();
|
|
1937
|
+
}, 300);
|
|
1938
|
+
}, 5000);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* Builds structured recipe instructions text
|
|
1943
|
+
*/
|
|
1944
|
+
function generateMarkdownRecipe() {
|
|
1945
|
+
if (state.stagedChanges.size === 0) {
|
|
1946
|
+
return "No changes have been staged in the Visual AI Sandbox.";
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
const now = new Date();
|
|
1950
|
+
const timestamp = getFormattedTimestamp();
|
|
1951
|
+
const fileSavePath = `.ai-staging/feedback/${timestamp}_feedback.md`;
|
|
1952
|
+
|
|
1953
|
+
let markdown = `# SYSTEM PRE-PROMPT (AI DEVELOPER INSTRUCTIONS)
|
|
1954
|
+
You are an expert AI frontend engineering assistant (e.g., GitHub Copilot, ChatGPT, Gemini). You have been provided with a structured visual staging recipe generated by the Visual AI Staging Companion.
|
|
1955
|
+
Your task is to implement the specified user interface changes and additions with absolute fidelity, following these precise rules:
|
|
1956
|
+
|
|
1957
|
+
1. **Interpret Element Visual Staging**:
|
|
1958
|
+
- For each CSS Selector, examine the target tag type and current text.
|
|
1959
|
+
- Apply the listed **Visual Styles Requeridos (Staged)**. Note that modified CSS properties have been automatically mapped to design tokens (e.g. \`var(--spacing-md)\`, \`var(--border-radius-lg)\`) where applicable. Do not revert these back to raw pixel values; implement them using the design system variables directly.
|
|
1960
|
+
- Pay special attention to the **Developer Notes** and any localized **Voice Note File References** (feedback WAV files). The voice notes contain spoken directives on UX behavior, animation requirements, and detail layout requirements that must be followed.
|
|
1961
|
+
|
|
1962
|
+
2. **Interpret Spatial Bounding Box Insertions**:
|
|
1963
|
+
- For each Bounding Box Insertion, you must inject/insert a new component or visual zone inside the specified **Resolved Nearest Parent Container**.
|
|
1964
|
+
- The **Ubicación Bounding Box (relativa al lienzo)** specifies the relative \`(x, y)\` coordinate offsets and the bounding box dimension guidelines (\`width\` and \`height\` in pixels). Use these to correctly position and scale the newly constructed component within the target layout, utilizing modern responsive layout structures (like CSS Flexbox or Grid) that reflect these dimensions.
|
|
1965
|
+
- Construct the layout based on the requested **Preset Type** (e.g., Carousel, Form, Grid, Custom Component) and read the **Developer Notes** for detailed design and functionality constraints.
|
|
1966
|
+
- Incorporate any associated voice notes or multimedia references to refine the component's interactive behaviors.
|
|
1967
|
+
|
|
1968
|
+
Please apply these updates cleanly, maintaining perfect thematic cohesion (Deep Space Dark glassmorphic design system) and ensuring no regressions are introduced in existing mock page behaviors.
|
|
1969
|
+
|
|
1970
|
+
---
|
|
1971
|
+
|
|
1972
|
+
# VISUAL AI STAGING RECIPE
|
|
1973
|
+
- **Target Save Path**: \`${fileSavePath}\`
|
|
1974
|
+
- **Generated Timestamp**: \`${now.toISOString()}\`
|
|
1975
|
+
|
|
1976
|
+
=========================================
|
|
1977
|
+
DETALLE DE MODIFICACIONES REQUERIDAS (ELEMENT VISUAL STAGING)
|
|
1978
|
+
=========================================
|
|
1979
|
+
`;
|
|
1980
|
+
|
|
1981
|
+
let modificationsCount = 0;
|
|
1982
|
+
let insertionsCount = 0;
|
|
1983
|
+
let insertionsText = `
|
|
1984
|
+
=========================================
|
|
1985
|
+
SPATIAL ANNOTATIONS & INSERTS DETAILS
|
|
1986
|
+
=========================================
|
|
1987
|
+
`;
|
|
1988
|
+
|
|
1989
|
+
state.stagedChanges.forEach((changeData, selector) => {
|
|
1990
|
+
if (changeData.type === 'insertion') {
|
|
1991
|
+
insertionsCount++;
|
|
1992
|
+
const hasVoiceNote = !!changeData.voiceNote;
|
|
1993
|
+
|
|
1994
|
+
insertionsText += `
|
|
1995
|
+
### Insertion #${changeData.boxId}: ${changeData.template}
|
|
1996
|
+
- **Preset Type**: ${changeData.template}
|
|
1997
|
+
- **Resolved Nearest Parent Container Selector**: \`${changeData.parentSelector}\`
|
|
1998
|
+
- **Bounding Box Position (relative to canvas)**: x: ${Math.round(changeData.x)}px, y: ${Math.round(changeData.y)}px, width: ${Math.round(changeData.width)}px, height: ${Math.round(changeData.height)}px
|
|
1999
|
+
- **Developer Notes**: ${changeData.notes || 'Spatial annotation and layout modification defined via the Bounding Box canvas.'}
|
|
2000
|
+
`;
|
|
2001
|
+
if (hasVoiceNote) {
|
|
2002
|
+
insertionsText += `- **Voice Note File Reference**:
|
|
2003
|
+
- File: \`${changeData.voiceNote.absolutePath}\`
|
|
2004
|
+
- Filename: \`${changeData.voiceNote.filename}\`
|
|
2005
|
+
- URL: \`[${changeData.voiceNote.filename}](${changeData.voiceNote.urlPath})\`
|
|
2006
|
+
`;
|
|
2007
|
+
} else {
|
|
2008
|
+
insertionsText += `- **Voice Note File Reference**: None\n`;
|
|
2009
|
+
}
|
|
2010
|
+
insertionsText += `-----------------------------------------\n`;
|
|
2011
|
+
} else {
|
|
2012
|
+
let propertiesText = '';
|
|
2013
|
+
let hasStyleChanges = false;
|
|
2014
|
+
|
|
2015
|
+
for (const [prop, val] of Object.entries(changeData.currentStyles)) {
|
|
2016
|
+
const origVal = changeData.originalStyles[prop];
|
|
2017
|
+
if (origVal === val) continue;
|
|
2018
|
+
hasStyleChanges = true;
|
|
2019
|
+
propertiesText += ` - \`${prop}\`: "${val}" (original: "${origVal}")\n`;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const hasVoiceNote = !!changeData.voiceNote;
|
|
2023
|
+
if (!hasStyleChanges && !hasVoiceNote) return;
|
|
2024
|
+
|
|
2025
|
+
modificationsCount++;
|
|
2026
|
+
const tagName = changeData.element.tagName.toLowerCase();
|
|
2027
|
+
const textVal = changeData.element.innerText || changeData.element.textContent || '';
|
|
2028
|
+
const textSnippet = textVal.trim().slice(0, 100) || '(sin texto)';
|
|
2029
|
+
|
|
2030
|
+
markdown += `
|
|
2031
|
+
### Selector: \`${selector}\`
|
|
2032
|
+
- **Tag Type**: ${tagName}
|
|
2033
|
+
- **Texto actual**: "${textSnippet}"`;
|
|
2034
|
+
|
|
2035
|
+
if (hasStyleChanges) {
|
|
2036
|
+
markdown += `
|
|
2037
|
+
- **Estilos Visuales Requeridos (Staged)**:
|
|
2038
|
+
${propertiesText.trim()}`;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
markdown += `
|
|
2042
|
+
- **Developer Notes**: Modificación visual realizada en el Sandbox.`;
|
|
2043
|
+
|
|
2044
|
+
if (hasVoiceNote) {
|
|
2045
|
+
markdown += `
|
|
2046
|
+
- **Voice Note File Reference**:
|
|
2047
|
+
- **Archivo Local**: \`${changeData.voiceNote.absolutePath}\`
|
|
2048
|
+
- **URI de referencia**: [${changeData.voiceNote.filename}](${changeData.voiceNote.urlPath})`;
|
|
2049
|
+
} else {
|
|
2050
|
+
markdown += `
|
|
2051
|
+
- **Voice Note File Reference**: None`;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
markdown += `
|
|
2055
|
+
-----------------------------------------
|
|
2056
|
+
`;
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
let finalMarkdown = '';
|
|
2061
|
+
if (modificationsCount === 0 && insertionsCount > 0) {
|
|
2062
|
+
finalMarkdown = markdown + "\n*(No element style changes staged)*\n" + insertionsText;
|
|
2063
|
+
} else if (insertionsCount > 0) {
|
|
2064
|
+
finalMarkdown = markdown + insertionsText;
|
|
2065
|
+
} else {
|
|
2066
|
+
finalMarkdown = markdown;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
return finalMarkdown;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/**
|
|
2073
|
+
* Copies the compiled system pre-prompt recipe into Clipboard memory and triggers auto-save download
|
|
2074
|
+
*/
|
|
2075
|
+
function copyGeneratedPrompt() {
|
|
2076
|
+
const recipe = generateMarkdownRecipe();
|
|
2077
|
+
|
|
2078
|
+
navigator.clipboard.writeText(recipe).then(() => {
|
|
2079
|
+
let filename = '';
|
|
2080
|
+
if (state.stagedChanges.size > 0) {
|
|
2081
|
+
filename = triggerFeedbackDownload(recipe);
|
|
2082
|
+
}
|
|
2083
|
+
showToastNotification(filename);
|
|
2084
|
+
}).catch((err) => {
|
|
2085
|
+
console.error("Clipboard copy error:", err);
|
|
2086
|
+
let filename = '';
|
|
2087
|
+
if (state.stagedChanges.size > 0) {
|
|
2088
|
+
filename = triggerFeedbackDownload(recipe);
|
|
2089
|
+
}
|
|
2090
|
+
showToastNotification(filename, true);
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/* ==========================================================================
|
|
2095
|
+
FAB & WORKSPACE HELPERS
|
|
2096
|
+
========================================================================== */
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Toggles circular floating panel details visibility
|
|
2100
|
+
*/
|
|
2101
|
+
function toggleFabMenu() {
|
|
2102
|
+
const menu = document.getElementById('fab-menu');
|
|
2103
|
+
if (menu) {
|
|
2104
|
+
menu.classList.toggle('hidden');
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* Empty interface skeletons matching Milestones 3 & 4 layout contracts
|
|
2110
|
+
*/
|
|
2111
|
+
/* ==========================================================================
|
|
2112
|
+
BOUNDING BOX VECTOR OVERLAY & DRAWING SYSTEM
|
|
2113
|
+
========================================================================== */
|
|
2114
|
+
|
|
2115
|
+
let isDrawing = false;
|
|
2116
|
+
let startX = 0;
|
|
2117
|
+
let startY = 0;
|
|
2118
|
+
let tempRect = null;
|
|
2119
|
+
let nextBoxId = 1;
|
|
2120
|
+
let pendingBoxData = null;
|
|
2121
|
+
|
|
2122
|
+
/**
|
|
2123
|
+
* Toggles Free-Zone Drawing Mode
|
|
2124
|
+
*/
|
|
2125
|
+
function toggleDrawingMode(forceState) {
|
|
2126
|
+
const targetState = forceState !== undefined ? forceState : !state.drawingMode;
|
|
2127
|
+
state.drawingMode = targetState;
|
|
2128
|
+
|
|
2129
|
+
const btnDrawing = document.getElementById('btn-drawing');
|
|
2130
|
+
const fab = document.getElementById('fab-trigger');
|
|
2131
|
+
const canvasOverlay = document.getElementById('canvas-overlay');
|
|
2132
|
+
|
|
2133
|
+
if (targetState) {
|
|
2134
|
+
// Disable Inspection Mode if active
|
|
2135
|
+
if (state.inspectionMode) {
|
|
2136
|
+
toggleInspectionMode(false);
|
|
2137
|
+
}
|
|
2138
|
+
if (btnDrawing) btnDrawing.classList.add('active');
|
|
2139
|
+
if (fab) {
|
|
2140
|
+
fab.classList.add('drawing-active');
|
|
2141
|
+
}
|
|
2142
|
+
if (canvasOverlay) {
|
|
2143
|
+
canvasOverlay.classList.add('drawing-active');
|
|
2144
|
+
}
|
|
2145
|
+
} else {
|
|
2146
|
+
if (btnDrawing) btnDrawing.classList.remove('active');
|
|
2147
|
+
if (fab) {
|
|
2148
|
+
fab.classList.remove('drawing-active');
|
|
2149
|
+
}
|
|
2150
|
+
if (canvasOverlay) {
|
|
2151
|
+
canvasOverlay.classList.remove('drawing-active');
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Clean drawing state
|
|
2155
|
+
isDrawing = false;
|
|
2156
|
+
if (tempRect) {
|
|
2157
|
+
tempRect.remove();
|
|
2158
|
+
tempRect = null;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// Render or clear boxes accordingly
|
|
2163
|
+
renderBoundingBoxes();
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/**
|
|
2167
|
+
* Finds the nearest container element inside #mock-page sifting upward
|
|
2168
|
+
*/
|
|
2169
|
+
function findNearestParentContainer(element) {
|
|
2170
|
+
const mockPage = document.getElementById('mock-page');
|
|
2171
|
+
if (!mockPage) return null;
|
|
2172
|
+
|
|
2173
|
+
let current = element;
|
|
2174
|
+
while (current && current !== document.documentElement) {
|
|
2175
|
+
if (current === mockPage) {
|
|
2176
|
+
return mockPage;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
const isSectionOrHeader = ['section', 'header', 'nav', 'article', 'aside', 'footer'].includes(current.tagName.toLowerCase());
|
|
2180
|
+
const hasContainerClass = Array.from(current.classList).some(cls =>
|
|
2181
|
+
cls.includes('card') ||
|
|
2182
|
+
cls.includes('grid') ||
|
|
2183
|
+
cls.includes('hero') ||
|
|
2184
|
+
cls.includes('section') ||
|
|
2185
|
+
cls.includes('container')
|
|
2186
|
+
);
|
|
2187
|
+
|
|
2188
|
+
if (isSectionOrHeader || hasContainerClass) {
|
|
2189
|
+
if (mockPage.contains(current)) {
|
|
2190
|
+
return current;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
current = current.parentElement;
|
|
2195
|
+
}
|
|
2196
|
+
return mockPage; // Fallback to mock-page itself
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Handles mouse release after drawing a bounding box
|
|
2201
|
+
*/
|
|
2202
|
+
function handleCompletedDraw(x, y, width, height, clientX, clientY) {
|
|
2203
|
+
const canvasOverlay = document.getElementById('canvas-overlay');
|
|
2204
|
+
if (!canvasOverlay) return;
|
|
2205
|
+
|
|
2206
|
+
// Calculate center of drawn rectangle in viewport coordinates
|
|
2207
|
+
const rect = canvasOverlay.getBoundingClientRect();
|
|
2208
|
+
const centerX = rect.left + x + width / 2;
|
|
2209
|
+
const centerY = rect.top + y + height / 2;
|
|
2210
|
+
|
|
2211
|
+
// Temporarily disable overlay pointer-events to query underneath
|
|
2212
|
+
const originalPointerEvents = canvasOverlay.style.pointerEvents;
|
|
2213
|
+
canvasOverlay.style.pointerEvents = 'none';
|
|
2214
|
+
|
|
2215
|
+
const elementUnder = document.elementFromPoint(centerX, centerY);
|
|
2216
|
+
canvasOverlay.style.pointerEvents = originalPointerEvents;
|
|
2217
|
+
|
|
2218
|
+
const parentContainer = findNearestParentContainer(elementUnder);
|
|
2219
|
+
const parentSelector = getUniqueSelector(parentContainer);
|
|
2220
|
+
|
|
2221
|
+
openDrawingModal(x, y, width, height, parentSelector);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* Opens modal to configure bounding box details
|
|
2226
|
+
*/
|
|
2227
|
+
function openDrawingModal(x, y, width, height, parentSelector) {
|
|
2228
|
+
pendingBoxData = { x, y, width, height, parentSelector };
|
|
2229
|
+
|
|
2230
|
+
const modal = document.getElementById('drawing-modal');
|
|
2231
|
+
const selectorDisplay = document.getElementById('modal-resolved-selector');
|
|
2232
|
+
const selectPreset = document.getElementById('modal-template-select');
|
|
2233
|
+
const notesTextarea = document.getElementById('modal-notes-textarea');
|
|
2234
|
+
|
|
2235
|
+
if (selectorDisplay) {
|
|
2236
|
+
selectorDisplay.textContent = parentSelector;
|
|
2237
|
+
}
|
|
2238
|
+
if (selectPreset) {
|
|
2239
|
+
selectPreset.value = 'Carousel Slider';
|
|
2240
|
+
}
|
|
2241
|
+
if (notesTextarea) {
|
|
2242
|
+
notesTextarea.value = '';
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (modal) {
|
|
2246
|
+
modal.classList.remove('hidden');
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
/**
|
|
2251
|
+
* Closes modal and resets pending drawing data
|
|
2252
|
+
*/
|
|
2253
|
+
function closeDrawingModal() {
|
|
2254
|
+
const modal = document.getElementById('drawing-modal');
|
|
2255
|
+
if (modal) {
|
|
2256
|
+
modal.classList.add('hidden');
|
|
2257
|
+
}
|
|
2258
|
+
pendingBoxData = null;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Confirms bounding box insertion details, saving it to stagedChanges
|
|
2263
|
+
*/
|
|
2264
|
+
function confirmDrawingInsertion() {
|
|
2265
|
+
if (!pendingBoxData) return;
|
|
2266
|
+
|
|
2267
|
+
const selectPreset = document.getElementById('modal-template-select');
|
|
2268
|
+
const notesTextarea = document.getElementById('modal-notes-textarea');
|
|
2269
|
+
|
|
2270
|
+
const template = selectPreset ? selectPreset.value : 'Carousel Slider';
|
|
2271
|
+
const notes = notesTextarea ? notesTextarea.value : '';
|
|
2272
|
+
|
|
2273
|
+
const boxId = nextBoxId++;
|
|
2274
|
+
const uniqueKey = `[Insertion #${boxId}] inside ${pendingBoxData.parentSelector}`;
|
|
2275
|
+
|
|
2276
|
+
state.stagedChanges.set(uniqueKey, {
|
|
2277
|
+
type: 'insertion',
|
|
2278
|
+
boxId: boxId,
|
|
2279
|
+
parentSelector: pendingBoxData.parentSelector,
|
|
2280
|
+
template: template,
|
|
2281
|
+
notes: notes,
|
|
2282
|
+
x: pendingBoxData.x,
|
|
2283
|
+
y: pendingBoxData.y,
|
|
2284
|
+
width: pendingBoxData.width,
|
|
2285
|
+
height: pendingBoxData.height
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
closeDrawingModal();
|
|
2289
|
+
renderBoundingBoxes();
|
|
2290
|
+
renderStagedChanges();
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Cancels pending drawing insertion
|
|
2295
|
+
*/
|
|
2296
|
+
function cancelDrawingInsertion() {
|
|
2297
|
+
closeDrawingModal();
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Deletes a bounding box annotation from staged changes
|
|
2302
|
+
*/
|
|
2303
|
+
function deleteBoundingBox(boxId) {
|
|
2304
|
+
let targetKey = null;
|
|
2305
|
+
state.stagedChanges.forEach((changeData, key) => {
|
|
2306
|
+
if (changeData.type === 'insertion' && changeData.boxId === boxId) {
|
|
2307
|
+
targetKey = key;
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
|
|
2311
|
+
if (targetKey) {
|
|
2312
|
+
state.stagedChanges.delete(targetKey);
|
|
2313
|
+
renderBoundingBoxes();
|
|
2314
|
+
renderStagedChanges();
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
/**
|
|
2319
|
+
* Renders all current completed bounding boxes as vector elements on the SVG overlay
|
|
2320
|
+
*/
|
|
2321
|
+
function renderBoundingBoxes() {
|
|
2322
|
+
const canvasOverlay = document.getElementById('canvas-overlay');
|
|
2323
|
+
if (!canvasOverlay) return;
|
|
2324
|
+
|
|
2325
|
+
canvasOverlay.innerHTML = '';
|
|
2326
|
+
|
|
2327
|
+
// Only display bounding boxes when Drawing Mode is active
|
|
2328
|
+
if (!state.drawingMode) return;
|
|
2329
|
+
|
|
2330
|
+
state.stagedChanges.forEach((changeData) => {
|
|
2331
|
+
if (changeData.type !== 'insertion') return;
|
|
2332
|
+
|
|
2333
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
2334
|
+
g.setAttribute('class', 'completed-box-group');
|
|
2335
|
+
g.setAttribute('data-box-id', changeData.boxId);
|
|
2336
|
+
|
|
2337
|
+
// Bounding Box Rectangle
|
|
2338
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
2339
|
+
rect.setAttribute('class', 'drawing-rect-completed');
|
|
2340
|
+
rect.setAttribute('x', changeData.x);
|
|
2341
|
+
rect.setAttribute('y', changeData.y);
|
|
2342
|
+
rect.setAttribute('width', changeData.width);
|
|
2343
|
+
rect.setAttribute('height', changeData.height);
|
|
2344
|
+
|
|
2345
|
+
// Label text content
|
|
2346
|
+
const labelTextStr = `#${changeData.boxId}: ${changeData.template}`;
|
|
2347
|
+
|
|
2348
|
+
// Label text
|
|
2349
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
2350
|
+
text.setAttribute('class', 'drawing-label-text');
|
|
2351
|
+
text.setAttribute('x', changeData.x + 8);
|
|
2352
|
+
text.setAttribute('y', changeData.y + 11);
|
|
2353
|
+
text.style.textAnchor = 'start';
|
|
2354
|
+
text.style.dominantBaseline = 'middle';
|
|
2355
|
+
text.textContent = labelTextStr;
|
|
2356
|
+
|
|
2357
|
+
// Label Background (6.5px per character width approximation + margin)
|
|
2358
|
+
const labelWidth = labelTextStr.length * 6.5 + 12;
|
|
2359
|
+
const labelHeight = 18;
|
|
2360
|
+
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
2361
|
+
bg.setAttribute('class', 'drawing-label-bg');
|
|
2362
|
+
bg.setAttribute('x', changeData.x + 2);
|
|
2363
|
+
bg.setAttribute('y', changeData.y + 2);
|
|
2364
|
+
bg.setAttribute('width', labelWidth);
|
|
2365
|
+
bg.setAttribute('height', labelHeight);
|
|
2366
|
+
|
|
2367
|
+
g.appendChild(rect);
|
|
2368
|
+
g.appendChild(bg);
|
|
2369
|
+
g.appendChild(text);
|
|
2370
|
+
canvasOverlay.appendChild(g);
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/* ==========================================================================
|
|
2375
|
+
INITIALIZATION
|
|
2376
|
+
========================================================================== */
|
|
2377
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2378
|
+
initInspector();
|
|
2379
|
+
|
|
2380
|
+
// Bind dynamic event listeners for CSP compliance
|
|
2381
|
+
|
|
2382
|
+
// Header Buttons
|
|
2383
|
+
const btnInspect = document.getElementById('btn-inspect');
|
|
2384
|
+
if (btnInspect) {
|
|
2385
|
+
btnInspect.addEventListener('click', () => toggleInspectionMode());
|
|
2386
|
+
}
|
|
2387
|
+
const btnDrawing = document.getElementById('btn-drawing');
|
|
2388
|
+
if (btnDrawing) {
|
|
2389
|
+
btnDrawing.addEventListener('click', () => toggleDrawingMode());
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// Bounding Box Drawing Interaction
|
|
2393
|
+
const canvasOverlay = document.getElementById('canvas-overlay');
|
|
2394
|
+
if (canvasOverlay) {
|
|
2395
|
+
canvasOverlay.addEventListener('mousedown', (e) => {
|
|
2396
|
+
if (!state.drawingMode) return;
|
|
2397
|
+
isDrawing = true;
|
|
2398
|
+
const rect = canvasOverlay.getBoundingClientRect();
|
|
2399
|
+
startX = e.clientX - rect.left;
|
|
2400
|
+
startY = e.clientY - rect.top;
|
|
2401
|
+
|
|
2402
|
+
tempRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
2403
|
+
tempRect.setAttribute('class', 'drawing-rect-temp');
|
|
2404
|
+
tempRect.setAttribute('x', startX);
|
|
2405
|
+
tempRect.setAttribute('y', startY);
|
|
2406
|
+
tempRect.setAttribute('width', 0);
|
|
2407
|
+
tempRect.setAttribute('height', 0);
|
|
2408
|
+
canvasOverlay.appendChild(tempRect);
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
canvasOverlay.addEventListener('mousemove', (e) => {
|
|
2412
|
+
if (!isDrawing || !tempRect) return;
|
|
2413
|
+
const rect = canvasOverlay.getBoundingClientRect();
|
|
2414
|
+
const currentX = e.clientX - rect.left;
|
|
2415
|
+
const currentY = e.clientY - rect.top;
|
|
2416
|
+
|
|
2417
|
+
const x = Math.min(startX, currentX);
|
|
2418
|
+
const y = Math.min(startY, currentY);
|
|
2419
|
+
const width = Math.abs(startX - currentX);
|
|
2420
|
+
const height = Math.abs(startY - currentY);
|
|
2421
|
+
|
|
2422
|
+
tempRect.setAttribute('x', x);
|
|
2423
|
+
tempRect.setAttribute('y', y);
|
|
2424
|
+
tempRect.setAttribute('width', width);
|
|
2425
|
+
tempRect.setAttribute('height', height);
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
canvasOverlay.addEventListener('mouseup', (e) => {
|
|
2429
|
+
if (!isDrawing) return;
|
|
2430
|
+
isDrawing = false;
|
|
2431
|
+
if (tempRect) {
|
|
2432
|
+
tempRect.remove();
|
|
2433
|
+
tempRect = null;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
const rect = canvasOverlay.getBoundingClientRect();
|
|
2437
|
+
const currentX = e.clientX - rect.left;
|
|
2438
|
+
const currentY = e.clientY - rect.top;
|
|
2439
|
+
const x = Math.min(startX, currentX);
|
|
2440
|
+
const y = Math.min(startY, currentY);
|
|
2441
|
+
const width = Math.abs(startX - currentX);
|
|
2442
|
+
const height = Math.abs(startY - currentY);
|
|
2443
|
+
|
|
2444
|
+
if (width < 10 || height < 10) return;
|
|
2445
|
+
|
|
2446
|
+
handleCompletedDraw(x, y, width, height, e.clientX, e.clientY);
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
canvasOverlay.addEventListener('mouseleave', () => {
|
|
2450
|
+
if (isDrawing) {
|
|
2451
|
+
isDrawing = false;
|
|
2452
|
+
if (tempRect) {
|
|
2453
|
+
tempRect.remove();
|
|
2454
|
+
tempRect = null;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// Modal actions binding
|
|
2461
|
+
const btnCloseModal = document.getElementById('btn-close-modal');
|
|
2462
|
+
if (btnCloseModal) {
|
|
2463
|
+
btnCloseModal.addEventListener('click', cancelDrawingInsertion);
|
|
2464
|
+
}
|
|
2465
|
+
const btnCancelModal = document.getElementById('btn-cancel-modal');
|
|
2466
|
+
if (btnCancelModal) {
|
|
2467
|
+
btnCancelModal.addEventListener('click', cancelDrawingInsertion);
|
|
2468
|
+
}
|
|
2469
|
+
const btnConfirmModal = document.getElementById('btn-confirm-modal');
|
|
2470
|
+
if (btnConfirmModal) {
|
|
2471
|
+
btnConfirmModal.addEventListener('click', confirmDrawingInsertion);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// Sliders
|
|
2475
|
+
const sliders = [
|
|
2476
|
+
{ id: 'slider-padding', property: 'padding' },
|
|
2477
|
+
{ id: 'slider-margin', property: 'margin' },
|
|
2478
|
+
{ id: 'slider-width', property: 'width' },
|
|
2479
|
+
{ id: 'slider-height', property: 'height' },
|
|
2480
|
+
{ id: 'slider-borderRadius', property: 'borderRadius' },
|
|
2481
|
+
{ id: 'slider-fontSize', property: 'fontSize' }
|
|
2482
|
+
];
|
|
2483
|
+
sliders.forEach(s => {
|
|
2484
|
+
const el = document.getElementById(s.id);
|
|
2485
|
+
if (el) {
|
|
2486
|
+
el.addEventListener('input', (e) => {
|
|
2487
|
+
handleSliderChange(s.property, e.target.value);
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
// Color Sliders (Background)
|
|
2493
|
+
['bg-h', 'bg-s', 'bg-l'].forEach(id => {
|
|
2494
|
+
const el = document.getElementById(id);
|
|
2495
|
+
if (el) {
|
|
2496
|
+
el.addEventListener('input', () => handleColorChange('background', document));
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
|
|
2500
|
+
// Color Sliders (Text)
|
|
2501
|
+
['text-h', 'text-s', 'text-l'].forEach(id => {
|
|
2502
|
+
const el = document.getElementById(id);
|
|
2503
|
+
if (el) {
|
|
2504
|
+
el.addEventListener('input', () => handleColorChange('text', document));
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
// Text Content Input
|
|
2509
|
+
const textInput = document.getElementById('input-textContent');
|
|
2510
|
+
if (textInput) {
|
|
2511
|
+
textInput.addEventListener('input', (e) => {
|
|
2512
|
+
updateElementTextContent(e.target.value);
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// FAB trigger and menu items
|
|
2517
|
+
const fabTrigger = document.getElementById('fab-trigger');
|
|
2518
|
+
if (fabTrigger) {
|
|
2519
|
+
fabTrigger.addEventListener('click', toggleFabMenu);
|
|
2520
|
+
}
|
|
2521
|
+
const fabInspect = document.getElementById('fab-btn-inspect');
|
|
2522
|
+
if (fabInspect) {
|
|
2523
|
+
fabInspect.addEventListener('click', () => {
|
|
2524
|
+
toggleInspectionMode();
|
|
2525
|
+
toggleFabMenu();
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
const fabClear = document.getElementById('fab-btn-clear');
|
|
2529
|
+
if (fabClear) {
|
|
2530
|
+
fabClear.addEventListener('click', () => {
|
|
2531
|
+
clearAllStagedChanges();
|
|
2532
|
+
toggleFabMenu();
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
const fabCopy = document.getElementById('fab-btn-copy');
|
|
2536
|
+
if (fabCopy) {
|
|
2537
|
+
fabCopy.addEventListener('click', () => {
|
|
2538
|
+
copyGeneratedPrompt();
|
|
2539
|
+
toggleFabMenu();
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// Voice recorder buttons binding
|
|
2544
|
+
const btnVoiceRecord = document.getElementById('btn-voice-record');
|
|
2545
|
+
if (btnVoiceRecord) {
|
|
2546
|
+
btnVoiceRecord.addEventListener('click', () => {
|
|
2547
|
+
if (state.recordingMode) {
|
|
2548
|
+
stopAudioRecording();
|
|
2549
|
+
} else {
|
|
2550
|
+
startAudioRecording();
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const btnVoiceDelete = document.getElementById('btn-voice-delete');
|
|
2556
|
+
if (btnVoiceDelete) {
|
|
2557
|
+
btnVoiceDelete.addEventListener('click', deleteVoiceNote);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// Undock Panel Trigger Action
|
|
2561
|
+
const btnUndockPanel = document.getElementById('btn-undock-panel');
|
|
2562
|
+
if (btnUndockPanel) {
|
|
2563
|
+
btnUndockPanel.addEventListener('click', toggleUndockPanel);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// Sincronización al cerrar la pestaña principal (seguro para entorno de pruebas Node)
|
|
2567
|
+
if (typeof window.addEventListener === 'function') {
|
|
2568
|
+
window.addEventListener('beforeunload', () => {
|
|
2569
|
+
if (state.floatingWindow && !state.floatingWindow.closed) {
|
|
2570
|
+
state.floatingWindow.close();
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// Mock page navigation links
|
|
2576
|
+
document.querySelectorAll('.mock-nav-links a, .mock-nav-link').forEach(link => {
|
|
2577
|
+
link.addEventListener('click', (e) => {
|
|
2578
|
+
e.preventDefault();
|
|
2579
|
+
});
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
// Bind all necessary actions to window context for backward compatibility and test access
|
|
2583
|
+
window.state = state;
|
|
2584
|
+
window.selectElement = selectElement;
|
|
2585
|
+
window.getUniqueSelector = getUniqueSelector;
|
|
2586
|
+
window.toggleInspectionMode = toggleInspectionMode;
|
|
2587
|
+
window.toggleDrawingMode = toggleDrawingMode;
|
|
2588
|
+
window.handleSliderChange = handleSliderChange;
|
|
2589
|
+
window.handleColorChange = handleColorChange;
|
|
2590
|
+
window.revertAllChangesFor = revertAllChangesFor;
|
|
2591
|
+
window.clearAllStagedChanges = clearAllStagedChanges;
|
|
2592
|
+
window.copyGeneratedPrompt = copyGeneratedPrompt;
|
|
2593
|
+
window.generateMarkdownRecipe = generateMarkdownRecipe;
|
|
2594
|
+
window.triggerFeedbackDownload = triggerFeedbackDownload;
|
|
2595
|
+
window.showToastNotification = showToastNotification;
|
|
2596
|
+
window.toggleFabMenu = toggleFabMenu;
|
|
2597
|
+
window.confirmDrawingInsertion = confirmDrawingInsertion;
|
|
2598
|
+
window.cancelDrawingInsertion = cancelDrawingInsertion;
|
|
2599
|
+
window.deleteBoundingBox = deleteBoundingBox;
|
|
2600
|
+
window.renderBoundingBoxes = renderBoundingBoxes;
|
|
2601
|
+
window.startAudioRecording = startAudioRecording;
|
|
2602
|
+
window.stopAudioRecording = stopAudioRecording;
|
|
2603
|
+
window.deleteVoiceNote = deleteVoiceNote;
|
|
2604
|
+
window.updateVoicePanel = updateVoicePanel;
|
|
2605
|
+
window.updateVoiceBadges = updateVoiceBadges;
|
|
2606
|
+
window.filterSlidersByElementType = filterSlidersByElementType;
|
|
2607
|
+
window.renderHierarchyTree = renderHierarchyTree;
|
|
2608
|
+
window.toggleUndockPanel = toggleUndockPanel;
|
|
2609
|
+
window.undockPanel = undockPanel;
|
|
2610
|
+
window.dockPanel = dockPanel;
|
|
2611
|
+
window.syncFloatingWindowDOM = syncFloatingWindowDOM;
|
|
2612
|
+
});
|