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/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
+ });