// ==UserScript== // @name Web Inspector // @namespace https://github.com/ibucon // @version 1.0.0 // @description 页面元素检查与标注工具 - 支持框架组件源码定位、快捷键自定义、剪贴板复制 // @author ibucon // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect 127.0.0.1 // @connect localhost // @license MIT // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/568941/Web%20Inspector.user.js // @updateURL https://update.greasyfork.icu/scripts/568941/Web%20Inspector.meta.js // ==/UserScript== ;(function() { 'use strict'; let menuCommandId = null; function isEnabledForSite() { const sites = GM_getValue('wi-enabledSites', {}); return sites[location.host] === true; } function setEnabledForSite(enabled) { const sites = GM_getValue('wi-enabledSites', {}); if (enabled) { sites[location.host] = true; } else { delete sites[location.host]; } GM_setValue('wi-enabledSites', sites); } function updateMenuCommand() { if (menuCommandId !== null) GM_unregisterMenuCommand(menuCommandId); const enabled = isEnabledForSite(); const label = enabled ? '关闭 Web Inspector' : '开启 Web Inspector'; menuCommandId = GM_registerMenuCommand(label, () => { setEnabledForSite(!enabled); updateMenuCommand(); location.reload(); }); } updateMenuCommand(); if (!isEnabledForSite()) return; // ===== index.js content below ===== if (document.getElementById('wi-toolbar')) return; // ========================================================================= // Lucide Icons (inline SVG, 24x24 viewBox) // ========================================================================= function lucide(w, paths) { return `${paths}`; } const icons = { // Search Plus (logo) logo: lucide(18, ''), // Crosshair (inspect) inspect: lucide(16, ''), // Play play: lucide(16, ''), // Pause pause: lucide(16, ''), // Pencil (for marker hover) pencil: lucide(12, ''), // Plus (for new annotation marker) plus: lucide(12, ''), // Eye (show markers) eye: lucide(16, ''), // Eye Off (hide markers) eyeOff: lucide(16, ''), // Send copy: lucide(16, ''), // Trash trash: lucide(16, ''), // Trash small (for popup delete button) trashSm: lucide(14, ''), // Settings settings: lucide(16, ''), // X (close) close: lucide(16, ''), // Sun (light mode) sun: lucide(16, ''), // Moon (dark mode) moon: lucide(16, ''), // Check small (for checkbox) checkSm: lucide(14, ''), // Chevron right (for nav link) chevronR: lucide(16, ''), // Chevron left (for back) chevronL: lucide(16, ''), // Terminal (console) — kept for future console: lucide(16, ''), // Globe (network) — kept for future network: lucide(16, ''), // Database (storage) — kept for future storage: lucide(16, ''), // Keyboard (shortcuts) keyboard: lucide(16, ''), }; // ========================================================================= // Settings // ========================================================================= const SETTINGS_KEY = 'wi-settings'; const COLOR_OPTIONS = [ { value: '#AF52DE', label: 'Purple' }, { value: '#3c82f7', label: 'Blue' }, { value: '#5AC8FA', label: 'Cyan' }, { value: '#34C759', label: 'Green' }, { value: '#FFD60A', label: 'Yellow' }, { value: '#FF9500', label: 'Orange' }, { value: '#FF3B30', label: 'Red' }, ]; const DEFAULT_SETTINGS = { annotationColor: '#3c82f7', autoClearAfterSend: false, blockInteractions: true, darkMode: true, webhookUrl: 'http://127.0.0.1:18765/inspect', webhooksEnabled: false, shortcuts: { inspect: { altKey: true, shiftKey: true, ctrlKey: false, metaKey: false, code: 'KeyI' }, copy: { altKey: true, shiftKey: true, ctrlKey: false, metaKey: false, code: 'KeyC' }, }, }; const SHORTCUT_LABELS = { inspect: '切换检查', copy: '复制标注' }; function loadSettings() { try { const stored = localStorage.getItem(SETTINGS_KEY); if (stored) { const parsed = JSON.parse(stored); if (!parsed.webhookUrl) delete parsed.webhookUrl; return { ...DEFAULT_SETTINGS, ...parsed }; } } catch {} return { ...DEFAULT_SETTINGS }; } function saveSettings(s) { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); } catch {} } let settings = loadSettings(); // ========================================================================= // Shortcut Utilities // ========================================================================= function matchShortcut(e, shortcut) { return e.altKey === shortcut.altKey && e.shiftKey === shortcut.shiftKey && e.ctrlKey === shortcut.ctrlKey && e.metaKey === shortcut.metaKey && e.code === shortcut.code; } function formatShortcut(shortcut) { const parts = []; if (shortcut.ctrlKey) parts.push('Ctrl'); if (shortcut.altKey) parts.push('Alt'); if (shortcut.shiftKey) parts.push('Shift'); if (shortcut.metaKey) parts.push('⌘'); const keyMap = { Backquote:'`', Minus:'-', Equal:'=', BracketLeft:'[', BracketRight:']', Backslash:'\\', Semicolon:';', Quote:"'", Comma:',', Period:'.', Slash:'/' }; const code = shortcut.code; if (code.startsWith('Key')) parts.push(code.slice(3)); else if (code.startsWith('Digit')) parts.push(code.slice(5)); else parts.push(keyMap[code] || code.replace('Arrow', '')); return parts.join('+'); } // ========================================================================= // Styles // ========================================================================= const style = document.createElement('style'); style.textContent = ` @keyframes wi-toolbar-enter { from { opacity: 0; transform: scale(0.5) rotate(90deg); } to { opacity: 1; transform: scale(1) rotate(0deg); } } @keyframes wi-controls-in { from { opacity: 0; filter: blur(10px); transform: scale(0.4); } to { opacity: 1; filter: blur(0); transform: scale(1); } } @keyframes wi-controls-out { from { opacity: 1; filter: blur(0); transform: scale(1); } to { opacity: 0; filter: blur(10px); transform: scale(0.4); } } @keyframes wi-highlight-in { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } } @keyframes wi-tooltip-in { from { opacity: 0; transform: scale(0.95) translateY(4px); } to { opacity: 1; transform: scale(1) translateY(0); } } @keyframes wi-info-in { from { opacity: 0; transform: translateY(10px) scale(0.95); filter: blur(5px); } to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } } @keyframes wi-info-out { from { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } to { opacity: 0; transform: translateY(10px) scale(0.95); filter: blur(5px); } } #wi-toolbar { position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; pointer-events: none; transition: left 0s, top 0s, right 0s, bottom 0s; } #wi-toolbar-container { user-select: none; display: flex; align-items: center; justify-content: center; background: #1a1a1a; color: #fff; border: none; box-shadow: 0 2px 8px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.1); pointer-events: auto; cursor: grab; transition: width 0.4s cubic-bezier(0.19,1,0.22,1), height 0.4s cubic-bezier(0.19,1,0.22,1), border-radius 0.4s cubic-bezier(0.19,1,0.22,1), transform 0.4s cubic-bezier(0.19,1,0.22,1); animation: wi-toolbar-enter 0.5s cubic-bezier(0.34,1.2,0.64,1) forwards; } #wi-toolbar-container.wi-dragging { cursor: grabbing; transition: width 0.4s cubic-bezier(0.19,1,0.22,1); } /* Collapsed state */ #wi-toolbar-container.wi-collapsed { width: 44px; height: 44px; border-radius: 22px; padding: 0; cursor: pointer; } #wi-toolbar-container.wi-collapsed:hover { background: #2a2a2a; } #wi-toolbar-container.wi-collapsed:active { transform: scale(0.95); } /* Expanded state */ #wi-toolbar-container.wi-expanded { height: 44px; border-radius: 1.5rem; padding: 0 6px; } /* Toggle icon (logo in collapsed) */ #wi-toggle-icon { position: absolute; display: flex; align-items: center; justify-content: center; transition: opacity 0.15s ease; } #wi-toggle-icon.wi-visible { opacity: 1; pointer-events: auto; } #wi-toggle-icon.wi-hidden { opacity: 0; pointer-events: none; } /* Controls row */ #wi-controls { display: flex; align-items: center; gap: 4px; transform-origin: right center; } #wi-controls.wi-visible { animation: wi-controls-in 0.35s cubic-bezier(0.19,1,0.22,1) forwards; pointer-events: auto; } #wi-controls.wi-hidden { animation: wi-controls-out 0.2s ease forwards; pointer-events: none; } /* Control buttons */ .wi-btn { position: relative; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 50%; border: none; background: transparent; color: rgba(255,255,255,0.85); transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease; padding: 0; } .wi-btn:hover { background: rgba(255,255,255,0.12); color: #fff; } .wi-btn:active { transform: scale(0.92); } .wi-btn[data-active="true"] { color: #3c82f7; background: rgba(60,130,247,0.25); } .wi-btn[data-active="paused"] { color: #f59e0b; background: rgba(245,158,11,0.2); } .wi-btn[data-danger]:hover { background: rgba(255,59,48,0.25); color: #ff3b30; } .wi-btn:disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; } /* Divider */ .wi-divider { width: 1px; height: 12px; background: rgba(255,255,255,0.15); margin: 0 2px; flex-shrink: 0; } /* Button tooltip */ .wi-btn-wrap { position: relative; display: flex; align-items: center; justify-content: center; } .wi-tooltip { position: absolute; bottom: calc(100% + 14px); left: 50%; transform: translateX(-50%) scale(0.95); padding: 6px 10px; background: #1a1a1a; color: rgba(255,255,255,0.9); font-size: 12px; font-weight: 500; border-radius: 8px; white-space: nowrap; opacity: 0; visibility: hidden; pointer-events: none; z-index: 2147483647; box-shadow: 0 2px 8px rgba(0,0,0,0.3); transition: opacity 0.135s ease, transform 0.135s ease, visibility 0.135s ease; } .wi-tooltip::after { content: ""; position: absolute; top: calc(100% - 4px); left: 50%; transform: translateX(-50%) rotate(45deg); width: 8px; height: 8px; background: #1a1a1a; border-radius: 0 0 2px 0; } .wi-btn-wrap:hover .wi-tooltip { opacity: 1; visibility: visible; transform: translateX(-50%) scale(1); transition-delay: 0.6s; } /* Tooltip below (when toolbar near top) */ #wi-toolbar.wi-tooltip-below .wi-tooltip { bottom: auto; top: calc(100% + 14px); transform: translateX(-50%) scale(0.95); } #wi-toolbar.wi-tooltip-below .wi-tooltip::after { top: -4px; bottom: auto; border-radius: 2px 0 0 0; } #wi-toolbar.wi-tooltip-below .wi-btn-wrap:hover .wi-tooltip { transform: translateX(-50%) scale(1); } /* ================================================================ */ /* Inspect Mode Overlay */ /* ================================================================ */ #wi-overlay { position: fixed; inset: 0; z-index: 2147483640; pointer-events: none; } #wi-overlay > * { pointer-events: auto; } /* Hover highlight box */ #wi-hover-highlight { position: fixed; border: 2px solid rgba(60,130,247,0.5); border-radius: 4px; background: rgba(60,130,247,0.04); pointer-events: none !important; box-sizing: border-box; will-change: opacity; animation: wi-highlight-in 0.12s ease-out forwards; } /* Hover element tooltip */ #wi-hover-tooltip { position: fixed; font-size: 11px; font-weight: 500; color: #fff; background: rgba(0,0,0,0.85); padding: 5px 10px; border-radius: 6px; pointer-events: none !important; white-space: nowrap; max-width: 360px; overflow: hidden; text-overflow: ellipsis; z-index: 2147483645; animation: wi-tooltip-in 0.1s ease-out forwards; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #wi-hover-tooltip .wi-hover-path { font-size: 10px; color: rgba(255,255,255,0.5); margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; } #wi-hover-tooltip .wi-hover-name { overflow: hidden; text-overflow: ellipsis; } #wi-hover-tooltip .wi-hover-size { font-size: 10px; color: rgba(255,255,255,0.45); margin-top: 2px; } /* Annotation marker dot */ .wi-marker { position: fixed; width: 22px; height: 22px; background: #3c82f7; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; transform: translate(-50%, -50%) scale(1); cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.2), inset 0 0 0 1px rgba(0,0,0,0.04); user-select: none; z-index: 2147483643; animation: wi-marker-in 0.25s cubic-bezier(0.22,1,0.36,1) both; } .wi-marker:hover { transform: translate(-50%,-50%) scale(1.1); } .wi-marker .wi-marker-num { display: flex; align-items: center; justify-content: center; } .wi-marker .wi-marker-edit { display: none; align-items: center; justify-content: center; } .wi-marker:hover .wi-marker-num { display: none; } .wi-marker:hover .wi-marker-edit { display: flex; } .wi-marker.wi-marker-exit { animation: wi-marker-out 0.2s ease-out both; pointer-events: none; } @keyframes wi-marker-in { from { opacity: 0; transform: translate(-50%,-50%) scale(0.3); } to { opacity: 1; transform: translate(-50%,-50%) scale(1); } } @keyframes wi-marker-out { from { opacity: 1; transform: translate(-50%,-50%) scale(1); } to { opacity: 0; transform: translate(-50%,-50%) scale(0.3); } } /* Annotation popup (matches agentation) */ @keyframes wi-popup-enter { from { opacity: 0; transform: translateX(-50%) scale(0.95) translateY(4px); } to { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); } } @keyframes wi-popup-exit { from { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); } to { opacity: 0; transform: translateX(-50%) scale(0.95) translateY(4px); } } @keyframes wi-popup-shake { 0%,100% { transform: translateX(-50%) scale(1) translateY(0) translateX(0); } 20% { transform: translateX(-50%) scale(1) translateY(0) translateX(-3px); } 40% { transform: translateX(-50%) scale(1) translateY(0) translateX(3px); } 60% { transform: translateX(-50%) scale(1) translateY(0) translateX(-2px); } 80% { transform: translateX(-50%) scale(1) translateY(0) translateX(2px); } } .wi-popup { position: fixed; transform: translateX(-50%); width: 280px; padding: 12px 16px 14px; background: #1a1a1a; border-radius: 16px; box-shadow: 0 4px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08); cursor: default; z-index: 2147483645; font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; will-change: transform, opacity; opacity: 0; } .wi-popup.wi-popup-enter { animation: wi-popup-enter 0.2s cubic-bezier(0.34,1.56,0.64,1) forwards; } .wi-popup.wi-popup-entered { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); } .wi-popup.wi-popup-exit { animation: wi-popup-exit 0.15s ease-in forwards; } .wi-popup.wi-popup-shake { animation: wi-popup-shake 0.25s ease-out; } .wi-popup-header { display: flex; align-items: center; margin-bottom: 9px; } .wi-popup-header-toggle { display: flex; align-items: center; gap: 4px; background: none; border: none; padding: 0; cursor: pointer; flex: 1; min-width: 0; text-align: left; } .wi-popup-element { font-size: 12px; font-weight: 400; color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } .wi-popup-chevron { color: rgba(255,255,255,0.5); transition: transform 0.25s cubic-bezier(0.16,1,0.3,1); flex-shrink: 0; } .wi-popup-chevron.wi-chevron-expanded { transform: rotate(90deg); } /* Computed styles accordion */ .wi-popup-styles-wrapper { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s cubic-bezier(0.16,1,0.3,1); } .wi-popup-styles-wrapper.wi-styles-expanded { grid-template-rows: 1fr; } .wi-popup-styles-inner { overflow: hidden; } .wi-popup-styles-block { background: rgba(255,255,255,0.05); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; font-family: ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace; font-size: 11px; line-height: 1.5; } .wi-popup-style-line { color: rgba(255,255,255,0.85); word-break: break-word; } .wi-popup-style-prop { color: #c792ea; } .wi-popup-style-val { color: rgba(255,255,255,0.85); } .wi-popup-textarea { width: 100%; padding: 8px 10px; font-size: 13px; font-family: inherit; background: rgba(255,255,255,0.05); color: #fff; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; resize: none; outline: none; box-sizing: border-box; transition: border-color 0.15s ease; } .wi-popup-textarea:focus { border-color: var(--wi-accent, #3c82f7); } .wi-popup-textarea::placeholder { color: rgba(255,255,255,0.35); } .wi-popup-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 8px; } .wi-popup-delete-wrap { margin-right: auto; } .wi-popup-delete { cursor: pointer; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; border: none; background: transparent; color: rgba(255,255,255,0.4); transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease; padding: 0; } .wi-popup-delete:hover { background: rgba(255,59,48,0.25); color: #ff3b30; } .wi-popup-delete:active { transform: scale(0.92); } .wi-popup-cancel, .wi-popup-submit { padding: 6px 14px; font-size: 12px; font-weight: 500; border-radius: 1rem; border: none; cursor: pointer; transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease; font-family: inherit; } .wi-popup-cancel { background: transparent; color: rgba(255,255,255,0.5); } .wi-popup-cancel:hover { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.8); } .wi-popup-submit { background: #3c82f7; color: white; } .wi-popup-submit:hover:not(:disabled) { filter: brightness(0.9); } .wi-popup-submit:disabled { cursor: not-allowed; opacity: 0.4; } /* Settings panel */ @keyframes wi-settings-in { from { opacity: 0; transform: translateY(8px) scale(0.95); filter: blur(5px); } to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } } @keyframes wi-settings-out { from { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } to { opacity: 0; transform: translateY(8px) scale(0.95); filter: blur(5px); } } .wi-settings { position: absolute; right: 5px; bottom: calc(100% + 0.5rem); z-index: 2147483647; overflow: hidden; background: #1a1a1a; border-radius: 1rem; padding: 13px 1rem 16px; min-width: 220px; cursor: default; box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08); font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; pointer-events: auto; } .wi-settings.wi-settings-enter { animation: wi-settings-in 0.2s ease forwards; } .wi-settings.wi-settings-exit { animation: wi-settings-out 0.1s ease forwards; pointer-events: none; } /* Settings: light mode */ .wi-settings.wi-light { background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04); } /* Settings: tooltip below when toolbar near top */ #wi-toolbar.wi-tooltip-below .wi-settings { bottom: auto; top: calc(100% + 0.5rem); } .wi-settings-header { display: flex; align-items: center; justify-content: space-between; min-height: 24px; margin-bottom: 8px; padding-bottom: 9px; border-bottom: 1px solid rgba(255,255,255,0.07); } .wi-light .wi-settings-header { border-bottom-color: rgba(0,0,0,0.08); } .wi-settings-brand { font-size: 13px; font-weight: 600; letter-spacing: -0.0094em; color: #fff; } .wi-light .wi-settings-brand { color: rgba(0,0,0,0.85); } .wi-settings-brand-slash { transition: color 0.2s ease; } .wi-settings-version { font-size: 11px; font-weight: 400; color: rgba(255,255,255,0.4); margin-left: auto; } .wi-light .wi-settings-version { color: rgba(0,0,0,0.4); } .wi-theme-toggle { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin-left: 8px; border: none; border-radius: 6px; background: transparent; color: rgba(255,255,255,0.4); cursor: pointer; transition: background-color 0.15s ease, color 0.15s ease; padding: 0; } .wi-theme-toggle:hover { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.8); } .wi-light .wi-theme-toggle { color: rgba(0,0,0,0.4); } .wi-light .wi-theme-toggle:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.7); } .wi-settings-section + .wi-settings-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.07); } .wi-light .wi-settings-section + .wi-settings-section { border-top-color: rgba(0,0,0,0.08); } .wi-settings-row { display: flex; align-items: center; justify-content: space-between; min-height: 24px; } .wi-settings-label { font-size: 13px; font-weight: 400; letter-spacing: -0.0094em; color: rgba(255,255,255,0.5); } .wi-light .wi-settings-label { color: rgba(0,0,0,0.5); } .wi-settings-label-marker { padding-top: 3px; margin-bottom: 10px; } /* Color options */ .wi-color-options { display: flex; gap: 8px; margin-top: 6px; margin-bottom: 1px; } .wi-color-ring { display: flex; width: 24px; height: 24px; border: 2px solid transparent; border-radius: 50%; transition: border-color 0.3s ease; cursor: pointer; } .wi-color-dot { display: block; width: 20px; height: 20px; border-radius: 50%; transition: transform 0.2s cubic-bezier(0.25,1,0.5,1); } .wi-color-ring:hover .wi-color-dot { transform: scale(1.15); } .wi-color-ring.wi-selected .wi-color-dot { transform: scale(0.83); } /* Custom checkbox toggle */ .wi-settings-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; } .wi-settings-toggle + .wi-settings-toggle { margin-top: 14px; } .wi-settings-toggle input { position: absolute; opacity: 0; width: 0; height: 0; } .wi-checkbox { position: relative; width: 14px; height: 14px; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; background: rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background 0.25s ease, border-color 0.25s ease; } .wi-checkbox svg { color: #1a1a1a; } .wi-checkbox.wi-checked { border-color: rgba(255,255,255,0.3); background: rgba(255,255,255,1); } .wi-light .wi-checkbox { border-color: rgba(0,0,0,0.15); background: #fff; } .wi-light .wi-checkbox.wi-checked { border-color: #1a1a1a; background: #1a1a1a; } .wi-light .wi-checkbox.wi-checked svg { color: #fff; } .wi-toggle-label { font-size: 13px; font-weight: 400; color: rgba(255,255,255,0.5); letter-spacing: -0.0094em; } .wi-light .wi-toggle-label { color: rgba(0,0,0,0.5); } /* Nav link (to webhook page) */ .wi-settings-nav { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 0; border: none; background: transparent; font-family: inherit; font-size: 13px; font-weight: 400; color: rgba(255,255,255,0.5); cursor: pointer; transition: color 0.15s ease; } .wi-settings-nav:hover { color: rgba(255,255,255,0.9); } .wi-settings-nav svg { color: rgba(255,255,255,0.4); transition: color 0.15s ease; } .wi-settings-nav:hover svg { color: #fff; } .wi-light .wi-settings-nav { color: rgba(0,0,0,0.5); } .wi-light .wi-settings-nav:hover { color: rgba(0,0,0,0.8); } .wi-light .wi-settings-nav svg { color: rgba(0,0,0,0.25); } .wi-light .wi-settings-nav:hover svg { color: rgba(0,0,0,0.8); } /* Settings page sliding */ .wi-settings-pages { overflow: visible; position: relative; display: flex; } .wi-settings-pages.wi-transitioning { overflow-x: clip; overflow-y: visible; } .wi-settings-page { min-width: 100%; flex-shrink: 0; transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), opacity 0.2s ease-out; opacity: 1; } .wi-settings-page.wi-slide-left { transform: translateX(-100%); opacity: 0; } .wi-settings-page-webhooks { position: absolute; top: 0; left: 100%; width: 100%; box-sizing: border-box; display: flex; flex-direction: column; transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), opacity 0.25s ease-out 0.1s; opacity: 0; } .wi-settings-page-webhooks.wi-slide-in { transform: translateX(-100%); opacity: 1; } .wi-settings-page-shortcuts { position: absolute; top: 0; left: 100%; width: 100%; box-sizing: border-box; display: flex; flex-direction: column; transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), opacity 0.25s ease-out 0.1s; opacity: 0; } .wi-settings-page-shortcuts.wi-slide-in { transform: translateX(-100%); opacity: 1; } /* Webhook page */ .wi-settings-back { display: flex; align-items: center; gap: 4px; padding: 0; margin-bottom: 10px; border: none; background: transparent; font-family: inherit; font-size: 13px; font-weight: 400; color: rgba(255,255,255,0.5); cursor: pointer; transition: color 0.15s ease; } .wi-settings-back:hover { color: rgba(255,255,255,0.9); } .wi-light .wi-settings-back { color: rgba(0,0,0,0.5); } .wi-light .wi-settings-back:hover { color: rgba(0,0,0,0.8); } /* iOS-style toggle switch */ .wi-toggle-switch { position: relative; display: inline-block; width: 24px; height: 16px; flex-shrink: 0; cursor: pointer; } .wi-toggle-switch input { opacity: 0; width: 0; height: 0; } .wi-toggle-slider { position: absolute; cursor: pointer; inset: 0; border-radius: 16px; background: #484848; transition: background 0.2s ease; } .wi-light .wi-toggle-slider { background: #ddd; } .wi-toggle-slider::before { content: ""; position: absolute; height: 12px; width: 12px; left: 2px; bottom: 2px; background: white; border-radius: 50%; transition: transform 0.2s cubic-bezier(0.4,0,0.2,1); box-shadow: 0 1px 2px rgba(0,0,0,0.2); } .wi-toggle-switch input:checked + .wi-toggle-slider { background: var(--wi-accent, #3c82f7); } .wi-toggle-switch input:checked + .wi-toggle-slider::before { transform: translateX(8px); } .wi-toggle-switch.wi-disabled { opacity: 0.4; pointer-events: none; } .wi-auto-send-row { display: flex; align-items: center; gap: 8px; } .wi-auto-send-label { font-size: 13px; font-weight: 400; color: rgba(255,255,255,0.5); transition: color 0.15s ease; } .wi-auto-send-label.wi-active { color: rgba(255,255,255,0.85); } .wi-light .wi-auto-send-label { color: rgba(0,0,0,0.4); } .wi-light .wi-auto-send-label.wi-active { color: rgba(0,0,0,0.7); } .wi-webhook-desc { font-size: 12px; color: rgba(255,255,255,0.4); margin-top: 4px; margin-bottom: 8px; line-height: 1.4; } .wi-light .wi-webhook-desc { color: rgba(0,0,0,0.4); } .wi-webhook-input { width: 100%; padding: 6px 8px; font-size: 12px; font-family: ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace; background: rgba(255,255,255,0.05); color: #fff; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; resize: none; outline: none; box-sizing: border-box; transition: border-color 0.15s ease; } .wi-webhook-input:focus { border-color: var(--wi-accent, #3c82f7); } .wi-webhook-input::placeholder { color: rgba(255,255,255,0.3); } .wi-light .wi-webhook-input { background: rgba(0,0,0,0.03); color: #1a1a1a; border-color: rgba(0,0,0,0.12); } .wi-light .wi-webhook-input::placeholder { color: rgba(0,0,0,0.35); } /* Selected element outline (persistent while popup open) */ .wi-selected-outline { position: fixed; border: 2px solid rgba(60,130,247,0.6); border-radius: 4px; background: rgba(60,130,247,0.05); pointer-events: none !important; box-sizing: border-box; z-index: 2147483641; } .wi-selected-outline.wi-outline-enter { animation: wi-highlight-in 0.15s ease-out forwards; } .wi-selected-outline.wi-outline-exit { animation: wi-highlight-out 0.15s ease-out forwards; } @keyframes wi-highlight-out { from { opacity: 1; } to { opacity: 0; } } /* ================================================================ */ /* Light Mode (toolbar, popup, markers) */ /* ================================================================ */ #wi-toolbar-container.wi-light-toolbar { background: #fff; color: rgba(0,0,0,0.85); box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06); } #wi-toolbar-container.wi-light-toolbar.wi-collapsed:hover { background: #f5f5f5; } .wi-light-toolbar .wi-btn { color: rgba(0,0,0,0.65); } .wi-light-toolbar .wi-btn:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.85); } .wi-light-toolbar .wi-btn[data-active="true"] { color: #3c82f7; background: rgba(60,130,247,0.12); } .wi-light-toolbar .wi-btn[data-active="paused"] { color: #f59e0b; background: rgba(245,158,11,0.12); } .wi-light-toolbar .wi-btn[data-danger]:hover { background: rgba(255,59,48,0.1); color: #ff3b30; } .wi-light-toolbar .wi-divider { background: rgba(0,0,0,0.1); } .wi-light-toolbar .wi-tooltip { background: #fff; color: rgba(0,0,0,0.75); box-shadow: 0 2px 8px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.06); } .wi-light-toolbar .wi-tooltip::after { background: #fff; } /* Light popup */ .wi-popup.wi-popup-light { background: #fff; box-shadow: 0 4px 24px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.06); } .wi-popup-light .wi-popup-element { color: rgba(0,0,0,0.6); } .wi-popup-light .wi-popup-chevron { color: rgba(0,0,0,0.4); } .wi-popup-light .wi-popup-styles-block { background: rgba(0,0,0,0.03); } .wi-popup-light .wi-popup-style-line { color: rgba(0,0,0,0.75); } .wi-popup-light .wi-popup-style-prop { color: #7c3aed; } .wi-popup-light .wi-popup-style-val { color: rgba(0,0,0,0.75); } .wi-popup-light .wi-popup-textarea { background: rgba(0,0,0,0.03); color: #1a1a1a; border-color: rgba(0,0,0,0.12); } .wi-popup-light .wi-popup-textarea::placeholder { color: rgba(0,0,0,0.4); } .wi-popup-light .wi-popup-cancel { color: rgba(0,0,0,0.5); } .wi-popup-light .wi-popup-cancel:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.75); } .wi-popup-light .wi-popup-delete { color: rgba(0,0,0,0.4); } .wi-popup-light .wi-popup-delete:hover { background: rgba(255,59,48,0.15); color: #ff3b30; } /* Shortcut recording */ .wi-shortcut-tabs { display: flex; gap: 0; margin-bottom: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; overflow: hidden; } .wi-light .wi-shortcut-tabs { border-color: rgba(0,0,0,0.1); } .wi-shortcut-tab { flex: 1; padding: 6px 8px; font-size: 12px; font-weight: 500; border: none; cursor: pointer; transition: all 0.2s; background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.5); font-family: inherit; } .wi-light .wi-shortcut-tab { background: #f5f5f5; color: rgba(0,0,0,0.5); } .wi-shortcut-tab + .wi-shortcut-tab { border-left: 1px solid rgba(255,255,255,0.12); } .wi-light .wi-shortcut-tab + .wi-shortcut-tab { border-left-color: rgba(0,0,0,0.1); } .wi-shortcut-tab.wi-tab-active { background: var(--wi-accent, #3c82f7); color: white; } .wi-shortcut-desc { font-size: 12px; color: rgba(255,255,255,0.4); margin-bottom: 8px; } .wi-light .wi-shortcut-desc { color: rgba(0,0,0,0.4); } .wi-shortcut-kbd { display: block; text-align: center; padding: 10px 16px; background: rgba(255,255,255,0.05); border: 1.5px dashed rgba(255,255,255,0.2); border-radius: 8px; font-size: 14px; font-weight: 600; color: var(--wi-accent, #3c82f7); font-family: ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace; transition: all 0.2s; margin-bottom: 10px; } .wi-light .wi-shortcut-kbd { background: rgba(0,0,0,0.03); border-color: rgba(0,0,0,0.15); } .wi-shortcut-kbd.wi-recording { border-color: var(--wi-accent, #3c82f7); background: rgba(79,70,229,0.05); } .wi-shortcut-actions { display: flex; gap: 6px; justify-content: flex-end; } .wi-shortcut-actions button { padding: 6px 14px; border-radius: 1rem; border: none; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s; font-family: inherit; } .wi-shortcut-actions .wi-sc-cancel { background: transparent; color: rgba(255,255,255,0.5); } .wi-shortcut-actions .wi-sc-cancel:hover { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.8); } .wi-light .wi-shortcut-actions .wi-sc-cancel { color: rgba(0,0,0,0.5); } .wi-light .wi-shortcut-actions .wi-sc-cancel:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.75); } .wi-shortcut-actions .wi-sc-save { background: var(--wi-accent, #3c82f7); color: white; } .wi-shortcut-actions .wi-sc-save:disabled { opacity: 0.4; cursor: not-allowed; } /* Toast */ .wi-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-20px); background: rgba(31,41,55,0.95); backdrop-filter: blur(8px); color: white; padding: 10px 18px; border-radius: 10px; font-size: 13px; font-weight: 500; z-index: 2147483647; opacity: 0; pointer-events: none; box-shadow: 0 8px 24px rgba(0,0,0,0.2); transition: all 0.3s cubic-bezier(0.175,0.885,0.32,1.275); font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; } .wi-toast.wi-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); } `; document.head.appendChild(style); // ========================================================================= // DOM Structure // ========================================================================= const toolbar = document.createElement('div'); toolbar.id = 'wi-toolbar'; const container = document.createElement('div'); container.id = 'wi-toolbar-container'; container.className = 'wi-collapsed'; // Toggle icon (visible when collapsed) const toggleIcon = document.createElement('div'); toggleIcon.id = 'wi-toggle-icon'; toggleIcon.className = 'wi-visible'; toggleIcon.innerHTML = icons.logo; // Controls (visible when expanded) const controls = document.createElement('div'); controls.id = 'wi-controls'; controls.className = 'wi-hidden'; const buttons = [ { id: 'inspect', icon: icons.inspect, tip: '元素检查' }, { id: 'eye', icon: icons.eye, tip: '隐藏标注', disabled: true }, { id: 'copy', icon: icons.copy, tip: '复制', disabled: true }, { id: 'trash', icon: icons.trash, tip: '删除全部', danger: true, disabled: true }, { divider: true }, { id: 'settings', icon: icons.settings, tip: '设置' }, { id: 'close', icon: icons.close, tip: '关闭', danger: true }, ]; buttons.forEach(btn => { if (btn.divider) { const d = document.createElement('div'); d.className = 'wi-divider'; controls.appendChild(d); return; } const wrap = document.createElement('div'); wrap.className = 'wi-btn-wrap'; const el = document.createElement('button'); el.className = 'wi-btn'; el.id = `wi-btn-${btn.id}`; el.innerHTML = btn.icon; if (btn.danger) el.setAttribute('data-danger', ''); if (btn.disabled) el.disabled = true; const tip = document.createElement('div'); tip.className = 'wi-tooltip'; tip.textContent = btn.tip; wrap.appendChild(el); wrap.appendChild(tip); controls.appendChild(wrap); }); container.appendChild(toggleIcon); container.appendChild(controls); toolbar.appendChild(container); document.body.appendChild(toolbar); // ========================================================================= // Expand / Collapse // ========================================================================= let expanded = false; function expand() { expanded = true; container.classList.remove('wi-collapsed'); container.classList.add('wi-expanded'); toggleIcon.classList.remove('wi-visible'); toggleIcon.classList.add('wi-hidden'); controls.classList.remove('wi-hidden'); controls.classList.add('wi-visible'); } function collapse() { expanded = false; container.classList.remove('wi-expanded'); container.classList.add('wi-collapsed'); toggleIcon.classList.remove('wi-hidden'); toggleIcon.classList.add('wi-visible'); controls.classList.remove('wi-visible'); controls.classList.add('wi-hidden'); } // Click collapsed → expand container.addEventListener('click', (e) => { if (!expanded && !isDragging) expand(); }); // Close button → collapse document.getElementById('wi-btn-close').addEventListener('click', (e) => { e.stopPropagation(); collapse(); }); // ========================================================================= // Drag // ========================================================================= let isDragging = false; let dragStarted = false; let startX, startY, origX, origY; container.addEventListener('mousedown', (e) => { if (e.button !== 0) return; isDragging = false; dragStarted = true; startX = e.clientX; startY = e.clientY; const rect = toolbar.getBoundingClientRect(); origX = rect.left; origY = rect.top; container.classList.add('wi-dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!dragStarted) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (!isDragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) { isDragging = true; } if (isDragging) { let nx = origX + dx; let ny = origY + dy; const rect = container.getBoundingClientRect(); nx = Math.max(0, Math.min(window.innerWidth - rect.width, nx)); ny = Math.max(0, Math.min(window.innerHeight - rect.height, ny)); toolbar.style.right = (window.innerWidth - nx - rect.width) + 'px'; toolbar.style.top = ny + 'px'; toolbar.style.left = 'auto'; toolbar.style.bottom = 'auto'; // Tooltip direction if (ny < 80) { toolbar.classList.add('wi-tooltip-below'); } else { toolbar.classList.remove('wi-tooltip-below'); } } }); document.addEventListener('mouseup', () => { if (dragStarted) { container.classList.remove('wi-dragging'); dragStarted = false; // Prevent click from firing when we were dragging if (isDragging) { setTimeout(() => { isDragging = false; }, 0); } } }); // ========================================================================= // Element Identification (ported from agentation) // ========================================================================= function deepElementFromPoint(x, y) { let el = document.elementFromPoint(x, y); if (!el) return null; while (el?.shadowRoot) { const deeper = el.shadowRoot.elementFromPoint(x, y); if (!deeper || deeper === el) break; el = deeper; } return el; } function isOwnElement(el) { let cur = el; while (cur) { if (cur.id === 'wi-toolbar' || cur.id === 'wi-overlay' || cur.id === 'wi-hover-highlight' || cur.id === 'wi-hover-tooltip' || cur.hasAttribute?.('data-wi-popup') || cur.hasAttribute?.('data-wi-marker') || cur.classList?.contains('wi-selected-outline') || cur.classList?.contains('wi-settings')) return true; cur = cur.parentElement; } return false; } function getElementPath(target) { const parts = []; let cur = target; while (cur && cur !== document.body && cur !== document.documentElement) { const tag = cur.tagName.toLowerCase(); let selector = tag; if (cur.id) { selector += `#${cur.id}`; parts.unshift(selector); break; } else if (cur.className && typeof cur.className === 'string') { const cls = cur.className.trim().split(/\s+/).filter(c => c && !c.includes(':')); if (cls.length) selector += `.${cls[0]}`; } const parent = cur.parentElement; if (parent) { const siblings = Array.from(parent.children).filter(c => c.tagName === cur.tagName); if (siblings.length > 1) selector += `:nth-of-type(${siblings.indexOf(cur) + 1})`; } parts.unshift(selector); cur = parent; } return parts.join(' > '); } function identifyElement(target) { const path = getElementPath(target); if (target.dataset?.element) return { name: target.dataset.element, path }; const tag = target.tagName.toLowerCase(); if (['path','circle','rect','line','g'].includes(tag)) { const svg = target.closest('svg'); if (svg?.parentElement) { const pn = identifyElement(svg.parentElement).name; return { name: `graphic in ${pn}`, path }; } return { name: 'graphic element', path }; } if (tag === 'svg') return { name: 'icon', path }; if (tag === 'button') { const text = target.textContent?.trim(); const aria = target.getAttribute('aria-label'); if (aria) return { name: `button [${aria}]`, path }; return { name: text ? `button "${text.slice(0,25)}"` : 'button', path }; } if (tag === 'a') { const text = target.textContent?.trim(); if (text) return { name: `link "${text.slice(0,25)}"`, path }; return { name: 'link', path }; } if (tag === 'input') { const type = target.getAttribute('type') || 'text'; const ph = target.getAttribute('placeholder'); const nm = target.getAttribute('name'); if (ph) return { name: `input "${ph}"`, path }; if (nm) return { name: `input [${nm}]`, path }; return { name: `${type} input`, path }; } if (['h1','h2','h3','h4','h5','h6'].includes(tag)) { const text = target.textContent?.trim(); return { name: text ? `${tag} "${text.slice(0,35)}"` : tag, path }; } if (tag === 'p') { const text = target.textContent?.trim(); if (text) return { name: `paragraph: "${text.slice(0,40)}${text.length > 40 ? '...' : ''}"`, path }; return { name: 'paragraph', path }; } if (tag === 'span' || tag === 'label') { const text = target.textContent?.trim(); if (text && text.length < 40) return { name: `"${text}"`, path }; return { name: tag, path }; } if (tag === 'img') { const alt = target.getAttribute('alt'); return { name: alt ? `image "${alt.slice(0,30)}"` : 'image', path }; } if (['div','section','article','nav','header','footer','aside','main'].includes(tag)) { const role = target.getAttribute('role'); const aria = target.getAttribute('aria-label'); if (aria) return { name: `${tag} [${aria}]`, path }; if (role) return { name: role, path }; if (typeof target.className === 'string' && target.className) { const words = target.className.split(/[\s_-]+/) .map(c => c.replace(/[A-Z0-9]{5,}.*$/, '')) .filter(c => c.length > 2 && !/^[a-z]{1,2}$/.test(c)) .slice(0, 2); if (words.length > 0) return { name: words.join(' '), path }; } return { name: tag === 'div' ? 'container' : tag, path }; } return { name: tag, path }; } function getComputedStylesSnapshot(target) { const s = window.getComputedStyle(target); const result = {}; const tag = target.tagName.toLowerCase(); const textTags = new Set(['p','span','h1','h2','h3','h4','h5','h6','label','li','a','code','pre','em','strong','b','i']); const containerTags = new Set(['div','section','article','nav','header','footer','aside','main','ul','ol','form']); const defaults = new Set(['none','normal','auto','0px','rgba(0, 0, 0, 0)','transparent','static','visible']); let props; if (textTags.has(tag)) { props = ['color','fontSize','fontWeight','fontFamily','lineHeight']; } else if (tag === 'button') { props = ['backgroundColor','color','padding','borderRadius','fontSize']; } else if (['input','textarea','select'].includes(tag)) { props = ['backgroundColor','color','padding','borderRadius','fontSize']; } else if (['img','video','canvas','svg'].includes(tag)) { props = ['width','height','objectFit','borderRadius']; } else if (containerTags.has(tag)) { props = ['display','padding','margin','gap','backgroundColor']; } else { props = ['color','fontSize','margin','padding','backgroundColor']; } for (const prop of props) { const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); const val = s.getPropertyValue(cssProp); if (val && !defaults.has(val)) result[prop] = val; } return result; } function getElementClasses(target) { if (typeof target.className !== 'string' || !target.className) return ''; return target.className.split(/\s+/).filter(c => c.length > 0) .map(c => { const m = c.match(/^([a-zA-Z][a-zA-Z0-9_-]*?)(?:_[a-zA-Z0-9]{5,})?$/); return m ? m[1] : c; }) .filter((c, i, a) => a.indexOf(c) === i).join(', '); } // ========================================================================= // Framework Component Source Detection // ========================================================================= const MAX_WALK_UP_DEPTH = 15; function parseVueInspectorString(str) { if (!str || typeof str !== 'string') return null; const match = str.match(/^(.+?):(\d+)(?::(\d+))?$/); if (match) { return { framework: 'vue', file: match[1], line: parseInt(match[2], 10), column: match[3] ? parseInt(match[3], 10) : undefined }; } return { framework: 'vue', file: str }; } function getSourceFromDOM(el) { if (!el || typeof el.getAttribute !== 'function') return null; const inspectorAttr = el.getAttribute('data-v-inspector'); if (inspectorAttr) return parseVueInspectorString(inspectorAttr); const vnodePaths = [el.__vnode?.props?.__v_inspector, el.__vnode?.ctx?.vnode?.props?.__v_inspector, el.__vnode?.component?.vnode?.props?.__v_inspector]; for (const data of vnodePaths) { if (data) return parseVueInspectorString(data); } const vueFile = el.__vueParentComponent?.type?.__file; if (vueFile) return { framework: 'vue', file: vueFile }; const vue2File = el.__vue__?.$options?.__file; if (vue2File) return { framework: 'vue', file: vue2File }; const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')); if (fiberKey) { let fiber = el[fiberKey]; while (fiber) { const source = fiber._debugSource || fiber._debugOwner?._debugSource; if (source) return { framework: 'react', file: source.fileName, line: source.lineNumber, column: source.columnNumber }; fiber = fiber.return; } } return null; } function findSourceByWalkUp(el) { let cur = el; for (let i = 0; i < MAX_WALK_UP_DEPTH && cur; i++) { const source = getSourceFromDOM(cur); if (source) return source; cur = cur.parentElement; } return null; } // ========================================================================= // Inspect Mode // ========================================================================= let inspectState = 'off'; // 'off' | 'active' | 'paused' let hoverHighlight = null; let hoverTooltip = null; let overlay = null; // Annotation state let annotations = []; // { text, element, path, x, y, styles, markerEl } let pendingPopup = null; // { popup, marker, outline, enterTimer } let pendingTarget = null; let editingIndex = -1; // index of annotation being edited, -1 if none const chevronSvg = ``; function createOverlay() { overlay = document.createElement('div'); overlay.id = 'wi-overlay'; document.body.appendChild(overlay); } function removeOverlay() { overlay?.remove(); overlay = null; } function showHoverHighlight(rect) { if (!hoverHighlight) { hoverHighlight = document.createElement('div'); hoverHighlight.id = 'wi-hover-highlight'; document.body.appendChild(hoverHighlight); } const c = settings.annotationColor; hoverHighlight.style.borderColor = c + '80'; hoverHighlight.style.backgroundColor = c + '0a'; hoverHighlight.style.left = rect.left + 'px'; hoverHighlight.style.top = rect.top + 'px'; hoverHighlight.style.width = rect.width + 'px'; hoverHighlight.style.height = rect.height + 'px'; } function hideHoverHighlight() { hoverHighlight?.remove(); hoverHighlight = null; } function showHoverTooltip(x, y, info, rect) { if (!hoverTooltip) { hoverTooltip = document.createElement('div'); hoverTooltip.id = 'wi-hover-tooltip'; document.body.appendChild(hoverTooltip); } const w = Math.round(rect.width); const h = Math.round(rect.height); hoverTooltip.innerHTML = `
${info.path}
` + `
${info.name}
` + `
${w} × ${h}
`; const tipX = Math.max(8, Math.min(x, window.innerWidth - 200)); const tipY = Math.max(y - 52, 8); hoverTooltip.style.left = tipX + 'px'; hoverTooltip.style.top = tipY + 'px'; } function hideHoverTooltip() { hoverTooltip?.remove(); hoverTooltip = null; } // ---- Selected element outline ---- function showSelectedOutline(el) { hideSelectedOutline(); const outline = document.createElement('div'); outline.className = 'wi-selected-outline wi-outline-enter'; const rect = el.getBoundingClientRect(); const c = settings.annotationColor; outline.style.borderColor = c + '99'; outline.style.backgroundColor = c + '0d'; outline.style.left = rect.left + 'px'; outline.style.top = rect.top + 'px'; outline.style.width = rect.width + 'px'; outline.style.height = rect.height + 'px'; document.body.appendChild(outline); return outline; } function hideSelectedOutline() { document.querySelectorAll('.wi-selected-outline').forEach(el => { el.classList.remove('wi-outline-enter'); el.classList.add('wi-outline-exit'); setTimeout(() => el.remove(), 150); }); } // ---- Annotation Popup ---- function buildMarkerContent(num) { return `${num}${icons.pencil}`; } function createAnnotationPopup(el, clickX, clickY, editIdx) { cancelPendingPopup(); pendingTarget = el; editingIndex = editIdx ?? -1; const isEdit = editingIndex >= 0; const annotation = isEdit ? annotations[editingIndex] : null; const info = identifyElement(el); const computedStyles = getComputedStylesSnapshot(el); const styleEntries = Object.entries(computedStyles); const hasStyles = styleEntries.length > 0; // Marker position (percentage X, fixed Y) const markerXPct = isEdit ? annotation.x : (clickX / window.innerWidth) * 100; const markerY = isEdit ? annotation.y : clickY; // Create marker dot (only for new annotations) let marker; if (isEdit) { marker = annotation.markerEl; } else { marker = document.createElement('div'); marker.className = 'wi-marker'; marker.setAttribute('data-wi-marker', ''); marker.innerHTML = icons.plus; marker.style.left = markerXPct + '%'; marker.style.top = markerY + 'px'; marker.style.backgroundColor = settings.annotationColor; document.body.appendChild(marker); } // Create popup const popup = document.createElement('div'); popup.className = 'wi-popup'; if (!settings.darkMode) popup.classList.add('wi-popup-light'); popup.setAttribute('data-wi-popup', ''); popup.style.setProperty('--wi-accent', settings.annotationColor); popup.addEventListener('click', e => e.stopPropagation()); // Header with chevron toggle (if computed styles exist) let headerHTML = ''; if (hasStyles) { headerHTML = `
`; } else { headerHTML = `
${info.name}
`; } // Computed styles block (collapsed by default) let stylesHTML = ''; if (hasStyles) { let linesHTML = ''; for (const [prop, val] of styleEntries) { const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); linesHTML += `
${cssProp}: ${val};
`; } stylesHTML = `
${linesHTML}
`; } const initialValue = isEdit ? annotation.text : ''; const submitLabel = isEdit ? 'Update' : 'Add'; const hasInitText = initialValue.trim().length > 0; const deleteHTML = isEdit ? `
` : ''; popup.innerHTML = headerHTML + stylesHTML + `
${deleteHTML}
`; // Position popup const popupLeft = Math.max(160, Math.min(window.innerWidth - 160, (markerXPct / 100) * window.innerWidth)); popup.style.left = popupLeft + 'px'; if (markerY > window.innerHeight - 290) { popup.style.bottom = (window.innerHeight - markerY + 20) + 'px'; } else { popup.style.top = (markerY + 20) + 'px'; } document.body.appendChild(popup); // Selected outline const outline = showSelectedOutline(el); // Animate in requestAnimationFrame(() => { popup.classList.add('wi-popup-enter'); }); const enterTimer = setTimeout(() => { popup.classList.remove('wi-popup-enter'); popup.classList.add('wi-popup-entered'); }, 200); // Focus textarea const textarea = popup.querySelector('.wi-popup-textarea'); const submitBtn = popup.querySelector('.wi-popup-submit'); const cancelBtn = popup.querySelector('.wi-popup-cancel'); setTimeout(() => { textarea.focus(); textarea.selectionStart = textarea.selectionEnd = textarea.value.length; }, 50); // Text change → enable/disable submit textarea.addEventListener('input', () => { const hasText = textarea.value.trim().length > 0; submitBtn.disabled = !hasText; submitBtn.style.opacity = hasText ? '1' : '0.4'; }); // Chevron toggle for computed styles if (hasStyles) { const toggleBtn = popup.querySelector('.wi-popup-header-toggle'); const chevron = popup.querySelector('.wi-popup-chevron'); const wrapper = popup.querySelector('.wi-popup-styles-wrapper'); toggleBtn.addEventListener('click', () => { const isExpanded = wrapper.classList.contains('wi-styles-expanded'); if (isExpanded) { wrapper.classList.remove('wi-styles-expanded'); chevron.classList.remove('wi-chevron-expanded'); setTimeout(() => textarea.focus(), 0); } else { wrapper.classList.add('wi-styles-expanded'); chevron.classList.add('wi-chevron-expanded'); } }); } // Submit handler function handleSubmit() { const text = textarea.value.trim(); if (!text) return; if (isEdit) { // Update existing annotation annotations[editingIndex].text = text; annotations[editingIndex].styles = computedStyles; console.log(`[WI] Annotation #${editingIndex + 1} updated: "${text}"`); } else { // Create new annotation const num = annotations.length + 1; // Convert marker to numbered with hover-pencil marker.innerHTML = buildMarkerContent(num); bindMarkerEvents(marker, annotations.length); annotations.push({ text, element: info.name, path: info.path, x: markerXPct, y: markerY, styles: computedStyles, markerEl: marker, targetEl: el, }); console.log(`[WI] Annotation #${num}: "${text}" on ${info.name}`); } // Close popup popup.classList.remove('wi-popup-entered'); popup.classList.add('wi-popup-exit'); hideSelectedOutline(); setTimeout(() => popup.remove(), 150); pendingPopup = null; pendingTarget = null; editingIndex = -1; updateAnnotationButtons(); } // Delete single annotation (edit mode only) function handleDelete() { const idx = editingIndex; const ann = annotations[idx]; // Close popup popup.classList.remove('wi-popup-entered'); popup.classList.add('wi-popup-exit'); ann.markerEl.classList.add('wi-marker-exit'); hideSelectedOutline(); setTimeout(() => { popup.remove(); ann.markerEl.remove(); }, 200); // Remove from array and renumber remaining markers annotations.splice(idx, 1); annotations.forEach((a, i) => { a.markerEl.querySelector('.wi-marker-num').textContent = i + 1; }); pendingPopup = null; pendingTarget = null; editingIndex = -1; updateAnnotationButtons(); console.log(`[WI] Annotation #${idx + 1} deleted`); } submitBtn.addEventListener('click', handleSubmit); cancelBtn.addEventListener('click', () => cancelPendingPopup()); if (isEdit) { const deleteBtn = popup.querySelector('.wi-popup-delete'); if (deleteBtn) deleteBtn.addEventListener('click', handleDelete); } // Keyboard textarea.addEventListener('keydown', (e) => { if (e.isComposing) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(); } if (e.key === 'Escape') { cancelPendingPopup(); } }); pendingPopup = { popup, marker, outline, enterTimer, isEdit }; } function bindMarkerEvents(marker, index) { marker.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const ann = annotations[index]; if (!ann) return; // Open edit popup const el = ann.targetEl && document.contains(ann.targetEl) ? ann.targetEl : null; if (el) { createAnnotationPopup(el, (ann.x / 100) * window.innerWidth, ann.y, index); } }); } function cancelPendingPopup() { if (!pendingPopup) return; const { popup, marker, outline, enterTimer, isEdit } = pendingPopup; clearTimeout(enterTimer); // Exit animation popup.classList.remove('wi-popup-enter', 'wi-popup-entered'); popup.classList.add('wi-popup-exit'); if (!isEdit) { marker.classList.add('wi-marker-exit'); } hideSelectedOutline(); setTimeout(() => { popup.remove(); if (!isEdit) marker.remove(); }, 200); pendingPopup = null; pendingTarget = null; editingIndex = -1; } function shakePendingPopup() { if (!pendingPopup) return; const { popup } = pendingPopup; popup.classList.add('wi-popup-shake'); setTimeout(() => { popup.classList.remove('wi-popup-shake'); popup.querySelector('.wi-popup-textarea')?.focus(); }, 250); } // ========================================================================= // Inspect event handlers // ========================================================================= function onInspectMouseMove(e) { if (inspectState !== 'active' || pendingPopup) return; const target = (e.composedPath?.()?.[0] || e.target); if (!target || isOwnElement(target)) { hideHoverHighlight(); hideHoverTooltip(); return; } const el = deepElementFromPoint(e.clientX, e.clientY); if (!el || isOwnElement(el)) { hideHoverHighlight(); hideHoverTooltip(); return; } const rect = el.getBoundingClientRect(); const info = identifyElement(el); showHoverHighlight(rect); showHoverTooltip(e.clientX, e.clientY, info, rect); } function onInspectClick(e) { const target = (e.composedPath?.()?.[0] || e.target); if (isOwnElement(target)) return; // Block page interactions if (settings.blockInteractions || inspectState === 'active') { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } // Paused state: don't create annotations if (inspectState === 'paused') return; // If popup is already open, shake it if (pendingPopup) { shakePendingPopup(); return; } const el = deepElementFromPoint(e.clientX, e.clientY); if (!el || isOwnElement(el)) return; hideHoverHighlight(); hideHoverTooltip(); createAnnotationPopup(el, e.clientX, e.clientY); } function onInspectKeydown(e) { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); stopInspect(); } } function startInspect() { inspectState = 'active'; createOverlay(); document.addEventListener('mousemove', onInspectMouseMove, true); document.addEventListener('click', onInspectClick, true); document.addEventListener('keydown', onInspectKeydown, true); inspectBtn.innerHTML = icons.pause; inspectBtn.setAttribute('data-active', 'true'); inspectBtn.style.color = settings.annotationColor; inspectBtn.style.backgroundColor = settings.annotationColor + '40'; inspectTip.textContent = '暂停检查'; } function pauseInspect() { inspectState = 'paused'; hideHoverHighlight(); hideHoverTooltip(); cancelPendingPopup(); inspectBtn.innerHTML = icons.play; inspectBtn.setAttribute('data-active', 'paused'); inspectBtn.style.color = ''; inspectBtn.style.backgroundColor = ''; inspectTip.textContent = '继续检查'; } function resumeInspect() { inspectState = 'active'; inspectBtn.innerHTML = icons.pause; inspectBtn.setAttribute('data-active', 'true'); inspectBtn.style.color = settings.annotationColor; inspectBtn.style.backgroundColor = settings.annotationColor + '40'; inspectTip.textContent = '暂停检查'; } function stopInspect() { if (inspectState === 'off') return; inspectState = 'off'; removeOverlay(); hideHoverHighlight(); hideHoverTooltip(); cancelPendingPopup(); hideSelectedOutline(); document.removeEventListener('mousemove', onInspectMouseMove, true); document.removeEventListener('click', onInspectClick, true); document.removeEventListener('keydown', onInspectKeydown, true); inspectBtn.innerHTML = icons.inspect; inspectBtn.setAttribute('data-active', 'false'); inspectBtn.style.color = ''; inspectBtn.style.backgroundColor = ''; inspectTip.textContent = '元素检查'; } // ========================================================================= // Button bindings // ========================================================================= const inspectBtn = document.getElementById('wi-btn-inspect'); const inspectTip = inspectBtn.parentElement.querySelector('.wi-tooltip'); const eyeBtn = document.getElementById('wi-btn-eye'); const eyeTip = eyeBtn.parentElement.querySelector('.wi-tooltip'); const copyBtn = document.getElementById('wi-btn-copy'); const trashBtn = document.getElementById('wi-btn-trash'); const settingsBtn = document.getElementById('wi-btn-settings'); let markersVisible = true; function updateAnnotationButtons() { const hasAnnotations = annotations.length > 0; eyeBtn.disabled = !hasAnnotations; copyBtn.disabled = !hasAnnotations; trashBtn.disabled = !hasAnnotations; } // ========================================================================= // Theme management // ========================================================================= function applyTheme() { const dark = settings.darkMode; if (dark) { container.classList.remove('wi-light-toolbar'); } else { container.classList.add('wi-light-toolbar'); } } applyTheme(); // Update all marker colors function applyAnnotationColor() { const color = settings.annotationColor; annotations.forEach(ann => { ann.markerEl.style.backgroundColor = color; }); } // ========================================================================= // Settings panel // ========================================================================= let settingsPanelEl = null; let settingsVisible = false; let settingsPage = 'main'; // 'main' | 'webhooks' | 'shortcuts' function buildSettingsHTML() { const dark = settings.darkMode; const color = settings.annotationColor; // Color dots let colorDotsHTML = ''; for (const c of COLOR_OPTIONS) { const selected = c.value === color; colorDotsHTML += `
`; } const checkIcon = icons.checkSm; const clearChecked = settings.autoClearAfterSend; const blockChecked = settings.blockInteractions; return `
/web-inspector v1.0.0
标注颜色
${colorDotsHTML}
Webhooks
Auto-Send
标注数据将发送到此 URL 端点。
${Object.keys(SHORTCUT_LABELS).map((n, i) => ``).join('')}
按下新的快捷键组合(需包含修饰键)
${formatShortcut(settings.shortcuts[Object.keys(SHORTCUT_LABELS)[0]])}
`; } function openSettings() { closeSettings(); settingsPage = 'main'; settingsPanelEl = document.createElement('div'); settingsPanelEl.className = `wi-settings ${settings.darkMode ? '' : 'wi-light'}`; settingsPanelEl.style.setProperty('--wi-accent', settings.annotationColor); settingsPanelEl.innerHTML = buildSettingsHTML(); settingsPanelEl.addEventListener('click', e => e.stopPropagation()); // Position: above toolbar by default, below if near top container.style.position = 'relative'; container.appendChild(settingsPanelEl); requestAnimationFrame(() => settingsPanelEl.classList.add('wi-settings-enter')); settingsVisible = true; bindSettingsEvents(); } function closeSettings() { if (!settingsPanelEl) return; settingsPanelEl.classList.remove('wi-settings-enter'); settingsPanelEl.classList.add('wi-settings-exit'); const el = settingsPanelEl; setTimeout(() => el.remove(), 100); settingsPanelEl = null; settingsVisible = false; container.style.position = ''; } function refreshSettings() { if (!settingsPanelEl) return; const page = settingsPage; settingsPanelEl.className = `wi-settings wi-settings-enter ${settings.darkMode ? '' : 'wi-light'}`; settingsPanelEl.style.setProperty('--wi-accent', settings.annotationColor); settingsPanelEl.innerHTML = buildSettingsHTML(); bindSettingsEvents(); // Restore page if (page === 'webhooks' || page === 'shortcuts') { navigateSettingsPage(page); } } function navigateSettingsPage(page) { settingsPage = page; if (!settingsPanelEl) return; const mainPage = settingsPanelEl.querySelector('.wi-settings-page-main'); const webhooksPage = settingsPanelEl.querySelector('.wi-settings-page-webhooks'); const shortcutsPage = settingsPanelEl.querySelector('.wi-settings-page-shortcuts'); const pages = settingsPanelEl.querySelector('.wi-settings-pages'); if (page === 'webhooks' || page === 'shortcuts') { pages.classList.add('wi-transitioning'); mainPage.classList.add('wi-slide-left'); if (page === 'webhooks') webhooksPage.classList.add('wi-slide-in'); else shortcutsPage.classList.add('wi-slide-in'); } else { mainPage.classList.remove('wi-slide-left'); webhooksPage.classList.remove('wi-slide-in'); shortcutsPage.classList.remove('wi-slide-in'); setTimeout(() => pages.classList.remove('wi-transitioning'), 350); } } function bindSettingsEvents() { if (!settingsPanelEl) return; // Theme toggle const themeBtn = settingsPanelEl.querySelector('.wi-theme-toggle'); themeBtn?.addEventListener('click', () => { settings.darkMode = !settings.darkMode; saveSettings(settings); applyTheme(); refreshSettings(); }); // Color options settingsPanelEl.querySelectorAll('.wi-color-ring').forEach(ring => { ring.addEventListener('click', () => { settings.annotationColor = ring.dataset.color; saveSettings(settings); applyAnnotationColor(); refreshSettings(); }); }); // Checkboxes settingsPanelEl.querySelectorAll('input[data-setting]').forEach(input => { input.addEventListener('change', () => { const key = input.dataset.setting; if (key === 'webhooksEnabled') { settings[key] = input.checked; } else { settings[key] = input.checked; } saveSettings(settings); refreshSettings(); }); }); // Nav links settingsPanelEl.querySelectorAll('[data-nav]').forEach(btn => { btn.addEventListener('click', () => { navigateSettingsPage(btn.dataset.nav); }); }); // Webhook URL input const webhookInput = settingsPanelEl.querySelector('.wi-webhook-input'); webhookInput?.addEventListener('input', () => { settings.webhookUrl = webhookInput.value; saveSettings(settings); // Enable/disable toggle const toggle = settingsPanelEl.querySelector('.wi-toggle-switch'); const enableInput = settingsPanelEl.querySelector('input[data-setting="webhooksEnabled"]'); if (toggle && enableInput) { if (settings.webhookUrl.trim()) { toggle.classList.remove('wi-disabled'); enableInput.disabled = false; } else { toggle.classList.add('wi-disabled'); enableInput.disabled = true; } } }); // Shortcut recording const scNames = Object.keys(SHORTCUT_LABELS); const scTabs = settingsPanelEl.querySelectorAll('.wi-shortcut-tab'); const scKbd = settingsPanelEl.querySelector('.wi-shortcut-kbd'); const scSave = settingsPanelEl.querySelector('.wi-sc-save'); const scCancel = settingsPanelEl.querySelector('.wi-sc-cancel'); if (scKbd && scSave) { let activeScName = scNames[0]; const pendings = {}; const switchScTab = (name) => { activeScName = name; scTabs.forEach(t => t.classList.toggle('wi-tab-active', t.dataset.sc === name)); const display = pendings[name] || settings.shortcuts[name]; scKbd.textContent = formatShortcut(display); scKbd.classList.toggle('wi-recording', !!pendings[name]); scSave.disabled = !Object.keys(pendings).length; }; scTabs.forEach(t => t.addEventListener('click', () => switchScTab(t.dataset.sc))); const onScKeyDown = (e) => { if (!settingsPanelEl?.querySelector('.wi-settings-page-shortcuts')) return; e.preventDefault(); e.stopPropagation(); if (['Alt','Shift','Control','Meta'].includes(e.key)) return; if (!e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) return; const shortcut = { altKey: e.altKey, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, code: e.code }; pendings[activeScName] = shortcut; scKbd.textContent = formatShortcut(shortcut); scKbd.classList.add('wi-recording'); scSave.disabled = false; }; document.addEventListener('keydown', onScKeyDown, true); const cleanupSc = () => { document.removeEventListener('keydown', onScKeyDown, true); }; scSave.addEventListener('click', () => { for (const [name, shortcut] of Object.entries(pendings)) { settings.shortcuts[name] = shortcut; } saveSettings(settings); cleanupSc(); navigateSettingsPage('main'); }); scCancel.addEventListener('click', () => { cleanupSc(); navigateSettingsPage('main'); }); } } function showToast(msg) { const t = document.createElement('div'); t.className = 'wi-toast'; t.textContent = msg; document.body.appendChild(t); requestAnimationFrame(() => t.classList.add('wi-toast-show')); setTimeout(() => { t.classList.remove('wi-toast-show'); setTimeout(() => t.remove(), 300); }, 2000); } function copyAnnotations() { const data = annotations.map((ann) => { const el = ann.targetEl; return { url: location.href, element: `<${el.tagName.toLowerCase()}${el.id ? ` id="${el.id}"` : ''}${el.className ? ` class="${el.className}"` : ''}>`, path: getElementPath(el), note: ann.text, innerText: el.innerText?.substring(0, 200) || '', component: findSourceByWalkUp(el), }; }); navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => { showToast('已复制到剪贴板'); }).catch(err => { showToast('复制失败'); console.warn('[WI] Clipboard write failed:', err); }); stopInspect(); if (settings.autoClearAfterSend) { cancelPendingPopup(); annotations.forEach(ann => { ann.markerEl.classList.add('wi-marker-exit'); setTimeout(() => ann.markerEl.remove(), 200); }); annotations = []; markersVisible = true; eyeBtn.innerHTML = icons.eye; eyeBtn.setAttribute('data-active', 'false'); eyeTip.textContent = '隐藏标注'; updateAnnotationButtons(); console.log('[WI] Annotations cleared after copy'); } } // Close settings when clicking outside document.addEventListener('click', (e) => { if (settingsVisible && settingsPanelEl && !settingsPanelEl.contains(e.target) && !settingsBtn.contains(e.target)) { closeSettings(); } }); // ========================================================================= // Button event listeners // ========================================================================= inspectBtn.addEventListener('click', (e) => { e.stopPropagation(); if (inspectState === 'off') startInspect(); else stopInspect(); }); // Eye: toggle markers visibility eyeBtn.addEventListener('click', (e) => { e.stopPropagation(); markersVisible = !markersVisible; annotations.forEach(ann => { ann.markerEl.style.display = markersVisible ? '' : 'none'; }); eyeBtn.innerHTML = markersVisible ? icons.eye : icons.eyeOff; eyeBtn.setAttribute('data-active', markersVisible ? 'false' : 'true'); eyeTip.textContent = markersVisible ? '隐藏标注' : '显示标注'; }); // Copy: copy annotations to clipboard copyBtn.addEventListener('click', (e) => { e.stopPropagation(); copyAnnotations(); }); // Trash: delete all annotations trashBtn.addEventListener('click', (e) => { e.stopPropagation(); cancelPendingPopup(); annotations.forEach(ann => { ann.markerEl.classList.add('wi-marker-exit'); setTimeout(() => ann.markerEl.remove(), 200); }); annotations = []; markersVisible = true; eyeBtn.innerHTML = icons.eye; eyeBtn.setAttribute('data-active', 'false'); eyeTip.textContent = '隐藏标注'; updateAnnotationButtons(); console.log('[WI] All annotations deleted'); }); // Settings: toggle panel settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); if (settingsVisible) closeSettings(); else openSettings(); }); // Close also stops inspect + closes settings const origCollapse = collapse; collapse = function() { stopInspect(); closeSettings(); origCollapse(); }; // ========================================================================= // Global Shortcuts // ========================================================================= document.addEventListener('keydown', (e) => { if (settingsVisible) return; const sc = settings.shortcuts; if (matchShortcut(e, sc.inspect)) { e.preventDefault(); e.stopPropagation(); if (!expanded) expand(); if (inspectState === 'off') startInspect(); else stopInspect(); } if (matchShortcut(e, sc.copy)) { e.preventDefault(); e.stopPropagation(); if (annotations.length > 0) copyAnnotations(); } }); })();