Polish Chrome extension for Web Store submission

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>
This commit is contained in:
Paul Bakaus
2026-04-06 21:06:09 -07:00
co-authored by Claude Opus 4.6
parent e961d56252
commit 13b2f763d9
16 changed files with 1465 additions and 178 deletions
+63
View File
@@ -0,0 +1,63 @@
# Chrome Web Store Listing
## Name
Impeccable
## Short description (132 chars max)
Detect AI slop and design anti-patterns in any web page. Open DevTools and see what needs fixing.
## Detailed description
Impeccable detects 24 common UI anti-patterns directly in your browser. Open DevTools on any page and overlays instantly highlight issues, from AI-generated design tells to accessibility and quality problems.
WHAT IT DETECTS
AI slop (design tells that scream "AI made this"):
- Side-tab accent borders
- Gradient text on headings
- Purple/violet AI color palettes
- Nested cards, monotonous spacing
- Bounce/elastic easing
- Dark mode with glowing accents
- Overused fonts, flat type hierarchy
Quality issues (general design and accessibility):
- Low contrast text (WCAG AA)
- Cramped padding, tight line height
- Skipped heading levels
- Line length too long
- Tiny body text, justified text
- Layout property animations
HOW IT WORKS
1. Install the extension
2. Open DevTools on any page (Cmd+Opt+I / F12)
3. Overlays appear automatically, highlighting issues
4. Click the "Impeccable" panel tab for a structured list of all findings
5. Click any finding to jump to the element in the Elements panel
FEATURES
- Auto-scans when DevTools opens, no manual step needed
- Grouped findings: AI tells vs. quality issues
- Click-to-inspect: jump from a finding to the element
- Toggle overlays on/off from the panel or toolbar popup
- Per-rule settings: disable detections you don't care about
- Re-scans on navigation, including SPA route changes
- Works on any website
- Runs 100% locally, no data sent anywhere
Open source at https://github.com/pbakaus/impeccable
## Category
Developer Tools
## Language
English
## Privacy policy URL
https://impeccable.style/privacy
## Single purpose description
Detects and highlights UI anti-patterns (AI-generated design tells and general quality issues) on any web page.
+94 -23
View File
@@ -13,7 +13,7 @@ const panelPorts = new Map();
function getState(tabId) { function getState(tabId) {
if (!tabState.has(tabId)) { if (!tabState.has(tabId)) {
tabState.set(tabId, { findings: [], overlaysVisible: true, injected: false }); tabState.set(tabId, { findings: [], overlaysVisible: true, injected: false, csInjected: false });
} }
return tabState.get(tabId); return tabState.get(tabId);
} }
@@ -35,17 +35,47 @@ function notifyPanels(tabId, message) {
} }
} }
async function getDisabledRules() { async function getSettings() {
const result = await chrome.storage.sync.get({ disabledRules: [] }); return chrome.storage.sync.get({
return result.disabledRules; disabledRules: [],
lineLengthMode: 'strict', // 'strict' = 80, 'lax' = 120
spotlightBlur: true, // dim/blur the page on hover-highlight
autoScan: 'panel', // 'panel' = scan when Impeccable UI opens, 'devtools' = scan when DevTools opens
});
} }
async function buildScanConfig() { async function buildScanConfig() {
const disabledRules = await getDisabledRules(); const { disabledRules, lineLengthMode, spotlightBlur } = await getSettings();
return disabledRules.length ? { disabledRules } : null; const config = {};
if (disabledRules.length) config.disabledRules = disabledRules;
config.lineLengthMax = lineLengthMode === 'lax' ? 120 : 80;
config.spotlightBlur = spotlightBlur;
return config;
}
// Inject the content script on-demand. We removed the static content_scripts entry to
// minimize the always-on footprint; the script is only loaded when the user explicitly
// engages with the extension (DevTools panel/sidebar opened, popup scan, etc).
async function ensureContentScriptInjected(tabId) {
const state = getState(tabId);
if (state.csInjected) return true;
try {
await chrome.scripting.executeScript({
target: { tabId },
files: ['content/content-script.js'],
injectImmediately: true,
});
state.csInjected = true;
return true;
} catch (err) {
// Common cause: chrome:// pages, the web store, or other restricted URLs
return false;
}
} }
async function sendScanToTab(tabId) { async function sendScanToTab(tabId) {
const ok = await ensureContentScriptInjected(tabId);
if (!ok) return;
const config = await buildScanConfig(); const config = await buildScanConfig();
chrome.tabs.sendMessage(tabId, { action: 'scan', config }).catch(() => {}); chrome.tabs.sendMessage(tabId, { action: 'scan', config }).catch(() => {});
} }
@@ -75,6 +105,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
sendResponse({ ok: true }); sendResponse({ ok: true });
} }
else if (msg.action === 'page-pointer-active' && tabId) {
notifyPanels(tabId, { action: 'page-pointer-active' });
sendResponse({ ok: true });
}
else if (msg.action === 'overlays-toggled' && tabId) { else if (msg.action === 'overlays-toggled' && tabId) {
const state = getState(tabId); const state = getState(tabId);
state.overlaysVisible = msg.visible; state.overlaysVisible = msg.visible;
@@ -115,6 +150,23 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Track which tabs have DevTools open (via the devtools.js lifecycle port) // Track which tabs have DevTools open (via the devtools.js lifecycle port)
const devtoolsTabs = new Set(); const devtoolsTabs = new Set();
async function tearDownTab(tabId) {
devtoolsTabs.delete(tabId);
// Send the remove command and await it — this keeps the SW alive long enough
// to actually deliver the message (setTimeout doesn't survive SW termination in MV3).
try {
await chrome.tabs.sendMessage(tabId, { action: 'remove' });
} catch { /* tab might be closed or content script gone */ }
const state = tabState.get(tabId);
if (state) {
state.findings = [];
state.injected = false;
state.csInjected = false;
}
updateBadge(tabId);
panelPorts.delete(tabId);
}
// Handle long-lived connections from DevTools pages and panels // Handle long-lived connections from DevTools pages and panels
chrome.runtime.onConnect.addListener((port) => { chrome.runtime.onConnect.addListener((port) => {
// Lifecycle port from devtools.js -- tracks DevTools open/close // Lifecycle port from devtools.js -- tracks DevTools open/close
@@ -124,19 +176,13 @@ chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((msg) => { port.onMessage.addListener((msg) => {
if (msg.action === 'scan') sendScanToTab(tabId); if (msg.action === 'scan') sendScanToTab(tabId);
// 'ping' is just a keepalive; no action needed
}); });
port.onDisconnect.addListener(() => { port.onDisconnect.addListener(() => {
devtoolsTabs.delete(tabId); // Tear down immediately — defer with setTimeout doesn't work reliably in MV3
// DevTools closed -- remove overlays and clear state // because the SW can be terminated before the timer fires.
chrome.tabs.sendMessage(tabId, { action: 'remove' }).catch(() => {}); tearDownTab(tabId);
const state = tabState.get(tabId);
if (state) {
state.findings = [];
state.injected = false;
}
updateBadge(tabId);
panelPorts.delete(tabId);
}); });
} }
@@ -160,6 +206,10 @@ chrome.runtime.onConnect.addListener((port) => {
sendScanToTab(tabId); sendScanToTab(tabId);
} else if (msg.action === 'toggle-overlays') { } else if (msg.action === 'toggle-overlays') {
chrome.tabs.sendMessage(tabId, { action: 'toggle-overlays' }).catch(() => {}); chrome.tabs.sendMessage(tabId, { action: 'toggle-overlays' }).catch(() => {});
} else if (msg.action === 'highlight') {
chrome.tabs.sendMessage(tabId, { action: 'highlight', selector: msg.selector }).catch(() => {});
} else if (msg.action === 'unhighlight') {
chrome.tabs.sendMessage(tabId, { action: 'unhighlight' }).catch(() => {});
} }
}); });
@@ -168,19 +218,40 @@ chrome.runtime.onConnect.addListener((port) => {
if (panelPorts.get(tabId)?.size === 0) panelPorts.delete(tabId); if (panelPorts.get(tabId)?.size === 0) panelPorts.delete(tabId);
}); });
} }
// Sidebar pane port (Elements panel sidebar) -- receives findings updates.
// Connecting the sidebar is a strong signal of "user engaged with Impeccable"
// so we trigger a scan if no findings exist yet (matches the panel port behavior).
if (port.name.startsWith('impeccable-sidebar-')) {
const tabId = parseInt(port.name.replace('impeccable-sidebar-', ''), 10);
if (!panelPorts.has(tabId)) panelPorts.set(tabId, new Set());
panelPorts.get(tabId).add(port);
const state = getState(tabId);
port.postMessage({ action: 'state', ...state });
if (!state.findings.length) sendScanToTab(tabId);
port.onDisconnect.addListener(() => {
panelPorts.get(tabId)?.delete(port);
if (panelPorts.get(tabId)?.size === 0) panelPorts.delete(tabId);
});
}
}); });
// Re-scan on navigation (only if DevTools is open for that tab) // Re-scan on navigation (only if DevTools is open AND user was actively scanning)
chrome.webNavigation?.onCompleted?.addListener((details) => { chrome.webNavigation?.onCompleted?.addListener((details) => {
if (details.frameId !== 0) return; if (details.frameId !== 0) return;
if (!devtoolsTabs.has(details.tabId)) return; if (!devtoolsTabs.has(details.tabId)) return;
const state = tabState.get(details.tabId); const state = tabState.get(details.tabId);
if (state) { if (!state) return;
state.findings = []; // Only re-scan if the user has actively engaged (had findings or injected previously)
state.injected = false; const wasActive = state.injected || state.findings.length > 0;
updateBadge(details.tabId); state.findings = [];
notifyPanels(details.tabId, { action: 'navigated' }); state.injected = false;
// Re-inject and scan after a short delay for the page to settle state.csInjected = false; // page reload destroys the content script
updateBadge(details.tabId);
notifyPanels(details.tabId, { action: 'navigated' });
if (wasActive) {
setTimeout(() => sendScanToTab(details.tabId), 300); setTimeout(() => sendScanToTab(details.tabId), 300);
} }
}); });
+106 -79
View File
@@ -4,95 +4,122 @@
* Bridges between the extension messaging system and the page-context detector. * Bridges between the extension messaging system and the page-context detector.
* The detector must run in page context (not isolated world) because it needs * The detector must run in page context (not isolated world) because it needs
* access to getComputedStyle, document.styleSheets.cssRules, etc. * access to getComputedStyle, document.styleSheets.cssRules, etc.
*
* Wrapped in an IIFE with an idempotency flag so re-injection (via
* chrome.scripting.executeScript) is a no-op and doesn't cause:
* - SyntaxError: Identifier 'foo' has already been declared
* - Duplicate event listeners accumulating over time
*/ */
(function () {
if (window.__IMPECCABLE_CS_LOADED__) return;
window.__IMPECCABLE_CS_LOADED__ = true;
let injected = false; let injected = false;
let pendingScan = false; let pendingScan = false;
let scanConfig = null; let scanConfig = null;
// Listen for commands from the service worker // Listen for commands from the service worker
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === 'scan') { if (msg.action === 'scan') {
scanConfig = msg.config || null; scanConfig = msg.config || null;
injectAndScan(); injectAndScan();
sendResponse({ ok: true }); sendResponse({ ok: true });
} else if (msg.action === 'toggle-overlays') { } else if (msg.action === 'toggle-overlays') {
window.postMessage({ source: 'impeccable-command', action: 'toggle-overlays' }, '*'); window.postMessage({ source: 'impeccable-command', action: 'toggle-overlays' }, '*');
sendResponse({ ok: true }); sendResponse({ ok: true });
} else if (msg.action === 'remove') { } else if (msg.action === 'remove') {
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*'); window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
injected = false; injected = false;
sendResponse({ ok: true }); sendResponse({ ok: true });
} } else if (msg.action === 'highlight') {
return true; window.postMessage({ source: 'impeccable-command', action: 'highlight', selector: msg.selector }, '*');
}); sendResponse({ ok: true });
} else if (msg.action === 'unhighlight') {
window.postMessage({ source: 'impeccable-command', action: 'unhighlight' }, '*');
sendResponse({ ok: true });
}
return true;
});
// Listen for results and state changes from the detector in page context // Listen for results and state changes from the detector in page context
window.addEventListener('message', (e) => { window.addEventListener('message', (e) => {
if (e.source !== window || !e.data) return; if (e.source !== window || !e.data) return;
if (e.data.source === 'impeccable-results') { if (e.data.source === 'impeccable-results') {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
action: 'findings', action: 'findings',
findings: e.data.findings, findings: e.data.findings,
count: e.data.count, count: e.data.count,
}).catch(() => {}); }).catch(() => {});
} }
if (e.data.source === 'impeccable-overlays-toggled') { if (e.data.source === 'impeccable-overlays-toggled') {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
action: 'overlays-toggled', action: 'overlays-toggled',
visible: e.data.visible, visible: e.data.visible,
}).catch(() => {}); }).catch(() => {});
} }
if (e.data.source === 'impeccable-ready') { if (e.data.source === 'impeccable-ready') {
injected = true; injected = true;
if (pendingScan) { if (pendingScan) {
pendingScan = false; pendingScan = false;
sendScanCommand(); sendScanCommand();
}
}
});
// Forward "page is active" signal to the extension when the cursor moves over the page.
// This is the reliable way to know the user has left the DevTools panel — the panel's
// own pointerleave/mouseleave events are unreliable on fast cursor movement.
let lastPageActive = 0;
document.addEventListener('pointermove', () => {
const now = Date.now();
if (now - lastPageActive < 150) return; // throttle
lastPageActive = now;
chrome.runtime.sendMessage({ action: 'page-pointer-active' }).catch(() => {});
}, { passive: true, capture: true });
// SPA navigation detection (pushState/replaceState don't fire events, but
// popstate and hashchange cover back/forward and hash navigation)
let lastUrl = location.href;
function onPossibleNavigation() {
if (location.href === lastUrl) return;
lastUrl = location.href;
if (injected) {
// Detector is still loaded in page context, just re-scan after DOM settles
setTimeout(sendScanCommand, 500);
} }
} }
}); window.addEventListener('popstate', onPossibleNavigation);
window.addEventListener('hashchange', onPossibleNavigation);
// SPA navigation detection (pushState/replaceState don't fire events, but function sendScanCommand() {
// popstate and hashchange cover back/forward and hash navigation) const msg = { source: 'impeccable-command', action: 'scan' };
let lastUrl = location.href; if (scanConfig) msg.config = scanConfig;
function onPossibleNavigation() { window.postMessage(msg, '*');
if (location.href === lastUrl) return;
lastUrl = location.href;
if (injected) {
// Detector is still loaded in page context, just re-scan after DOM settles
setTimeout(sendScanCommand, 500);
}
}
window.addEventListener('popstate', onPossibleNavigation);
window.addEventListener('hashchange', onPossibleNavigation);
function sendScanCommand() {
const msg = { source: 'impeccable-command', action: 'scan' };
if (scanConfig) msg.config = scanConfig;
window.postMessage(msg, '*');
}
function injectAndScan() {
if (injected) {
sendScanCommand();
return;
} }
// Set the extension flag via a data attribute (CSP-safe: content scripts share the DOM) function injectAndScan() {
document.documentElement.dataset.impeccableExtension = 'true'; if (injected) {
sendScanCommand();
return;
}
// Inject the detector script into page context // Set the extension flag via a data attribute (CSP-safe: content scripts share the DOM)
const script = document.createElement('script'); document.documentElement.dataset.impeccableExtension = 'true';
script.src = chrome.runtime.getURL('detector/detect.js');
pendingScan = true; // Inject the detector script into page context
script.onload = () => script.remove(); const script = document.createElement('script');
script.onerror = () => { script.src = chrome.runtime.getURL('detector/detect.js');
script.remove(); script.dataset.impeccableExtension = 'true';
// Fallback: use chrome.scripting.executeScript for strict CSP pages pendingScan = true;
chrome.runtime.sendMessage({ action: 'inject-fallback' }); script.onload = () => script.remove();
}; script.onerror = () => {
(document.head || document.documentElement).appendChild(script); script.remove();
} // Fallback: use chrome.scripting.executeScript for strict CSP pages
chrome.runtime.sendMessage({ action: 'inject-fallback' });
};
(document.head || document.documentElement).appendChild(script);
}
})();
+34 -5
View File
@@ -12,10 +12,39 @@ chrome.devtools.panels.create(
'devtools/panel.html' 'devtools/panel.html'
); );
// Connect a lifecycle port so the service worker knows when DevTools closes // Sidebar pane in the Elements panel: shows findings for the currently selected element
const port = chrome.runtime.connect({ chrome.devtools.panels.elements.createSidebarPane('Impeccable', (sidebar) => {
name: `impeccable-devtools-${chrome.devtools.inspectedWindow.tabId}`, sidebar.setPage('devtools/sidebar.html');
sidebar.setHeight('200px');
}); });
// Auto-scan when DevTools opens (regardless of which panel is active). // Lifecycle port to the service worker. Auto-reconnects if the SW gets terminated
port.postMessage({ action: 'scan' }); // (which can happen in MV3 after ~30s of inactivity, especially when the browser is unfocused).
const portName = `impeccable-devtools-${chrome.devtools.inspectedWindow.tabId}`;
let lifecyclePort = null;
let firstConnect = true;
function connectLifecycle() {
lifecyclePort = chrome.runtime.connect({ name: portName });
// On the very first connection, decide whether to auto-scan based on the user's setting.
// Default ('panel'): wait until the user opens the Impeccable panel or sidebar.
// Opt-in ('devtools'): scan immediately when DevTools opens.
if (firstConnect) {
firstConnect = false;
chrome.storage.sync.get({ autoScan: 'panel' }, (settings) => {
if (settings.autoScan === 'devtools') {
try { lifecyclePort?.postMessage({ action: 'scan' }); } catch {}
}
});
}
lifecyclePort.onDisconnect.addListener(() => {
lifecyclePort = null;
// Reconnect on the next tick so the SW sees a fresh connection
setTimeout(connectLifecycle, 100);
});
}
connectLifecycle();
// Heartbeat to keep the SW alive
setInterval(() => {
try { lifecyclePort?.postMessage({ action: 'ping' }); } catch {}
}, 20000);
+167 -12
View File
@@ -113,12 +113,8 @@ h1 {
color: var(--text); color: var(--text);
} }
.tool-btn.active { .tool-btn.inactive {
color: var(--accent); opacity: 0.4;
}
.tool-btn.active:hover {
color: var(--text);
} }
/* Findings */ /* Findings */
@@ -192,6 +188,50 @@ h1 {
background: var(--bg-hover); background: var(--bg-hover);
} }
.finding-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.finding-row .finding-selector {
flex: 1;
min-width: 0;
}
.finding-copy {
display: none;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-dim);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.finding-item:hover .finding-copy {
display: flex;
}
.finding-copy:hover {
background: var(--bg);
color: var(--accent);
}
.finding-copy.copied {
color: var(--accent);
}
.tool-btn.copied {
color: var(--accent);
}
.finding-selector { .finding-selector {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace; font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
font-size: 11px; font-size: 11px;
@@ -222,13 +262,35 @@ h1 {
display: block; display: block;
} }
/* Page-level findings */ /* Finding tags (page-level, hidden, etc.) */
.page-level-tag { .finding-tag {
font-size: 10px; display: inline-block;
font-weight: 500; font-size: 9px;
color: var(--accent-dim); font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
padding: 1px 5px;
border-radius: 3px;
margin-bottom: 2px;
}
.tag-page {
color: var(--accent-dim);
background: transparent;
}
.tag-hidden {
color: var(--text-dim);
background: var(--bg-hover);
}
.finding-item.is-hidden {
opacity: 0.55;
cursor: default;
}
.finding-item.is-hidden:hover {
background: transparent;
} }
/* Empty state */ /* Empty state */
@@ -315,10 +377,103 @@ h1 {
padding: 8px 8px 6px; padding: 8px 8px 6px;
} }
#settings-list { .settings-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 1px 12px; gap: 1px 12px;
padding-bottom: 8px;
}
#settings-list .settings-header {
padding: 8px 8px 4px;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px 8px;
gap: 12px;
}
.setting-label {
font-size: 11px;
color: var(--text);
}
.setting-segmented {
display: inline-flex;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.setting-segmented button {
background: transparent;
border: none;
color: var(--text-dim);
font-size: 11px;
padding: 3px 8px;
cursor: pointer;
font-family: inherit;
border-right: 1px solid var(--border);
}
.setting-segmented button:last-child {
border-right: none;
}
.setting-segmented button:hover {
color: var(--text);
}
.setting-segmented button.active {
background: var(--accent);
color: white;
}
.setting-switch {
position: relative;
display: inline-block;
width: 28px;
height: 16px;
cursor: pointer;
flex-shrink: 0;
}
.setting-switch input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.setting-switch-track {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--border);
border-radius: 8px;
transition: background 0.15s ease;
}
.setting-switch-track::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
background: var(--bg);
border-radius: 50%;
transition: transform 0.15s ease;
}
.setting-switch input:checked + .setting-switch-track {
background: var(--accent);
}
.setting-switch input:checked + .setting-switch-track::before {
transform: translateX(12px);
} }
.setting-rule { .setting-rule {
+26 -2
View File
@@ -12,10 +12,13 @@
<span class="badge" id="badge">0</span> <span class="badge" id="badge">0</span>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
<button class="tool-btn" id="btn-copy-all" title="Copy all findings">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M11 1H3a2 2 0 0 0-2 2v10h2V3h8V1zm3 3H7a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 11H7V6h7v9z" fill="currentColor"/></svg>
</button>
<button class="tool-btn" id="btn-rescan" title="Re-scan page"> <button class="tool-btn" id="btn-rescan" title="Re-scan page">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M13.65 2.35A8 8 0 1 0 16 8h-2a6 6 0 1 1-1.76-4.24L10 6h6V0l-2.35 2.35z" fill="currentColor"/></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M13.65 2.35A8 8 0 1 0 16 8h-2a6 6 0 1 1-1.76-4.24L10 6h6V0l-2.35 2.35z" fill="currentColor"/></svg>
</button> </button>
<button class="tool-btn active" id="btn-toggle" title="Toggle overlays"> <button class="tool-btn" id="btn-toggle" title="Toggle overlays">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3C4.36 3 1.26 5.28 0 8.5c1.26 3.22 4.36 5.5 8 5.5s6.74-2.28 8-5.5C14.74 5.28 11.64 3 8 3zm0 9.17c-2.58 0-4.67-2.09-4.67-4.67S5.42 2.83 8 2.83s4.67 2.09 4.67 4.67S10.58 12.17 8 12.17zM8 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7z" fill="currentColor"/></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3C4.36 3 1.26 5.28 0 8.5c1.26 3.22 4.36 5.5 8 5.5s6.74-2.28 8-5.5C14.74 5.28 11.64 3 8 3zm0 9.17c-2.58 0-4.67-2.09-4.67-4.67S5.42 2.83 8 2.83s4.67 2.09 4.67 4.67S10.58 12.17 8 12.17zM8 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7z" fill="currentColor"/></svg>
</button> </button>
<button class="tool-btn" id="btn-settings" title="Settings"> <button class="tool-btn" id="btn-settings" title="Settings">
@@ -25,7 +28,28 @@
</header> </header>
<div id="settings-container" style="display: none"> <div id="settings-container" style="display: none">
<div class="settings-header">Rules</div> <div class="settings-header">Preferences</div>
<div class="setting-row">
<span class="setting-label">Auto-scan</span>
<div class="setting-segmented" id="auto-scan-mode">
<button data-value="panel">When panel opens</button>
<button data-value="devtools">When DevTools opens</button>
</div>
</div>
<div class="setting-row">
<span class="setting-label">Line length</span>
<div class="setting-segmented" id="line-length-mode">
<button data-value="strict">Strict (80)</button>
<button data-value="lax">Lax (120)</button>
</div>
</div>
<div class="setting-row">
<span class="setting-label">Highlight blur</span>
<label class="setting-switch">
<input type="checkbox" id="spotlight-blur-toggle">
<span class="setting-switch-track"></span>
</label>
</div>
<div id="settings-list"></div> <div id="settings-list"></div>
</div> </div>
+225 -23
View File
@@ -11,13 +11,34 @@ if (chrome.devtools.panels.themeName === 'dark') {
} }
const tabId = chrome.devtools.inspectedWindow.tabId; const tabId = chrome.devtools.inspectedWindow.tabId;
const port = chrome.runtime.connect({ name: `impeccable-panel-${tabId}` });
// Auto-reconnecting port. Service workers in MV3 can be terminated after ~30s of
// inactivity (especially when the browser window is unfocused). When they restart,
// the existing port becomes invalid. We recreate it lazily on the next use.
let port = null;
function getPort() {
if (port) return port;
port = chrome.runtime.connect({ name: `impeccable-panel-${tabId}` });
port.onMessage.addListener(handlePortMessage);
port.onDisconnect.addListener(() => { port = null; });
return port;
}
function postToPort(msg) {
try {
getPort().postMessage(msg);
} catch {
// Port died mid-call. Drop it and try once more with a fresh port.
port = null;
try { getPort().postMessage(msg); } catch { /* give up silently */ }
}
}
const badge = document.getElementById('badge'); const badge = document.getElementById('badge');
const container = document.getElementById('findings-container'); const container = document.getElementById('findings-container');
const emptyState = document.getElementById('empty-state'); const emptyState = document.getElementById('empty-state');
const btnRescan = document.getElementById('btn-rescan'); const btnRescan = document.getElementById('btn-rescan');
const btnToggle = document.getElementById('btn-toggle'); const btnToggle = document.getElementById('btn-toggle');
const btnCopyAll = document.getElementById('btn-copy-all');
const settingsContainer = document.getElementById('settings-container'); const settingsContainer = document.getElementById('settings-container');
const settingsList = document.getElementById('settings-list'); const settingsList = document.getElementById('settings-list');
const btnSettings = document.getElementById('btn-settings'); const btnSettings = document.getElementById('btn-settings');
@@ -25,6 +46,7 @@ const btnSettings = document.getElementById('btn-settings');
let overlaysVisible = true; let overlaysVisible = true;
let allAntipatterns = []; let allAntipatterns = [];
let disabledRules = []; let disabledRules = [];
let currentFindings = [];
// Load antipatterns list and disabled rules // Load antipatterns list and disabled rules
async function initSettings() { async function initSettings() {
@@ -33,28 +55,100 @@ async function initSettings() {
allAntipatterns = await resp.json(); allAntipatterns = await resp.json();
} catch { allAntipatterns = []; } } catch { allAntipatterns = []; }
const stored = await chrome.storage.sync.get({ disabledRules: [] }); const stored = await chrome.storage.sync.get({
disabledRules: [],
lineLengthMode: 'strict',
spotlightBlur: true,
autoScan: 'panel',
});
disabledRules = stored.disabledRules; disabledRules = stored.disabledRules;
renderSettings(); renderSettings();
initLineLengthControl(stored.lineLengthMode);
initSpotlightBlurToggle(stored.spotlightBlur);
initAutoScanControl(stored.autoScan);
}
function initAutoScanControl(currentMode) {
const group = document.getElementById('auto-scan-mode');
if (!group) return;
for (const btn of group.querySelectorAll('button')) {
btn.classList.toggle('active', btn.dataset.value === currentMode);
btn.addEventListener('click', async () => {
const mode = btn.dataset.value;
for (const b of group.querySelectorAll('button')) {
b.classList.toggle('active', b === btn);
}
await chrome.storage.sync.set({ autoScan: mode });
});
}
}
function initLineLengthControl(currentMode) {
const group = document.getElementById('line-length-mode');
if (!group) return;
for (const btn of group.querySelectorAll('button')) {
btn.classList.toggle('active', btn.dataset.value === currentMode);
btn.addEventListener('click', async () => {
const mode = btn.dataset.value;
for (const b of group.querySelectorAll('button')) {
b.classList.toggle('active', b === btn);
}
await chrome.storage.sync.set({ lineLengthMode: mode });
chrome.runtime.sendMessage({ action: 'disabled-rules-changed' });
});
}
}
function initSpotlightBlurToggle(currentValue) {
const cb = document.getElementById('spotlight-blur-toggle');
if (!cb) return;
cb.checked = currentValue;
cb.addEventListener('change', async () => {
await chrome.storage.sync.set({ spotlightBlur: cb.checked });
chrome.runtime.sendMessage({ action: 'disabled-rules-changed' });
});
} }
function renderSettings() { function renderSettings() {
settingsList.innerHTML = ''; settingsList.innerHTML = '';
const categories = {
slop: { label: 'AI tells', items: [] },
quality: { label: 'Quality', items: [] },
};
for (const ap of allAntipatterns) { for (const ap of allAntipatterns) {
const label = document.createElement('label'); const cat = ap.category || 'quality';
label.className = 'setting-rule'; (categories[cat] || categories.quality).items.push(ap);
}
const checkbox = document.createElement('input'); for (const [, group] of Object.entries(categories)) {
checkbox.type = 'checkbox'; if (!group.items.length) continue;
checkbox.checked = !disabledRules.includes(ap.id);
checkbox.addEventListener('change', () => toggleRule(ap.id, checkbox.checked));
const text = document.createElement('span'); const header = document.createElement('div');
text.textContent = ap.name; header.className = 'settings-header';
header.textContent = group.label;
settingsList.appendChild(header);
label.appendChild(checkbox); const grid = document.createElement('div');
label.appendChild(text); grid.className = 'settings-grid';
settingsList.appendChild(label);
for (const ap of group.items) {
const label = document.createElement('label');
label.className = 'setting-rule';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = !disabledRules.includes(ap.id);
checkbox.addEventListener('change', () => toggleRule(ap.id, checkbox.checked));
const text = document.createElement('span');
text.textContent = ap.name;
label.appendChild(checkbox);
label.appendChild(text);
grid.appendChild(label);
}
settingsList.appendChild(grid);
} }
} }
@@ -68,8 +162,13 @@ async function toggleRule(ruleId, enabled) {
chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); chrome.runtime.sendMessage({ action: 'disabled-rules-changed' });
} }
// Listen for messages from the service worker // Listen for messages from the service worker (called by getPort() on each new connection)
port.onMessage.addListener((msg) => { function handlePortMessage(msg) {
if (msg.action === 'page-pointer-active') {
// Cursor is active on the page → user has left the panel
setHoveredItem(null);
return;
}
if (msg.action === 'findings' || msg.action === 'state') { if (msg.action === 'findings' || msg.action === 'state') {
renderFindings(msg.findings || []); renderFindings(msg.findings || []);
if (msg.overlaysVisible !== undefined) { if (msg.overlaysVisible !== undefined) {
@@ -84,16 +183,23 @@ port.onMessage.addListener((msg) => {
if (msg.action === 'navigated') { if (msg.action === 'navigated') {
showScanning(); showScanning();
} }
}); }
// Initial connection
getPort();
// Heartbeat to keep the MV3 service worker alive while the panel is open.
// SWs can be terminated after ~30s of inactivity, especially when the browser is unfocused.
setInterval(() => postToPort({ action: 'ping' }), 20000);
// Controls // Controls
btnRescan.addEventListener('click', () => { btnRescan.addEventListener('click', () => {
showScanning(); showScanning();
port.postMessage({ action: 'scan' }); postToPort({ action: 'scan' });
}); });
btnToggle.addEventListener('click', () => { btnToggle.addEventListener('click', () => {
port.postMessage({ action: 'toggle-overlays' }); postToPort({ action: 'toggle-overlays' });
}); });
btnSettings.addEventListener('click', () => { btnSettings.addEventListener('click', () => {
@@ -103,8 +209,8 @@ btnSettings.addEventListener('click', () => {
}); });
function updateToggleButton() { function updateToggleButton() {
btnToggle.classList.toggle('active', overlaysVisible);
btnToggle.title = overlaysVisible ? 'Hide overlays' : 'Show overlays'; btnToggle.title = overlaysVisible ? 'Hide overlays' : 'Show overlays';
btnToggle.classList.toggle('inactive', !overlaysVisible);
} }
function showScanning() { function showScanning() {
@@ -115,7 +221,86 @@ function showScanning() {
</div>`; </div>`;
} }
function formatFindingsForCopy(findings) {
if (!findings.length) return 'No anti-patterns detected.';
const lines = ['Impeccable detected the following UI anti-patterns:', ''];
const groups = { slop: [], quality: [] };
for (const item of findings) {
for (const f of item.findings) {
const cat = f.category || 'quality';
groups[cat].push({ ...f, selector: item.selector, isPageLevel: item.isPageLevel });
}
}
if (groups.slop.length) {
lines.push('## AI tells');
for (const f of groups.slop) {
lines.push(`- ${f.name} ${f.isPageLevel ? '(page-level)' : `at \`${f.selector}\``}: ${f.detail}`);
}
lines.push('');
}
if (groups.quality.length) {
lines.push('## Quality issues');
for (const f of groups.quality) {
lines.push(`- ${f.name} ${f.isPageLevel ? '(page-level)' : `at \`${f.selector}\``}: ${f.detail}`);
}
lines.push('');
}
lines.push('Please fix these issues.');
return lines.join('\n');
}
function formatSingleFindingForCopy(item, finding) {
const where = item.isPageLevel ? '(page-level)' : `at \`${item.selector}\``;
return `Impeccable detected: ${finding.name} ${where}\nDetail: ${finding.detail}\n\n${finding.description}\n\nPlease fix this.`;
}
async function copyToClipboard(text, btn) {
try {
await navigator.clipboard.writeText(text);
if (btn) {
const orig = btn.title;
btn.title = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.title = orig;
btn.classList.remove('copied');
}, 1200);
}
} catch (err) {
console.warn('Copy failed', err);
}
}
btnCopyAll.addEventListener('click', () => {
copyToClipboard(formatFindingsForCopy(currentFindings), btnCopyAll);
});
// Delegated hover tracking on the findings container.
// Reliably handles cursor moving between items, into children, or out of the panel.
let currentHoverSelector = null;
function setHoveredItem(selector) {
if (selector === currentHoverSelector) return;
currentHoverSelector = selector;
if (selector) {
postToPort({ action: 'highlight', selector });
} else {
postToPort({ action: 'unhighlight' });
}
}
container.addEventListener('pointermove', (e) => {
const item = e.target.closest('.finding-item');
const selector = item && !item.classList.contains('is-hidden') ? item.dataset.selector || null : null;
setHoveredItem(selector);
});
// Slow-cursor fallbacks (these fire reliably for slow movements)
container.addEventListener('pointerleave', () => setHoveredItem(null));
window.addEventListener('blur', () => setHoveredItem(null));
function renderFindings(findings) { function renderFindings(findings) {
currentFindings = findings;
if (!findings.length) { if (!findings.length) {
container.innerHTML = ''; container.innerHTML = '';
container.appendChild(emptyState); container.appendChild(emptyState);
@@ -145,6 +330,7 @@ function renderFindings(findings) {
selector: item.selector, selector: item.selector,
tagName: item.tagName, tagName: item.tagName,
isPageLevel: item.isPageLevel, isPageLevel: item.isPageLevel,
isHidden: item.isHidden,
detail: f.detail, detail: f.detail,
}); });
} }
@@ -186,14 +372,30 @@ function renderFindings(findings) {
for (const item of group.items) { for (const item of group.items) {
const itemEl = document.createElement('div'); const itemEl = document.createElement('div');
itemEl.className = 'finding-item'; itemEl.className = 'finding-item' + (item.isHidden ? ' is-hidden' : '');
const tag = item.isPageLevel
? '<span class="finding-tag tag-page">page</span>'
: item.isHidden ? '<span class="finding-tag tag-hidden" title="Element is currently hidden on the page">hidden</span>' : '';
itemEl.innerHTML = ` itemEl.innerHTML = `
${item.isPageLevel ? '<span class="page-level-tag">page</span>' : ''} ${tag}
<span class="finding-selector">${escapeHtml(item.selector)}</span> <div class="finding-row">
<span class="finding-selector">${escapeHtml(item.selector)}</span>
<button class="finding-copy" title="Copy this finding">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 1H3a2 2 0 0 0-2 2v10h2V3h8V1zm3 3H7a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 11H7V6h7v9z" fill="currentColor"/></svg>
</button>
</div>
<span class="finding-detail">${escapeHtml(item.detail)}</span> <span class="finding-detail">${escapeHtml(item.detail)}</span>
<span class="finding-description">${escapeHtml(group.description)}</span>`; <span class="finding-description">${escapeHtml(group.description)}</span>`;
if (!item.isPageLevel) { const copyBtn = itemEl.querySelector('.finding-copy');
const finding = { name: group.name, description: group.description, detail: item.detail };
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
copyToClipboard(formatSingleFindingForCopy(item, finding), copyBtn);
});
if (!item.isPageLevel && !item.isHidden) {
itemEl.dataset.selector = item.selector;
itemEl.addEventListener('click', () => inspectElement(item.selector)); itemEl.addEventListener('click', () => inspectElement(item.selector));
} }
+97
View File
@@ -0,0 +1,97 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #fff;
--text: #1a1a1a;
--text-dim: #666;
--text-faint: #999;
--accent: oklch(48% 0.25 350);
--rule: #e8e6e2;
}
.theme-dark {
--bg: #1a1a1a;
--text: #f5f3ef;
--text-dim: #9a9590;
--text-faint: #666;
--accent: oklch(60% 0.25 350);
--rule: #2a2a2a;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 12px;
line-height: 1.5;
padding: 12px 14px;
}
/* Empty / no-findings states */
.state {
color: var(--text-faint);
font-size: 12px;
font-style: italic;
padding: 4px 0;
}
.state strong {
color: var(--text-dim);
font-style: normal;
font-weight: 600;
}
/* Finding list */
.finding + .finding {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--rule);
}
.finding-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.finding-name {
font-weight: 600;
font-size: 12px;
color: var(--text);
letter-spacing: -0.005em;
}
.finding-name .marker {
color: var(--accent);
margin-right: 4px;
}
.finding-kind {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-faint);
flex-shrink: 0;
}
.finding-detail {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
font-size: 11px;
color: var(--text-dim);
margin-bottom: 6px;
}
.finding-description {
font-size: 11px;
color: var(--text-dim);
line-height: 1.55;
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="sidebar.css">
</head>
<body>
<div id="sidebar-content">
<div class="state">Select an element to see Impeccable findings.</div>
</div>
<script src="sidebar.js"></script>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
/**
* 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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

-7
View File
@@ -8,13 +8,6 @@
"background": { "background": {
"service_worker": "background/service-worker.js" "service_worker": "background/service-worker.js"
}, },
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content-script.js"],
"run_at": "document_idle"
}
],
"devtools_page": "devtools/devtools.html", "devtools_page": "devtools/devtools.html",
"action": { "action": {
"default_popup": "popup/popup.html", "default_popup": "popup/popup.html",
+12 -1
View File
@@ -18,7 +18,7 @@
<body> <body>
<a href="/" class="back">&larr; Back to impeccable.style</a> <a href="/" class="back">&larr; Back to impeccable.style</a>
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<p class="updated">Last updated: March 24, 2026</p> <p class="updated">Last updated: April 6, 2026</p>
<h2>What Impeccable is</h2> <h2>What Impeccable is</h2>
<p>Impeccable is an open-source collection of agent skills (text files) that run locally in your AI coding tool. The skills themselves collect no data, make no network requests, and have no analytics.</p> <p>Impeccable is an open-source collection of agent skills (text files) that run locally in your AI coding tool. The skills themselves collect no data, make no network requests, and have no analytics.</p>
@@ -32,6 +32,17 @@
<h2>Claude Code Plugin</h2> <h2>Claude Code Plugin</h2>
<p>When installed as a Claude Code plugin, Impeccable runs entirely within your local Claude Code session. No data is sent to Impeccable's servers. Anthropic's own privacy policy governs the Claude Code application itself.</p> <p>When installed as a Claude Code plugin, Impeccable runs entirely within your local Claude Code session. No data is sent to Impeccable's servers. Anthropic's own privacy policy governs the Claude Code application itself.</p>
<h2>Chrome Extension</h2>
<p>The Impeccable Chrome DevTools extension runs entirely in your browser. All anti-pattern detection happens locally on the page you are inspecting. No page content, URLs, or detection results are ever sent to any external server.</p>
<p>The extension stores your rule preferences (which detections are enabled or disabled) using Chrome's built-in sync storage (<code>chrome.storage.sync</code>), which syncs settings across your Chrome instances via your Google account. No other data is stored or transmitted.</p>
<p>The extension requests the following permissions:</p>
<ul>
<li><strong>activeTab / scripting</strong> - to inject the detector script into the page you are inspecting</li>
<li><strong>storage</strong> - to save your rule preferences</li>
<li><strong>webNavigation</strong> - to re-scan automatically when you navigate to a new page</li>
<li><strong>Host permissions (all URLs)</strong> - so the detector can run on any website you choose to inspect</li>
</ul>
<h2>GitHub</h2> <h2>GitHub</h2>
<p>The source code is hosted on GitHub. Interactions with the repository (issues, pull requests, stars) are governed by <a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">GitHub's privacy policy</a>.</p> <p>The source code is hosted on GitHub. Interactions with the repository (issues, pull requests, stars) are governed by <a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">GitHub's privacy policy</a>.</p>
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* Generates the Chrome Web Store small promo tile (440x280) from an SVG template.
*
* Run: node scripts/generate-promo-tile.js
*/
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const OUT = path.join(ROOT, 'extension/icons/promo-small.png');
// Brand colors
const BG = '#0e0d10';
const BG_TOP = '#161318';
const MAGENTA = '#cc1b89'; // approximates oklch(55% 0.25 350)
const TEXT = '#f5f3ef';
const TEXT_DIM = '#7a7680';
const svg = `
<svg width="440" height="280" viewBox="0 0 440 280" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="${BG_TOP}"/>
<stop offset="1" stop-color="${BG}"/>
</linearGradient>
<linearGradient id="slop" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#a855f7"/>
<stop offset="0.5" stop-color="#ec4899"/>
<stop offset="1" stop-color="#06b6d4"/>
</linearGradient>
<radialGradient id="cardGlow" cx="50%" cy="50%" r="60%">
<stop offset="0" stop-color="#3a1a4a" stop-opacity="0.6"/>
<stop offset="1" stop-color="#1a0f24" stop-opacity="0.3"/>
</radialGradient>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="6"/>
<feOffset dx="0" dy="3" result="offsetblur"/>
<feComponentTransfer><feFuncA type="linear" slope="0.4"/></feComponentTransfer>
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="440" height="280" fill="url(#bg)"/>
<!-- Subtle grid texture -->
<g opacity="0.04" stroke="${TEXT}" stroke-width="0.5">
<line x1="0" y1="70" x2="440" y2="70"/>
<line x1="0" y1="140" x2="440" y2="140"/>
<line x1="0" y1="210" x2="440" y2="210"/>
<line x1="110" y1="0" x2="110" y2="280"/>
<line x1="220" y1="0" x2="220" y2="280"/>
<line x1="330" y1="0" x2="330" y2="280"/>
</g>
<!-- Brand wordmark (top-left) -->
<g transform="translate(28, 26)">
<rect width="24" height="24" rx="4.5" fill="#1a1a1a"/>
<!-- Slash matches the brand icon: (76,24)→(52,104) in 128 viewBox, scaled to 24 -->
<line x1="14.25" y1="4.5" x2="9.75" y2="19.5" stroke="${TEXT}" stroke-width="1.3" stroke-linecap="round"/>
<text x="34" y="17" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" font-size="16" font-weight="600" fill="${TEXT}" letter-spacing="-0.01em">Impeccable</text>
</g>
<!-- Small badge top-right -->
<g transform="translate(338, 28)">
<rect width="74" height="20" rx="10" fill="${TEXT}" fill-opacity="0.06"/>
<text x="37" y="14" text-anchor="middle" font-family="-apple-system, system-ui, sans-serif" font-size="10" font-weight="500" fill="${TEXT_DIM}" letter-spacing="0.04em">DEVTOOLS</text>
</g>
<!-- Demo card group (centered, slightly offset) -->
<g transform="translate(64, 92)">
<!-- Faux UI card -->
<g filter="url(#softShadow)">
<rect x="0" y="0" width="312" height="92" rx="14" fill="#1a1422"/>
<rect x="0" y="0" width="312" height="92" rx="14" fill="url(#cardGlow)"/>
</g>
<!-- Sparkles inside the card (more AI slop vibes) -->
<text x="32" y="56" font-family="-apple-system, system-ui, sans-serif" font-size="20" fill="#fbbf24">✨</text>
<text x="268" y="56" font-family="-apple-system, system-ui, sans-serif" font-size="20" fill="#fbbf24">✨</text>
<!-- Gradient text headline (the slop being detected) -->
<text x="156" y="52" text-anchor="middle"
font-family="-apple-system, BlinkMacSystemFont, system-ui, sans-serif"
font-size="20" font-weight="700"
fill="url(#slop)">AI-Powered Magic</text>
<!-- Subline -->
<text x="156" y="72" text-anchor="middle"
font-family="-apple-system, system-ui, sans-serif"
font-size="10" fill="#9b94a8" letter-spacing="0.02em">Reimagining the future of everything</text>
<!-- Impeccable magenta outline (offset by 2px outside the card)
Path so top-left corner is square (where label meets it) -->
<path d="M -4 -4 L 312 -4 Q 316 -4 316 0 L 316 92 Q 316 96 312 96 L 0 96 Q -4 96 -4 92 L -4 -4 Z"
fill="none" stroke="${MAGENTA}" stroke-width="2" stroke-linejoin="round"/>
<!-- Label tab on top, flush with outline's outer edge.
Label extends to x=-5 (matching outline visible left edge) and y=-3 (covering the outline's top stroke). -->
<path d="M -5 -3 L -5 -22 Q -5 -26 -1 -26 L 113 -26 Q 117 -26 117 -22 L 117 -3 Z"
fill="${MAGENTA}"/>
<text x="2" y="-10"
font-family="-apple-system, BlinkMacSystemFont, system-ui, sans-serif"
font-size="11" font-weight="600" fill="white" letter-spacing="0.01em">
✦ gradient text
</text>
</g>
<!-- Tagline at bottom -->
<text x="28" y="244"
font-family="-apple-system, BlinkMacSystemFont, system-ui, sans-serif"
font-size="15" font-weight="600" fill="${TEXT}" letter-spacing="-0.01em">
Detect AI slop in any web page.
</text>
<text x="28" y="262"
font-family="-apple-system, system-ui, sans-serif"
font-size="11" fill="${TEXT_DIM}" letter-spacing="0.01em">
24 detections · Open DevTools and see what needs fixing.
</text>
</svg>
`;
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 440, height: 280, deviceScaleFactor: 1 });
await page.setContent(`
<!DOCTYPE html>
<html>
<head><style>
* { margin: 0; padding: 0; }
body { width: 440px; height: 280px; overflow: hidden; }
</style></head>
<body>${svg}</body>
</html>
`);
await page.screenshot({ path: OUT, omitBackground: false });
await browser.close();
console.log(`Generated ${path.relative(ROOT, OUT)}`);
+190 -13
View File
@@ -52,6 +52,26 @@ const OVERUSED_FONTS = new Set([
'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica',
]); ]);
// Brand-associated fonts: don't flag these as "overused" on the brand's own domains.
// Keys are font names, values are arrays of hostname suffixes where the font is allowed.
const GOOGLE_DOMAINS = [
'google.com', 'youtube.com', 'android.com', 'chromium.org',
'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com',
];
const BRAND_FONT_DOMAINS = {
'roboto': GOOGLE_DOMAINS,
'google sans': GOOGLE_DOMAINS,
'product sans': GOOGLE_DOMAINS,
};
function isBrandFontOnOwnDomain(font) {
if (typeof location === 'undefined') return false;
const allowed = BRAND_FONT_DOMAINS[font];
if (!allowed) return false;
const host = location.hostname.toLowerCase();
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
@@ -808,11 +828,12 @@ function checkElementQualityDOM(el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
// --- Line length too long --- // --- Line length too long ---
// Only flag if text is long enough to actually fill the line (>80 chars) // Threshold is configurable via window.__IMPECCABLE_CONFIG__.lineLengthMax (default 80)
if (hasDirectText && QUALITY_TEXT_TAGS.has(tag) && rect.width > 0 && textLen > 80) { const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80;
if (hasDirectText && QUALITY_TEXT_TAGS.has(tag) && rect.width > 0 && textLen > lineMax) {
const charsPerLine = rect.width / (fontSize * 0.5); const charsPerLine = rect.width / (fontSize * 0.5);
if (charsPerLine > 85) { if (charsPerLine > lineMax + 5) {
findings.push({ id: 'line-length', snippet: `~${Math.round(charsPerLine)} chars/line (aim for <80)` }); findings.push({ id: 'line-length', snippet: `~${Math.round(charsPerLine)} chars/line (aim for <${lineMax})` });
} }
} }
@@ -864,8 +885,12 @@ function checkElementQualityDOM(el) {
} }
// --- Tiny body text --- // --- Tiny body text ---
// Only flag actual body content, not UI labels (buttons, tabs, badges, captions, footer text, etc.)
if (hasDirectText && textLen > 20 && fontSize < 12) { if (hasDirectText && textLen > 20 && fontSize < 12) {
if (!['sub', 'sup', 'code', 'kbd', 'samp', 'var'].includes(tag)) { const skipTags = ['sub', 'sup', 'code', 'kbd', 'samp', 'var', 'caption', 'figcaption'];
const inUIContext = el.closest('button, a, label, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"], nav, footer, [class*="badge" i], [class*="chip" i], [class*="pill" i], [class*="tag" i], [class*="label" i], [class*="caption" i]');
const isUppercase = style.textTransform === 'uppercase';
if (!skipTags.includes(tag) && !inUIContext && !isUppercase) {
findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` }); findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` });
} }
} }
@@ -990,6 +1015,7 @@ function checkTypography() {
} }
for (const font of overusedFound) { for (const font of overusedFound) {
if (isBrandFontOnOwnDomain(font)) continue;
findings.push({ type: 'overused-font', detail: `Primary font: ${font}` }); findings.push({ type: 'overused-font', detail: `Primary font: ${font}` });
} }
if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) { if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) {
@@ -1236,7 +1262,11 @@ function checkPageLayout(doc, win) {
// ─── Section 7: Browser UI (IS_BROWSER only) ──────────────────────────────── // ─── Section 7: Browser UI (IS_BROWSER only) ────────────────────────────────
if (IS_BROWSER) { if (IS_BROWSER) {
const EXTENSION_MODE = document.documentElement.dataset.impeccableExtension === 'true'; // Detect extension mode via the script tag's data attribute or the document element fallback.
// currentScript is reliable for synchronously-executing scripts (which our IIFE is).
const _myScript = document.currentScript;
const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true')
|| document.documentElement.dataset.impeccableExtension === 'true';
const BRAND_COLOR = 'oklch(55% 0.25 350)'; const BRAND_COLOR = 'oklch(55% 0.25 350)';
const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)';
@@ -1266,18 +1296,103 @@ if (IS_BROWSER) {
outline-color: ${BRAND_COLOR_HOVER}; outline-color: ${BRAND_COLOR_HOVER};
z-index: 100001 !important; z-index: 100001 !important;
} }
.impeccable-label {
transition: background 0.15s ease;
}
.impeccable-overlay.impeccable-hover .impeccable-label { .impeccable-overlay.impeccable-hover .impeccable-label {
background: ${BRAND_COLOR_HOVER}; background: ${BRAND_COLOR_HOVER};
} }
.impeccable-overlay.impeccable-spotlight {
z-index: 100002 !important;
}
.impeccable-overlay.impeccable-spotlight-dimmed {
opacity: 0.15 !important;
animation: none !important;
filter: blur(3px);
}
.impeccable-spotlight-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
backdrop-filter: blur(3px) brightness(0.6);
-webkit-backdrop-filter: blur(3px) brightness(0.6);
pointer-events: none;
z-index: 99998;
opacity: 0;
outline: none !important;
animation: none !important;
}
.impeccable-spotlight-backdrop.impeccable-visible {
opacity: 1;
}
.impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} {
display: none !important;
}
.impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} { .impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} {
display: none !important; display: none !important;
} }
`; `;
(document.head || document.documentElement).appendChild(styleEl); (document.head || document.documentElement).appendChild(styleEl);
// Spotlight backdrop element (created lazily on first use)
let spotlightBackdrop = null;
let spotlightTarget = null;
let spotlightTimer = null;
function getSpotlightBackdrop() {
if (!spotlightBackdrop) {
spotlightBackdrop = document.createElement('div');
spotlightBackdrop.className = 'impeccable-spotlight-backdrop';
document.body.appendChild(spotlightBackdrop);
}
return spotlightBackdrop;
}
function updateSpotlightClipPath() {
if (!spotlightBackdrop || !spotlightTarget) return;
const r = spotlightTarget.getBoundingClientRect();
// Match the overlay's outer edge: element rect + 4px (2px overlay offset + 2px outline width)
const inset = 4;
const radius = 6; // outline border-radius (4) + outline width (2)
const x1 = r.left - inset;
const y1 = r.top - inset;
const x2 = r.right + inset;
const y2 = r.bottom + inset;
const vw = window.innerWidth;
const vh = window.innerHeight;
// Outer rect + rounded inner rect (evenodd creates a hole)
const path = `M0 0H${vw}V${vh}H0Z M${x1 + radius} ${y1}H${x2 - radius}A${radius} ${radius} 0 0 1 ${x2} ${y1 + radius}V${y2 - radius}A${radius} ${radius} 0 0 1 ${x2 - radius} ${y2}H${x1 + radius}A${radius} ${radius} 0 0 1 ${x1} ${y2 - radius}V${y1 + radius}A${radius} ${radius} 0 0 1 ${x1 + radius} ${y1}Z`;
spotlightBackdrop.style.clipPath = `path(evenodd, "${path}")`;
}
function showSpotlight(target) {
if (!target || !target.getBoundingClientRect) return;
// Respect the spotlightBlur setting: if disabled, don't show the backdrop
if (window.__IMPECCABLE_CONFIG__?.spotlightBlur === false) {
spotlightTarget = target;
return;
}
spotlightTarget = target;
const bd = getSpotlightBackdrop();
updateSpotlightClipPath();
bd.classList.add('impeccable-visible');
}
function hideSpotlight() {
spotlightTarget = null;
if (spotlightTimer) { clearTimeout(spotlightTimer); spotlightTimer = null; }
if (spotlightBackdrop) spotlightBackdrop.classList.remove('impeccable-visible');
}
function isInViewport(el) {
const r = el.getBoundingClientRect();
return r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
}
// Reposition spotlight on scroll/resize
window.addEventListener('scroll', () => {
if (spotlightTarget) updateSpotlightClipPath();
}, { passive: true });
window.addEventListener('resize', () => {
if (spotlightTarget) updateSpotlightClipPath();
});
const overlays = []; const overlays = [];
const TYPE_LABELS = {}; const TYPE_LABELS = {};
const RULE_CATEGORY = {}; const RULE_CATEGORY = {};
@@ -1315,6 +1430,8 @@ if (IS_BROWSER) {
function repositionOverlays() { function repositionOverlays() {
for (const o of overlays) { for (const o of overlays) {
if (!o._targetEl || o.classList.contains('impeccable-banner')) continue; if (!o._targetEl || o.classList.contains('impeccable-banner')) continue;
// Skip overlays whose target is currently hidden (display: none on the overlay)
if (o.style.display === 'none') continue;
positionOverlay(o); positionOverlay(o);
} }
} }
@@ -1325,6 +1442,13 @@ if (IS_BROWSER) {
resizeRAF = requestAnimationFrame(repositionOverlays); resizeRAF = requestAnimationFrame(repositionOverlays);
}; };
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);
// Reposition on scroll too -- catches sticky/parallax shifts
window.addEventListener('scroll', onResize, { passive: true });
// Reposition when body resizes (lazy-loaded images, dynamic content, fonts loading)
if (typeof ResizeObserver !== 'undefined') {
const bodyResizeObserver = new ResizeObserver(onResize);
bodyResizeObserver.observe(document.body);
}
// Track target element visibility via IntersectionObserver. // Track target element visibility via IntersectionObserver.
// Uses a huge rootMargin so all *rendered* elements count as intersecting, // Uses a huge rootMargin so all *rendered* elements count as intersecting,
@@ -1340,7 +1464,13 @@ if (IS_BROWSER) {
positionOverlay(overlay); positionOverlay(overlay);
if (!overlay._revealed) { if (!overlay._revealed) {
overlay._revealed = true; overlay._revealed = true;
overlay.style.animationDelay = `${(overlay._staggerIndex || 0) * 80}ms`; if (firstScanDone) {
// Subsequent reveals (re-scans, scroll-into-view): instant, no animation
overlay.style.animation = 'none';
} else {
// Initial scan: staggered cascade reveal
overlay.style.animationDelay = `${Math.min((overlay._staggerIndex || 0) * 60, 600)}ms`;
}
requestAnimationFrame(() => { requestAnimationFrame(() => {
overlay.classList.add('impeccable-visible'); overlay.classList.add('impeccable-visible');
if (overlay._checkLabel) overlay._checkLabel(); if (overlay._checkLabel) overlay._checkLabel();
@@ -1606,6 +1736,13 @@ if (IS_BROWSER) {
return parts.join(' > '); return parts.join(' > ');
} }
function isElementHidden(el) {
if (!el || el === document.body || el === document.documentElement) return false;
if (typeof el.checkVisibility === 'function') return !el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
// Fallback: zero size or no offsetParent (covers display:none and detached subtrees)
return el.offsetWidth === 0 && el.offsetHeight === 0;
}
function serializeFindings(allFindings) { function serializeFindings(allFindings) {
return allFindings.map(({ el, findings }) => ({ return allFindings.map(({ el, findings }) => ({
selector: generateSelector(el), selector: generateSelector(el),
@@ -1613,6 +1750,7 @@ if (IS_BROWSER) {
rect: (el !== document.body && el !== document.documentElement && el.getBoundingClientRect) rect: (el !== document.body && el !== document.documentElement && el.getBoundingClientRect)
? el.getBoundingClientRect().toJSON() : null, ? el.getBoundingClientRect().toJSON() : null,
isPageLevel: el === document.body || el === document.documentElement, isPageLevel: el === document.body || el === document.documentElement,
isHidden: isElementHidden(el),
findings: findings.map(f => { findings: findings.map(f => {
const ap = ANTIPATTERNS.find(a => a.id === (f.type || f.id)); const ap = ANTIPATTERNS.find(a => a.id === (f.type || f.id));
return { return {
@@ -1644,18 +1782,19 @@ if (IS_BROWSER) {
console.groupEnd(); console.groupEnd();
}; };
let firstScanDone = false;
const scan = function() { const scan = function() {
for (const o of overlays) o.remove(); for (const o of overlays) o.remove();
overlays.length = 0; overlays.length = 0;
visibilityObserver.disconnect(); visibilityObserver.disconnect();
overlayIndex = 0;
const allFindings = []; const allFindings = [];
const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : [];
const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
for (const el of document.querySelectorAll('*')) { for (const el of document.querySelectorAll('*')) {
if (el.classList.contains('impeccable-overlay') || // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons)
el.classList.contains('impeccable-label') || if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
el.classList.contains('impeccable-tooltip')) continue;
// Skip browser extension elements (Claude, etc.) // Skip browser extension elements (Claude, etc.)
const elId = el.id || ''; const elId = el.id || '';
if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue; if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
@@ -1733,6 +1872,9 @@ if (IS_BROWSER) {
}, '*'); }, '*');
} }
// After this scan completes, all subsequent reveals are instant (no stagger, no animation)
setTimeout(() => { firstScanDone = true; }, 1000);
return allFindings; return allFindings;
}; };
@@ -1754,8 +1896,43 @@ if (IS_BROWSER) {
overlays.length = 0; overlays.length = 0;
visibilityObserver.disconnect(); visibilityObserver.disconnect();
styleEl.remove(); styleEl.remove();
if (spotlightBackdrop) { spotlightBackdrop.remove(); spotlightBackdrop = null; }
document.body.classList.remove('impeccable-hidden'); document.body.classList.remove('impeccable-hidden');
} }
if (e.data.action === 'highlight') {
if (spotlightTimer) { clearTimeout(spotlightTimer); spotlightTimer = null; }
try {
const target = e.data.selector ? document.querySelector(e.data.selector) : null;
if (target) {
// Scroll first so positionOverlay reads the post-scroll rect
if (!isInViewport(target) && target.scrollIntoView) {
target.scrollIntoView({ behavior: 'instant', block: 'center' });
}
for (const o of overlays) {
if (o.classList.contains('impeccable-banner')) continue;
const isMatch = o._targetEl === target;
o.classList.toggle('impeccable-spotlight', isMatch);
o.classList.toggle('impeccable-spotlight-dimmed', !isMatch);
if (isMatch) {
// Force the matching overlay visible immediately, don't wait for IntersectionObserver
o.style.display = '';
o.style.animation = 'none';
o.classList.add('impeccable-visible');
o._revealed = true;
positionOverlay(o);
}
}
showSpotlight(target);
}
} catch { /* invalid selector */ }
}
if (e.data.action === 'unhighlight') {
hideSpotlight();
for (const o of overlays) {
o.classList.remove('impeccable-spotlight');
o.classList.remove('impeccable-spotlight-dimmed');
}
}
}); });
window.postMessage({ source: 'impeccable-ready' }, '*'); window.postMessage({ source: 'impeccable-ready' }, '*');
} else { } else {
+190 -13
View File
@@ -47,6 +47,26 @@ const OVERUSED_FONTS = new Set([
'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica',
]); ]);
// Brand-associated fonts: don't flag these as "overused" on the brand's own domains.
// Keys are font names, values are arrays of hostname suffixes where the font is allowed.
const GOOGLE_DOMAINS = [
'google.com', 'youtube.com', 'android.com', 'chromium.org',
'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com',
];
const BRAND_FONT_DOMAINS = {
'roboto': GOOGLE_DOMAINS,
'google sans': GOOGLE_DOMAINS,
'product sans': GOOGLE_DOMAINS,
};
function isBrandFontOnOwnDomain(font) {
if (typeof location === 'undefined') return false;
const allowed = BRAND_FONT_DOMAINS[font];
if (!allowed) return false;
const host = location.hostname.toLowerCase();
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
}
const GENERIC_FONTS = new Set([ const GENERIC_FONTS = new Set([
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
@@ -803,11 +823,12 @@ function checkElementQualityDOM(el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
// --- Line length too long --- // --- Line length too long ---
// Only flag if text is long enough to actually fill the line (>80 chars) // Threshold is configurable via window.__IMPECCABLE_CONFIG__.lineLengthMax (default 80)
if (hasDirectText && QUALITY_TEXT_TAGS.has(tag) && rect.width > 0 && textLen > 80) { const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80;
if (hasDirectText && QUALITY_TEXT_TAGS.has(tag) && rect.width > 0 && textLen > lineMax) {
const charsPerLine = rect.width / (fontSize * 0.5); const charsPerLine = rect.width / (fontSize * 0.5);
if (charsPerLine > 85) { if (charsPerLine > lineMax + 5) {
findings.push({ id: 'line-length', snippet: `~${Math.round(charsPerLine)} chars/line (aim for <80)` }); findings.push({ id: 'line-length', snippet: `~${Math.round(charsPerLine)} chars/line (aim for <${lineMax})` });
} }
} }
@@ -859,8 +880,12 @@ function checkElementQualityDOM(el) {
} }
// --- Tiny body text --- // --- Tiny body text ---
// Only flag actual body content, not UI labels (buttons, tabs, badges, captions, footer text, etc.)
if (hasDirectText && textLen > 20 && fontSize < 12) { if (hasDirectText && textLen > 20 && fontSize < 12) {
if (!['sub', 'sup', 'code', 'kbd', 'samp', 'var'].includes(tag)) { const skipTags = ['sub', 'sup', 'code', 'kbd', 'samp', 'var', 'caption', 'figcaption'];
const inUIContext = el.closest('button, a, label, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"], nav, footer, [class*="badge" i], [class*="chip" i], [class*="pill" i], [class*="tag" i], [class*="label" i], [class*="caption" i]');
const isUppercase = style.textTransform === 'uppercase';
if (!skipTags.includes(tag) && !inUIContext && !isUppercase) {
findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` }); findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` });
} }
} }
@@ -985,6 +1010,7 @@ function checkTypography() {
} }
for (const font of overusedFound) { for (const font of overusedFound) {
if (isBrandFontOnOwnDomain(font)) continue;
findings.push({ type: 'overused-font', detail: `Primary font: ${font}` }); findings.push({ type: 'overused-font', detail: `Primary font: ${font}` });
} }
if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) { if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) {
@@ -1231,7 +1257,11 @@ function checkPageLayout(doc, win) {
// ─── Section 7: Browser UI (IS_BROWSER only) ──────────────────────────────── // ─── Section 7: Browser UI (IS_BROWSER only) ────────────────────────────────
if (IS_BROWSER) { if (IS_BROWSER) {
const EXTENSION_MODE = document.documentElement.dataset.impeccableExtension === 'true'; // Detect extension mode via the script tag's data attribute or the document element fallback.
// currentScript is reliable for synchronously-executing scripts (which our IIFE is).
const _myScript = document.currentScript;
const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true')
|| document.documentElement.dataset.impeccableExtension === 'true';
const BRAND_COLOR = 'oklch(55% 0.25 350)'; const BRAND_COLOR = 'oklch(55% 0.25 350)';
const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)';
@@ -1261,18 +1291,103 @@ if (IS_BROWSER) {
outline-color: ${BRAND_COLOR_HOVER}; outline-color: ${BRAND_COLOR_HOVER};
z-index: 100001 !important; z-index: 100001 !important;
} }
.impeccable-label {
transition: background 0.15s ease;
}
.impeccable-overlay.impeccable-hover .impeccable-label { .impeccable-overlay.impeccable-hover .impeccable-label {
background: ${BRAND_COLOR_HOVER}; background: ${BRAND_COLOR_HOVER};
} }
.impeccable-overlay.impeccable-spotlight {
z-index: 100002 !important;
}
.impeccable-overlay.impeccable-spotlight-dimmed {
opacity: 0.15 !important;
animation: none !important;
filter: blur(3px);
}
.impeccable-spotlight-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
backdrop-filter: blur(3px) brightness(0.6);
-webkit-backdrop-filter: blur(3px) brightness(0.6);
pointer-events: none;
z-index: 99998;
opacity: 0;
outline: none !important;
animation: none !important;
}
.impeccable-spotlight-backdrop.impeccable-visible {
opacity: 1;
}
.impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} {
display: none !important;
}
.impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} { .impeccable-hidden .impeccable-overlay${EXTENSION_MODE ? '' : ':not(.impeccable-banner)'} {
display: none !important; display: none !important;
} }
`; `;
(document.head || document.documentElement).appendChild(styleEl); (document.head || document.documentElement).appendChild(styleEl);
// Spotlight backdrop element (created lazily on first use)
let spotlightBackdrop = null;
let spotlightTarget = null;
let spotlightTimer = null;
function getSpotlightBackdrop() {
if (!spotlightBackdrop) {
spotlightBackdrop = document.createElement('div');
spotlightBackdrop.className = 'impeccable-spotlight-backdrop';
document.body.appendChild(spotlightBackdrop);
}
return spotlightBackdrop;
}
function updateSpotlightClipPath() {
if (!spotlightBackdrop || !spotlightTarget) return;
const r = spotlightTarget.getBoundingClientRect();
// Match the overlay's outer edge: element rect + 4px (2px overlay offset + 2px outline width)
const inset = 4;
const radius = 6; // outline border-radius (4) + outline width (2)
const x1 = r.left - inset;
const y1 = r.top - inset;
const x2 = r.right + inset;
const y2 = r.bottom + inset;
const vw = window.innerWidth;
const vh = window.innerHeight;
// Outer rect + rounded inner rect (evenodd creates a hole)
const path = `M0 0H${vw}V${vh}H0Z M${x1 + radius} ${y1}H${x2 - radius}A${radius} ${radius} 0 0 1 ${x2} ${y1 + radius}V${y2 - radius}A${radius} ${radius} 0 0 1 ${x2 - radius} ${y2}H${x1 + radius}A${radius} ${radius} 0 0 1 ${x1} ${y2 - radius}V${y1 + radius}A${radius} ${radius} 0 0 1 ${x1 + radius} ${y1}Z`;
spotlightBackdrop.style.clipPath = `path(evenodd, "${path}")`;
}
function showSpotlight(target) {
if (!target || !target.getBoundingClientRect) return;
// Respect the spotlightBlur setting: if disabled, don't show the backdrop
if (window.__IMPECCABLE_CONFIG__?.spotlightBlur === false) {
spotlightTarget = target;
return;
}
spotlightTarget = target;
const bd = getSpotlightBackdrop();
updateSpotlightClipPath();
bd.classList.add('impeccable-visible');
}
function hideSpotlight() {
spotlightTarget = null;
if (spotlightTimer) { clearTimeout(spotlightTimer); spotlightTimer = null; }
if (spotlightBackdrop) spotlightBackdrop.classList.remove('impeccable-visible');
}
function isInViewport(el) {
const r = el.getBoundingClientRect();
return r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
}
// Reposition spotlight on scroll/resize
window.addEventListener('scroll', () => {
if (spotlightTarget) updateSpotlightClipPath();
}, { passive: true });
window.addEventListener('resize', () => {
if (spotlightTarget) updateSpotlightClipPath();
});
const overlays = []; const overlays = [];
const TYPE_LABELS = {}; const TYPE_LABELS = {};
const RULE_CATEGORY = {}; const RULE_CATEGORY = {};
@@ -1310,6 +1425,8 @@ if (IS_BROWSER) {
function repositionOverlays() { function repositionOverlays() {
for (const o of overlays) { for (const o of overlays) {
if (!o._targetEl || o.classList.contains('impeccable-banner')) continue; if (!o._targetEl || o.classList.contains('impeccable-banner')) continue;
// Skip overlays whose target is currently hidden (display: none on the overlay)
if (o.style.display === 'none') continue;
positionOverlay(o); positionOverlay(o);
} }
} }
@@ -1320,6 +1437,13 @@ if (IS_BROWSER) {
resizeRAF = requestAnimationFrame(repositionOverlays); resizeRAF = requestAnimationFrame(repositionOverlays);
}; };
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);
// Reposition on scroll too -- catches sticky/parallax shifts
window.addEventListener('scroll', onResize, { passive: true });
// Reposition when body resizes (lazy-loaded images, dynamic content, fonts loading)
if (typeof ResizeObserver !== 'undefined') {
const bodyResizeObserver = new ResizeObserver(onResize);
bodyResizeObserver.observe(document.body);
}
// Track target element visibility via IntersectionObserver. // Track target element visibility via IntersectionObserver.
// Uses a huge rootMargin so all *rendered* elements count as intersecting, // Uses a huge rootMargin so all *rendered* elements count as intersecting,
@@ -1335,7 +1459,13 @@ if (IS_BROWSER) {
positionOverlay(overlay); positionOverlay(overlay);
if (!overlay._revealed) { if (!overlay._revealed) {
overlay._revealed = true; overlay._revealed = true;
overlay.style.animationDelay = `${(overlay._staggerIndex || 0) * 80}ms`; if (firstScanDone) {
// Subsequent reveals (re-scans, scroll-into-view): instant, no animation
overlay.style.animation = 'none';
} else {
// Initial scan: staggered cascade reveal
overlay.style.animationDelay = `${Math.min((overlay._staggerIndex || 0) * 60, 600)}ms`;
}
requestAnimationFrame(() => { requestAnimationFrame(() => {
overlay.classList.add('impeccable-visible'); overlay.classList.add('impeccable-visible');
if (overlay._checkLabel) overlay._checkLabel(); if (overlay._checkLabel) overlay._checkLabel();
@@ -1601,6 +1731,13 @@ if (IS_BROWSER) {
return parts.join(' > '); return parts.join(' > ');
} }
function isElementHidden(el) {
if (!el || el === document.body || el === document.documentElement) return false;
if (typeof el.checkVisibility === 'function') return !el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
// Fallback: zero size or no offsetParent (covers display:none and detached subtrees)
return el.offsetWidth === 0 && el.offsetHeight === 0;
}
function serializeFindings(allFindings) { function serializeFindings(allFindings) {
return allFindings.map(({ el, findings }) => ({ return allFindings.map(({ el, findings }) => ({
selector: generateSelector(el), selector: generateSelector(el),
@@ -1608,6 +1745,7 @@ if (IS_BROWSER) {
rect: (el !== document.body && el !== document.documentElement && el.getBoundingClientRect) rect: (el !== document.body && el !== document.documentElement && el.getBoundingClientRect)
? el.getBoundingClientRect().toJSON() : null, ? el.getBoundingClientRect().toJSON() : null,
isPageLevel: el === document.body || el === document.documentElement, isPageLevel: el === document.body || el === document.documentElement,
isHidden: isElementHidden(el),
findings: findings.map(f => { findings: findings.map(f => {
const ap = ANTIPATTERNS.find(a => a.id === (f.type || f.id)); const ap = ANTIPATTERNS.find(a => a.id === (f.type || f.id));
return { return {
@@ -1639,18 +1777,19 @@ if (IS_BROWSER) {
console.groupEnd(); console.groupEnd();
}; };
let firstScanDone = false;
const scan = function() { const scan = function() {
for (const o of overlays) o.remove(); for (const o of overlays) o.remove();
overlays.length = 0; overlays.length = 0;
visibilityObserver.disconnect(); visibilityObserver.disconnect();
overlayIndex = 0;
const allFindings = []; const allFindings = [];
const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : [];
const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
for (const el of document.querySelectorAll('*')) { for (const el of document.querySelectorAll('*')) {
if (el.classList.contains('impeccable-overlay') || // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons)
el.classList.contains('impeccable-label') || if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
el.classList.contains('impeccable-tooltip')) continue;
// Skip browser extension elements (Claude, etc.) // Skip browser extension elements (Claude, etc.)
const elId = el.id || ''; const elId = el.id || '';
if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue; if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
@@ -1728,6 +1867,9 @@ if (IS_BROWSER) {
}, '*'); }, '*');
} }
// After this scan completes, all subsequent reveals are instant (no stagger, no animation)
setTimeout(() => { firstScanDone = true; }, 1000);
return allFindings; return allFindings;
}; };
@@ -1749,8 +1891,43 @@ if (IS_BROWSER) {
overlays.length = 0; overlays.length = 0;
visibilityObserver.disconnect(); visibilityObserver.disconnect();
styleEl.remove(); styleEl.remove();
if (spotlightBackdrop) { spotlightBackdrop.remove(); spotlightBackdrop = null; }
document.body.classList.remove('impeccable-hidden'); document.body.classList.remove('impeccable-hidden');
} }
if (e.data.action === 'highlight') {
if (spotlightTimer) { clearTimeout(spotlightTimer); spotlightTimer = null; }
try {
const target = e.data.selector ? document.querySelector(e.data.selector) : null;
if (target) {
// Scroll first so positionOverlay reads the post-scroll rect
if (!isInViewport(target) && target.scrollIntoView) {
target.scrollIntoView({ behavior: 'instant', block: 'center' });
}
for (const o of overlays) {
if (o.classList.contains('impeccable-banner')) continue;
const isMatch = o._targetEl === target;
o.classList.toggle('impeccable-spotlight', isMatch);
o.classList.toggle('impeccable-spotlight-dimmed', !isMatch);
if (isMatch) {
// Force the matching overlay visible immediately, don't wait for IntersectionObserver
o.style.display = '';
o.style.animation = 'none';
o.classList.add('impeccable-visible');
o._revealed = true;
positionOverlay(o);
}
}
showSpotlight(target);
}
} catch { /* invalid selector */ }
}
if (e.data.action === 'unhighlight') {
hideSpotlight();
for (const o of overlays) {
o.classList.remove('impeccable-spotlight');
o.classList.remove('impeccable-spotlight-dimmed');
}
}
}); });
window.postMessage({ source: 'impeccable-ready' }, '*'); window.postMessage({ source: 'impeccable-ready' }, '*');
} else { } else {