smooth detect outline transitions

This commit is contained in:
Paul Bakaus
2026-04-20 13:13:59 -07:00
parent 66630a0ead
commit 51d28cf1eb
3 changed files with 55 additions and 16 deletions
@@ -47,6 +47,14 @@
const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
const PREFIX = 'impeccable-live';
const HIGHLIGHT_TRANSITION =
'top 140ms ' + EASE +
', left 140ms ' + EASE +
', width 140ms ' + EASE +
', height 140ms ' + EASE +
', opacity 150ms ease';
const TOOLTIP_TRANSITION =
'top 140ms ' + EASE + ', left 140ms ' + EASE + ', opacity 150ms ease';
const SKIP_TAGS = new Set([
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
@@ -128,9 +136,7 @@
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
border: '2px solid ' + C.brand, borderRadius: '3px',
pointerEvents: 'none', zIndex: Z.highlight, boxSizing: 'border-box',
// No transition on position/size: avoids layout-property animation detection
// AND gives instant cursor tracking (no lag)
transition: 'opacity 0.15s ease',
transition: HIGHLIGHT_TRANSITION,
display: 'none', opacity: '0',
});
document.body.appendChild(highlightEl);
@@ -145,6 +151,7 @@
zIndex: Z.highlight + 1, pointerEvents: 'none',
whiteSpace: 'nowrap', display: 'none',
letterSpacing: '0.02em',
transition: TOOLTIP_TRANSITION,
});
document.body.appendChild(tooltipEl);
}
@@ -152,22 +159,34 @@
function showHighlight(el) {
if (!el || !highlightEl) return;
const r = el.getBoundingClientRect();
Object.assign(highlightEl.style, {
top: (r.top - 2) + 'px', left: (r.left - 2) + 'px',
width: (r.width + 4) + 'px', height: (r.height + 4) + 'px',
display: 'block', opacity: '1',
});
tooltipEl.textContent = desc(el);
const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px';
const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px';
const tipTop = r.top - 20;
Object.assign(tooltipEl.style, {
top: (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px',
left: Math.max(4, r.left) + 'px', display: 'block',
});
const tipY = (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px';
const tipX = Math.max(4, r.left) + 'px';
tooltipEl.textContent = desc(el);
const hiWasHidden = highlightEl.style.display === 'none' || highlightEl.style.opacity === '0';
if (hiWasHidden) {
// Snap to first target without animating from (0,0), then fade in.
highlightEl.style.transition = 'none';
Object.assign(highlightEl.style, { top, left, width, height, display: 'block' });
tooltipEl.style.transition = 'none';
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block' });
void highlightEl.offsetWidth;
highlightEl.style.transition = HIGHLIGHT_TRANSITION;
highlightEl.style.opacity = '1';
tooltipEl.style.transition = TOOLTIP_TRANSITION;
tooltipEl.style.opacity = '1';
} else {
Object.assign(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
function hideHighlight() {
if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }
if (tooltipEl) tooltipEl.style.display = 'none';
if (tooltipEl) { tooltipEl.style.opacity = '0'; tooltipEl.style.display = 'none'; }
}
// ---------------------------------------------------------------------------
+11 -1
View File
@@ -2246,6 +2246,9 @@ if (IS_BROWSER) {
// Skip browser extension elements (Claude, etc.)
const elId = el.id || '';
if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
// Skip the impeccable live-mode overlay (highlight, tooltip, bar, picker, toast).
// These are inspector chrome, not part of the user's design.
if (el.closest('[id^="impeccable-live-"]')) continue;
// Skip html/body -- page-level findings go in the banner, not a full-page overlay
if (el === document.body || el === document.documentElement) continue;
@@ -2299,7 +2302,14 @@ if (IS_BROWSER) {
}
// Regex-on-HTML checks (shared with Node)
const htmlPatternFindings = checkHtmlPatterns(document.documentElement.outerHTML);
// Clone the document and strip impeccable-live overlay nodes before the
// regex scan, so the inspector's own inline styles (transitions on top/
// left/width/height, etc.) don't register as page anti-patterns.
const docClone = document.documentElement.cloneNode(true);
for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) {
node.remove();
}
const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML);
if (htmlPatternFindings.length > 0) {
const mapped = htmlPatternFindings.map(f => ({ type: f.id, detail: f.snippet })).filter(f => _ruleOk(f.type));
pageLevelFindings.push(...mapped);
+11 -1
View File
@@ -2241,6 +2241,9 @@ if (IS_BROWSER) {
// Skip browser extension elements (Claude, etc.)
const elId = el.id || '';
if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
// Skip the impeccable live-mode overlay (highlight, tooltip, bar, picker, toast).
// These are inspector chrome, not part of the user's design.
if (el.closest('[id^="impeccable-live-"]')) continue;
// Skip html/body -- page-level findings go in the banner, not a full-page overlay
if (el === document.body || el === document.documentElement) continue;
@@ -2294,7 +2297,14 @@ if (IS_BROWSER) {
}
// Regex-on-HTML checks (shared with Node)
const htmlPatternFindings = checkHtmlPatterns(document.documentElement.outerHTML);
// Clone the document and strip impeccable-live overlay nodes before the
// regex scan, so the inspector's own inline styles (transitions on top/
// left/width/height, etc.) don't register as page anti-patterns.
const docClone = document.documentElement.cloneNode(true);
for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) {
node.remove();
}
const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML);
if (htmlPatternFindings.length > 0) {
const mapped = htmlPatternFindings.map(f => ({ type: f.id, detail: f.snippet })).filter(f => _ruleOk(f.type));
pageLevelFindings.push(...mapped);