Files
pbakaus_impeccable/browser-bundle/40-overlay.js
T
Paul BakausandClaude Fable 5.1 0547ed6a63 reorg C: the open Rust runtime joins this repo as one Cargo workspace
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.

The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.

What changed versus the engine repo copy:

- Every crate manifest moves from `license-file.workspace` to
  `license.workspace` (this workspace declares Apache-2.0), and the workspace
  gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
  a sibling `launcher/` dir, and `engine_binary` downloads from
  `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
  of the retired dist repo. No oracle golden carried the old URL, so no
  re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
  `../impeccable-second` or a hardcoded home directory now resolve the root as
  `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
  override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
  glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
  archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
  live mode serves that. `scripts/check-detector-release.mjs` now requires it
  and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
  `modern-screenshot.umd.js` directly rather than through vendored copies, so
  the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
  plain `cargo build --release -p impeccable` is enough to run `bun run test`.

Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-01 15:31:26 -07:00

569 lines
21 KiB
JavaScript

// --- 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 <details> / 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,
};
}