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.
Files changed (3) hide show
  1. package/index.js +673 -0
  2. package/overlay.js +1248 -0
  3. 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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;">&times;</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
+ &lt;${esc(tag)}&gt;${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
+ })();