Files
gh-anthemflynn-ccmp-plugins…/skills/website-debug/scripts/browser-pick.js
2025-11-29 17:55:23 +08:00

252 lines
9.2 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* browser-pick.js - Interactive element picker for DOM selection
*
* IMPORTANT: Use this when the user wants to select specific DOM elements.
* The user can click elements, multi-select with Cmd/Ctrl+Click, and press Enter when done.
* Returns CSS selectors and element details.
*
* Usage:
* ./browser-pick.js "Click on the broken element"
* ./browser-pick.js "Select the buttons to style"
*/
import puppeteer from "puppeteer-core";
const message = process.argv.slice(2).filter(a => !a.startsWith("--")).join(" ") || "Select an element";
const port = process.argv.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
browser-pick.js - Interactive element picker
Usage:
./browser-pick.js "<message>"
The picker lets users visually select elements:
- Click: Select single element
- Cmd/Ctrl+Click: Add to multi-selection
- Enter: Finish multi-selection
- Escape: Cancel
Returns:
- CSS selector(s) for selected elements
- Tag name, ID, classes
- Text content preview
- Computed styles summary
- Parent chain for context
Examples:
./browser-pick.js "Click on the element that looks wrong"
./browser-pick.js "Select all the buttons to update"
./browser-pick.js "Which element should I fix?"
`);
process.exit(0);
}
try {
const browser = await puppeteer.connect({
browserURL: `http://localhost:${port}`,
defaultViewport: null
});
const pages = await browser.pages();
const page = pages[pages.length - 1];
if (!page) {
console.error("✗ No active tab found");
process.exit(1);
}
console.log(`Picker active: ${message}`);
console.log(" Click to select, Cmd/Ctrl+Click for multi-select, Enter to finish, Escape to cancel\n");
// Inject picker helper
await page.evaluate(() => {
if (!window.__webdebugPick) {
window.__webdebugPick = async (message) => {
return new Promise((resolve) => {
const selections = [];
const selectedElements = new Set();
// Create overlay
const overlay = document.createElement("div");
overlay.id = "__webdebug-picker-overlay";
overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
// Highlight element
const highlight = document.createElement("div");
highlight.style.cssText = "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.15);transition:all 0.05s;pointer-events:none";
overlay.appendChild(highlight);
// Banner
const banner = document.createElement("div");
banner.style.cssText = "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px system-ui,sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647;max-width:80vw;text-align:center";
const updateBanner = () => {
banner.innerHTML = `<strong>${message}</strong><br><span style="opacity:0.8;font-size:12px">${selections.length} selected · Cmd/Ctrl+Click to add · Enter to finish · Escape to cancel</span>`;
};
updateBanner();
document.body.appendChild(banner);
document.body.appendChild(overlay);
// Build unique selector
const buildSelector = (el) => {
if (el.id) return `#${el.id}`;
let selector = el.tagName.toLowerCase();
if (el.className && typeof el.className === 'string') {
const classes = el.className.trim().split(/\s+/).filter(c => c && !c.startsWith('__'));
if (classes.length) selector += '.' + classes.slice(0, 3).join('.');
}
// Add nth-child if needed for uniqueness
const parent = el.parentElement;
if (parent) {
const siblings = [...parent.children].filter(c => c.tagName === el.tagName);
if (siblings.length > 1) {
const index = siblings.indexOf(el) + 1;
selector += `:nth-child(${index})`;
}
}
return selector;
};
// Get full selector path
const getFullSelector = (el) => {
const parts = [];
let current = el;
while (current && current !== document.body && parts.length < 4) {
parts.unshift(buildSelector(current));
current = current.parentElement;
}
return parts.join(' > ');
};
// Build element info
const buildElementInfo = (el) => {
const rect = el.getBoundingClientRect();
const styles = getComputedStyle(el);
return {
selector: getFullSelector(el),
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: el.className?.toString().trim() || null,
text: el.textContent?.trim().slice(0, 100) || null,
dimensions: `${Math.round(rect.width)}×${Math.round(rect.height)}`,
position: `${Math.round(rect.left)},${Math.round(rect.top)}`,
display: styles.display,
visibility: styles.visibility,
opacity: styles.opacity,
zIndex: styles.zIndex,
overflow: styles.overflow
};
};
const cleanup = () => {
document.removeEventListener("mousemove", onMove, true);
document.removeEventListener("click", onClick, true);
document.removeEventListener("keydown", onKey, true);
overlay.remove();
banner.remove();
selectedElements.forEach(el => {
el.style.outline = el.__originalOutline || "";
delete el.__originalOutline;
});
};
const onMove = (e) => {
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || overlay.contains(el) || banner.contains(el)) {
highlight.style.display = "none";
return;
}
const r = el.getBoundingClientRect();
highlight.style.display = "block";
highlight.style.top = r.top + "px";
highlight.style.left = r.left + "px";
highlight.style.width = r.width + "px";
highlight.style.height = r.height + "px";
};
const onClick = (e) => {
if (banner.contains(e.target)) return;
e.preventDefault();
e.stopPropagation();
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || overlay.contains(el) || banner.contains(el)) return;
if (e.metaKey || e.ctrlKey) {
// Multi-select mode
if (!selectedElements.has(el)) {
selectedElements.add(el);
el.__originalOutline = el.style.outline;
el.style.outline = "3px solid #10b981";
selections.push(buildElementInfo(el));
updateBanner();
}
} else {
// Single select - finish
cleanup();
if (selections.length > 0) {
resolve(selections);
} else {
resolve([buildElementInfo(el)]);
}
}
};
const onKey = (e) => {
if (e.key === "Escape") {
e.preventDefault();
cleanup();
resolve(null);
} else if (e.key === "Enter" && selections.length > 0) {
e.preventDefault();
cleanup();
resolve(selections);
}
};
document.addEventListener("mousemove", onMove, true);
document.addEventListener("click", onClick, true);
document.addEventListener("keydown", onKey, true);
});
};
}
});
// Run picker
const result = await page.evaluate((msg) => window.__webdebugPick(msg), message);
if (result === null) {
console.log("Picker cancelled");
} else if (Array.isArray(result)) {
console.log(`Selected ${result.length} element(s):\n`);
result.forEach((info, i) => {
if (i > 0) console.log("---");
console.log(`Selector: ${info.selector}`);
console.log(`Tag: ${info.tag}${info.id ? ` #${info.id}` : ""}${info.classes ? ` .${info.classes.split(" ").join(".")}` : ""}`);
console.log(`Size: ${info.dimensions} at (${info.position})`);
console.log(`Display: ${info.display}, Visibility: ${info.visibility}, Opacity: ${info.opacity}`);
if (info.zIndex !== "auto") console.log(`Z-Index: ${info.zIndex}`);
if (info.text) console.log(`Text: "${info.text.slice(0, 50)}${info.text.length > 50 ? "..." : ""}"`);
});
}
await browser.disconnect();
} catch (e) {
if (e.message?.includes("ECONNREFUSED")) {
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
} else {
console.error(`✗ Picker failed: ${e.message}`);
}
process.exit(1);
}