diff --git a/.claude/skills/critique/scripts/detect-antipatterns-browser.js b/.claude/skills/critique/scripts/detect-antipatterns-browser.js index 662166638..5aa83f9df 100644 --- a/.claude/skills/critique/scripts/detect-antipatterns-browser.js +++ b/.claude/skills/critique/scripts/detect-antipatterns-browser.js @@ -1221,12 +1221,31 @@ if (IS_BROWSER) { // Inject hover styles via CSS (more reliable than JS event listeners) const styleEl = document.createElement('style'); styleEl.textContent = ` - .impeccable-overlay { pointer-events: auto; } - .impeccable-overlay .impeccable-tooltip { display: none; } - .impeccable-overlay .impeccable-label { display: block; } - .impeccable-overlay:hover { border-color: transparent !important; } - .impeccable-overlay:hover .impeccable-tooltip { display: block; } - .impeccable-overlay:hover .impeccable-label { display: none; } + .impeccable-overlay:not(.impeccable-banner) { + pointer-events: auto; + outline: 2px solid ${OUTLINE_COLOR}; + outline-offset: 0px; + border-radius: 4px; + transition: outline-offset 0.2s ease; + } + .impeccable-overlay:not(.impeccable-banner):hover { + outline-offset: 4px; + z-index: 100001 !important; + } + .impeccable-overlay:not(.impeccable-banner):hover .impeccable-label { + transform: translateY(-4px); + } + .impeccable-overlay:not(.impeccable-banner) .impeccable-tooltip { + bottom: -28px; top: auto !important; + opacity: 0; + transform: translateY(-4px); + pointer-events: none; + transition: opacity 0.15s ease, transform 0.2s ease; + } + .impeccable-overlay:not(.impeccable-banner):hover .impeccable-tooltip { + opacity: 1; + transform: translateY(0); + } `; (document.head || document.documentElement).appendChild(styleEl); @@ -1236,15 +1255,33 @@ if (IS_BROWSER) { TYPE_LABELS[ap.id] = ap.name.toLowerCase().substring(0, 26); } + function repositionOverlays() { + for (const o of overlays) { + if (!o._targetEl || o.classList.contains('impeccable-banner')) continue; + const rect = o._targetEl.getBoundingClientRect(); + o.style.top = `${rect.top + scrollY - 2}px`; + o.style.left = `${rect.left + scrollX - 2}px`; + o.style.width = `${rect.width + 4}px`; + o.style.height = `${rect.height + 4}px`; + } + } + + let resizeRAF; + const onResize = () => { + cancelAnimationFrame(resizeRAF); + resizeRAF = requestAnimationFrame(repositionOverlays); + }; + window.addEventListener('resize', onResize); + const highlight = function(el, findings) { const rect = el.getBoundingClientRect(); const outline = document.createElement('div'); outline.className = 'impeccable-overlay'; + outline._targetEl = el; Object.assign(outline.style, { position: 'absolute', top: `${rect.top + scrollY - 2}px`, left: `${rect.left + scrollX - 2}px`, width: `${rect.width + 4}px`, height: `${rect.height + 4}px`, - border: `2px solid ${OUTLINE_COLOR}`, borderRadius: '4px', zIndex: '99999', boxSizing: 'border-box', }); @@ -1257,6 +1294,7 @@ if (IS_BROWSER) { fontSize: '11px', fontFamily: 'system-ui, sans-serif', fontWeight: '600', padding: '2px 8px', borderRadius: '3px', whiteSpace: 'nowrap', lineHeight: '16px', letterSpacing: '0.02em', + transition: 'transform 0.2s ease', }); outline.appendChild(label); @@ -1264,7 +1302,7 @@ if (IS_BROWSER) { tooltip.className = 'impeccable-tooltip'; tooltip.innerHTML = findings.map(f => f.detail || f.snippet).join('
'); Object.assign(tooltip.style, { - position: 'absolute', top: '-22px', left: '0', + position: 'absolute', bottom: '-28px', left: '0', background: 'rgba(0,0,0,0.85)', color: '#e5e5e5', fontSize: '11px', fontFamily: 'ui-monospace, monospace', padding: '2px 8px', borderRadius: '3px', whiteSpace: 'nowrap', @@ -1279,7 +1317,7 @@ if (IS_BROWSER) { const showPageBanner = function(findings) { if (!findings.length) return; const banner = document.createElement('div'); - banner.className = 'impeccable-overlay'; + banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', background: LABEL_BG, color: 'white', diff --git a/source/skills/critique/scripts/detect-antipatterns.mjs b/source/skills/critique/scripts/detect-antipatterns.mjs index 4b09420d1..b6dbcef46 100644 --- a/source/skills/critique/scripts/detect-antipatterns.mjs +++ b/source/skills/critique/scripts/detect-antipatterns.mjs @@ -1219,12 +1219,31 @@ if (IS_BROWSER) { // Inject hover styles via CSS (more reliable than JS event listeners) const styleEl = document.createElement('style'); styleEl.textContent = ` - .impeccable-overlay { pointer-events: auto; } - .impeccable-overlay .impeccable-tooltip { display: none; } - .impeccable-overlay .impeccable-label { display: block; } - .impeccable-overlay:hover { border-color: transparent !important; } - .impeccable-overlay:hover .impeccable-tooltip { display: block; } - .impeccable-overlay:hover .impeccable-label { display: none; } + .impeccable-overlay:not(.impeccable-banner) { + pointer-events: auto; + outline: 2px solid ${OUTLINE_COLOR}; + outline-offset: 0px; + border-radius: 4px; + transition: outline-offset 0.2s ease; + } + .impeccable-overlay:not(.impeccable-banner):hover { + outline-offset: 4px; + z-index: 100001 !important; + } + .impeccable-overlay:not(.impeccable-banner):hover .impeccable-label { + transform: translateY(-4px); + } + .impeccable-overlay:not(.impeccable-banner) .impeccable-tooltip { + bottom: -28px; top: auto !important; + opacity: 0; + transform: translateY(-4px); + pointer-events: none; + transition: opacity 0.15s ease, transform 0.2s ease; + } + .impeccable-overlay:not(.impeccable-banner):hover .impeccable-tooltip { + opacity: 1; + transform: translateY(0); + } `; (document.head || document.documentElement).appendChild(styleEl); @@ -1234,15 +1253,33 @@ if (IS_BROWSER) { TYPE_LABELS[ap.id] = ap.name.toLowerCase().substring(0, 26); } + function repositionOverlays() { + for (const o of overlays) { + if (!o._targetEl || o.classList.contains('impeccable-banner')) continue; + const rect = o._targetEl.getBoundingClientRect(); + o.style.top = `${rect.top + scrollY - 2}px`; + o.style.left = `${rect.left + scrollX - 2}px`; + o.style.width = `${rect.width + 4}px`; + o.style.height = `${rect.height + 4}px`; + } + } + + let resizeRAF; + const onResize = () => { + cancelAnimationFrame(resizeRAF); + resizeRAF = requestAnimationFrame(repositionOverlays); + }; + window.addEventListener('resize', onResize); + const highlight = function(el, findings) { const rect = el.getBoundingClientRect(); const outline = document.createElement('div'); outline.className = 'impeccable-overlay'; + outline._targetEl = el; Object.assign(outline.style, { position: 'absolute', top: `${rect.top + scrollY - 2}px`, left: `${rect.left + scrollX - 2}px`, width: `${rect.width + 4}px`, height: `${rect.height + 4}px`, - border: `2px solid ${OUTLINE_COLOR}`, borderRadius: '4px', zIndex: '99999', boxSizing: 'border-box', }); @@ -1255,6 +1292,7 @@ if (IS_BROWSER) { fontSize: '11px', fontFamily: 'system-ui, sans-serif', fontWeight: '600', padding: '2px 8px', borderRadius: '3px', whiteSpace: 'nowrap', lineHeight: '16px', letterSpacing: '0.02em', + transition: 'transform 0.2s ease', }); outline.appendChild(label); @@ -1262,7 +1300,7 @@ if (IS_BROWSER) { tooltip.className = 'impeccable-tooltip'; tooltip.innerHTML = findings.map(f => f.detail || f.snippet).join('
'); Object.assign(tooltip.style, { - position: 'absolute', top: '-22px', left: '0', + position: 'absolute', bottom: '-28px', left: '0', background: 'rgba(0,0,0,0.85)', color: '#e5e5e5', fontSize: '11px', fontFamily: 'ui-monospace, monospace', padding: '2px 8px', borderRadius: '3px', whiteSpace: 'nowrap', @@ -1277,7 +1315,7 @@ if (IS_BROWSER) { const showPageBanner = function(findings) { if (!findings.length) return; const banner = document.createElement('div'); - banner.className = 'impeccable-overlay'; + banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', background: LABEL_BG, color: 'white',