Greasy Fork is available in English.
页面元素检查与标注工具 - 支持框架组件源码定位、快捷键自定义、剪贴板复制
// ==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
// ==/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 `<svg width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
}
const icons = {
// Search Plus (logo)
logo: lucide(18, '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6"/><path d="M8 11h6"/>'),
// Crosshair (inspect)
inspect: lucide(16, '<circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/>'),
// Play
play: lucide(16, '<polygon points="6 3 20 12 6 21 6 3"/>'),
// Pause
pause: lucide(16, '<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>'),
// Pencil (for marker hover)
pencil: lucide(12, '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'),
// Plus (for new annotation marker)
plus: lucide(12, '<path d="M5 12h14"/><path d="M12 5v14"/>'),
// Eye (show markers)
eye: lucide(16, '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>'),
// Eye Off (hide markers)
eyeOff: lucide(16, '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>'),
// Send
copy: lucide(16, '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'),
// Trash
trash: lucide(16, '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>'),
// Trash small (for popup delete button)
trashSm: lucide(14, '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>'),
// Settings
settings: lucide(16, '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>'),
// X (close)
close: lucide(16, '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
// Sun (light mode)
sun: lucide(16, '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>'),
// Moon (dark mode)
moon: lucide(16, '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'),
// Check small (for checkbox)
checkSm: lucide(14, '<polyline points="20 6 9 17 4 12"/>'),
// Chevron right (for nav link)
chevronR: lucide(16, '<path d="M7.5 12.5L12 8L7.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'),
// Chevron left (for back)
chevronL: lucide(16, '<path d="M15 19l-7-7 7-7"/>'),
// Terminal (console) — kept for future
console: lucide(16, '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
// Globe (network) — kept for future
network: lucide(16, '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>'),
// Database (storage) — kept for future
storage: lucide(16, '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/>'),
// Keyboard (shortcuts)
keyboard: lucide(16, '<rect width="20" height="16" x="2" y="4" rx="2" ry="2"/><path d="M6 8h.001"/><path d="M10 8h.001"/><path d="M14 8h.001"/><path d="M18 8h.001"/><path d="M8 12h.001"/><path d="M12 12h.001"/><path d="M16 12h.001"/><path d="M7 16h10"/>'),
};
// =========================================================================
// 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 = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M5.5 10.25L9 7.25L5.75 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
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 =
`<div class="wi-hover-path">${info.path}</div>` +
`<div class="wi-hover-name">${info.name}</div>` +
`<div class="wi-hover-size">${w} × ${h}</div>`;
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 `<span class="wi-marker-num">${num}</span><span class="wi-marker-edit">${icons.pencil}</span>`;
}
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 = `<div class="wi-popup-header">
<button class="wi-popup-header-toggle" type="button">
<span class="wi-popup-chevron">${chevronSvg}</span>
<span class="wi-popup-element">${info.name}</span>
</button>
</div>`;
} else {
headerHTML = `<div class="wi-popup-header">
<span class="wi-popup-element">${info.name}</span>
</div>`;
}
// 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 += `<div class="wi-popup-style-line"><span class="wi-popup-style-prop">${cssProp}</span>: <span class="wi-popup-style-val">${val}</span>;</div>`;
}
stylesHTML = `<div class="wi-popup-styles-wrapper">
<div class="wi-popup-styles-inner">
<div class="wi-popup-styles-block">${linesHTML}</div>
</div>
</div>`;
}
const initialValue = isEdit ? annotation.text : '';
const submitLabel = isEdit ? 'Update' : 'Add';
const hasInitText = initialValue.trim().length > 0;
const deleteHTML = isEdit
? `<div class="wi-popup-delete-wrap"><button class="wi-popup-delete" type="button">${icons.trashSm}</button></div>`
: '';
popup.innerHTML = headerHTML + stylesHTML +
`<textarea class="wi-popup-textarea" placeholder="What should change?" rows="2">${initialValue}</textarea>
<div class="wi-popup-actions">
${deleteHTML}
<button class="wi-popup-cancel">Cancel</button>
<button class="wi-popup-submit" ${hasInitText ? '' : 'disabled'} style="background-color:${settings.annotationColor};opacity:${hasInitText ? '1' : '0.4'}">${submitLabel}</button>
</div>`;
// 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 += `<div class="wi-color-ring ${selected ? 'wi-selected' : ''}" data-color="${c.value}" style="border-color:${selected ? c.value : 'transparent'}" title="${c.label}"><div class="wi-color-dot" style="background-color:${c.value}"></div></div>`;
}
const checkIcon = icons.checkSm;
const clearChecked = settings.autoClearAfterSend;
const blockChecked = settings.blockInteractions;
return `
<div class="wi-settings-header">
<span class="wi-settings-brand"><span class="wi-settings-brand-slash" style="color:${color}">/</span>web-inspector</span>
<span class="wi-settings-version">v1.0.0</span>
<button class="wi-theme-toggle" type="button" title="${dark ? '浅色模式' : '深色模式'}">${dark ? icons.sun : icons.moon}</button>
</div>
<div class="wi-settings-pages">
<div class="wi-settings-page wi-settings-page-main">
<div class="wi-settings-section">
<div class="wi-settings-label wi-settings-label-marker">标注颜色</div>
<div class="wi-color-options">${colorDotsHTML}</div>
</div>
<div class="wi-settings-section">
<label class="wi-settings-toggle">
<input type="checkbox" data-setting="autoClearAfterSend" ${clearChecked ? 'checked' : ''}>
<div class="wi-checkbox ${clearChecked ? 'wi-checked' : ''}">${clearChecked ? checkIcon : ''}</div>
<span class="wi-toggle-label">复制后清空</span>
</label>
<label class="wi-settings-toggle">
<input type="checkbox" data-setting="blockInteractions" ${blockChecked ? 'checked' : ''}>
<div class="wi-checkbox ${blockChecked ? 'wi-checked' : ''}">${blockChecked ? checkIcon : ''}</div>
<span class="wi-toggle-label">阻止页面交互</span>
</label>
</div>
<div class="wi-settings-section" style="padding-top:12px">
<button class="wi-settings-nav" type="button" data-nav="shortcuts">
<span>快捷键设置</span>
<span>${icons.chevronR}</span>
</button>
</div>
<div class="wi-settings-section" style="padding-top:12px">
<button class="wi-settings-nav" type="button" data-nav="webhooks">
<span>Webhook 配置</span>
<span>${icons.chevronR}</span>
</button>
</div>
</div>
<div class="wi-settings-page wi-settings-page-webhooks">
<button class="wi-settings-back" type="button" data-nav="main">${icons.chevronL}<span>Webhook 配置</span></button>
<div class="wi-settings-section">
<div class="wi-settings-row">
<span class="wi-settings-label">Webhooks</span>
<div class="wi-auto-send-row">
<span class="wi-auto-send-label ${settings.webhooksEnabled ? 'wi-active' : ''}">Auto-Send</span>
<label class="wi-toggle-switch ${!settings.webhookUrl ? 'wi-disabled' : ''}">
<input type="checkbox" data-setting="webhooksEnabled" ${settings.webhooksEnabled ? 'checked' : ''} ${!settings.webhookUrl ? 'disabled' : ''}>
<span class="wi-toggle-slider"></span>
</label>
</div>
</div>
<div class="wi-webhook-desc">标注数据将发送到此 URL 端点。</div>
<textarea class="wi-webhook-input" placeholder="Webhook URL" rows="2">${settings.webhookUrl}</textarea>
</div>
</div>
<div class="wi-settings-page wi-settings-page-shortcuts">
<button class="wi-settings-back" type="button" data-nav="main">${icons.chevronL}<span>快捷键设置</span></button>
<div class="wi-settings-section">
<div class="wi-shortcut-tabs">
${Object.keys(SHORTCUT_LABELS).map((n, i) => `<button class="wi-shortcut-tab${i === 0 ? ' wi-tab-active' : ''}" data-sc="${n}">${SHORTCUT_LABELS[n]}</button>`).join('')}
</div>
<div class="wi-shortcut-desc">按下新的快捷键组合(需包含修饰键)</div>
<div class="wi-shortcut-kbd">${formatShortcut(settings.shortcuts[Object.keys(SHORTCUT_LABELS)[0]])}</div>
<div class="wi-shortcut-actions">
<button class="wi-sc-cancel">取消</button>
<button class="wi-sc-save" disabled>保存</button>
</div>
</div>
</div>
</div>`;
}
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();
}
});
})();