sidebar.js

  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}