pixelpick 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/chrome-mv3/assets/sidepanel-D7qwGDau.css +1 -0
- package/dist/chrome-mv3/background.js +1 -0
- package/dist/chrome-mv3/chunks/sidepanel-CzCbYvqW.js +13 -0
- package/dist/chrome-mv3/content-scripts/content.js +34 -0
- package/dist/chrome-mv3/icon/128.png +0 -0
- package/dist/chrome-mv3/icon/16.png +0 -0
- package/dist/chrome-mv3/icon/48.png +0 -0
- package/dist/chrome-mv3/manifest.json +1 -0
- package/dist/chrome-mv3/sidepanel.html +13 -0
- package/dist/chrome-mv3/src/constants.js +63 -0
- package/dist/chrome-mv3/src/inspector/dom-inspector.js +165 -0
- package/dist/chrome-mv3/src/inspector/highlight.js +155 -0
- package/dist/chrome-mv3/src/inspector/index.js +82 -0
- package/dist/chrome-mv3/src/inspector/picker.js +101 -0
- package/dist/chrome-mv3/src/inspector/react-detector.js +225 -0
- package/package.json +75 -0
- package/public/icon/128.png +0 -0
- package/public/icon/16.png +0 -0
- package/public/icon/48.png +0 -0
- package/src/cli/index.js +278 -0
- package/src/hooks/check-selection.mjs +123 -0
- package/src/server/index.js +571 -0
- package/src/shared/constants.js +97 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var content=(function(){"use strict";function E(n){return n}const u={matches:["<all_urls>"],runAt:"document_start",main(){function n(){return!!(chrome.runtime&&chrome.runtime.id)}function t(){if(document.getElementById("pixelpick-reload-notification"))return;const e=document.createElement("div");if(e.id="pixelpick-reload-notification",e.style.cssText=`
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 20px;
|
|
4
|
+
right: 20px;
|
|
5
|
+
background: #1e293b;
|
|
6
|
+
color: white;
|
|
7
|
+
padding: 16px 20px;
|
|
8
|
+
border-radius: 8px;
|
|
9
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
10
|
+
z-index: 2147483647;
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
12
|
+
font-size: 14px;
|
|
13
|
+
max-width: 320px;
|
|
14
|
+
animation: pixelpick-slidein 0.3s ease-out;
|
|
15
|
+
`,e.innerHTML=`
|
|
16
|
+
<div style="font-weight: 600; margin-bottom: 8px;">🔍 PixelPick Extension Updated</div>
|
|
17
|
+
<div style="opacity: 0.9; margin-bottom: 12px;">Please reload this page to continue using element selection.</div>
|
|
18
|
+
<button id="pixelpick-reload-btn" style="
|
|
19
|
+
background: #3b82f6;
|
|
20
|
+
color: white;
|
|
21
|
+
border: none;
|
|
22
|
+
padding: 8px 16px;
|
|
23
|
+
border-radius: 6px;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
">Reload Page</button>
|
|
28
|
+
`,!document.getElementById("pixelpick-reload-styles")){const i=document.createElement("style");i.id="pixelpick-reload-styles",i.textContent=`
|
|
29
|
+
@keyframes pixelpick-slidein {
|
|
30
|
+
from { transform: translateX(400px); opacity: 0; }
|
|
31
|
+
to { transform: translateX(0); opacity: 1; }
|
|
32
|
+
}
|
|
33
|
+
`,document.head.appendChild(i)}document.body.appendChild(e),document.getElementById("pixelpick-reload-btn")?.addEventListener("click",()=>{window.location.reload()}),setTimeout(()=>{e.parentNode&&(e.style.animation="pixelpick-slidein 0.3s ease-out reverse",setTimeout(()=>e.remove(),300))},1e4)}if(n()){const e=document.createElement("script");e.src=chrome.runtime.getURL("src/inspector/index.js"),e.type="module",(document.head||document.documentElement).appendChild(e)}else console.error("[PixelPick Bridge] Extension context invalidated on load"),t();window.addEventListener("message",e=>{if(e.source!==window||!e.data||e.data.source!=="pixelpick-inspector")return;const{type:i,data:r}=e.data;switch(i){case"selection":if(!n()){console.error("[PixelPick Bridge] Extension context invalidated - cannot send selection"),t();return}chrome.runtime.sendMessage({type:"selection",data:r},x=>{chrome.runtime.lastError?(console.error("[PixelPick Bridge] Error sending to background:",chrome.runtime.lastError),chrome.runtime.lastError.message?.includes("Extension context invalidated")&&t()):console.log("[PixelPick Bridge] Selection sent to background")});break;case"picker_deactivated":chrome.runtime.sendMessage({type:"picker_deactivated"},x=>{chrome.runtime.lastError&&console.error("[PixelPick Bridge] Error sending picker_deactivated:",chrome.runtime.lastError)});break;default:console.log("[PixelPick Bridge] Unknown message type:",i)}}),chrome.runtime.onMessage.addListener((e,i,r)=>{if(e.type==="activate_picker")return window.postMessage({source:"pixelpick-bridge",type:"activate_picker"},"*"),r({success:!0}),!0;if(e.type==="deactivate_picker")return window.postMessage({source:"pixelpick-bridge",type:"deactivate_picker"},"*"),r({success:!0}),!0}),console.log("[PixelPick Bridge] Content bridge loaded")}};function s(n,...t){}const p={debug:(...n)=>s(console.debug,...n),log:(...n)=>s(console.log,...n),warn:(...n)=>s(console.warn,...n),error:(...n)=>s(console.error,...n)},l=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;var m=class d extends Event{static EVENT_NAME=c("wxt:locationchange");constructor(t,e){super(d.EVENT_NAME,{}),this.newUrl=t,this.oldUrl=e}};function c(n){return`${l?.runtime?.id}:content:${n}`}function h(n){let t,e;return{run(){t==null&&(e=new URL(location.href),t=n.setInterval(()=>{let i=new URL(location.href);i.href!==e.href&&(window.dispatchEvent(new m(i,e)),e=i)},1e3))}}}var g=class o{static SCRIPT_STARTED_MESSAGE_TYPE=c("wxt:content-script-started");id;abortController;locationWatcher=h(this);constructor(t,e){this.contentScriptName=t,this.options=e,this.id=Math.random().toString(36).slice(2),this.abortController=new AbortController,this.stopOldScripts(),this.listenForNewerScripts()}get signal(){return this.abortController.signal}abort(t){return this.abortController.abort(t)}get isInvalid(){return l.runtime?.id==null&&this.notifyInvalidated(),this.signal.aborted}get isValid(){return!this.isInvalid}onInvalidated(t){return this.signal.addEventListener("abort",t),()=>this.signal.removeEventListener("abort",t)}block(){return new Promise(()=>{})}setInterval(t,e){const i=setInterval(()=>{this.isValid&&t()},e);return this.onInvalidated(()=>clearInterval(i)),i}setTimeout(t,e){const i=setTimeout(()=>{this.isValid&&t()},e);return this.onInvalidated(()=>clearTimeout(i)),i}requestAnimationFrame(t){const e=requestAnimationFrame((...i)=>{this.isValid&&t(...i)});return this.onInvalidated(()=>cancelAnimationFrame(e)),e}requestIdleCallback(t,e){const i=requestIdleCallback((...r)=>{this.signal.aborted||t(...r)},e);return this.onInvalidated(()=>cancelIdleCallback(i)),i}addEventListener(t,e,i,r){e==="wxt:locationchange"&&this.isValid&&this.locationWatcher.run(),t.addEventListener?.(e.startsWith("wxt:")?c(e):e,i,{...r,signal:this.signal})}notifyInvalidated(){this.abort("Content script context invalidated"),p.debug(`Content script "${this.contentScriptName}" context invalidated`)}stopOldScripts(){document.dispatchEvent(new CustomEvent(o.SCRIPT_STARTED_MESSAGE_TYPE,{detail:{contentScriptName:this.contentScriptName,messageId:this.id}})),window.postMessage({type:o.SCRIPT_STARTED_MESSAGE_TYPE,contentScriptName:this.contentScriptName,messageId:this.id},"*")}verifyScriptStartedEvent(t){const e=t.detail?.contentScriptName===this.contentScriptName,i=t.detail?.messageId===this.id;return e&&!i}listenForNewerScripts(){const t=e=>{!(e instanceof CustomEvent)||!this.verifyScriptStartedEvent(e)||this.notifyInvalidated()};document.addEventListener(o.SCRIPT_STARTED_MESSAGE_TYPE,t),this.onInvalidated(()=>document.removeEventListener(o.SCRIPT_STARTED_MESSAGE_TYPE,t))}};function b(){}function a(n,...t){}const f={debug:(...n)=>a(console.debug,...n),log:(...n)=>a(console.log,...n),warn:(...n)=>a(console.warn,...n),error:(...n)=>a(console.error,...n)};return(async()=>{try{const{main:n,...t}=u;return await n(new g("content",t))}catch(n){throw f.error('The content script "content" crashed on startup!',n),n}})()})();
|
|
34
|
+
content;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"manifest_version":3,"name":"PixelPick","description":"Select UI elements and send context to Claude Code","version":"2.0.0","icons":{"16":"icon/16.png","48":"icon/48.png","128":"icon/128.png"},"permissions":["activeTab","scripting","alarms","sidePanel"],"host_permissions":["<all_urls>"],"action":{"default_title":"Open PixelPick","default_icon":{"16":"icon/16.png","48":"icon/48.png","128":"icon/128.png"}},"side_panel":{"default_path":"sidepanel.html"},"minimum_chrome_version":"116","content_security_policy":{"extension_pages":"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"},"web_accessible_resources":[{"resources":["src/inspector/index.js","src/inspector/dom-inspector.js","src/inspector/react-detector.js","src/inspector/picker.js","src/inspector/highlight.js","src/constants.js"],"matches":["<all_urls>"]}],"background":{"service_worker":"background.js"},"content_scripts":[{"matches":["<all_urls>"],"run_at":"document_start","js":["content-scripts/content.js"]}]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>PixelPick</title>
|
|
7
|
+
<script type="module" crossorigin src="/chunks/sidepanel-CzCbYvqW.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/sidepanel-D7qwGDau.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Extension-specific constants for PixelPick Chrome Extension
|
|
2
|
+
// Note: Extension files cannot import from parent directories due to Chrome manifest restrictions
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket connection configuration
|
|
6
|
+
*/
|
|
7
|
+
export const WS_PORT = 9315; // TODO: Make configurable via chrome.storage if needed
|
|
8
|
+
export const WS_URL = `ws://localhost:${WS_PORT}`;
|
|
9
|
+
|
|
10
|
+
export const WEBSOCKET_CONFIG = {
|
|
11
|
+
HEARTBEAT_INTERVAL: 20000, // 20 seconds - keep service worker alive
|
|
12
|
+
RECONNECT_DELAYS: [3000, 6000, 12000, 24000, 60000], // Exponential backoff
|
|
13
|
+
MAX_RECONNECT_ATTEMPTS: 10,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Badge states for extension icon
|
|
18
|
+
*/
|
|
19
|
+
export const BADGE_STATES = {
|
|
20
|
+
CONNECTED: {
|
|
21
|
+
color: '#10b981', // Green
|
|
22
|
+
text: '',
|
|
23
|
+
},
|
|
24
|
+
DISCONNECTED: {
|
|
25
|
+
color: '#f59e0b', // Yellow/Orange
|
|
26
|
+
text: '!',
|
|
27
|
+
},
|
|
28
|
+
ERROR: {
|
|
29
|
+
color: '#ef4444', // Red
|
|
30
|
+
text: 'X',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Highlight overlay styles for picker mode
|
|
36
|
+
*/
|
|
37
|
+
export const HIGHLIGHT_STYLES = {
|
|
38
|
+
BORDER_COLOR: '#0ea5e9', // Sky blue
|
|
39
|
+
BORDER_WIDTH: '2px',
|
|
40
|
+
BACKGROUND_COLOR: 'rgba(14, 165, 233, 0.1)', // 10% opacity
|
|
41
|
+
BORDER_RADIUS: '2px',
|
|
42
|
+
TRANSITION: 'all 0.05s ease-out',
|
|
43
|
+
Z_INDEX: '2147483647', // Max z-index
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* React detection configuration
|
|
48
|
+
*/
|
|
49
|
+
export const REACT_CONFIG = {
|
|
50
|
+
MAX_COMPONENT_TREE_DEPTH: 5,
|
|
51
|
+
MAX_PROPS_LENGTH: 200,
|
|
52
|
+
FIBER_KEY_PATTERNS: ['__reactFiber', '__reactInternalInstance'],
|
|
53
|
+
CONTAINER_KEY_PATTERN: '__reactContainer',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tailwind CSS class patterns for detection
|
|
58
|
+
*/
|
|
59
|
+
export const TAILWIND_PATTERNS = [
|
|
60
|
+
/^bg-/, /^text-/, /^flex/, /^grid/, /^p-/, /^m-/,
|
|
61
|
+
/^w-/, /^h-/, /^rounded/, /^shadow/, /^border/,
|
|
62
|
+
/^hover:/, /^focus:/, /^active:/, /^transition/,
|
|
63
|
+
];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// DOM element inspection utilities
|
|
2
|
+
import { TAILWIND_PATTERNS } from '../constants.js';
|
|
3
|
+
import { detectFramework, extractReactData, getReactVersion } from './react-detector.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Inspects a DOM element and extracts all relevant data
|
|
7
|
+
* @param {HTMLElement} element
|
|
8
|
+
* @returns {Object} - Complete element inspection data
|
|
9
|
+
*/
|
|
10
|
+
export function inspectElement(element) {
|
|
11
|
+
const result = {
|
|
12
|
+
tagName: element.tagName.toLowerCase(),
|
|
13
|
+
id: element.id || null,
|
|
14
|
+
className: element.className || null,
|
|
15
|
+
textContent: element.textContent?.trim().slice(0, 100) || null,
|
|
16
|
+
selector: generateSelector(element),
|
|
17
|
+
styles: getComputedStylesAndTailwind(element),
|
|
18
|
+
boundingBox: element.getBoundingClientRect().toJSON(),
|
|
19
|
+
accessibility: getAccessibilityInfo(element),
|
|
20
|
+
framework: detectFramework(element),
|
|
21
|
+
component: null,
|
|
22
|
+
props: null,
|
|
23
|
+
fileLocation: null,
|
|
24
|
+
componentTree: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Framework-specific enrichment
|
|
28
|
+
if (result.framework === 'react') {
|
|
29
|
+
const reactData = extractReactData(element);
|
|
30
|
+
if (reactData) {
|
|
31
|
+
result.component = reactData.name;
|
|
32
|
+
result.props = reactData.props;
|
|
33
|
+
result.fileLocation = reactData.fileLocation;
|
|
34
|
+
result.componentTree = reactData.tree;
|
|
35
|
+
|
|
36
|
+
// Add React version warning if applicable
|
|
37
|
+
const version = getReactVersion();
|
|
38
|
+
if (version.startsWith('19') && !reactData.fileLocation) {
|
|
39
|
+
result.reactVersionNote = 'React 19 detected - file location may be unavailable';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates a unique CSS selector for an element
|
|
49
|
+
* Priority: id > data-testid > nth-child path
|
|
50
|
+
* @param {HTMLElement} element
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function generateSelector(element) {
|
|
54
|
+
if (element.id) {
|
|
55
|
+
return `#${element.id}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (element.hasAttribute('data-testid')) {
|
|
59
|
+
return `[data-testid="${element.getAttribute('data-testid')}"]`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Generate nth-child path
|
|
63
|
+
const path = [];
|
|
64
|
+
let current = element;
|
|
65
|
+
|
|
66
|
+
while (current && current !== document.documentElement) {
|
|
67
|
+
let selector = current.tagName.toLowerCase();
|
|
68
|
+
|
|
69
|
+
if (current.id) {
|
|
70
|
+
selector = `#${current.id}`;
|
|
71
|
+
path.unshift(selector);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get nth-child index
|
|
76
|
+
let sibling = current;
|
|
77
|
+
let nth = 1;
|
|
78
|
+
while (sibling.previousElementSibling) {
|
|
79
|
+
sibling = sibling.previousElementSibling;
|
|
80
|
+
if (sibling.tagName === current.tagName) {
|
|
81
|
+
nth++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (nth > 1 || current.parentElement?.children.length > 1) {
|
|
86
|
+
selector += `:nth-child(${nth})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
path.unshift(selector);
|
|
90
|
+
current = current.parentElement;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return path.join(' > ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gets computed styles and detects Tailwind classes
|
|
98
|
+
* @param {HTMLElement} element
|
|
99
|
+
* @returns {Object}
|
|
100
|
+
*/
|
|
101
|
+
export function getComputedStylesAndTailwind(element) {
|
|
102
|
+
const computed = window.getComputedStyle(element);
|
|
103
|
+
|
|
104
|
+
const styles = {
|
|
105
|
+
display: computed.display,
|
|
106
|
+
position: computed.position,
|
|
107
|
+
width: computed.width,
|
|
108
|
+
height: computed.height,
|
|
109
|
+
padding: computed.padding,
|
|
110
|
+
margin: computed.margin,
|
|
111
|
+
backgroundColor: computed.backgroundColor,
|
|
112
|
+
color: computed.color,
|
|
113
|
+
fontSize: computed.fontSize,
|
|
114
|
+
fontFamily: computed.fontFamily,
|
|
115
|
+
fontWeight: computed.fontWeight,
|
|
116
|
+
lineHeight: computed.lineHeight,
|
|
117
|
+
borderRadius: computed.borderRadius,
|
|
118
|
+
boxShadow: computed.boxShadow,
|
|
119
|
+
transition: computed.transition,
|
|
120
|
+
transform: computed.transform,
|
|
121
|
+
opacity: computed.opacity,
|
|
122
|
+
zIndex: computed.zIndex,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Detect Tailwind classes
|
|
126
|
+
const classNames = element.className?.toString().split(/\s+/).filter(Boolean) || [];
|
|
127
|
+
|
|
128
|
+
// Use patterns from constants
|
|
129
|
+
const tailwindClasses = classNames.filter(cls =>
|
|
130
|
+
TAILWIND_PATTERNS.some(pattern => pattern.test(cls))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (tailwindClasses.length > 0) {
|
|
134
|
+
styles._tailwindClasses = tailwindClasses;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return styles;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets accessibility information for an element
|
|
142
|
+
* @param {HTMLElement} element
|
|
143
|
+
* @returns {Object}
|
|
144
|
+
*/
|
|
145
|
+
export function getAccessibilityInfo(element) {
|
|
146
|
+
const role = element.getAttribute('role') || element.tagName.toLowerCase();
|
|
147
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
148
|
+
const ariaLabelledBy = element.getAttribute('aria-labelledby');
|
|
149
|
+
const tabIndex = element.tabIndex;
|
|
150
|
+
|
|
151
|
+
// Check if element is interactive
|
|
152
|
+
const interactiveTags = ['a', 'button', 'input', 'select', 'textarea', 'details', 'summary'];
|
|
153
|
+
const isInteractive = interactiveTags.includes(element.tagName.toLowerCase()) ||
|
|
154
|
+
element.hasAttribute('onclick') ||
|
|
155
|
+
element.hasAttribute('role') ||
|
|
156
|
+
tabIndex >= 0;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
role,
|
|
160
|
+
ariaLabel,
|
|
161
|
+
ariaLabelledBy,
|
|
162
|
+
tabIndex,
|
|
163
|
+
isInteractive,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Visual highlight overlay for element selection
|
|
2
|
+
import { HIGHLIGHT_STYLES } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
let highlightOverlay = null;
|
|
5
|
+
let tooltipOverlay = null;
|
|
6
|
+
let currentHighlightedElement = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Highlights an element with a visual overlay
|
|
10
|
+
* @param {HTMLElement} element - The element to highlight
|
|
11
|
+
*/
|
|
12
|
+
export function highlightElement(element) {
|
|
13
|
+
removeHighlight();
|
|
14
|
+
currentHighlightedElement = element;
|
|
15
|
+
|
|
16
|
+
const rect = element.getBoundingClientRect();
|
|
17
|
+
|
|
18
|
+
if (!highlightOverlay) {
|
|
19
|
+
highlightOverlay = document.createElement('div');
|
|
20
|
+
highlightOverlay.style.position = 'fixed';
|
|
21
|
+
highlightOverlay.style.pointerEvents = 'none';
|
|
22
|
+
highlightOverlay.style.zIndex = HIGHLIGHT_STYLES.Z_INDEX;
|
|
23
|
+
highlightOverlay.style.transition = HIGHLIGHT_STYLES.TRANSITION;
|
|
24
|
+
document.body.appendChild(highlightOverlay);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
highlightOverlay.style.left = rect.left + 'px';
|
|
28
|
+
highlightOverlay.style.top = rect.top + 'px';
|
|
29
|
+
highlightOverlay.style.width = rect.width + 'px';
|
|
30
|
+
highlightOverlay.style.height = rect.height + 'px';
|
|
31
|
+
highlightOverlay.style.border = `${HIGHLIGHT_STYLES.BORDER_WIDTH} solid ${HIGHLIGHT_STYLES.BORDER_COLOR}`;
|
|
32
|
+
highlightOverlay.style.backgroundColor = HIGHLIGHT_STYLES.BACKGROUND_COLOR;
|
|
33
|
+
highlightOverlay.style.borderRadius = HIGHLIGHT_STYLES.BORDER_RADIUS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Removes the highlight overlay
|
|
38
|
+
*/
|
|
39
|
+
export function removeHighlight() {
|
|
40
|
+
if (highlightOverlay) {
|
|
41
|
+
highlightOverlay.remove();
|
|
42
|
+
highlightOverlay = null;
|
|
43
|
+
}
|
|
44
|
+
hideTooltip();
|
|
45
|
+
currentHighlightedElement = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gets the currently highlighted element
|
|
50
|
+
* @returns {HTMLElement|null}
|
|
51
|
+
*/
|
|
52
|
+
export function getCurrentHighlightedElement() {
|
|
53
|
+
return currentHighlightedElement;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Flash the highlight to provide visual feedback
|
|
58
|
+
* @param {string} color - The color to flash (defaults to green)
|
|
59
|
+
*/
|
|
60
|
+
export function flashHighlight(color = 'rgba(16, 185, 129, 0.2)') {
|
|
61
|
+
if (highlightOverlay) {
|
|
62
|
+
const originalColor = highlightOverlay.style.backgroundColor;
|
|
63
|
+
highlightOverlay.style.backgroundColor = color;
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
if (highlightOverlay) {
|
|
66
|
+
highlightOverlay.style.backgroundColor = originalColor;
|
|
67
|
+
}
|
|
68
|
+
}, 150);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Shows a tooltip with element information
|
|
74
|
+
* @param {HTMLElement} element - The element being hovered
|
|
75
|
+
* @param {Object} data - Element data (selector, component, etc.)
|
|
76
|
+
* @param {number} mouseX - Mouse X position
|
|
77
|
+
* @param {number} mouseY - Mouse Y position
|
|
78
|
+
*/
|
|
79
|
+
export function showTooltip(element, data, mouseX, mouseY) {
|
|
80
|
+
const rect = element.getBoundingClientRect();
|
|
81
|
+
|
|
82
|
+
if (!tooltipOverlay) {
|
|
83
|
+
tooltipOverlay = document.createElement('div');
|
|
84
|
+
tooltipOverlay.style.cssText = `
|
|
85
|
+
position: fixed;
|
|
86
|
+
background: #1e293b;
|
|
87
|
+
color: white;
|
|
88
|
+
padding: 8px 12px;
|
|
89
|
+
border-radius: 6px;
|
|
90
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
line-height: 1.5;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
z-index: ${parseInt(HIGHLIGHT_STYLES.Z_INDEX) + 1};
|
|
95
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
transition: opacity 0.1s ease;
|
|
98
|
+
`;
|
|
99
|
+
document.body.appendChild(tooltipOverlay);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build tooltip content
|
|
103
|
+
let content = `<div style="font-weight: 600; color: #0ea5e9;">${data.selector || element.tagName.toLowerCase()}</div>`;
|
|
104
|
+
|
|
105
|
+
if (data.component?.name) {
|
|
106
|
+
content += `<div style="color: #10b981; margin-top: 2px;">${data.component.name}</div>`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const width = Math.round(rect.width);
|
|
110
|
+
const height = Math.round(rect.height);
|
|
111
|
+
content += `<div style="color: #94a3b8; margin-top: 2px; font-size: 11px;">${width} × ${height}</div>`;
|
|
112
|
+
|
|
113
|
+
if (data.component?.file) {
|
|
114
|
+
const fileName = data.component.file.split('/').pop();
|
|
115
|
+
const lineInfo = data.component.line ? `:${data.component.line}` : '';
|
|
116
|
+
content += `<div style="color: #94a3b8; margin-top: 2px; font-size: 10px;">${fileName}${lineInfo}</div>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
tooltipOverlay.innerHTML = content;
|
|
120
|
+
|
|
121
|
+
// Position tooltip near cursor, but avoid edges
|
|
122
|
+
const tooltipRect = tooltipOverlay.getBoundingClientRect();
|
|
123
|
+
const padding = 12;
|
|
124
|
+
|
|
125
|
+
let left = mouseX + padding;
|
|
126
|
+
let top = mouseY + padding;
|
|
127
|
+
|
|
128
|
+
// Adjust if tooltip goes off right edge
|
|
129
|
+
if (left + tooltipRect.width > window.innerWidth - padding) {
|
|
130
|
+
left = mouseX - tooltipRect.width - padding;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Adjust if tooltip goes off bottom edge
|
|
134
|
+
if (top + tooltipRect.height > window.innerHeight - padding) {
|
|
135
|
+
top = mouseY - tooltipRect.height - padding;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Keep within bounds
|
|
139
|
+
left = Math.max(padding, Math.min(left, window.innerWidth - tooltipRect.width - padding));
|
|
140
|
+
top = Math.max(padding, Math.min(top, window.innerHeight - tooltipRect.height - padding));
|
|
141
|
+
|
|
142
|
+
tooltipOverlay.style.left = left + 'px';
|
|
143
|
+
tooltipOverlay.style.top = top + 'px';
|
|
144
|
+
tooltipOverlay.style.opacity = '1';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Hides the tooltip overlay
|
|
149
|
+
*/
|
|
150
|
+
export function hideTooltip() {
|
|
151
|
+
if (tooltipOverlay) {
|
|
152
|
+
tooltipOverlay.remove();
|
|
153
|
+
tooltipOverlay = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Inspector - MAIN world element selection and inspection
|
|
2
|
+
// Orchestrator that coordinates picker, highlight, and inspection modules
|
|
3
|
+
|
|
4
|
+
import { activatePicker, deactivatePicker, isPickerActive } from './picker.js';
|
|
5
|
+
import { flashHighlight, removeHighlight } from './highlight.js';
|
|
6
|
+
import { inspectElement } from './dom-inspector.js';
|
|
7
|
+
|
|
8
|
+
console.log('[PixelPick Inspector] Loaded');
|
|
9
|
+
|
|
10
|
+
// Event listeners
|
|
11
|
+
document.addEventListener('click', handleElementClick, true);
|
|
12
|
+
|
|
13
|
+
// Listen for picker activation/deactivation from bridge
|
|
14
|
+
window.addEventListener('message', (event) => {
|
|
15
|
+
if (event.source !== window) return;
|
|
16
|
+
if (!event.data || event.data.source !== 'pixelpick-bridge') return;
|
|
17
|
+
|
|
18
|
+
if (event.data.type === 'activate_picker') {
|
|
19
|
+
activatePicker();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (event.data.type === 'deactivate_picker') {
|
|
23
|
+
deactivatePicker();
|
|
24
|
+
notifyPickerDeactivated();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handles element clicks (Cmd+Shift+Click or picker mode)
|
|
30
|
+
* @param {MouseEvent} event
|
|
31
|
+
*/
|
|
32
|
+
function handleElementClick(event) {
|
|
33
|
+
// Check for Cmd+Shift+Click (Mac) or Ctrl+Shift+Click (Windows/Linux)
|
|
34
|
+
const isModifierClick = (event.metaKey || event.ctrlKey) && event.shiftKey;
|
|
35
|
+
|
|
36
|
+
if (!isModifierClick && !isPickerActive()) {
|
|
37
|
+
return; // Not a PixelPick interaction
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
event.stopPropagation();
|
|
42
|
+
event.stopImmediatePropagation();
|
|
43
|
+
|
|
44
|
+
const element = event.target;
|
|
45
|
+
inspectAndSendElement(element);
|
|
46
|
+
|
|
47
|
+
if (isPickerActive()) {
|
|
48
|
+
deactivatePicker();
|
|
49
|
+
notifyPickerDeactivated();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Inspects an element and sends data to content bridge
|
|
56
|
+
* @param {HTMLElement} element
|
|
57
|
+
*/
|
|
58
|
+
function inspectAndSendElement(element) {
|
|
59
|
+
const data = inspectElement(element);
|
|
60
|
+
|
|
61
|
+
console.log('[PixelPick Inspector] Element inspected:', data);
|
|
62
|
+
|
|
63
|
+
// Send to content bridge
|
|
64
|
+
window.postMessage({
|
|
65
|
+
source: 'pixelpick-inspector',
|
|
66
|
+
type: 'selection',
|
|
67
|
+
data,
|
|
68
|
+
}, '*');
|
|
69
|
+
|
|
70
|
+
// Visual feedback - flash green
|
|
71
|
+
flashHighlight('rgba(16, 185, 129, 0.2)');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Notifies that picker was deactivated
|
|
76
|
+
*/
|
|
77
|
+
function notifyPickerDeactivated() {
|
|
78
|
+
window.postMessage({
|
|
79
|
+
source: 'pixelpick-inspector',
|
|
80
|
+
type: 'picker_deactivated',
|
|
81
|
+
}, '*');
|
|
82
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Picker mode for visual element selection
|
|
2
|
+
import { highlightElement, removeHighlight, showTooltip, hideTooltip } from './highlight.js';
|
|
3
|
+
import { inspectElement } from './dom-inspector.js';
|
|
4
|
+
|
|
5
|
+
let pickerActive = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handles Escape key to cancel picker
|
|
9
|
+
* @param {KeyboardEvent} event
|
|
10
|
+
*/
|
|
11
|
+
function handleEscapeKey(event) {
|
|
12
|
+
console.log('[PixelPick Picker] Key pressed:', event.key);
|
|
13
|
+
if (event.key === 'Escape') {
|
|
14
|
+
console.log('[PixelPick Picker] Escape detected, deactivating picker');
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
event.stopPropagation();
|
|
17
|
+
event.stopImmediatePropagation();
|
|
18
|
+
deactivatePicker();
|
|
19
|
+
|
|
20
|
+
// Notify that picker was deactivated
|
|
21
|
+
window.postMessage({
|
|
22
|
+
source: 'pixelpick-inspector',
|
|
23
|
+
type: 'picker_deactivated',
|
|
24
|
+
}, '*');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Activates picker mode - user can click to select elements
|
|
30
|
+
*/
|
|
31
|
+
export function activatePicker() {
|
|
32
|
+
pickerActive = true;
|
|
33
|
+
document.body.style.cursor = 'crosshair';
|
|
34
|
+
|
|
35
|
+
// Add mousemove listener for visual highlight
|
|
36
|
+
document.addEventListener('mousemove', handleMouseMove, true);
|
|
37
|
+
|
|
38
|
+
// Add escape key listener
|
|
39
|
+
document.addEventListener('keydown', handleEscapeKey, true);
|
|
40
|
+
window.addEventListener('keydown', handleEscapeKey, true);
|
|
41
|
+
|
|
42
|
+
console.log('[PixelPick Inspector] Picker activated - click to select, press Esc to cancel');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Deactivates picker mode
|
|
47
|
+
*/
|
|
48
|
+
export function deactivatePicker() {
|
|
49
|
+
pickerActive = false;
|
|
50
|
+
document.body.style.cursor = '';
|
|
51
|
+
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
52
|
+
document.removeEventListener('keydown', handleEscapeKey, true);
|
|
53
|
+
window.removeEventListener('keydown', handleEscapeKey, true);
|
|
54
|
+
removeHighlight();
|
|
55
|
+
hideTooltip();
|
|
56
|
+
|
|
57
|
+
console.log('[PixelPick Inspector] Picker deactivated');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks if picker mode is currently active
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function isPickerActive() {
|
|
65
|
+
return pickerActive;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Handles mouse movement to highlight elements under cursor
|
|
70
|
+
* @param {MouseEvent} event
|
|
71
|
+
*/
|
|
72
|
+
function handleMouseMove(event) {
|
|
73
|
+
const element = document.elementFromPoint(event.clientX, event.clientY);
|
|
74
|
+
if (element) {
|
|
75
|
+
highlightElement(element);
|
|
76
|
+
|
|
77
|
+
// Inspect element for tooltip data
|
|
78
|
+
const data = inspectElement(element);
|
|
79
|
+
|
|
80
|
+
// Show tooltip with element info
|
|
81
|
+
showTooltip(element, {
|
|
82
|
+
selector: data.selector,
|
|
83
|
+
component: data.component ? {
|
|
84
|
+
name: data.component,
|
|
85
|
+
file: data.fileLocation,
|
|
86
|
+
line: extractLineNumber(data.fileLocation),
|
|
87
|
+
} : null,
|
|
88
|
+
}, event.clientX, event.clientY);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extracts line number from file location string
|
|
94
|
+
* @param {string} fileLocation - e.g., "src/App.tsx:42" or "src/App.tsx:42:10"
|
|
95
|
+
* @returns {number|null}
|
|
96
|
+
*/
|
|
97
|
+
function extractLineNumber(fileLocation) {
|
|
98
|
+
if (!fileLocation) return null;
|
|
99
|
+
const match = fileLocation.match(/:(\d+)/);
|
|
100
|
+
return match ? parseInt(match[1], 10) : null;
|
|
101
|
+
}
|