design-mode-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +673 -0
- package/overlay.js +1248 -0
- package/package.json +40 -0
package/overlay.js
ADDED
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Mode Overlay for Claude Code
|
|
3
|
+
* Injects a visual annotation layer onto any web page.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Hover highlight with box model visualization
|
|
7
|
+
* - Click to select + annotation input
|
|
8
|
+
* - Shift+click for multi-select
|
|
9
|
+
* - Source file mapping (React, Vue, Svelte)
|
|
10
|
+
* - Annotation pins on annotated elements
|
|
11
|
+
* - Annotations list panel with edit/delete
|
|
12
|
+
* - Responsive viewport switcher
|
|
13
|
+
* - "Copy to Claude" button
|
|
14
|
+
* - Ctrl+Shift+D to toggle on/off
|
|
15
|
+
*
|
|
16
|
+
* All state stored in window.__designMode for Claude to read.
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
// Prevent double-injection
|
|
22
|
+
if (window.__designMode && window.__designMode._initialized) {
|
|
23
|
+
// Toggle visibility if re-injected
|
|
24
|
+
window.__designMode._toggle();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Utilities ──────────────────────────────────────────────
|
|
29
|
+
const ESC_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
30
|
+
function esc(str) { return String(str).replace(/[&<>"']/g, c => ESC_MAP[c]); }
|
|
31
|
+
|
|
32
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
33
|
+
function transition(val) { return prefersReducedMotion ? 'none' : val; }
|
|
34
|
+
|
|
35
|
+
// ─── Animation Easing & Helpers ────────────────────────────
|
|
36
|
+
const EASE_OUT_QUART = 'cubic-bezier(0.25, 1, 0.5, 1)';
|
|
37
|
+
const EASE_OUT_EXPO = 'cubic-bezier(0.16, 1, 0.3, 1)';
|
|
38
|
+
|
|
39
|
+
function animateIn(el, from, to, duration) {
|
|
40
|
+
if (prefersReducedMotion) {
|
|
41
|
+
Object.assign(el.style, to);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
Object.assign(el.style, from);
|
|
45
|
+
requestAnimationFrame(() => {
|
|
46
|
+
requestAnimationFrame(() => {
|
|
47
|
+
el.style.transition = `opacity ${duration}ms ${EASE_OUT_QUART}, transform ${duration}ms ${EASE_OUT_QUART}`;
|
|
48
|
+
Object.assign(el.style, to);
|
|
49
|
+
setTimeout(() => { el.style.transition = ''; }, duration);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function animateOut(el, to, duration, onDone) {
|
|
55
|
+
if (prefersReducedMotion) {
|
|
56
|
+
Object.assign(el.style, to);
|
|
57
|
+
if (onDone) onDone();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
el.style.transition = `opacity ${duration}ms ${EASE_OUT_QUART}, transform ${duration}ms ${EASE_OUT_QUART}`;
|
|
61
|
+
Object.assign(el.style, to);
|
|
62
|
+
setTimeout(() => { el.style.transition = ''; if (onDone) onDone(); }, duration);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Constants ──────────────────────────────────────────────
|
|
66
|
+
const OVERLAY_Z = 2147483641;
|
|
67
|
+
const TOOLBAR_Z = 2147483642;
|
|
68
|
+
const PANEL_Z = 2147483643;
|
|
69
|
+
|
|
70
|
+
const SELECTOR_FOR_ELEMENTS = [
|
|
71
|
+
'a', 'button', 'input', 'select', 'textarea', 'img', 'video', 'audio',
|
|
72
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'div', 'section',
|
|
73
|
+
'article', 'nav', 'header', 'footer', 'main', 'aside', 'form',
|
|
74
|
+
'table', 'ul', 'ol', 'li', 'label', 'svg', 'canvas',
|
|
75
|
+
'[role]', '[data-testid]', '[class]'
|
|
76
|
+
].join(',');
|
|
77
|
+
|
|
78
|
+
// ─── Design Tokens ──────────────────────────────────────────
|
|
79
|
+
const T = {
|
|
80
|
+
// Surfaces
|
|
81
|
+
bg: '#0c0c0c',
|
|
82
|
+
bgElevated: '#161616',
|
|
83
|
+
bgToolbar: '#141414',
|
|
84
|
+
bgInset: '#0a0a0a',
|
|
85
|
+
// Text
|
|
86
|
+
text: '#d1d1d1',
|
|
87
|
+
textMuted: '#8a8a8a',
|
|
88
|
+
textBright: '#ffffff',
|
|
89
|
+
// Accent
|
|
90
|
+
accent: '#e8590c',
|
|
91
|
+
accentHover: 'rgba(232,89,12,0.10)',
|
|
92
|
+
accentSelected: 'rgba(232,89,12,0.16)',
|
|
93
|
+
accentFocus: 'rgba(232,89,12,0.35)',
|
|
94
|
+
accentFocusRing: '0 0 0 2px rgba(232,89,12,0.18)',
|
|
95
|
+
accentGlow: '0 0 8px rgba(232,89,12,0.15), 0 0 3px rgba(232,89,12,0.25)',
|
|
96
|
+
// Semantic
|
|
97
|
+
danger: '#c4382a',
|
|
98
|
+
success: '#2d8a4e',
|
|
99
|
+
// Borders
|
|
100
|
+
border: 'rgba(255,255,255,0.07)',
|
|
101
|
+
borderFaint: 'rgba(255,255,255,0.04)',
|
|
102
|
+
borderHighlight: 'rgba(255,255,255,0.10)',
|
|
103
|
+
hoverBg: 'rgba(255,255,255,0.06)',
|
|
104
|
+
hoverBgSubtle: 'rgba(255,255,255,0.03)',
|
|
105
|
+
// Box model overlays
|
|
106
|
+
boxMargin: 'rgba(214,133,110,0.2)',
|
|
107
|
+
boxPadding: 'rgba(110,214,162,0.2)',
|
|
108
|
+
boxBorder: 'rgba(214,198,110,0.3)',
|
|
109
|
+
// Shadows
|
|
110
|
+
shadowSm: '0 2px 8px rgba(0,0,0,0.5)',
|
|
111
|
+
shadowMd: '0 4px 20px rgba(0,0,0,0.6), 0 1px 3px rgba(0,0,0,0.4)',
|
|
112
|
+
shadowLg: '0 8px 32px rgba(0,0,0,0.7), 0 2px 6px rgba(0,0,0,0.4)',
|
|
113
|
+
shadowPin: '0 1px 4px rgba(0,0,0,0.5)',
|
|
114
|
+
shadowMenu: '0 8px 32px rgba(0,0,0,0.7)',
|
|
115
|
+
shadowInset: 'inset 0 1px 3px rgba(0,0,0,0.5)',
|
|
116
|
+
shadowDock: '0 8px 40px rgba(0,0,0,0.7), 0 2px 8px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.04)',
|
|
117
|
+
// Typography
|
|
118
|
+
font: "-apple-system,SF Pro Display,system-ui,sans-serif",
|
|
119
|
+
// Radius
|
|
120
|
+
radiusSm: '2px',
|
|
121
|
+
radius: '6px',
|
|
122
|
+
radiusLg: '12px',
|
|
123
|
+
radiusPill: '999px',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Shared style fragments
|
|
127
|
+
const DOCK_BTN = `padding:6px 14px;background:${T.bgInset};color:${T.textMuted};border:1px solid ${T.border};border-radius:${T.radiusPill};cursor:pointer;font-size:11px;font-weight:500;letter-spacing:0.3px;box-shadow:${T.shadowInset};`;
|
|
128
|
+
const DOCK_BTN_PRIMARY = `padding:6px 14px;background:${T.accent};color:${T.textBright};border:none;border-radius:${T.radiusPill};cursor:pointer;font-size:11px;font-weight:600;letter-spacing:0.3px;box-shadow:${T.accentGlow};`;
|
|
129
|
+
const MENU_ITEM = `display:block;width:100%;text-align:left;padding:7px 14px;background:none;color:${T.text};border:none;cursor:pointer;font-size:11px;font-family:inherit;`;
|
|
130
|
+
const PANEL_BASE = `border:1px solid ${T.border};border-radius:${T.radiusLg};box-shadow:${T.shadowLg};font-family:${T.font};`;
|
|
131
|
+
|
|
132
|
+
// ─── State ──────────────────────────────────────────────────
|
|
133
|
+
let elementCounter = 0;
|
|
134
|
+
let hoveredEl = null;
|
|
135
|
+
|
|
136
|
+
const state = {
|
|
137
|
+
_initialized: true,
|
|
138
|
+
active: true,
|
|
139
|
+
elements: new Map(),
|
|
140
|
+
annotations: [],
|
|
141
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
142
|
+
_toggle: null,
|
|
143
|
+
_destroy: null,
|
|
144
|
+
_refresh: null,
|
|
145
|
+
};
|
|
146
|
+
window.__designMode = state;
|
|
147
|
+
|
|
148
|
+
// ─── DOM Containers ─────────────────────────────────────────
|
|
149
|
+
const root = document.createElement('div');
|
|
150
|
+
root.id = '__design-mode-root';
|
|
151
|
+
root.style.cssText = 'all:initial;position:fixed;top:0;left:0;width:0;height:0;z-index:' + OVERLAY_Z + ';pointer-events:none;';
|
|
152
|
+
document.body.appendChild(root);
|
|
153
|
+
|
|
154
|
+
// Hover overlay
|
|
155
|
+
const hoverOverlay = document.createElement('div');
|
|
156
|
+
hoverOverlay.style.cssText = 'position:fixed;pointer-events:none;border:2px solid ' + T.accent + ';background:' + T.accentHover + ';transition:' + transition('all 0.1s ease') + ';display:none;z-index:' + OVERLAY_Z + ';';
|
|
157
|
+
root.appendChild(hoverOverlay);
|
|
158
|
+
|
|
159
|
+
// Box model overlays
|
|
160
|
+
const marginOverlay = document.createElement('div');
|
|
161
|
+
marginOverlay.style.cssText = 'position:fixed;pointer-events:none;background:' + T.boxMargin + ';display:none;z-index:' + (OVERLAY_Z - 2) + ';';
|
|
162
|
+
root.appendChild(marginOverlay);
|
|
163
|
+
|
|
164
|
+
const paddingOverlay = document.createElement('div');
|
|
165
|
+
paddingOverlay.style.cssText = 'position:fixed;pointer-events:none;background:' + T.boxPadding + ';display:none;z-index:' + (OVERLAY_Z - 1) + ';';
|
|
166
|
+
root.appendChild(paddingOverlay);
|
|
167
|
+
|
|
168
|
+
// Element info tooltip
|
|
169
|
+
const tooltip = document.createElement('div');
|
|
170
|
+
tooltip.style.cssText = 'position:fixed;pointer-events:none;background:' + T.bgElevated + ';color:' + T.text + ';font:11px/1.4 ' + T.font + ';padding:5px 12px;border-radius:' + T.radiusPill + ';border:1px solid ' + T.border + ';display:none;z-index:' + PANEL_Z + ';white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;box-shadow:' + T.shadowMd + ';';
|
|
171
|
+
root.appendChild(tooltip);
|
|
172
|
+
|
|
173
|
+
// Annotation input panel
|
|
174
|
+
const annotationPanel = document.createElement('div');
|
|
175
|
+
annotationPanel.setAttribute('role', 'dialog');
|
|
176
|
+
annotationPanel.setAttribute('aria-label', 'Annotate element');
|
|
177
|
+
annotationPanel.setAttribute('aria-modal', 'false');
|
|
178
|
+
annotationPanel.style.cssText = 'position:fixed;display:none;z-index:' + PANEL_Z + ';pointer-events:auto;background:' + T.bgElevated + ';' + PANEL_BASE + 'padding:16px;width:300px;';
|
|
179
|
+
annotationPanel.innerHTML = `
|
|
180
|
+
<div style="color:${T.text};font-size:13px;margin-bottom:10px;font-weight:600;letter-spacing:0.3px;" id="__dm-annotation-title">Annotate Element #0</div>
|
|
181
|
+
<textarea id="__dm-annotation-input" placeholder="Describe what to change..." aria-label="Annotation comment" style="width:100%;box-sizing:border-box;height:60px;background:${T.bgInset};color:${T.text};border:1px solid ${T.border};border-radius:${T.radius};padding:8px;font-size:13px;resize:vertical;font-family:inherit;outline:none;box-shadow:${T.shadowInset};"></textarea>
|
|
182
|
+
<div style="display:flex;gap:8px;margin-top:10px;">
|
|
183
|
+
<button id="__dm-annotation-save" style="${DOCK_BTN_PRIMARY}flex:1;">Save</button>
|
|
184
|
+
<button id="__dm-annotation-cancel" style="${DOCK_BTN}flex:1;">Cancel</button>
|
|
185
|
+
</div>
|
|
186
|
+
`;
|
|
187
|
+
root.appendChild(annotationPanel);
|
|
188
|
+
|
|
189
|
+
const annotationTitle = annotationPanel.querySelector('#__dm-annotation-title');
|
|
190
|
+
const annotationInput = annotationPanel.querySelector('#__dm-annotation-input');
|
|
191
|
+
const annotationSaveBtn = annotationPanel.querySelector('#__dm-annotation-save');
|
|
192
|
+
const annotationCancelBtn = annotationPanel.querySelector('#__dm-annotation-cancel');
|
|
193
|
+
let annotationTriggerEl = null; // element that opened the panel, for focus return
|
|
194
|
+
|
|
195
|
+
// Textarea focus glow
|
|
196
|
+
if (!prefersReducedMotion) {
|
|
197
|
+
annotationInput.style.transition = `border-color 200ms ${EASE_OUT_QUART}, box-shadow 200ms ${EASE_OUT_QUART}`;
|
|
198
|
+
annotationInput.addEventListener('focus', () => {
|
|
199
|
+
annotationInput.style.borderColor = T.accentFocus;
|
|
200
|
+
annotationInput.style.boxShadow = T.accentFocusRing;
|
|
201
|
+
});
|
|
202
|
+
annotationInput.addEventListener('blur', () => {
|
|
203
|
+
annotationInput.style.borderColor = T.border;
|
|
204
|
+
annotationInput.style.boxShadow = 'none';
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Focus trap: Tab cycles within textarea → Save → Cancel
|
|
209
|
+
const panelFocusables = [annotationInput, annotationSaveBtn, annotationCancelBtn];
|
|
210
|
+
annotationPanel.addEventListener('keydown', (e) => {
|
|
211
|
+
if (e.key !== 'Tab') return;
|
|
212
|
+
const idx = panelFocusables.indexOf(document.activeElement);
|
|
213
|
+
if (idx < 0) return;
|
|
214
|
+
if (e.shiftKey) {
|
|
215
|
+
if (idx === 0) { e.preventDefault(); panelFocusables[panelFocusables.length - 1].focus(); }
|
|
216
|
+
} else {
|
|
217
|
+
if (idx === panelFocusables.length - 1) { e.preventDefault(); panelFocusables[0].focus(); }
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ─── Toolbar ────────────────────────────────────────────────
|
|
222
|
+
const toolbar = document.createElement('div');
|
|
223
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
224
|
+
toolbar.setAttribute('aria-label', 'Design Mode controls');
|
|
225
|
+
toolbar.style.cssText = `
|
|
226
|
+
position:fixed;top:14px;right:14px;z-index:${TOOLBAR_Z};
|
|
227
|
+
pointer-events:auto;background:${T.bgToolbar};
|
|
228
|
+
border:1px solid ${T.borderHighlight};border-radius:${T.radiusPill};
|
|
229
|
+
padding:6px 8px;display:flex;gap:5px;align-items:center;
|
|
230
|
+
box-shadow:${T.shadowDock};
|
|
231
|
+
font-family:${T.font};font-size:11px;color:${T.text};
|
|
232
|
+
opacity:0;transform:translateY(-12px);user-select:none;
|
|
233
|
+
`;
|
|
234
|
+
toolbar.innerHTML = `
|
|
235
|
+
<span id="__dm-drag-handle" style="display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:50%;background:${T.bgInset};border:1px solid ${T.border};box-shadow:${T.shadowInset}, ${T.accentGlow};cursor:grab;flex-shrink:0;" title="Drag to reposition">
|
|
236
|
+
<span style="display:block;width:8px;height:8px;border-radius:50%;background:${T.accent};box-shadow:${T.accentGlow};"></span>
|
|
237
|
+
</span>
|
|
238
|
+
<div style="position:relative;">
|
|
239
|
+
<button id="__dm-btn-tools" title="Tools" aria-label="Tools menu" aria-expanded="false" style="${DOCK_BTN}">Tools ▾</button>
|
|
240
|
+
<div id="__dm-tools-menu" style="display:none;position:absolute;top:calc(100% + 8px);right:0;background:${T.bgElevated};border:1px solid ${T.borderHighlight};border-radius:${T.radiusLg};padding:4px 0;min-width:160px;box-shadow:${T.shadowMenu};z-index:1;">
|
|
241
|
+
<button id="__dm-btn-refresh" style="${MENU_ITEM}" title="Re-scan elements">Refresh elements</button>
|
|
242
|
+
<div style="height:1px;background:${T.borderFaint};margin:4px 0;"></div>
|
|
243
|
+
<button id="__dm-btn-375" style="${MENU_ITEM}" title="Mobile 375px">Mobile — 375px</button>
|
|
244
|
+
<button id="__dm-btn-768" style="${MENU_ITEM}" title="Tablet 768px">Tablet — 768px</button>
|
|
245
|
+
<button id="__dm-btn-1280" style="${MENU_ITEM}" title="Desktop 1280px">Desktop — 1280px</button>
|
|
246
|
+
<button id="__dm-btn-reset" style="${MENU_ITEM}" title="Reset viewport">Reset viewport</button>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<button id="__dm-btn-list" title="Show annotations list" aria-label="Show annotations list" style="${DOCK_BTN}">Notes <span id="__dm-count" aria-label="annotation count" style="color:${T.accent};">0</span></button>
|
|
250
|
+
<button id="__dm-btn-copy" title="Copy annotations to clipboard" aria-label="Copy annotations to clipboard" style="${DOCK_BTN_PRIMARY}">Copy to Claude</button>
|
|
251
|
+
<button id="__dm-btn-toggle" title="Toggle Design Mode (Ctrl+Shift+D)" aria-label="Toggle Design Mode" style="${DOCK_BTN}color:${T.textMuted};">Hide</button>
|
|
252
|
+
`;
|
|
253
|
+
root.appendChild(toolbar);
|
|
254
|
+
|
|
255
|
+
// ─── Toolbar Drag ──────────────────────────────────────────
|
|
256
|
+
{
|
|
257
|
+
const handle = toolbar.querySelector('#__dm-drag-handle');
|
|
258
|
+
let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;
|
|
259
|
+
|
|
260
|
+
handle.addEventListener('mousedown', (e) => {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
dragging = true;
|
|
263
|
+
handle.style.cursor = 'grabbing';
|
|
264
|
+
const rect = toolbar.getBoundingClientRect();
|
|
265
|
+
// Switch from right-anchored to left-anchored positioning
|
|
266
|
+
toolbar.style.left = rect.left + 'px';
|
|
267
|
+
toolbar.style.top = rect.top + 'px';
|
|
268
|
+
toolbar.style.right = 'auto';
|
|
269
|
+
startX = e.clientX;
|
|
270
|
+
startY = e.clientY;
|
|
271
|
+
startLeft = rect.left;
|
|
272
|
+
startTop = rect.top;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
document.addEventListener('mousemove', (e) => {
|
|
276
|
+
if (!dragging) return;
|
|
277
|
+
const dx = e.clientX - startX;
|
|
278
|
+
const dy = e.clientY - startY;
|
|
279
|
+
const newLeft = Math.max(0, Math.min(window.innerWidth - toolbar.offsetWidth, startLeft + dx));
|
|
280
|
+
const newTop = Math.max(0, Math.min(window.innerHeight - toolbar.offsetHeight, startTop + dy));
|
|
281
|
+
toolbar.style.left = newLeft + 'px';
|
|
282
|
+
toolbar.style.top = newTop + 'px';
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
document.addEventListener('mouseup', () => {
|
|
286
|
+
if (!dragging) return;
|
|
287
|
+
dragging = false;
|
|
288
|
+
handle.style.cursor = 'grab';
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Pin Container (for annotation markers) ─────────────────
|
|
293
|
+
const pinContainer = document.createElement('div');
|
|
294
|
+
pinContainer.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:' + (OVERLAY_Z + 1) + ';';
|
|
295
|
+
root.appendChild(pinContainer);
|
|
296
|
+
|
|
297
|
+
// ─── Annotations List Panel ─────────────────────────────────
|
|
298
|
+
const listPanel = document.createElement('div');
|
|
299
|
+
listPanel.setAttribute('role', 'region');
|
|
300
|
+
listPanel.setAttribute('aria-label', 'Annotations list');
|
|
301
|
+
listPanel.style.cssText = `
|
|
302
|
+
position:fixed;top:60px;right:14px;z-index:${PANEL_Z};
|
|
303
|
+
pointer-events:auto;background:${T.bgElevated};${PANEL_BASE}
|
|
304
|
+
width:320px;max-height:60vh;overflow-y:auto;display:none;
|
|
305
|
+
`;
|
|
306
|
+
listPanel.innerHTML = `
|
|
307
|
+
<div style="padding:10px 14px;border-bottom:1px solid ${T.border};display:flex;justify-content:space-between;align-items:center;">
|
|
308
|
+
<span style="color:${T.text};font-size:13px;font-weight:600;letter-spacing:0.3px;">Annotations</span>
|
|
309
|
+
<button id="__dm-list-close" aria-label="Close annotations list" style="background:none;border:none;color:${T.textMuted};cursor:pointer;font-size:16px;line-height:1;">×</button>
|
|
310
|
+
</div>
|
|
311
|
+
<div id="__dm-list-body" style="padding:0;"></div>
|
|
312
|
+
<div id="__dm-list-empty" style="padding:20px;text-align:center;color:${T.textMuted};font-size:12px;">No annotations yet. Click an element to annotate it.</div>
|
|
313
|
+
`;
|
|
314
|
+
root.appendChild(listPanel);
|
|
315
|
+
|
|
316
|
+
const listBody = listPanel.querySelector('#__dm-list-body');
|
|
317
|
+
const listEmpty = listPanel.querySelector('#__dm-list-empty');
|
|
318
|
+
const countBadge = toolbar.querySelector('#__dm-count');
|
|
319
|
+
|
|
320
|
+
// ─── Source File Mapping ────────────────────────────────────
|
|
321
|
+
function getSourceInfo(el) {
|
|
322
|
+
// React (dev mode)
|
|
323
|
+
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
|
|
324
|
+
if (fiberKey) {
|
|
325
|
+
let fiber = el[fiberKey];
|
|
326
|
+
// Walk up to find a named component
|
|
327
|
+
while (fiber) {
|
|
328
|
+
if (fiber.type && typeof fiber.type === 'function') {
|
|
329
|
+
const name = fiber.type.displayName || fiber.type.name || null;
|
|
330
|
+
// Check for _debugSource (React 16+)
|
|
331
|
+
const source = fiber._debugSource || null;
|
|
332
|
+
return {
|
|
333
|
+
framework: 'react',
|
|
334
|
+
componentName: name,
|
|
335
|
+
fileName: source ? source.fileName : null,
|
|
336
|
+
lineNumber: source ? source.lineNumber : null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// Also check for forwardRef/memo wrappers
|
|
340
|
+
if (fiber.type && fiber.type.render) {
|
|
341
|
+
const name = fiber.type.render.displayName || fiber.type.render.name || null;
|
|
342
|
+
return {
|
|
343
|
+
framework: 'react',
|
|
344
|
+
componentName: name,
|
|
345
|
+
fileName: null,
|
|
346
|
+
lineNumber: null,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
fiber = fiber.return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Vue 2/3
|
|
354
|
+
if (el.__vue__) {
|
|
355
|
+
const vm = el.__vue__;
|
|
356
|
+
return {
|
|
357
|
+
framework: 'vue',
|
|
358
|
+
componentName: vm.$options.name || vm.$options._componentTag || null,
|
|
359
|
+
fileName: vm.$options.__file || null,
|
|
360
|
+
lineNumber: null,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (el.__vueParentComponent) {
|
|
364
|
+
const c = el.__vueParentComponent;
|
|
365
|
+
return {
|
|
366
|
+
framework: 'vue',
|
|
367
|
+
componentName: c.type?.name || c.type?.__name || null,
|
|
368
|
+
fileName: c.type?.__file || null,
|
|
369
|
+
lineNumber: null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Svelte
|
|
374
|
+
if (el.__svelte_meta) {
|
|
375
|
+
return {
|
|
376
|
+
framework: 'svelte',
|
|
377
|
+
componentName: el.__svelte_meta.component || null,
|
|
378
|
+
fileName: el.__svelte_meta.loc?.file || null,
|
|
379
|
+
lineNumber: el.__svelte_meta.loc?.line || null,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// data-source attribute (custom)
|
|
384
|
+
if (el.dataset && el.dataset.source) {
|
|
385
|
+
const parts = el.dataset.source.split(':');
|
|
386
|
+
return {
|
|
387
|
+
framework: 'custom',
|
|
388
|
+
componentName: null,
|
|
389
|
+
fileName: parts[0] || null,
|
|
390
|
+
lineNumber: parts[1] ? parseInt(parts[1]) : null,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─── Selector Generation ────────────────────────────────────
|
|
398
|
+
function getUniqueSelector(el) {
|
|
399
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
400
|
+
|
|
401
|
+
const parts = [];
|
|
402
|
+
let current = el;
|
|
403
|
+
while (current && current !== document.body && parts.length < 5) {
|
|
404
|
+
let selector = current.tagName.toLowerCase();
|
|
405
|
+
if (current.id) {
|
|
406
|
+
selector = '#' + CSS.escape(current.id);
|
|
407
|
+
parts.unshift(selector);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
if (current.className && typeof current.className === 'string') {
|
|
411
|
+
const classes = current.className.trim().split(/\s+/).filter(c => !c.startsWith('__dm-')).slice(0, 2);
|
|
412
|
+
if (classes.length) selector += '.' + classes.map(c => CSS.escape(c)).join('.');
|
|
413
|
+
}
|
|
414
|
+
// Add nth-child if needed for disambiguation
|
|
415
|
+
const parent = current.parentElement;
|
|
416
|
+
if (parent) {
|
|
417
|
+
const siblings = Array.from(parent.children).filter(c => c.tagName === current.tagName);
|
|
418
|
+
if (siblings.length > 1) {
|
|
419
|
+
const index = siblings.indexOf(current) + 1;
|
|
420
|
+
selector += ':nth-of-type(' + index + ')';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
parts.unshift(selector);
|
|
424
|
+
current = current.parentElement;
|
|
425
|
+
}
|
|
426
|
+
return parts.join(' > ');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─── Computed Style Extraction (single getComputedStyle call) ─
|
|
430
|
+
function getElementStyles(el, cs) {
|
|
431
|
+
return {
|
|
432
|
+
styles: {
|
|
433
|
+
display: cs.display,
|
|
434
|
+
position: cs.position,
|
|
435
|
+
width: cs.width,
|
|
436
|
+
height: cs.height,
|
|
437
|
+
margin: cs.margin,
|
|
438
|
+
padding: cs.padding,
|
|
439
|
+
border: cs.border,
|
|
440
|
+
background: cs.backgroundColor,
|
|
441
|
+
color: cs.color,
|
|
442
|
+
fontSize: cs.fontSize,
|
|
443
|
+
fontWeight: cs.fontWeight,
|
|
444
|
+
fontFamily: cs.fontFamily,
|
|
445
|
+
lineHeight: cs.lineHeight,
|
|
446
|
+
textAlign: cs.textAlign,
|
|
447
|
+
flexDirection: cs.flexDirection,
|
|
448
|
+
justifyContent: cs.justifyContent,
|
|
449
|
+
alignItems: cs.alignItems,
|
|
450
|
+
gap: cs.gap,
|
|
451
|
+
overflow: cs.overflow,
|
|
452
|
+
opacity: cs.opacity,
|
|
453
|
+
borderRadius: cs.borderRadius,
|
|
454
|
+
boxShadow: cs.boxShadow,
|
|
455
|
+
zIndex: cs.zIndex,
|
|
456
|
+
},
|
|
457
|
+
boxModel: {
|
|
458
|
+
margin: {
|
|
459
|
+
top: parseFloat(cs.marginTop),
|
|
460
|
+
right: parseFloat(cs.marginRight),
|
|
461
|
+
bottom: parseFloat(cs.marginBottom),
|
|
462
|
+
left: parseFloat(cs.marginLeft),
|
|
463
|
+
},
|
|
464
|
+
padding: {
|
|
465
|
+
top: parseFloat(cs.paddingTop),
|
|
466
|
+
right: parseFloat(cs.paddingRight),
|
|
467
|
+
bottom: parseFloat(cs.paddingBottom),
|
|
468
|
+
left: parseFloat(cs.paddingLeft),
|
|
469
|
+
},
|
|
470
|
+
border: {
|
|
471
|
+
top: parseFloat(cs.borderTopWidth),
|
|
472
|
+
right: parseFloat(cs.borderRightWidth),
|
|
473
|
+
bottom: parseFloat(cs.borderBottomWidth),
|
|
474
|
+
left: parseFloat(cs.borderLeftWidth),
|
|
475
|
+
},
|
|
476
|
+
content: {
|
|
477
|
+
width: el.offsetWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) - parseFloat(cs.borderLeftWidth) - parseFloat(cs.borderRightWidth),
|
|
478
|
+
height: el.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom) - parseFloat(cs.borderTopWidth) - parseFloat(cs.borderBottomWidth),
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ─── Element Scanning ──────────────────────────────────────
|
|
485
|
+
function isInteresting(el, rect) {
|
|
486
|
+
const tag = el.tagName.toLowerCase();
|
|
487
|
+
if (['a', 'button', 'input', 'select', 'textarea', 'img', 'video', 'audio', 'canvas', 'svg'].includes(tag)) return true;
|
|
488
|
+
if (/^h[1-6]$/.test(tag)) return true;
|
|
489
|
+
if (el.getAttribute('role')) return true;
|
|
490
|
+
if (el.dataset && el.dataset.testid) return true;
|
|
491
|
+
if (el.children.length === 0 && el.textContent.trim().length > 0 && el.textContent.trim().length < 200) return true;
|
|
492
|
+
if (el.onclick || el.getAttribute('tabindex')) return true;
|
|
493
|
+
if (['nav', 'header', 'footer', 'main', 'aside', 'article', 'section', 'form'].includes(tag)) return true;
|
|
494
|
+
if ((tag === 'div' || tag === 'span') && el.className && typeof el.className === 'string' && el.className.trim().length > 0) {
|
|
495
|
+
if (rect.width > 50 && rect.height > 20) return true;
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Shared dump helper for skills/agents to call instead of duplicating JS snippets
|
|
501
|
+
state._dump = function () {
|
|
502
|
+
const result = { annotations: [], viewport: state.viewport };
|
|
503
|
+
state.annotations.forEach(function (a) {
|
|
504
|
+
var el = state.elements.get(a.elementId);
|
|
505
|
+
result.annotations.push(Object.assign({}, a, {
|
|
506
|
+
styles: el ? el.styles : null,
|
|
507
|
+
boxModel: el ? el.boxModel : null,
|
|
508
|
+
classes: el ? el.classes : null,
|
|
509
|
+
text: el ? (el.text || '').slice(0, 100) : null,
|
|
510
|
+
}));
|
|
511
|
+
});
|
|
512
|
+
return result;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
function scanElements() {
|
|
516
|
+
state.elements.clear();
|
|
517
|
+
elementCounter = 0;
|
|
518
|
+
|
|
519
|
+
const allEls = document.querySelectorAll(SELECTOR_FOR_ELEMENTS);
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
|
|
522
|
+
// Pass 1: fast filter — only getBoundingClientRect + basic checks
|
|
523
|
+
// getComputedStyle is deferred to pass 2 for elements that pass geometry checks
|
|
524
|
+
const candidates = [];
|
|
525
|
+
allEls.forEach(el => {
|
|
526
|
+
if (seen.has(el)) return;
|
|
527
|
+
if (el.closest('#__design-mode-root')) return;
|
|
528
|
+
const rect = el.getBoundingClientRect();
|
|
529
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
530
|
+
if (!isInteresting(el, rect)) return;
|
|
531
|
+
seen.add(el);
|
|
532
|
+
candidates.push({ el, rect });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Pass 2: expensive style reads only on candidates
|
|
536
|
+
candidates.forEach(({ el, rect }) => {
|
|
537
|
+
const cs = window.getComputedStyle(el);
|
|
538
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return;
|
|
539
|
+
|
|
540
|
+
elementCounter++;
|
|
541
|
+
const id = elementCounter;
|
|
542
|
+
|
|
543
|
+
const sourceInfo = getSourceInfo(el);
|
|
544
|
+
const { styles, boxModel } = getElementStyles(el, cs);
|
|
545
|
+
state.elements.set(id, {
|
|
546
|
+
_el: el,
|
|
547
|
+
selector: getUniqueSelector(el),
|
|
548
|
+
tagName: el.tagName.toLowerCase(),
|
|
549
|
+
classes: el.className && typeof el.className === 'string' ? el.className.trim().split(/\s+/) : [],
|
|
550
|
+
id: el.id || null,
|
|
551
|
+
text: (el.textContent || '').trim().slice(0, 100),
|
|
552
|
+
styles: styles,
|
|
553
|
+
boxModel: boxModel,
|
|
554
|
+
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
|
|
555
|
+
sourceFile: sourceInfo?.fileName || null,
|
|
556
|
+
componentName: sourceInfo?.componentName || null,
|
|
557
|
+
framework: sourceInfo?.framework || null,
|
|
558
|
+
sourceLineNumber: sourceInfo?.lineNumber || null,
|
|
559
|
+
annotation: null,
|
|
560
|
+
selected: false,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
el.__dmId = id;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
state.viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ─── Hover Handling ─────────────────────────────────────────
|
|
570
|
+
let hoverRafPending = false;
|
|
571
|
+
function handleMouseMove(e) {
|
|
572
|
+
if (!state.active) return;
|
|
573
|
+
if (e.target.closest('#__design-mode-root')) {
|
|
574
|
+
hoverOverlay.style.display = 'none';
|
|
575
|
+
marginOverlay.style.display = 'none';
|
|
576
|
+
paddingOverlay.style.display = 'none';
|
|
577
|
+
tooltip.style.display = 'none';
|
|
578
|
+
hoveredEl = null;
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const el = e.target;
|
|
583
|
+
if (el === hoveredEl) return;
|
|
584
|
+
hoveredEl = el;
|
|
585
|
+
|
|
586
|
+
if (hoverRafPending) return;
|
|
587
|
+
hoverRafPending = true;
|
|
588
|
+
requestAnimationFrame(updateHoverOverlay);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function updateHoverOverlay() {
|
|
592
|
+
hoverRafPending = false;
|
|
593
|
+
const el = hoveredEl;
|
|
594
|
+
if (!el || !el.isConnected) return;
|
|
595
|
+
|
|
596
|
+
const rect = el.getBoundingClientRect();
|
|
597
|
+
const cs = window.getComputedStyle(el);
|
|
598
|
+
|
|
599
|
+
hoverOverlay.style.display = 'block';
|
|
600
|
+
hoverOverlay.style.left = rect.left + 'px';
|
|
601
|
+
hoverOverlay.style.top = rect.top + 'px';
|
|
602
|
+
hoverOverlay.style.width = rect.width + 'px';
|
|
603
|
+
hoverOverlay.style.height = rect.height + 'px';
|
|
604
|
+
|
|
605
|
+
const mt = parseFloat(cs.marginTop), mr = parseFloat(cs.marginRight);
|
|
606
|
+
const mb = parseFloat(cs.marginBottom), ml = parseFloat(cs.marginLeft);
|
|
607
|
+
marginOverlay.style.display = 'block';
|
|
608
|
+
marginOverlay.style.left = (rect.left - ml) + 'px';
|
|
609
|
+
marginOverlay.style.top = (rect.top - mt) + 'px';
|
|
610
|
+
marginOverlay.style.width = (rect.width + ml + mr) + 'px';
|
|
611
|
+
marginOverlay.style.height = (rect.height + mt + mb) + 'px';
|
|
612
|
+
|
|
613
|
+
const pt = parseFloat(cs.paddingTop), pr = parseFloat(cs.paddingRight);
|
|
614
|
+
const pb = parseFloat(cs.paddingBottom), pl = parseFloat(cs.paddingLeft);
|
|
615
|
+
const bt = parseFloat(cs.borderTopWidth), br2 = parseFloat(cs.borderRightWidth);
|
|
616
|
+
const bb = parseFloat(cs.borderBottomWidth), bl = parseFloat(cs.borderLeftWidth);
|
|
617
|
+
paddingOverlay.style.display = 'block';
|
|
618
|
+
paddingOverlay.style.left = (rect.left + bl) + 'px';
|
|
619
|
+
paddingOverlay.style.top = (rect.top + bt) + 'px';
|
|
620
|
+
paddingOverlay.style.width = (rect.width - bl - br2) + 'px';
|
|
621
|
+
paddingOverlay.style.height = (rect.height - bt - bb) + 'px';
|
|
622
|
+
|
|
623
|
+
// Tooltip
|
|
624
|
+
const tag = el.tagName.toLowerCase();
|
|
625
|
+
const id = el.id ? '#' + el.id : '';
|
|
626
|
+
const cls = el.className && typeof el.className === 'string'
|
|
627
|
+
? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.')
|
|
628
|
+
: '';
|
|
629
|
+
const dmId = el.__dmId ? ` [#${el.__dmId}]` : '';
|
|
630
|
+
const dims = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
|
631
|
+
tooltip.textContent = `${tag}${id}${cls}${dmId} — ${dims}`;
|
|
632
|
+
tooltip.style.display = 'block';
|
|
633
|
+
tooltip.style.left = rect.left + 'px';
|
|
634
|
+
tooltip.style.top = Math.max(0, rect.top - 28) + 'px';
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ─── Click Handling (Select + Annotate) ─────────────────────
|
|
638
|
+
let currentAnnotationId = null;
|
|
639
|
+
|
|
640
|
+
function handleClick(e) {
|
|
641
|
+
if (!state.active) return;
|
|
642
|
+
if (e.target.closest('#__design-mode-root')) return;
|
|
643
|
+
|
|
644
|
+
e.preventDefault();
|
|
645
|
+
e.stopPropagation();
|
|
646
|
+
|
|
647
|
+
const el = e.target;
|
|
648
|
+
const dmId = el.__dmId;
|
|
649
|
+
|
|
650
|
+
if (!dmId || !state.elements.has(dmId)) return;
|
|
651
|
+
|
|
652
|
+
const entry = state.elements.get(dmId);
|
|
653
|
+
|
|
654
|
+
if (e.shiftKey) {
|
|
655
|
+
// Multi-select toggle
|
|
656
|
+
entry.selected = !entry.selected;
|
|
657
|
+
} else {
|
|
658
|
+
// Single select — deselect others
|
|
659
|
+
state.elements.forEach((ent, id) => {
|
|
660
|
+
if (id !== dmId) ent.selected = false;
|
|
661
|
+
});
|
|
662
|
+
entry.selected = true;
|
|
663
|
+
|
|
664
|
+
// Show annotation panel
|
|
665
|
+
showAnnotationPanel(dmId, el);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function showAnnotationPanel(dmId, el) {
|
|
670
|
+
currentAnnotationId = dmId;
|
|
671
|
+
annotationTriggerEl = document.activeElement; // remember what had focus
|
|
672
|
+
const rect = el.getBoundingClientRect();
|
|
673
|
+
const entry = state.elements.get(dmId);
|
|
674
|
+
|
|
675
|
+
annotationTitle.textContent = entry.componentName ? `${entry.componentName}` : `<${entry.tagName}>`;
|
|
676
|
+
annotationTitle.title = `Element #${dmId} — <${entry.tagName}>${entry.componentName ? ' (' + entry.componentName + ')' : ''}`;
|
|
677
|
+
annotationInput.value = entry.annotation || '';
|
|
678
|
+
|
|
679
|
+
// Position panel near element using actual panel dimensions
|
|
680
|
+
annotationPanel.style.display = 'block';
|
|
681
|
+
annotationPanel.style.opacity = '0';
|
|
682
|
+
annotationPanel.style.transform = 'scale(0.95)';
|
|
683
|
+
const panelW = annotationPanel.offsetWidth;
|
|
684
|
+
const panelH = annotationPanel.offsetHeight;
|
|
685
|
+
let left = rect.right + 12;
|
|
686
|
+
let top = rect.top;
|
|
687
|
+
if (left + panelW > window.innerWidth) left = rect.left - panelW - 12;
|
|
688
|
+
if (left < 0) left = 12;
|
|
689
|
+
if (top + panelH > window.innerHeight) top = window.innerHeight - panelH - 12;
|
|
690
|
+
|
|
691
|
+
annotationPanel.style.left = left + 'px';
|
|
692
|
+
annotationPanel.style.top = top + 'px';
|
|
693
|
+
|
|
694
|
+
animateIn(annotationPanel, { opacity: '0', transform: 'scale(0.97)' }, { opacity: '1', transform: 'scale(1)' }, 350);
|
|
695
|
+
setTimeout(() => annotationInput.focus(), 50);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function saveAnnotation() {
|
|
699
|
+
if (currentAnnotationId === null) return;
|
|
700
|
+
const comment = annotationInput.value.trim();
|
|
701
|
+
const entry = state.elements.get(currentAnnotationId);
|
|
702
|
+
|
|
703
|
+
if (entry && comment) {
|
|
704
|
+
entry.annotation = comment;
|
|
705
|
+
|
|
706
|
+
// Add to annotations list
|
|
707
|
+
const existing = state.annotations.findIndex(a => a.elementId === currentAnnotationId);
|
|
708
|
+
const annotationEntry = {
|
|
709
|
+
elementId: currentAnnotationId,
|
|
710
|
+
comment: comment,
|
|
711
|
+
selector: entry.selector,
|
|
712
|
+
tagName: entry.tagName,
|
|
713
|
+
componentName: entry.componentName,
|
|
714
|
+
sourceFile: entry.sourceFile,
|
|
715
|
+
sourceLineNumber: entry.sourceLineNumber,
|
|
716
|
+
timestamp: new Date().toISOString(),
|
|
717
|
+
};
|
|
718
|
+
if (existing >= 0) {
|
|
719
|
+
state.annotations[existing] = annotationEntry;
|
|
720
|
+
} else {
|
|
721
|
+
state.annotations.push(annotationEntry);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Create or update pin
|
|
725
|
+
if (!entry._pin) createPin(currentAnnotationId);
|
|
726
|
+
else entry._pin.title = comment;
|
|
727
|
+
} else if (entry && !comment) {
|
|
728
|
+
entry.annotation = null;
|
|
729
|
+
removePin(currentAnnotationId);
|
|
730
|
+
state.annotations = state.annotations.filter(a => a.elementId !== currentAnnotationId);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
closeAnnotationPanel();
|
|
734
|
+
renderAnnotationsList();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function closeAnnotationPanel() {
|
|
738
|
+
animateOut(annotationPanel, { opacity: '0', transform: 'scale(0.97)' }, 250, () => {
|
|
739
|
+
annotationPanel.style.display = 'none';
|
|
740
|
+
});
|
|
741
|
+
currentAnnotationId = null;
|
|
742
|
+
if (annotationTriggerEl && annotationTriggerEl.focus) annotationTriggerEl.focus();
|
|
743
|
+
annotationTriggerEl = null;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
// ─── Annotation Pins (visual markers on annotated elements) ──
|
|
748
|
+
function createPin(elementId) {
|
|
749
|
+
const entry = state.elements.get(elementId);
|
|
750
|
+
if (!entry || !entry._el.isConnected) return null;
|
|
751
|
+
const rect = entry._el.getBoundingClientRect();
|
|
752
|
+
const pin = document.createElement('div');
|
|
753
|
+
pin.className = '__dm-pin';
|
|
754
|
+
pin.dataset.dmId = elementId;
|
|
755
|
+
pin.setAttribute('role', 'button');
|
|
756
|
+
pin.setAttribute('aria-label', 'Edit annotation for element #' + elementId);
|
|
757
|
+
pin.setAttribute('tabindex', '0');
|
|
758
|
+
pin.style.cssText = `
|
|
759
|
+
position:fixed;left:${rect.right - 12}px;top:${rect.top - 12}px;
|
|
760
|
+
width:24px;height:24px;background:${T.accent};border:2px solid ${T.bg};
|
|
761
|
+
border-radius:50%;pointer-events:auto;cursor:pointer;
|
|
762
|
+
box-shadow:${T.shadowPin}, ${T.accentGlow};z-index:${OVERLAY_Z + 1};
|
|
763
|
+
display:flex;align-items:center;justify-content:center;
|
|
764
|
+
font:bold 10px ${T.font};color:white;
|
|
765
|
+
transition:${transition('transform 0.15s')};
|
|
766
|
+
`;
|
|
767
|
+
const pinIndex = state.annotations.findIndex(a => a.elementId === elementId);
|
|
768
|
+
pin.textContent = String(pinIndex >= 0 ? pinIndex + 1 : state.annotations.length + 1);
|
|
769
|
+
pin.title = entry.annotation || '';
|
|
770
|
+
const activatePin = (e) => {
|
|
771
|
+
e.stopPropagation();
|
|
772
|
+
showAnnotationPanel(elementId, entry._el);
|
|
773
|
+
};
|
|
774
|
+
pin.addEventListener('click', activatePin);
|
|
775
|
+
pin.addEventListener('keydown', (e) => {
|
|
776
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activatePin(e); }
|
|
777
|
+
});
|
|
778
|
+
pin.addEventListener('mouseenter', () => { pin.style.transform = 'scale(1.3)'; });
|
|
779
|
+
pin.addEventListener('mouseleave', () => { pin.style.transform = 'scale(1)'; });
|
|
780
|
+
pinContainer.appendChild(pin);
|
|
781
|
+
// Pop-in animation
|
|
782
|
+
animateIn(pin, { transform: 'scale(0)', opacity: '0' }, { transform: 'scale(1)', opacity: '1' }, 400);
|
|
783
|
+
entry._pin = pin;
|
|
784
|
+
return pin;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function removePin(elementId) {
|
|
788
|
+
const entry = state.elements.get(elementId);
|
|
789
|
+
if (entry && entry._pin) {
|
|
790
|
+
entry._pin.remove();
|
|
791
|
+
entry._pin = null;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function updatePinPositions() {
|
|
796
|
+
state.annotations.forEach((a) => {
|
|
797
|
+
const entry = state.elements.get(a.elementId);
|
|
798
|
+
if (entry && entry._pin && entry._el.isConnected) {
|
|
799
|
+
const rect = entry._el.getBoundingClientRect();
|
|
800
|
+
entry._pin.style.left = (rect.right - 12) + 'px';
|
|
801
|
+
entry._pin.style.top = (rect.top - 12) + 'px';
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ─── Annotations List Rendering ─────────────────────────────
|
|
807
|
+
function renderAnnotationsList() {
|
|
808
|
+
listBody.innerHTML = '';
|
|
809
|
+
countBadge.textContent = state.annotations.length;
|
|
810
|
+
|
|
811
|
+
if (state.annotations.length === 0) {
|
|
812
|
+
listEmpty.style.display = 'block';
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
listEmpty.style.display = 'none';
|
|
816
|
+
|
|
817
|
+
state.annotations.forEach((a, idx) => {
|
|
818
|
+
const entry = state.elements.get(a.elementId);
|
|
819
|
+
const item = document.createElement('div');
|
|
820
|
+
item.style.cssText = `
|
|
821
|
+
padding:10px 14px;border-bottom:1px solid ${T.borderFaint};
|
|
822
|
+
cursor:pointer;transition:${transition('background 0.1s')};
|
|
823
|
+
`;
|
|
824
|
+
item.addEventListener('mouseenter', () => { item.style.background = T.hoverBgSubtle; });
|
|
825
|
+
item.addEventListener('mouseleave', () => { item.style.background = 'none'; });
|
|
826
|
+
|
|
827
|
+
const tag = a.tagName || '?';
|
|
828
|
+
const comp = a.componentName ? ` (${a.componentName})` : '';
|
|
829
|
+
const text = entry ? (entry.text || '').slice(0, 40) : '';
|
|
830
|
+
|
|
831
|
+
item.innerHTML = `
|
|
832
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;">
|
|
833
|
+
<div style="flex:1;min-width:0;">
|
|
834
|
+
<div style="color:${T.textMuted};font-size:11px;font-weight:600;margin-bottom:3px;">
|
|
835
|
+
<${esc(tag)}>${esc(comp)}
|
|
836
|
+
</div>
|
|
837
|
+
<div style="color:${T.text};font-size:12px;margin-bottom:4px;word-break:break-word;">${esc(a.comment)}</div>
|
|
838
|
+
${text ? `<div style="color:${T.textMuted};font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">"${esc(text)}"</div>` : ''}
|
|
839
|
+
</div>
|
|
840
|
+
<div style="display:flex;gap:4px;flex-shrink:0;">
|
|
841
|
+
<button class="__dm-list-edit" data-idx="${idx}" style="background:${T.bgInset};color:${T.accent};border:1px solid ${T.border};border-radius:${T.radiusPill};padding:3px 8px;cursor:pointer;font-size:10px;box-shadow:${T.shadowInset};">Edit</button>
|
|
842
|
+
<button class="__dm-list-delete" data-idx="${idx}" style="background:${T.bgInset};color:${T.danger};border:1px solid ${T.border};border-radius:${T.radiusPill};padding:3px 8px;cursor:pointer;font-size:10px;box-shadow:${T.shadowInset};">Del</button>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
`;
|
|
846
|
+
|
|
847
|
+
// Click item to highlight element
|
|
848
|
+
item.addEventListener('click', (e) => {
|
|
849
|
+
if (e.target.closest('.__dm-list-edit') || e.target.closest('.__dm-list-delete')) return;
|
|
850
|
+
if (entry && entry._el.isConnected) {
|
|
851
|
+
entry._el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
852
|
+
// Flash the hover overlay on it
|
|
853
|
+
const rect = entry._el.getBoundingClientRect();
|
|
854
|
+
hoverOverlay.style.display = 'block';
|
|
855
|
+
hoverOverlay.style.left = rect.left + 'px';
|
|
856
|
+
hoverOverlay.style.top = rect.top + 'px';
|
|
857
|
+
hoverOverlay.style.width = rect.width + 'px';
|
|
858
|
+
hoverOverlay.style.height = rect.height + 'px';
|
|
859
|
+
hoverOverlay.style.borderColor = T.text;
|
|
860
|
+
setTimeout(() => { hoverOverlay.style.borderColor = T.accent; }, 1500);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
listBody.appendChild(item);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// Wire edit/delete buttons
|
|
868
|
+
listBody.querySelectorAll('.__dm-list-edit').forEach(btn => {
|
|
869
|
+
btn.addEventListener('click', (e) => {
|
|
870
|
+
e.stopPropagation();
|
|
871
|
+
const idx = parseInt(btn.dataset.idx);
|
|
872
|
+
const a = state.annotations[idx];
|
|
873
|
+
if (a) {
|
|
874
|
+
const entry = state.elements.get(a.elementId);
|
|
875
|
+
if (entry && entry._el.isConnected) {
|
|
876
|
+
entry._el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
877
|
+
setTimeout(() => showAnnotationPanel(a.elementId, entry._el), 300);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
listBody.querySelectorAll('.__dm-list-delete').forEach(btn => {
|
|
884
|
+
btn.addEventListener('click', (e) => {
|
|
885
|
+
e.stopPropagation();
|
|
886
|
+
const idx = parseInt(btn.dataset.idx);
|
|
887
|
+
const a = state.annotations[idx];
|
|
888
|
+
if (a) {
|
|
889
|
+
const entry = state.elements.get(a.elementId);
|
|
890
|
+
if (entry) {
|
|
891
|
+
entry.annotation = null;
|
|
892
|
+
entry.selected = false;
|
|
893
|
+
removePin(a.elementId);
|
|
894
|
+
}
|
|
895
|
+
state.annotations.splice(idx, 1);
|
|
896
|
+
renderAnnotationsList();
|
|
897
|
+
showToast('Annotation deleted');
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
let listPanelOpen = false;
|
|
904
|
+
function toggleListPanel() {
|
|
905
|
+
if (!listPanelOpen) {
|
|
906
|
+
listPanel.style.display = 'block';
|
|
907
|
+
animateIn(listPanel, { opacity: '0', transform: 'translateX(12px)' }, { opacity: '1', transform: 'translateX(0)' }, 400);
|
|
908
|
+
renderAnnotationsList();
|
|
909
|
+
listPanelOpen = true;
|
|
910
|
+
} else {
|
|
911
|
+
animateOut(listPanel, { opacity: '0', transform: 'translateX(12px)' }, 280, () => {
|
|
912
|
+
listPanel.style.display = 'none';
|
|
913
|
+
});
|
|
914
|
+
listPanelOpen = false;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ─── Copy to Claude ─────────────────────────────────────────
|
|
919
|
+
function copyToClipboard() {
|
|
920
|
+
if (state.annotations.length === 0) {
|
|
921
|
+
showToast('No annotations to copy');
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
let text = '## Design Mode Annotations\n\n';
|
|
926
|
+
text += `Viewport: ${state.viewport.width}x${state.viewport.height}\n\n`;
|
|
927
|
+
|
|
928
|
+
state.annotations.forEach(a => {
|
|
929
|
+
const entry = state.elements.get(a.elementId);
|
|
930
|
+
text += `### Element #${a.elementId}\n`;
|
|
931
|
+
text += `- **Tag**: \`<${a.tagName}>\`\n`;
|
|
932
|
+
text += `- **Selector**: \`${a.selector}\`\n`;
|
|
933
|
+
if (a.componentName) text += `- **Component**: ${a.componentName}\n`;
|
|
934
|
+
if (a.sourceFile) text += `- **Source**: ${a.sourceFile}${a.sourceLineNumber ? ':' + a.sourceLineNumber : ''}\n`;
|
|
935
|
+
if (entry) {
|
|
936
|
+
text += `- **Current styles**: ${JSON.stringify({
|
|
937
|
+
display: entry.styles.display,
|
|
938
|
+
fontSize: entry.styles.fontSize,
|
|
939
|
+
color: entry.styles.color,
|
|
940
|
+
background: entry.styles.background,
|
|
941
|
+
padding: entry.styles.padding,
|
|
942
|
+
margin: entry.styles.margin,
|
|
943
|
+
})}\n`;
|
|
944
|
+
}
|
|
945
|
+
text += `- **Comment**: ${a.comment}\n\n`;
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
text += '\n---\nGenerated by Design Mode plugin for Claude Code\n';
|
|
949
|
+
|
|
950
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
951
|
+
showToast('Copied ' + state.annotations.length + ' annotation(s) to clipboard!');
|
|
952
|
+
}).catch(() => {
|
|
953
|
+
const ta = document.createElement('textarea');
|
|
954
|
+
ta.value = text;
|
|
955
|
+
document.body.appendChild(ta);
|
|
956
|
+
try {
|
|
957
|
+
ta.select();
|
|
958
|
+
document.execCommand('copy');
|
|
959
|
+
showToast('Copied ' + state.annotations.length + ' annotation(s) to clipboard!');
|
|
960
|
+
} finally {
|
|
961
|
+
document.body.removeChild(ta);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ─── Toast Notification ─────────────────────────────────────
|
|
967
|
+
function showToast(msg) {
|
|
968
|
+
const toast = document.createElement('div');
|
|
969
|
+
toast.setAttribute('role', 'status');
|
|
970
|
+
toast.setAttribute('aria-live', 'polite');
|
|
971
|
+
toast.style.cssText = `
|
|
972
|
+
position:fixed;bottom:20px;left:50%;
|
|
973
|
+
background:${T.bgElevated};color:${T.text};padding:10px 20px;border-radius:${T.radiusPill};
|
|
974
|
+
border:1px solid ${T.border};
|
|
975
|
+
font:12px/1.4 ${T.font};z-index:${PANEL_Z + 1};
|
|
976
|
+
box-shadow:${T.shadowMd};
|
|
977
|
+
pointer-events:none;opacity:0;transform:translateX(-50%) translateY(8px);
|
|
978
|
+
`;
|
|
979
|
+
toast.textContent = msg;
|
|
980
|
+
root.appendChild(toast);
|
|
981
|
+
animateIn(toast,
|
|
982
|
+
{ opacity: '0', transform: 'translateX(-50%) translateY(8px)' },
|
|
983
|
+
{ opacity: '1', transform: 'translateX(-50%) translateY(0)' },
|
|
984
|
+
450
|
|
985
|
+
);
|
|
986
|
+
setTimeout(() => {
|
|
987
|
+
animateOut(toast, { opacity: '0', transform: 'translateX(-50%) translateY(8px)' }, 350, () => { toast.remove(); });
|
|
988
|
+
}, 2500);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ─── Viewport Resize ───────────────────────────────────────
|
|
992
|
+
function resizeViewport(width) {
|
|
993
|
+
// We store the requested width; Claude will use the browser MCP to actually resize
|
|
994
|
+
state.viewport.requestedWidth = width;
|
|
995
|
+
showToast('Viewport resize requested: ' + width + 'px — Claude will apply via browser tools');
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ─── Toggle Visibility ──────────────────────────────────────
|
|
999
|
+
const toggleBtn = toolbar.querySelector('#__dm-btn-toggle');
|
|
1000
|
+
function toggle() {
|
|
1001
|
+
state.active = !state.active;
|
|
1002
|
+
root.style.display = state.active ? '' : 'none';
|
|
1003
|
+
toggleBtn.style.background = T.bgInset;
|
|
1004
|
+
if (state.active) {
|
|
1005
|
+
toggleBtn.textContent = 'Hide';
|
|
1006
|
+
toggleBtn.style.color = T.textMuted;
|
|
1007
|
+
} else {
|
|
1008
|
+
toggleBtn.textContent = 'Show';
|
|
1009
|
+
toggleBtn.style.color = T.accent;
|
|
1010
|
+
}
|
|
1011
|
+
toolbar.style.display = 'flex';
|
|
1012
|
+
}
|
|
1013
|
+
state._toggle = toggle;
|
|
1014
|
+
|
|
1015
|
+
// ─── Destroy ────────────────────────────────────────────────
|
|
1016
|
+
function destroy() {
|
|
1017
|
+
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
1018
|
+
document.removeEventListener('click', handleClick, true);
|
|
1019
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
1020
|
+
window.removeEventListener('scroll', scheduleUpdateRects, true);
|
|
1021
|
+
window.removeEventListener('resize', scheduleUpdateRects);
|
|
1022
|
+
root.remove();
|
|
1023
|
+
delete window.__designMode;
|
|
1024
|
+
}
|
|
1025
|
+
state._destroy = destroy;
|
|
1026
|
+
state._refresh = scanElements;
|
|
1027
|
+
state._getSourceInfo = getSourceInfo;
|
|
1028
|
+
|
|
1029
|
+
// ─── Keyboard Element Navigation ─────────────────────────────
|
|
1030
|
+
let keyNavIndex = -1; // index into the sorted element IDs array
|
|
1031
|
+
|
|
1032
|
+
function getElementIds() {
|
|
1033
|
+
return Array.from(state.elements.keys());
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function highlightKeyNavElement(id) {
|
|
1037
|
+
const entry = state.elements.get(id);
|
|
1038
|
+
if (!entry || !entry._el.isConnected) return;
|
|
1039
|
+
entry._el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1040
|
+
const rect = entry._el.getBoundingClientRect();
|
|
1041
|
+
hoverOverlay.style.display = 'block';
|
|
1042
|
+
hoverOverlay.style.left = rect.left + 'px';
|
|
1043
|
+
hoverOverlay.style.top = rect.top + 'px';
|
|
1044
|
+
hoverOverlay.style.width = rect.width + 'px';
|
|
1045
|
+
hoverOverlay.style.height = rect.height + 'px';
|
|
1046
|
+
hoverOverlay.style.borderColor = T.text;
|
|
1047
|
+
|
|
1048
|
+
// Update tooltip
|
|
1049
|
+
const tag = entry.tagName;
|
|
1050
|
+
const elId = entry.id ? '#' + entry.id : '';
|
|
1051
|
+
const cls = entry.classes.length ? '.' + entry.classes.slice(0, 2).join('.') : '';
|
|
1052
|
+
tooltip.textContent = `${tag}${elId}${cls} [#${id}] — ${Math.round(rect.width)}x${Math.round(rect.height)} (keyboard)`;
|
|
1053
|
+
tooltip.style.display = 'block';
|
|
1054
|
+
tooltip.style.left = rect.left + 'px';
|
|
1055
|
+
tooltip.style.top = Math.max(0, rect.top - 28) + 'px';
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ─── Keyboard Shortcuts ─────────────────────────────────────
|
|
1059
|
+
function handleKeyDown(e) {
|
|
1060
|
+
// Ctrl+Shift+D to toggle
|
|
1061
|
+
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
|
1062
|
+
e.preventDefault();
|
|
1063
|
+
toggle();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
// Don't handle nav keys when focus is in annotation panel or other inputs
|
|
1067
|
+
const inPanel = annotationPanel.style.display !== 'none' && annotationPanel.contains(document.activeElement);
|
|
1068
|
+
|
|
1069
|
+
// Escape to close annotation panel
|
|
1070
|
+
if (e.key === 'Escape') {
|
|
1071
|
+
if (annotationPanel.style.display !== 'none') {
|
|
1072
|
+
closeAnnotationPanel();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
// Also clear keyboard nav highlight
|
|
1076
|
+
if (keyNavIndex >= 0) {
|
|
1077
|
+
keyNavIndex = -1;
|
|
1078
|
+
hoverOverlay.style.display = 'none';
|
|
1079
|
+
tooltip.style.display = 'none';
|
|
1080
|
+
hoverOverlay.style.borderColor = T.accent;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Enter in annotation saves
|
|
1084
|
+
if (e.key === 'Enter' && !e.shiftKey && inPanel) {
|
|
1085
|
+
if (document.activeElement === annotationInput) {
|
|
1086
|
+
e.preventDefault();
|
|
1087
|
+
saveAnnotation();
|
|
1088
|
+
}
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
// Arrow key navigation through elements (only when not in an input)
|
|
1092
|
+
if (!state.active || inPanel) return;
|
|
1093
|
+
if (document.activeElement && ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return;
|
|
1094
|
+
|
|
1095
|
+
const ids = getElementIds();
|
|
1096
|
+
if (ids.length === 0) return;
|
|
1097
|
+
|
|
1098
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
1099
|
+
e.preventDefault();
|
|
1100
|
+
keyNavIndex = (keyNavIndex + 1) % ids.length;
|
|
1101
|
+
highlightKeyNavElement(ids[keyNavIndex]);
|
|
1102
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
1103
|
+
e.preventDefault();
|
|
1104
|
+
keyNavIndex = (keyNavIndex - 1 + ids.length) % ids.length;
|
|
1105
|
+
highlightKeyNavElement(ids[keyNavIndex]);
|
|
1106
|
+
} else if (e.key === 'Enter' && keyNavIndex >= 0) {
|
|
1107
|
+
e.preventDefault();
|
|
1108
|
+
const id = ids[keyNavIndex];
|
|
1109
|
+
const entry = state.elements.get(id);
|
|
1110
|
+
if (entry && entry._el.isConnected) {
|
|
1111
|
+
state.elements.forEach((ent, eid) => { if (eid !== id) ent.selected = false; });
|
|
1112
|
+
entry.selected = true;
|
|
1113
|
+
showAnnotationPanel(id, entry._el);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ─── Event Binding ──────────────────────────────────────────
|
|
1119
|
+
document.addEventListener('mousemove', handleMouseMove, true);
|
|
1120
|
+
document.addEventListener('click', handleClick, true);
|
|
1121
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
1122
|
+
|
|
1123
|
+
// Toolbar + panel button wiring
|
|
1124
|
+
function onClick(el, fn) {
|
|
1125
|
+
el.addEventListener('click', (e) => { e.stopPropagation(); fn(); });
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Tools dropdown
|
|
1129
|
+
const toolsBtn = toolbar.querySelector('#__dm-btn-tools');
|
|
1130
|
+
const toolsMenu = toolbar.querySelector('#__dm-tools-menu');
|
|
1131
|
+
let toolsMenuOpen = false;
|
|
1132
|
+
|
|
1133
|
+
function toggleToolsMenu() {
|
|
1134
|
+
toolsMenuOpen = !toolsMenuOpen;
|
|
1135
|
+
toolsBtn.setAttribute('aria-expanded', String(toolsMenuOpen));
|
|
1136
|
+
if (toolsMenuOpen) {
|
|
1137
|
+
toolsMenu.style.display = 'block';
|
|
1138
|
+
animateIn(toolsMenu, { opacity: '0', transform: 'translateY(-4px)' }, { opacity: '1', transform: 'translateY(0)' }, 220);
|
|
1139
|
+
} else {
|
|
1140
|
+
animateOut(toolsMenu, { opacity: '0', transform: 'translateY(-4px)' }, 160, () => {
|
|
1141
|
+
toolsMenu.style.display = 'none';
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
onClick(toolsBtn, toggleToolsMenu);
|
|
1146
|
+
|
|
1147
|
+
// Close dropdown on outside click
|
|
1148
|
+
document.addEventListener('click', () => {
|
|
1149
|
+
if (toolsMenuOpen) toggleToolsMenu();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Menu item hover styles
|
|
1153
|
+
toolsMenu.querySelectorAll('button').forEach(item => {
|
|
1154
|
+
item.addEventListener('mouseenter', () => { item.style.background = T.hoverBg; });
|
|
1155
|
+
item.addEventListener('mouseleave', () => { item.style.background = 'none'; });
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
onClick(toolbar.querySelector('#__dm-btn-refresh'), () => {
|
|
1159
|
+
scanElements();
|
|
1160
|
+
showToast('Rescanned: ' + state.elements.size + ' elements found');
|
|
1161
|
+
if (toolsMenuOpen) toggleToolsMenu();
|
|
1162
|
+
});
|
|
1163
|
+
onClick(toolbar.querySelector('#__dm-btn-375'), () => { resizeViewport(375); if (toolsMenuOpen) toggleToolsMenu(); });
|
|
1164
|
+
onClick(toolbar.querySelector('#__dm-btn-768'), () => { resizeViewport(768); if (toolsMenuOpen) toggleToolsMenu(); });
|
|
1165
|
+
onClick(toolbar.querySelector('#__dm-btn-1280'), () => { resizeViewport(1280); if (toolsMenuOpen) toggleToolsMenu(); });
|
|
1166
|
+
onClick(toolbar.querySelector('#__dm-btn-reset'), () => {
|
|
1167
|
+
state.viewport.requestedWidth = null;
|
|
1168
|
+
showToast('Viewport reset requested');
|
|
1169
|
+
if (toolsMenuOpen) toggleToolsMenu();
|
|
1170
|
+
});
|
|
1171
|
+
onClick(toolbar.querySelector('#__dm-btn-list'), toggleListPanel);
|
|
1172
|
+
onClick(toolbar.querySelector('#__dm-btn-copy'), copyToClipboard);
|
|
1173
|
+
onClick(toggleBtn, toggle);
|
|
1174
|
+
onClick(annotationPanel.querySelector('#__dm-annotation-save'), saveAnnotation);
|
|
1175
|
+
onClick(annotationPanel.querySelector('#__dm-annotation-cancel'), closeAnnotationPanel);
|
|
1176
|
+
onClick(listPanel.querySelector('#__dm-list-close'), () => {
|
|
1177
|
+
if (listPanelOpen) {
|
|
1178
|
+
animateOut(listPanel, { opacity: '0', transform: 'translateX(12px)' }, 280, () => {
|
|
1179
|
+
listPanel.style.display = 'none';
|
|
1180
|
+
});
|
|
1181
|
+
listPanelOpen = false;
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// ─── Update element rects on scroll/resize (rAF-throttled) ──
|
|
1186
|
+
let rectRafPending = false;
|
|
1187
|
+
function updateElementRects() {
|
|
1188
|
+
state.elements.forEach((entry) => {
|
|
1189
|
+
const el = entry._el;
|
|
1190
|
+
if (!el.isConnected) return;
|
|
1191
|
+
const rect = el.getBoundingClientRect();
|
|
1192
|
+
entry.rect = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
|
|
1193
|
+
});
|
|
1194
|
+
updatePinPositions();
|
|
1195
|
+
if (hoveredEl && hoveredEl.isConnected) {
|
|
1196
|
+
updateHoverOverlay();
|
|
1197
|
+
} else if (hoveredEl) {
|
|
1198
|
+
hoverOverlay.style.display = 'none';
|
|
1199
|
+
marginOverlay.style.display = 'none';
|
|
1200
|
+
paddingOverlay.style.display = 'none';
|
|
1201
|
+
tooltip.style.display = 'none';
|
|
1202
|
+
hoveredEl = null;
|
|
1203
|
+
}
|
|
1204
|
+
state.viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
1205
|
+
}
|
|
1206
|
+
function scheduleUpdateRects() {
|
|
1207
|
+
if (rectRafPending) return;
|
|
1208
|
+
rectRafPending = true;
|
|
1209
|
+
requestAnimationFrame(() => {
|
|
1210
|
+
rectRafPending = false;
|
|
1211
|
+
updateElementRects();
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
window.addEventListener('scroll', scheduleUpdateRects, { passive: true, capture: true });
|
|
1216
|
+
window.addEventListener('resize', scheduleUpdateRects, { passive: true });
|
|
1217
|
+
|
|
1218
|
+
// ─── Button Hover Micro-interactions ─────────────────────────
|
|
1219
|
+
function addButtonHover(btn) {
|
|
1220
|
+
if (!btn || prefersReducedMotion) return;
|
|
1221
|
+
btn.style.transition = `background 150ms ${EASE_OUT_QUART}, color 150ms ${EASE_OUT_QUART}, transform 100ms ${EASE_OUT_QUART}`;
|
|
1222
|
+
btn.addEventListener('mouseenter', () => {
|
|
1223
|
+
const isCopy = btn.id === '__dm-btn-copy';
|
|
1224
|
+
if (!isCopy) btn.style.background = T.hoverBg;
|
|
1225
|
+
btn.style.color = T.text;
|
|
1226
|
+
});
|
|
1227
|
+
btn.addEventListener('mouseleave', () => {
|
|
1228
|
+
const isCopy = btn.id === '__dm-btn-copy';
|
|
1229
|
+
const isToggle = btn.id === '__dm-btn-toggle';
|
|
1230
|
+
btn.style.background = isCopy ? T.accent : T.bgInset;
|
|
1231
|
+
btn.style.color = isCopy ? T.textBright : (isToggle ? (state.active ? T.danger : T.success) : T.textMuted);
|
|
1232
|
+
});
|
|
1233
|
+
btn.addEventListener('mousedown', () => { btn.style.transform = 'scale(0.96)'; });
|
|
1234
|
+
btn.addEventListener('mouseup', () => { btn.style.transform = 'scale(1)'; });
|
|
1235
|
+
}
|
|
1236
|
+
// Apply hover to top-level toolbar buttons only (not dropdown menu items)
|
|
1237
|
+
['#__dm-btn-tools', '#__dm-btn-list', '#__dm-btn-copy', '#__dm-btn-toggle'].forEach(sel => {
|
|
1238
|
+
addButtonHover(toolbar.querySelector(sel));
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// ─── Initial Scan ───────────────────────────────────────────
|
|
1242
|
+
scanElements();
|
|
1243
|
+
showToast('Design Mode active — ' + state.elements.size + ' elements found. Ctrl+Shift+D to toggle.');
|
|
1244
|
+
|
|
1245
|
+
// ─── Toolbar Entrance Animation ─────────────────────────────
|
|
1246
|
+
animateIn(toolbar, { opacity: '0', transform: 'translateY(-12px)' }, { opacity: '1', transform: 'translateY(0)' }, 700);
|
|
1247
|
+
|
|
1248
|
+
})();
|