From f33a7a38b923f20ea54b9d6538a5ce671e4a6299 Mon Sep 17 00:00:00 2001 From: mdshakib007 Date: Tue, 28 Jul 2026 22:38:49 +0600 Subject: [PATCH] hint of inspector button enable and disable, iframe hover will show the content inside iframe basically the focus --- Apps/Web/aiplugin/content.js | 81 ++++++++- Apps/Web/aiplugin/inspect.js | 327 ++++++++++++++++++++++++++++++++--- 2 files changed, 380 insertions(+), 28 deletions(-) diff --git a/Apps/Web/aiplugin/content.js b/Apps/Web/aiplugin/content.js index ab6d4519..004c445c 100644 --- a/Apps/Web/aiplugin/content.js +++ b/Apps/Web/aiplugin/content.js @@ -99,6 +99,35 @@ function injectInspectorUI() { opacity: 1; } + /* Hint tooltip: explains how to start/stop inspecting */ + .hint { + position: absolute; + padding: 7px 11px; + background: #1f2937; + color: #f9fafb; + font-size: 12px; + line-height: 1.35; + font-weight: 500; + border-radius: 8px; + white-space: nowrap; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); + pointer-events: none; + opacity: 0; + transform: translateY(4px); + transition: opacity 0.18s ease, transform 0.18s ease; + } + .hint.show { + opacity: 1; + transform: translateY(0); + } + .hint b { + color: #c4b5fd; + font-weight: 700; + } + .hint.active b { + color: #fca5a5; + } + @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(220, 38, 38, 0); } @@ -122,17 +151,57 @@ function injectInspectorUI() { // close btn const closeBtn = document.createElement('div'); closeBtn.className = 'close-btn'; - closeBtn.innerHTML = '✕'; + closeBtn.innerHTML = '\u2715'; closeBtn.addEventListener('click', (e) => { e.stopPropagation(); host.remove(); // remove the whole UI }); + // hint tooltip + const hint = document.createElement('div'); + hint.className = 'hint'; + container.appendChild(btn); container.appendChild(closeBtn); + container.appendChild(hint); shadow.appendChild(container); + // hint: keeps the click / right-click affordance discoverable + let hintTimer = null; + + const placeHint = () => { + const rect = host.getBoundingClientRect(); + const onRightHalf = rect.left + rect.width / 2 > window.innerWidth / 2; + hint.style.left = onRightHalf ? 'auto' : '0'; + hint.style.right = onRightHalf ? '0' : 'auto'; + + const nearTop = rect.top < 70; + hint.style.top = nearTop ? 'calc(100% + 10px)' : 'auto'; + hint.style.bottom = nearTop ? 'auto' : 'calc(100% + 10px)'; + }; + + const showHint = (autoHideMs) => { + const active = btn.classList.contains('active'); + hint.classList.toggle('active', active); + hint.innerHTML = active + ? 'Inspecting \u2014 right-click the icon to stop' + : 'Click the icon to start inspecting'; + placeHint(); + hint.classList.add('show'); + + clearTimeout(hintTimer); + hintTimer = autoHideMs ? setTimeout(() => hint.classList.remove('show'), autoHideMs) : null; + }; + + const hideHint = () => { + clearTimeout(hintTimer); + hint.classList.remove('show'); + }; + + container.addEventListener('mouseenter', () => showHint()); + container.addEventListener('mouseleave', hideHint); + // drag let isDragging = false; let hasMoved = false; @@ -206,7 +275,17 @@ function injectInspectorUI() { } else if (request.action === 'deactivate') { btn.classList.remove('active'); btnImg.src = chrome.runtime.getURL('zeuz.png'); + } else { + return; } + // announce the new state, so the way in/out is never a guess + chrome.storage.local.set({ zeuzInspectorUsed: true }); + showHint(4000); + }); + + // first-run nudge, until the user has toggled the inspector at least once + chrome.storage.local.get({ zeuzInspectorUsed: false }, (result) => { + if (!result.zeuzInspectorUsed) showHint(5000); }); } diff --git a/Apps/Web/aiplugin/inspect.js b/Apps/Web/aiplugin/inspect.js index 981407f8..5e4315a2 100644 --- a/Apps/Web/aiplugin/inspect.js +++ b/Apps/Web/aiplugin/inspect.js @@ -49,6 +49,15 @@ setInterval(() => { }, 5000); +// Attribute panel limits: pages can carry huge attribute payloads (inline styles, +// data-* blobs), showing all of it makes the panel cover the page. +const MAX_ATTR_VALUE_CHARS = 60; +const MAX_ATTR_TOTAL_CHARS = 200; + +// How long the pointer must stay inside an iframe before we spotlight it. +const IFRAME_DWELL_MS = 1500; +const FRAME_MESSAGE_TAG = 'zeuz-iframe-spotlight'; + class Inspector { constructor() { this.win = window; @@ -57,12 +66,23 @@ class Inspector { this.draw = this.draw.bind(this); this.getData = this.getData.bind(this); this.setOptions = this.setOptions.bind(this); + this.onFrameMessage = this.onFrameMessage.bind(this); + this.startFrameDwell = this.startFrameDwell.bind(this); + this.endFrameDwell = this.endFrameDwell.bind(this); + this.onFrameMouseOut = this.onFrameMouseOut.bind(this); + this.syncSpotlight = this.syncSpotlight.bind(this); + this.trackPointer = this.trackPointer.bind(this); this.cssNode = 'xpath-css'; this.overlayElement = 'xpath-overlay'; this.modalNode = 'zeuzMyModal'; this.elementNode = 'zeuzMyElement'; + this.spotlightNode = 'zeuz-iframe-spotlight-host'; + this.isTopFrame = window.top === window.self; + this.dwellTimer = null; + this.dwellActive = false; + this.spotlightHost = null; } @@ -294,7 +314,6 @@ class Inspector { } getOptions() { - console.log(navigator.userAgentData.platform); const storage = browserAppData.storage && (browserAppData.storage.local); const promise = storage.get({ inspector: true, @@ -355,9 +374,10 @@ class Inspector { host.id = 'zeuz-attributes-host'; Object.assign(host.style, { position: 'fixed', - top: '10px', - left: '10px', - zIndex: '2147483647', + top: '12px', + left: '12px', + // just under the floating button, so the panel can never bury it + zIndex: '2147483640', pointerEvents: 'none' }); document.body.appendChild(host); @@ -366,26 +386,72 @@ class Inspector { const style = document.createElement('style'); style.textContent = ` .attributes-container { - background: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.82); color: white; padding: 6px 10px; - border-radius: 4px; + border-radius: 6px; font-family: monospace; font-size: 11px; - max-width: 300px; + line-height: 1.45; + max-width: 320px; word-break: break-all; backdrop-filter: blur(4px); border: 1px solid rgba(255, 255, 255, 0.2); } + .tag { + color: #c4b5fd; + font-weight: bold; + } + .attrs:empty, .more:empty { + display: none; + } + .more { + color: rgba(255, 255, 255, 0.55); + font-style: italic; + } `; shadow.appendChild(style); const container = document.createElement('div'); container.className = 'attributes-container'; + + const tag = document.createElement('div'); + tag.className = 'tag'; + const attrs = document.createElement('div'); + attrs.className = 'attrs'; + const more = document.createElement('div'); + more.className = 'more'; + container.append(tag, attrs, more); shadow.appendChild(container); this.attributesHost = host; - this.attributesContainer = container; + this.attributesTag = tag; + this.attributesText = attrs; + this.attributesMore = more; + } + + renderAttributes(node) { + const names = node.getAttributeNames(); + const parts = []; + let total = 0; + + for (const name of names) { + if (total >= MAX_ATTR_TOTAL_CHARS) break; + let value = node.getAttribute(name) || ''; + if (value.length > MAX_ATTR_VALUE_CHARS) { + // escaped, not literal: keeps this file pure ASCII so a page + // charset other than UTF-8 cannot mangle the glyph + value = value.slice(0, MAX_ATTR_VALUE_CHARS) + '\u2026'; + } + const part = `${name}="${value}"`; + parts.push(part); + total += part.length; + } + + const hidden = names.length - parts.length; + this.attributesTag.textContent = `<${node.localName}>`; + this.attributesText.textContent = parts.join(' '); + this.attributesMore.textContent = hidden > 0 ? `+${hidden} more attribute${hidden > 1 ? 's' : ''}` : ''; } createSuccessMessage() { @@ -433,17 +499,33 @@ class Inspector { this.successContainer = container; } - updateAttributePosition(mouseY) { - if (this.attributesHost) { - const isTopHalf = mouseY < window.innerHeight / 2; - if (isTopHalf) { - this.attributesHost.style.top = 'auto'; - this.attributesHost.style.bottom = '10px'; - } else { - this.attributesHost.style.top = '10px'; - this.attributesHost.style.bottom = 'auto'; - } - } + // mouseover alone is not enough: moving across one large element never + // re-fires it, so the panel would stay in a stale corner. + trackPointer(e) { + if (this.positionFrame) return; + const x = e.clientX; + const y = e.clientY; + this.positionFrame = requestAnimationFrame(() => { + this.positionFrame = null; + this.updateAttributePosition(x, y); + }); + } + + // Park the panel in whichever of the 4 corners is opposite the cursor, + // so it never sits under the element being inspected. + updateAttributePosition(mouseX, mouseY) { + if (!this.attributesHost) return; + + const margin = '12px'; + const toRight = mouseX < window.innerWidth / 2; + const toBottom = mouseY < window.innerHeight / 2; + + Object.assign(this.attributesHost.style, { + left: toRight ? 'auto' : margin, + right: toRight ? margin : 'auto', + top: toBottom ? 'auto' : margin, + bottom: toBottom ? margin : 'auto' + }); } copyText(XPath) { @@ -485,15 +567,13 @@ class Inspector { } // position based on mouse location - this.updateAttributePosition(e.clientY); + this.updateAttributePosition(e.clientX, e.clientY); + this.renderAttributes(node); - let elementText = ""; - for (let name of e.target.getAttributeNames()) { - let value = e.target.getAttribute(name); - elementText += `${name}="${value}" `; + // pointer is back on the host page, so any iframe spotlight is stale + if (this.spotlightHost && node !== this.spotlightFrame) { + this.hideSpotlight(); } - - this.attributesContainer.textContent = elementText.trim(); } activate() { @@ -508,11 +588,21 @@ class Inspector { // add listeners document.addEventListener('click', this.getData, true); this.options.inspector && (document.addEventListener('mouseover', this.draw)); + this.options.inspector && (document.addEventListener('mousemove', this.trackPointer, { passive: true })); + + // iframe spotlight: children report dwell, every frame relays it upwards + window.addEventListener('message', this.onFrameMessage); + if (!this.isTopFrame) { + document.addEventListener('mouseover', this.startFrameDwell); + document.addEventListener('mouseout', this.onFrameMouseOut); + } } deactivate() { // remove overlay this.removeOverlay(); + this.endFrameDwell(); + this.hideSpotlight(); let Remove = [ this.cssNode, @@ -529,14 +619,197 @@ class Inspector { // remove listeners document.removeEventListener('click', this.getData, true); this.options && this.options.inspector && (document.removeEventListener('mouseover', this.draw)); + document.removeEventListener('mousemove', this.trackPointer); + cancelAnimationFrame(this.positionFrame); + this.positionFrame = null; + window.removeEventListener('message', this.onFrameMessage); + document.removeEventListener('mouseover', this.startFrameDwell); + document.removeEventListener('mouseout', this.onFrameMouseOut); // reset this.attributesHost = null; - this.attributesContainer = null; + this.attributesTag = null; + this.attributesText = null; + this.attributesMore = null; this.successHost = null; this.successContainer = null; } + /* ---------------- iframe spotlight ---------------- */ + + // Inside a frame: once the pointer has stayed here long enough, ask the top + // frame to dim everything but us. + startFrameDwell() { + if (this.dwellTimer || this.dwellActive) return; + this.dwellTimer = setTimeout(() => { + this.dwellTimer = null; + this.dwellActive = true; + window.parent.postMessage({ + tag: FRAME_MESSAGE_TAG, + type: 'focus', + rect: null, + label: (document.title || location.hostname || '').slice(0, 60) + }, '*'); + }, IFRAME_DWELL_MS); + } + + // relatedTarget is null only when the pointer crosses out of this document + onFrameMouseOut(e) { + if (!e.relatedTarget) this.endFrameDwell(); + } + + endFrameDwell() { + clearTimeout(this.dwellTimer); + this.dwellTimer = null; + if (!this.dwellActive) return; + this.dwellActive = false; + window.parent.postMessage({ tag: FRAME_MESSAGE_TAG, type: 'blur' }, '*'); + } + + onFrameMessage(e) { + const data = e.data; + if (!data || data.tag !== FRAME_MESSAGE_TAG) return; + + if (data.type === 'blur') { + if (this.attributesHost) this.attributesHost.style.visibility = 'visible'; + this.isTopFrame ? this.hideSpotlight() : window.parent.postMessage(data, '*'); + return; + } + + const frame = this.findFrameElement(e.source); + if (!frame) return; + + // The pointer is inside the frame now and the frame highlights its own + // element. Our highlight is still sitting on the