mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-09 14:55:37 +02:00
feat: sidebar CSS inspector — element picker, box model, rule cascade, quick edit
Extension changes for the visual CSS inspector: - inspector.js: element picker with hover highlight, CSS selector generation, basic mode fallback (getComputedStyle + CSSOM), page alteration handlers - inspector.css: picker overlay styles (blue highlight + tooltip) - background.js: inspector message routing (picker <-> server <-> sidepanel) - sidepanel: Inspector tab with box model viz (gstack palette), matched rules with specificity badges, computed styles, click-to-edit quick edit, Send to Agent/Code button, empty/loading/error states Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,73 @@ async function fetchAndRelayRefs() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ─── Inspector ──────────────────────────────────────────────────
|
||||
|
||||
async function injectInspector(tabId) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId, allFrames: true },
|
||||
files: ['inspector.js'],
|
||||
});
|
||||
await chrome.scripting.insertCSS({
|
||||
target: { tabId, allFrames: true },
|
||||
files: ['inspector.css'],
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: 'Cannot inspect this page (CSP restriction)' };
|
||||
}
|
||||
// Send startPicker to all frames
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tabId, { type: 'startPicker' });
|
||||
} catch {}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function stopInspector(tabId) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tabId, { type: 'stopPicker' });
|
||||
} catch {}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function postInspectorPick(selector, frameInfo, basicData, activeTabUrl) {
|
||||
const base = getBaseUrl();
|
||||
if (!base || !authToken) {
|
||||
// No browse server — return basic data as fallback
|
||||
return { mode: 'basic', selector, basicData, frameInfo };
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${base}/inspector/pick`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({ selector, activeTabUrl, frameInfo }),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
// Server error — fall back to basic mode
|
||||
return { mode: 'basic', selector, basicData, frameInfo };
|
||||
}
|
||||
const data = await resp.json();
|
||||
return { mode: 'cdp', ...data };
|
||||
} catch {
|
||||
// No server or timeout — fall back to basic mode
|
||||
return { mode: 'basic', selector, basicData, frameInfo };
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToContentScript(tabId, message) {
|
||||
try {
|
||||
const response = await chrome.tabs.sendMessage(tabId, message);
|
||||
return response || { ok: true };
|
||||
} catch {
|
||||
return { error: 'Content script not available' };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Message Handling ──────────────────────────────────────────
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
@@ -194,6 +261,69 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inspector: inject + start picker
|
||||
if (msg.type === 'startInspector') {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
const tabId = tabs?.[0]?.id;
|
||||
if (!tabId) { sendResponse({ error: 'No active tab' }); return; }
|
||||
injectInspector(tabId).then(result => sendResponse(result));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inspector: stop picker
|
||||
if (msg.type === 'stopInspector') {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
const tabId = tabs?.[0]?.id;
|
||||
if (!tabId) { sendResponse({ error: 'No active tab' }); return; }
|
||||
stopInspector(tabId).then(result => sendResponse(result));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inspector: element picked by content script
|
||||
if (msg.type === 'elementPicked') {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
const activeTabUrl = tabs?.[0]?.url || null;
|
||||
const frameInfo = msg.frameSrc ? { frameSrc: msg.frameSrc, frameName: msg.frameName } : null;
|
||||
postInspectorPick(msg.selector, frameInfo, msg.basicData, activeTabUrl)
|
||||
.then(result => {
|
||||
// Forward enriched result to sidepanel
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'inspectResult',
|
||||
data: {
|
||||
...result,
|
||||
selector: msg.selector,
|
||||
tagName: msg.tagName,
|
||||
classes: msg.classes,
|
||||
id: msg.id,
|
||||
dimensions: msg.dimensions,
|
||||
basicData: msg.basicData,
|
||||
frameInfo,
|
||||
},
|
||||
}).catch(() => {});
|
||||
sendResponse({ ok: true });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inspector: picker cancelled
|
||||
if (msg.type === 'pickerCancelled') {
|
||||
chrome.runtime.sendMessage({ type: 'pickerCancelled' }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Inspector: route alteration commands to content script
|
||||
if (msg.type === 'applyStyle' || msg.type === 'toggleClass' || msg.type === 'injectCSS' || msg.type === 'resetAll') {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
const tabId = tabs?.[0]?.id;
|
||||
if (!tabId) { sendResponse({ error: 'No active tab' }); return; }
|
||||
sendToContentScript(tabId, msg).then(result => sendResponse(result));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sidebar → browse server command proxy
|
||||
if (msg.type === 'command') {
|
||||
executeCommand(msg.command, msg.args).then(result => sendResponse(result));
|
||||
|
||||
Reference in New Issue
Block a user