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.
@@ -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
+ }