From ce98272ea6aef61c29efbad884f6c1c8d1e4eb1d Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 28 Mar 2026 16:47:22 -0700 Subject: [PATCH] Polish browser overlay UI: label slide, banner scroll, toggle, passthrough - Replace hover outline-offset with outline-color transition (no flicker) - Label shows type name by default; on hover, detail slides up to replace it ticker-style with a dark background for visual distinction - Banner: horizontal scroll with scroll-snap for findings, fixed controls - Add toggle button to show/hide all overlays - Overlays use pointer-events:none so page interactions (text selection, clicks, links) pass through; hover driven via target mouseenter/leave Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/detect-antipatterns-browser.js | 137 ++++++++++++------ .../critique/scripts/detect-antipatterns.mjs | 137 ++++++++++++------ 2 files changed, 184 insertions(+), 90 deletions(-) diff --git a/.claude/skills/critique/scripts/detect-antipatterns-browser.js b/.claude/skills/critique/scripts/detect-antipatterns-browser.js index 1ea1210ac..15711e1d2 100644 --- a/.claude/skills/critique/scripts/detect-antipatterns-browser.js +++ b/.claude/skills/critique/scripts/detect-antipatterns-browser.js @@ -1211,29 +1211,28 @@ if (IS_BROWSER) { const styleEl = document.createElement('style'); styleEl.textContent = ` .impeccable-overlay:not(.impeccable-banner) { - pointer-events: auto; + pointer-events: none; outline: 2px solid ${OUTLINE_COLOR}; - outline-offset: 0px; border-radius: 4px; - transition: outline-offset 0.2s ease; + transition: outline-color 0.3s ease; } - .impeccable-overlay:not(.impeccable-banner):hover { - outline-offset: 4px; + .impeccable-overlay.impeccable-hover { + outline-color: rgba(0,0,0,0.85); z-index: 100001 !important; } - .impeccable-overlay:not(.impeccable-banner):hover .impeccable-label { - transform: translateY(-4px); + .impeccable-label-name, + .impeccable-label-detail { + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); } - .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-label-detail { + position: absolute; top: 100%; left: 0; } - .impeccable-overlay:not(.impeccable-banner):hover .impeccable-tooltip { - opacity: 1; - transform: translateY(0); + .impeccable-overlay.impeccable-hover .impeccable-label-name, + .impeccable-overlay.impeccable-hover .impeccable-label-detail { + transform: translateY(-100%); + } + .impeccable-hidden .impeccable-overlay:not(.impeccable-banner) { + display: none !important; } `; (document.head || document.documentElement).appendChild(styleEl); @@ -1329,36 +1328,45 @@ if (IS_BROWSER) { zIndex: '99999', boxSizing: 'border-box', }); + const typeText = findings.map(f => TYPE_LABELS[f.type || f.id] || f.type || f.id).join(', '); + const detailText = findings.map(f => f.detail || f.snippet).join(' | '); + const label = document.createElement('div'); label.className = 'impeccable-label'; - label.textContent = findings.map(f => TYPE_LABELS[f.type || f.id] || f.type || f.id).join(', '); Object.assign(label.style, { position: 'absolute', top: '-22px', left: '0', - background: LABEL_BG, color: 'white', - 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', + clipPath: 'inset(0 -999px)', }); - outline.appendChild(label); - const tooltip = document.createElement('div'); - tooltip.className = 'impeccable-tooltip'; - tooltip.innerHTML = findings.map(f => f.detail || f.snippet).join('
'); - Object.assign(tooltip.style, { - position: 'absolute', bottom: '-28px', left: '0', - background: 'rgba(0,0,0,0.85)', color: '#e5e5e5', - fontSize: '11px', fontFamily: 'ui-monospace, monospace', + const rowBase = { padding: '2px 8px', borderRadius: '3px', whiteSpace: 'nowrap', - lineHeight: '16px', letterSpacing: '0.02em', zIndex: '100000', - }); - outline.appendChild(tooltip); + fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', + color: 'white', lineHeight: '16px', + }; + + const nameRow = document.createElement('div'); + nameRow.className = 'impeccable-label-name'; + nameRow.textContent = typeText; + Object.assign(nameRow.style, { ...rowBase, background: LABEL_BG, fontFamily: 'system-ui, sans-serif' }); + label.appendChild(nameRow); + + const detailRow = document.createElement('div'); + detailRow.className = 'impeccable-label-detail'; + detailRow.textContent = detailText; + Object.assign(detailRow.style, { ...rowBase, background: 'rgba(0,0,0,0.85)', fontFamily: 'ui-monospace, monospace', fontWeight: '400' }); + label.appendChild(detailRow); + + outline.appendChild(label); // Start hidden; the IntersectionObserver will show it once the target is rendered outline.style.display = 'none'; el._impeccableOverlay = outline; visibilityObserver.observe(el); + // Drive hover state from the target element so pointer events pass through + el.addEventListener('mouseenter', () => outline.classList.add('impeccable-hover')); + el.addEventListener('mouseleave', () => outline.classList.remove('impeccable-hover')); + document.body.appendChild(outline); overlays.push(outline); }; @@ -1371,8 +1379,17 @@ if (IS_BROWSER) { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', background: LABEL_BG, color: 'white', fontFamily: 'system-ui, sans-serif', fontSize: '13px', - padding: '8px 16px', display: 'flex', flexWrap: 'wrap', - gap: '12px', alignItems: 'center', pointerEvents: 'auto', + display: 'flex', alignItems: 'center', pointerEvents: 'auto', + height: '36px', + }); + + // 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 tag = document.createElement('span'); @@ -1380,17 +1397,49 @@ if (IS_BROWSER) { 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', }); - banner.appendChild(tag); + scrollArea.appendChild(tag); } + banner.appendChild(scrollArea); + + // Controls area (always visible on the right) + 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, { - marginLeft: 'auto', background: 'none', border: 'none', + background: 'none', border: 'none', color: 'white', fontSize: '18px', cursor: 'pointer', padding: '0 4px', }); close.addEventListener('click', () => banner.remove()); - banner.appendChild(close); + controls.appendChild(close); + + banner.appendChild(controls); document.body.appendChild(banner); overlays.push(banner); }; @@ -1457,13 +1506,11 @@ if (IS_BROWSER) { // Merge into existing overlay if this element already has one const existing = el._impeccableOverlay; if (existing) { - const label = existing.querySelector('.impeccable-label'); - if (label) { - const newType = TYPE_LABELS[f.type] || f.type; - label.textContent += ', ' + newType; - } - const tooltip = existing.querySelector('.impeccable-tooltip'); - if (tooltip) tooltip.innerHTML += '
' + (f.detail || ''); + const nameRow = existing.querySelector('.impeccable-label-name'); + const detailRow = existing.querySelector('.impeccable-label-detail'); + const newType = TYPE_LABELS[f.type] || f.type; + if (nameRow) nameRow.textContent += ', ' + newType; + if (detailRow) detailRow.textContent += ' | ' + (f.detail || ''); } else { highlight(el, [f]); } diff --git a/source/skills/critique/scripts/detect-antipatterns.mjs b/source/skills/critique/scripts/detect-antipatterns.mjs index 85c7434d0..a9cc78d45 100644 --- a/source/skills/critique/scripts/detect-antipatterns.mjs +++ b/source/skills/critique/scripts/detect-antipatterns.mjs @@ -1209,29 +1209,28 @@ if (IS_BROWSER) { const styleEl = document.createElement('style'); styleEl.textContent = ` .impeccable-overlay:not(.impeccable-banner) { - pointer-events: auto; + pointer-events: none; outline: 2px solid ${OUTLINE_COLOR}; - outline-offset: 0px; border-radius: 4px; - transition: outline-offset 0.2s ease; + transition: outline-color 0.3s ease; } - .impeccable-overlay:not(.impeccable-banner):hover { - outline-offset: 4px; + .impeccable-overlay.impeccable-hover { + outline-color: rgba(0,0,0,0.85); z-index: 100001 !important; } - .impeccable-overlay:not(.impeccable-banner):hover .impeccable-label { - transform: translateY(-4px); + .impeccable-label-name, + .impeccable-label-detail { + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); } - .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-label-detail { + position: absolute; top: 100%; left: 0; } - .impeccable-overlay:not(.impeccable-banner):hover .impeccable-tooltip { - opacity: 1; - transform: translateY(0); + .impeccable-overlay.impeccable-hover .impeccable-label-name, + .impeccable-overlay.impeccable-hover .impeccable-label-detail { + transform: translateY(-100%); + } + .impeccable-hidden .impeccable-overlay:not(.impeccable-banner) { + display: none !important; } `; (document.head || document.documentElement).appendChild(styleEl); @@ -1327,36 +1326,45 @@ if (IS_BROWSER) { zIndex: '99999', boxSizing: 'border-box', }); + const typeText = findings.map(f => TYPE_LABELS[f.type || f.id] || f.type || f.id).join(', '); + const detailText = findings.map(f => f.detail || f.snippet).join(' | '); + const label = document.createElement('div'); label.className = 'impeccable-label'; - label.textContent = findings.map(f => TYPE_LABELS[f.type || f.id] || f.type || f.id).join(', '); Object.assign(label.style, { position: 'absolute', top: '-22px', left: '0', - background: LABEL_BG, color: 'white', - 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', + clipPath: 'inset(0 -999px)', }); - outline.appendChild(label); - const tooltip = document.createElement('div'); - tooltip.className = 'impeccable-tooltip'; - tooltip.innerHTML = findings.map(f => f.detail || f.snippet).join('
'); - Object.assign(tooltip.style, { - position: 'absolute', bottom: '-28px', left: '0', - background: 'rgba(0,0,0,0.85)', color: '#e5e5e5', - fontSize: '11px', fontFamily: 'ui-monospace, monospace', + const rowBase = { padding: '2px 8px', borderRadius: '3px', whiteSpace: 'nowrap', - lineHeight: '16px', letterSpacing: '0.02em', zIndex: '100000', - }); - outline.appendChild(tooltip); + fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', + color: 'white', lineHeight: '16px', + }; + + const nameRow = document.createElement('div'); + nameRow.className = 'impeccable-label-name'; + nameRow.textContent = typeText; + Object.assign(nameRow.style, { ...rowBase, background: LABEL_BG, fontFamily: 'system-ui, sans-serif' }); + label.appendChild(nameRow); + + const detailRow = document.createElement('div'); + detailRow.className = 'impeccable-label-detail'; + detailRow.textContent = detailText; + Object.assign(detailRow.style, { ...rowBase, background: 'rgba(0,0,0,0.85)', fontFamily: 'ui-monospace, monospace', fontWeight: '400' }); + label.appendChild(detailRow); + + outline.appendChild(label); // Start hidden; the IntersectionObserver will show it once the target is rendered outline.style.display = 'none'; el._impeccableOverlay = outline; visibilityObserver.observe(el); + // Drive hover state from the target element so pointer events pass through + el.addEventListener('mouseenter', () => outline.classList.add('impeccable-hover')); + el.addEventListener('mouseleave', () => outline.classList.remove('impeccable-hover')); + document.body.appendChild(outline); overlays.push(outline); }; @@ -1369,8 +1377,17 @@ if (IS_BROWSER) { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', background: LABEL_BG, color: 'white', fontFamily: 'system-ui, sans-serif', fontSize: '13px', - padding: '8px 16px', display: 'flex', flexWrap: 'wrap', - gap: '12px', alignItems: 'center', pointerEvents: 'auto', + display: 'flex', alignItems: 'center', pointerEvents: 'auto', + height: '36px', + }); + + // 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 tag = document.createElement('span'); @@ -1378,17 +1395,49 @@ if (IS_BROWSER) { 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', }); - banner.appendChild(tag); + scrollArea.appendChild(tag); } + banner.appendChild(scrollArea); + + // Controls area (always visible on the right) + 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, { - marginLeft: 'auto', background: 'none', border: 'none', + background: 'none', border: 'none', color: 'white', fontSize: '18px', cursor: 'pointer', padding: '0 4px', }); close.addEventListener('click', () => banner.remove()); - banner.appendChild(close); + controls.appendChild(close); + + banner.appendChild(controls); document.body.appendChild(banner); overlays.push(banner); }; @@ -1455,13 +1504,11 @@ if (IS_BROWSER) { // Merge into existing overlay if this element already has one const existing = el._impeccableOverlay; if (existing) { - const label = existing.querySelector('.impeccable-label'); - if (label) { - const newType = TYPE_LABELS[f.type] || f.type; - label.textContent += ', ' + newType; - } - const tooltip = existing.querySelector('.impeccable-tooltip'); - if (tooltip) tooltip.innerHTML += '
' + (f.detail || ''); + const nameRow = existing.querySelector('.impeccable-label-name'); + const detailRow = existing.querySelector('.impeccable-label-detail'); + const newType = TYPE_LABELS[f.type] || f.type; + if (nameRow) nameRow.textContent += ', ' + newType; + if (detailRow) detailRow.textContent += ' | ' + (f.detail || ''); } else { highlight(el, [f]); }