Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 53 additions & 21 deletions src/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
Expand All @@ -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() {
Expand Down