From 64d164bfae0dbde99e043423b0d3e3900a2a2e67 Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Wed, 8 Apr 2026 17:46:23 +0200 Subject: [PATCH 1/2] fix: resolve Shift+C shortcut conflict, CSP html2canvas loading, and add selection highlight - Shift+C no longer triggers annotation mode while typing in the feedback panel textarea (Shadow DOM focus was not detected by document.activeElement) - html2canvas now loads via fetch + new Function instead of script injection, avoiding CSP script-src restrictions in browser extension context - Selected element is highlighted with a blue border while the feedback panel is open, giving users a visual indicator of their selection Co-authored-by: Claude --- CHANGELOG.md | 7 +++++ src/widget.js | 74 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe38a4d..0897270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Blue highlight indicator on selected element while feedback panel is open, so users can see which element they picked + +### Fixed + +- Shift+C keyboard shortcut no longer fires while typing in the feedback textarea (Shadow DOM focus detection) +- html2canvas loading in browser extension context - uses fetch instead of script injection to avoid CSP restrictions + - CSS isolation wrapper (`.cf-root`) inside Shadow DOM - uses `all: initial` to fully break CSS inheritance from host page, preventing dark-themed sites from affecting widget appearance - Shadow DOM isolation for widget - host page CSS no longer leaks into the widget UI - Tooltip selector truncation - long CSS selectors in hover tooltip are now truncated to 2 levels with `... >` prefix diff --git a/src/widget.js b/src/widget.js index 5541ddb..0030793 100644 --- a/src/widget.js +++ b/src/widget.js @@ -405,6 +405,11 @@ display: none; } + #${WIDGET_ID}-highlight.selected { + border-color: #3b82f6; + background: rgba(59, 130, 246, 0.1); + } + #${WIDGET_ID}-tooltip { position: fixed; background: #1f2937; @@ -939,25 +944,31 @@ if (typeof html2canvas !== 'undefined') return Promise.resolve(); if (html2canvasPromise) return html2canvasPromise; - html2canvasPromise = new Promise((resolve, reject) => { - // Derive HTTP URL from WS_URL (same host/port) - let baseUrl; - try { - const wsUrl = new URL(WS_URL); - baseUrl = `http://${wsUrl.host}`; - } catch { - baseUrl = `http://localhost:9877`; - } - - const script = document.createElement('script'); - script.src = `${baseUrl}/html2canvas.min.js`; - script.onload = resolve; - script.onerror = () => { + // Derive HTTP URL from WS_URL (same host/port) + let baseUrl; + try { + const wsUrl = new URL(WS_URL); + baseUrl = `http://${wsUrl.host}`; + } catch { + baseUrl = `http://localhost:9877`; + } + + const url = `${baseUrl}/html2canvas.min.js`; + + // Use fetch + new Function to avoid CSP script-src restrictions + // (e.g. when loaded via browser extension on pages with strict CSP) + html2canvasPromise = fetch(url) + .then(res => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.text(); + }) + .then(scriptText => { + new Function(scriptText)(); + }) + .catch(err => { html2canvasPromise = null; - reject(new Error('Failed to load html2canvas')); - }; - document.head.appendChild(script); - }); + throw new Error('Failed to load html2canvas: ' + (err.message || err)); + }); return html2canvasPromise; } @@ -1431,9 +1442,15 @@ // Shift+C to start annotation mode if (e.key === 'C' && e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) { - // Don't trigger when typing in input fields - const isInputFocused = ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) - || document.activeElement.isContentEditable; + // Don't trigger when the feedback panel is open + const panel = getEl(`${WIDGET_ID}-panel`); + if (panel && panel.classList.contains('active')) return; + + // Don't trigger when typing in input fields (including inside Shadow DOM) + const active = document.activeElement; + const deepActive = active?.shadowRoot?.activeElement || active; + const isInputFocused = ['INPUT', 'TEXTAREA'].includes(deepActive.tagName) + || deepActive.isContentEditable; if (!isInputFocused && !isAnnotationMode) { e.preventDefault(); @@ -1522,6 +1539,18 @@ screenshotEl.style.display = 'none'; } + // Show confirmed-selection highlight on the selected element + if (selectedElement) { + const highlight = getEl(`${WIDGET_ID}-highlight`); + const rect = selectedElement.getBoundingClientRect(); + highlight.style.top = `${rect.top}px`; + highlight.style.left = `${rect.left}px`; + highlight.style.width = `${rect.width}px`; + highlight.style.height = `${rect.height}px`; + highlight.classList.add('selected'); + highlight.style.display = 'block'; + } + panel.classList.add('active'); getEl(`${WIDGET_ID}-description`).focus(); } @@ -1530,6 +1559,9 @@ getEl(`${WIDGET_ID}-panel`).classList.remove('active'); getEl(`${WIDGET_ID}-description`).value = ''; selectedElement = null; + const highlight = getEl(`${WIDGET_ID}-highlight`); + highlight.style.display = 'none'; + highlight.classList.remove('selected'); } async function addItem() { From f491e1ad066c689cbdde05731142d992ee8a73d8 Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Thu, 9 Apr 2026 15:12:37 +0200 Subject: [PATCH 2/2] feat: add version logging, demo page serving, and widget version injection - Add WIDGET_VERSION constant to widget, injected by server from package.json - Console now shows "Widget v0.4.4 initialized" for easy version verification - Server serves demo/index.html at /demo/ for local development testing Co-authored-by: Claude --- src/server.js | 24 +++++++++++++++++++----- src/widget.js | 3 ++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/server.js b/src/server.js index a75daf6..eb41d96 100644 --- a/src/server.js +++ b/src/server.js @@ -17,6 +17,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = parseInt(process.env.FEEDBACK_PORT || "9877"); +const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; // Store for received feedback let pendingFeedback = []; @@ -299,11 +300,10 @@ const httpServer = http.createServer((req, res) => { res.end("Error loading widget"); return; } - // Inject the WebSocket URL into the widget - const injectedContent = content.replace( - "__WEBSOCKET_URL__", - `ws://localhost:${PORT}/ws` - ); + // Inject runtime values into the widget + const injectedContent = content + .replace("__WEBSOCKET_URL__", `ws://localhost:${PORT}/ws`) + .replace("__WIDGET_VERSION__", PKG_VERSION); res.writeHead(200, { "Content-Type": "application/javascript" }); res.end(injectedContent); }); @@ -324,6 +324,20 @@ const httpServer = http.createServer((req, res) => { return; } + if (urlObj.pathname === "/demo/index.html" || urlObj.pathname === "/demo/") { + const demoPath = path.join(__dirname, "..", "demo", "index.html"); + fs.readFile(demoPath, "utf8", (err, content) => { + if (err) { + res.writeHead(404); + res.end("Demo page not found"); + return; + } + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(content); + }); + return; + } + if (urlObj.pathname === "/status") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( diff --git a/src/widget.js b/src/widget.js index 0030793..108c5b0 100644 --- a/src/widget.js +++ b/src/widget.js @@ -22,6 +22,7 @@ // Configuration const WS_URL = '__WEBSOCKET_URL__'; // Injected by server + const WIDGET_VERSION = '__WIDGET_VERSION__'; // Injected by server const WIDGET_ID = 'claude-feedback-widget'; // State @@ -1855,7 +1856,7 @@ connectWebSocket(); startSelfHealing(); - console.log('[Claude Feedback] Widget initialized'); + console.log(`[Claude Feedback] Widget v${WIDGET_VERSION} initialized`); } init();