// --- browser-bundle/40-overlay.js --- // The overlay UI: outlines + labels per flagged element, the page-level // banner, the hover spotlight, visibility toggling. Pure presentation over a // findings list — no rules, no thresholds, no snippet strings; the rule // names and categories come from the registry it is handed. Ported from // cli/engine/browser/injected/index.mjs Section 7. // // createImpeccableOverlay({ extensionMode, antipatterns }) -> // { highlight(el, findings), showPageBanner(findings), clearOverlays(), // remove(), toggleOverlays() -> visible, spotlight(target), // unspotlight(), highlightSelector(selector), setFirstScanDone(), // overlays } // Used by the in-page bundle (50-scan.js) and, as `overlay.js`, by the // extension's content script. function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } = {}) { // Kinpaku gold — pinned to the site's brand token (see // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with // the picker's C.brand in skill/scripts/live-browser.js and the kit's // picker section in site/styles/kinpaku-kit.css. // // One color across both light and dark host pages. The outline is a // 2px gesture pointing at an element + a labeled tag — it's a marker, // not body text, so it doesn't need WCAG AA against the page. The // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 // against the leaf gold, so reading the rule name is solid in both // modes. Hover deepens the gold (preserves chroma — never drops it, // dropping chroma washes the gold into a sand/olive tone). const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; // Inject hover styles via CSS (more reliable than JS event listeners) const styleEl = document.createElement('style'); styleEl.textContent = ` @keyframes impeccable-reveal { from { opacity: 0; } to { opacity: 1; } } .impeccable-overlay:not(.impeccable-banner) { pointer-events: none; outline: 2px solid ${OUTLINE_COLOR}; border-radius: 4px; transition: outline-color 0.15s ease; animation: impeccable-reveal 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; animation-play-state: paused; border-top-left-radius: 0; } .impeccable-overlay.impeccable-visible { animation-play-state: running; } .impeccable-overlay.impeccable-hover { outline-color: ${BRAND_COLOR_HOVER}; z-index: 100001 !important; } .impeccable-overlay.impeccable-hover .impeccable-label { 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${extensionMode ? '' : ':not(.impeccable-banner)'} { display: none !important; } `; (document.head || document.documentElement).appendChild(styleEl); let firstScanDone = false; // Spotlight backdrop element (created lazily on first use) let spotlightBackdrop = null; let spotlightTarget = 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 (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 ANTIPATTERNS = antipatterns || []; const TYPE_LABELS = {}; const RULE_CATEGORY = {}; for (const ap of ANTIPATTERNS) { TYPE_LABELS[ap.id] = ap.name.toLowerCase(); RULE_CATEGORY[ap.id] = ap.category || 'quality'; } function isInFixedContext(el) { let p = el; while (p && p !== document.body) { if (getComputedStyle(p).position === 'fixed') return true; p = p.parentElement; } return false; } function positionOverlay(overlay) { const el = overlay._targetEl; if (!el) return; const rect = el.getBoundingClientRect(); if (overlay._isFixed) { // Viewport-relative coords for fixed targets overlay.style.top = `${rect.top - 2}px`; overlay.style.left = `${rect.left - 2}px`; } else { // Document-relative coords for normal targets overlay.style.top = `${rect.top + scrollY - 2}px`; overlay.style.left = `${rect.left + scrollX - 2}px`; } overlay.style.width = `${rect.width + 4}px`; overlay.style.height = `${rect.height + 4}px`; } function repositionOverlays() { for (const o of overlays) { 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); } } let resizeRAF; const onResize = () => { cancelAnimationFrame(resizeRAF); resizeRAF = requestAnimationFrame(repositionOverlays); }; 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. // Uses a huge rootMargin so all *rendered* elements count as intersecting, // while display:none / closed
/ hidden modals etc. do not. // This is event-driven -- no polling needed. let overlayIndex = 0; const visibilityObserver = new IntersectionObserver((entries) => { for (const entry of entries) { const overlay = entry.target._impeccableOverlay; if (!overlay) continue; if (entry.isIntersecting) { overlay.style.display = ''; positionOverlay(overlay); if (!overlay._revealed) { overlay._revealed = true; 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(() => { overlay.classList.add('impeccable-visible'); if (overlay._checkLabel) overlay._checkLabel(); }); } } else { overlay.style.display = 'none'; } } }, { rootMargin: '99999px' }); function detachOverlay(overlay) { if (!overlay) return; if (typeof overlay._cleanup === 'function') { try { overlay._cleanup(); } catch { /* best effort overlay teardown */ } } if (overlay._targetEl && overlay._targetEl._impeccableOverlay === overlay) { visibilityObserver.unobserve(overlay._targetEl); delete overlay._targetEl._impeccableOverlay; } const idx = overlays.indexOf(overlay); if (idx >= 0) overlays.splice(idx, 1); overlay.remove(); } // Reposition overlays after CSS transitions end (e.g. reveal animations). // Listens at document level so it catches transitions on ancestor elements // (the transform may be on a parent, not the flagged element itself). document.addEventListener('transitionend', (e) => { if (e.propertyName !== 'transform') return; for (const o of overlays) { if (!o._targetEl || o.classList.contains('impeccable-banner') || o.style.display === 'none') continue; if (e.target === o._targetEl || e.target.contains(o._targetEl)) { positionOverlay(o); } } }); const highlight = function(el, findings) { if (el._impeccableOverlay) detachOverlay(el._impeccableOverlay); const hasSlop = findings.some(f => RULE_CATEGORY[f.type || f.id] === 'slop'); const fixed = isInFixedContext(el); const rect = el.getBoundingClientRect(); const outline = document.createElement('div'); outline.className = 'impeccable-overlay'; outline._targetEl = el; outline._isFixed = fixed; Object.assign(outline.style, { position: fixed ? 'fixed' : 'absolute', top: fixed ? `${rect.top - 2}px` : `${rect.top + scrollY - 2}px`, left: fixed ? `${rect.left - 2}px` : `${rect.left + scrollX - 2}px`, width: `${rect.width + 4}px`, height: `${rect.height + 4}px`, zIndex: '99999', boxSizing: 'border-box', }); // Build per-finding label entries: ✦ prefix for slop const entries = findings.map(f => { const name = TYPE_LABELS[f.type || f.id] || f.type || f.id; const prefix = RULE_CATEGORY[f.type || f.id] === 'slop' ? '\u2726 ' : ''; return { name: prefix + name, detail: f.detail || f.snippet }; }); const allText = entries.map(e => e.name).join(', '); const label = document.createElement('div'); label.className = 'impeccable-label'; Object.assign(label.style, { position: 'absolute', bottom: '100%', left: '-2px', display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', }); const textSpan = document.createElement('span'); textSpan.style.padding = '3px 8px'; textSpan.textContent = allText; label.appendChild(textSpan); // State for cycling mode let cycleMode = false; let cycleIndex = 0; let isHovered = false; let prevBtn, nextBtn; function updateCycleText() { const e = entries[cycleIndex]; textSpan.textContent = isHovered ? e.detail : e.name; } function enableCycleMode() { if (cycleMode || entries.length < 2) return; cycleMode = true; const btnStyle = { background: 'none', border: 'none', color: 'rgba(255,255,255,0.7)', fontSize: '11px', cursor: 'pointer', padding: '3px 4px', fontFamily: 'system-ui, sans-serif', lineHeight: '14px', pointerEvents: 'auto', }; const navGroup = document.createElement('span'); Object.assign(navGroup.style, { display: 'inline-flex', alignItems: 'center', flexShrink: '0', }); prevBtn = document.createElement('button'); prevBtn.textContent = '\u2039'; Object.assign(prevBtn.style, btnStyle); prevBtn.style.paddingLeft = '6px'; prevBtn.addEventListener('click', (e) => { e.stopPropagation(); cycleIndex = (cycleIndex - 1 + entries.length) % entries.length; updateCycleText(); }); nextBtn = document.createElement('button'); nextBtn.textContent = '\u203A'; Object.assign(nextBtn.style, btnStyle); nextBtn.style.paddingRight = '2px'; nextBtn.addEventListener('click', (e) => { e.stopPropagation(); cycleIndex = (cycleIndex + 1) % entries.length; updateCycleText(); }); navGroup.appendChild(prevBtn); navGroup.appendChild(nextBtn); label.insertBefore(navGroup, textSpan); textSpan.style.padding = '3px 8px 3px 4px'; updateCycleText(); } outline.appendChild(label); // Start hidden; the IntersectionObserver will show it once the target is rendered outline.style.display = 'none'; outline._staggerIndex = overlayIndex++; el._impeccableOverlay = outline; visibilityObserver.observe(el); // After first paint, check label width vs outline outline._checkLabel = () => { if (entries.length > 1 && label.offsetWidth > outline.offsetWidth) { enableCycleMode(); } }; // Hover: show detail text, darken const onMouseEnter = () => { isHovered = true; outline.classList.add('impeccable-hover'); outline.style.outlineColor = BRAND_COLOR_HOVER; label.style.background = BRAND_COLOR_HOVER; if (cycleMode) { updateCycleText(); } else { textSpan.textContent = entries.map(e => e.detail).join(' | '); } }; const onMouseLeave = () => { isHovered = false; outline.classList.remove('impeccable-hover'); outline.style.outlineColor = ''; label.style.background = LABEL_BG; if (cycleMode) { updateCycleText(); } else { textSpan.textContent = allText; } }; el.addEventListener('mouseenter', onMouseEnter); el.addEventListener('mouseleave', onMouseLeave); outline._cleanup = () => { el.removeEventListener('mouseenter', onMouseEnter); el.removeEventListener('mouseleave', onMouseLeave); }; document.body.appendChild(outline); overlays.push(outline); }; const showPageBanner = function(findings) { if (!findings.length) return; const banner = document.createElement('div'); banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', transform: 'translateY(-100%)', transition: 'transform 0.4s cubic-bezier(0.16, 1, 0.3, 1)', }); requestAnimationFrame(() => requestAnimationFrame(() => { banner.style.transform = 'translateY(0)'; })); // Scrollable findings area const scrollArea = document.createElement('div'); Object.assign(scrollArea.style, { flex: '1', minWidth: '0', overflowX: 'auto', overflowY: 'hidden', display: 'flex', gap: '8px', alignItems: 'center', padding: '0 12px', scrollSnapType: 'x mandatory', scrollbarWidth: 'none', }); for (const f of findings) { const prefix = RULE_CATEGORY[f.type] === 'slop' ? '\u2726 ' : ''; const tag = document.createElement('span'); tag.textContent = `${prefix}${TYPE_LABELS[f.type] || f.type}: ${f.detail}`; Object.assign(tag.style, { background: 'rgba(255,255,255,0.15)', padding: '2px 8px', borderRadius: '3px', fontSize: '12px', fontFamily: 'ui-monospace, monospace', whiteSpace: 'nowrap', flexShrink: '0', scrollSnapAlign: 'start', }); scrollArea.appendChild(tag); } banner.appendChild(scrollArea); // Controls area (only in standalone mode, not extension) if (!extensionMode) { const controls = document.createElement('div'); Object.assign(controls.style, { display: 'flex', alignItems: 'center', gap: '2px', padding: '0 8px', flexShrink: '0', }); // Toggle visibility button const toggle = document.createElement('button'); toggle.textContent = '\u25C9'; // circle with dot (visible state) toggle.title = 'Toggle overlay visibility'; Object.assign(toggle.style, { background: 'none', border: 'none', color: 'white', fontSize: '16px', cursor: 'pointer', padding: '0 4px', opacity: '0.85', transition: 'opacity 0.15s', }); let overlaysVisible = true; toggle.addEventListener('click', () => { overlaysVisible = !overlaysVisible; document.body.classList.toggle('impeccable-hidden', !overlaysVisible); toggle.textContent = overlaysVisible ? '\u25C9' : '\u25CB'; // filled vs empty circle toggle.style.opacity = overlaysVisible ? '0.85' : '0.5'; }); controls.appendChild(toggle); // Close button const close = document.createElement('button'); close.textContent = '\u00d7'; close.title = 'Dismiss banner'; Object.assign(close.style, { background: 'none', border: 'none', color: 'white', fontSize: '18px', cursor: 'pointer', padding: '0 4px', }); close.addEventListener('click', () => banner.remove()); controls.appendChild(close); banner.appendChild(controls); } document.body.appendChild(banner); overlays.push(banner); }; function clearOverlays() { for (const o of [...overlays]) detachOverlay(o); overlays.length = 0; visibilityObserver.disconnect(); overlayIndex = 0; } // Tear the UI down entirely (the extension's `remove` command). function remove() { clearOverlays(); styleEl.remove(); if (spotlightBackdrop) { spotlightBackdrop.remove(); spotlightBackdrop = null; } document.body.classList.remove('impeccable-hidden'); } // Toggle every overlay; returns the new visibility. function toggleOverlays() { const visible = !document.body.classList.contains('impeccable-hidden'); document.body.classList.toggle('impeccable-hidden', visible); return !visible; } // Spotlight the overlay of the element `selector` names (scrolling it into // view first so positionOverlay reads the post-scroll rect). function highlightSelector(selector) { try { const target = selector ? document.querySelector(selector) : null; if (!target) return; 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 */ } } function unspotlight() { hideSpotlight(); for (const o of overlays) { o.classList.remove('impeccable-spotlight'); o.classList.remove('impeccable-spotlight-dimmed'); } } return { highlight, showPageBanner, clearOverlays, remove, toggleOverlays, spotlight: showSpotlight, unspotlight, highlightSelector, setFirstScanDone() { firstScanDone = true; }, overlays, TYPE_LABELS, RULE_CATEGORY, }; }