mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Refactors the extension for on-demand injection (no static content_scripts entry — content script and detector are loaded only when the user actively opens the Impeccable panel, sidebar pane, or popup). Adds a new "Auto-scan" preference (default: scan when the Impeccable panel opens, opt-in: scan when DevTools opens) plus configurable line length (strict/lax) and highlight blur on/off settings. Adds an Elements panel sidebar that shows findings for the currently selected element. Includes substantial overlay UX work: page-pixel-perfect spotlight mask via clip-path, refined hover/dim states, instant transitions for snappier feel, copy buttons for findings, hover-from-panel highlighting, and a brand-aware exception list so the font check no longer flags Roboto on Google's own properties. Robustness fixes for the MV3 service worker lifecycle: heartbeat keepalive plus auto-reconnecting ports across panel/sidebar/devtools so transient SW restarts don't break the panel UI, and immediate teardown on DevTools close (replacing an unreliable setTimeout-based defer that didn't survive SW termination). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.0 KiB
JavaScript
104 lines
3.0 KiB
JavaScript
/**
|
|
* Impeccable DevTools Extension - Elements Sidebar Pane
|
|
*
|
|
* Shows Impeccable findings for the currently selected element ($0) in the Elements panel.
|
|
*/
|
|
|
|
if (chrome.devtools.panels.themeName === 'dark') {
|
|
document.documentElement.classList.add('theme-dark');
|
|
}
|
|
|
|
const tabId = chrome.devtools.inspectedWindow.tabId;
|
|
const content = document.getElementById('sidebar-content');
|
|
let currentFindings = [];
|
|
|
|
// Auto-reconnecting port (service worker may restart in MV3)
|
|
let port = null;
|
|
function getPort() {
|
|
if (port) return port;
|
|
port = chrome.runtime.connect({ name: `impeccable-sidebar-${tabId}` });
|
|
port.onMessage.addListener((msg) => {
|
|
if (msg.action === 'findings' || msg.action === 'state') {
|
|
currentFindings = msg.findings || [];
|
|
refreshForCurrentSelection();
|
|
}
|
|
});
|
|
port.onDisconnect.addListener(() => { port = null; });
|
|
return port;
|
|
}
|
|
getPort();
|
|
|
|
chrome.devtools.panels.elements.onSelectionChanged.addListener(refreshForCurrentSelection);
|
|
|
|
function refreshForCurrentSelection() {
|
|
if (!currentFindings.length) {
|
|
renderEmpty('No findings on this page yet.');
|
|
return;
|
|
}
|
|
|
|
// Collect non-page-level selectors and ask the inspected window which one matches $0
|
|
const selectors = [];
|
|
for (const item of currentFindings) {
|
|
if (item.isPageLevel || item.isHidden) continue;
|
|
selectors.push(item.selector);
|
|
}
|
|
if (!selectors.length) {
|
|
renderEmpty('No element-level findings on this page.');
|
|
return;
|
|
}
|
|
|
|
const code = `(function() {
|
|
var sels = ${JSON.stringify(selectors)};
|
|
var matched = [];
|
|
for (var i = 0; i < sels.length; i++) {
|
|
try { if (document.querySelector(sels[i]) === $0) matched.push(sels[i]); } catch (e) {}
|
|
}
|
|
return matched;
|
|
})()`;
|
|
|
|
chrome.devtools.inspectedWindow.eval(code, (matched) => {
|
|
if (!matched || !matched.length) {
|
|
renderNoFindings();
|
|
return;
|
|
}
|
|
const items = currentFindings.filter(item => matched.includes(item.selector));
|
|
render(items);
|
|
});
|
|
}
|
|
|
|
function renderEmpty(text) {
|
|
content.innerHTML = `<div class="state">${escapeHtml(text)}</div>`;
|
|
}
|
|
|
|
function renderNoFindings() {
|
|
content.innerHTML = `<div class="state"><strong>Clean.</strong> No anti-patterns on this element.</div>`;
|
|
}
|
|
|
|
function render(items) {
|
|
const html = [];
|
|
for (const item of items) {
|
|
for (const f of item.findings) {
|
|
const isSlop = f.category === 'slop';
|
|
const marker = isSlop ? '<span class="marker">\u2726</span>' : '';
|
|
const kind = isSlop ? 'AI tell' : 'Quality';
|
|
html.push(`
|
|
<div class="finding">
|
|
<div class="finding-header">
|
|
<span class="finding-name">${marker}${escapeHtml(f.name)}</span>
|
|
<span class="finding-kind">${kind}</span>
|
|
</div>
|
|
<div class="finding-detail">${escapeHtml(f.detail)}</div>
|
|
<div class="finding-description">${escapeHtml(f.description)}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
}
|
|
content.innerHTML = html.join('');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|