From 51d28cf1eb8b51091ce37e9a3682587ac2ad67a9 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 20 Apr 2026 13:13:59 -0700 Subject: [PATCH] smooth detect outline transitions --- .../skills/impeccable/scripts/live-browser.js | 47 +++++++++++++------ src/detect-antipatterns-browser.js | 12 ++++- src/detect-antipatterns.mjs | 12 ++++- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/source/skills/impeccable/scripts/live-browser.js b/source/skills/impeccable/scripts/live-browser.js index b9332e10d..d1b2808c8 100644 --- a/source/skills/impeccable/scripts/live-browser.js +++ b/source/skills/impeccable/scripts/live-browser.js @@ -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'; } } // --------------------------------------------------------------------------- diff --git a/src/detect-antipatterns-browser.js b/src/detect-antipatterns-browser.js index 550cdf17d..76830e57e 100644 --- a/src/detect-antipatterns-browser.js +++ b/src/detect-antipatterns-browser.js @@ -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); diff --git a/src/detect-antipatterns.mjs b/src/detect-antipatterns.mjs index 8c4a22a15..e196f3a8c 100644 --- a/src/detect-antipatterns.mjs +++ b/src/detect-antipatterns.mjs @@ -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);