1/**
2 * Impeccable DevTools Extension - Elements Sidebar Pane
3 *
4 * Shows Impeccable findings for the currently selected element ($0) in the Elements panel.
5 */
6
7if (chrome.devtools.panels.themeName === 'dark') {
8 document.documentElement.classList.add('theme-dark');
9}
10
11const tabId = chrome.devtools.inspectedWindow.tabId;
12const content = document.getElementById('sidebar-content');
13let currentFindings = [];
14
15// Auto-reconnecting port (service worker may restart in MV3)
16let port = null;
17function getPort() {
18 if (port) return port;
19 port = chrome.runtime.connect({ name: `impeccable-sidebar-${tabId}` });
20 port.onMessage.addListener((msg) => {
21 if (msg.action === 'findings' || msg.action === 'state') {
22 currentFindings = msg.findings || [];
23 refreshForCurrentSelection();
24 }
25 });
26 port.onDisconnect.addListener(() => { port = null; });
27 return port;
28}
29getPort();
30
31chrome.devtools.panels.elements.onSelectionChanged.addListener(refreshForCurrentSelection);
32
33function refreshForCurrentSelection() {
34 if (!currentFindings.length) {
35 renderEmpty('No findings on this page yet.');
36 return;
37 }
38
39 // Collect non-page-level selectors and ask the inspected window which one matches $0
40 const selectors = [];
41 for (const item of currentFindings) {
42 if (item.isPageLevel || item.isHidden) continue;
43 selectors.push(item.selector);
44 }
45 if (!selectors.length) {
46 renderEmpty('No element-level findings on this page.');
47 return;
48 }
49
50 const code = `(function() {
51 var sels = ${JSON.stringify(selectors)};
52 var matched = [];
53 for (var i = 0; i < sels.length; i++) {
54 try { if (document.querySelector(sels[i]) === $0) matched.push(sels[i]); } catch (e) {}
55 }
56 return matched;
57 })()`;
58
59 chrome.devtools.inspectedWindow.eval(code, (matched) => {
60 if (!matched || !matched.length) {
61 renderNoFindings();
62 return;
63 }
64 const items = currentFindings.filter(item => matched.includes(item.selector));
65 render(items);
66 });
67}
68
69function renderEmpty(text) {
70 content.innerHTML = `<div class="state">${escapeHtml(text)}</div>`;
71}
72
73function renderNoFindings() {
74 content.innerHTML = `<div class="state"><strong>Clean.</strong> No anti-patterns on this element.</div>`;
75}
76
77function render(items) {
78 const html = [];
79 for (const item of items) {
80 for (const f of item.findings) {
81 const isSlop = f.category === 'slop';
82 const marker = isSlop ? '<span class="marker">\u2726</span>' : '';
83 const kind = isSlop ? 'AI tell' : 'Quality';
84 html.push(`
85 <div class="finding">
86 <div class="finding-header">
87 <span class="finding-name">${marker}${escapeHtml(f.name)}</span>
88 <span class="finding-kind">${kind}</span>
89 </div>
90 <div class="finding-detail">${escapeHtml(f.detail)}</div>
91 <div class="finding-description">${escapeHtml(f.description)}</div>
92 </div>
93 `);
94 }
95 }
96 content.innerHTML = html.join('');
97}
98
99function escapeHtml(str) {
100 const div = document.createElement('div');
101 div.textContent = str;
102 return div.innerHTML;
103}