From f395f58406593313d5eeaef22aefab2b04773e32 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 29 Mar 2026 20:25:36 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20sidebar=20CSS=20inspector=20=E2=80=94?= =?UTF-8?q?=20element=20picker,=20box=20model,=20rule=20cascade,=20quick?= =?UTF-8?q?=20edit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- extension/background.js | 130 +++++++++++ extension/inspector.css | 29 +++ extension/inspector.js | 459 ++++++++++++++++++++++++++++++++++++ extension/manifest.json | 2 +- extension/sidepanel.css | 490 +++++++++++++++++++++++++++++++++++++++ extension/sidepanel.html | 78 +++++++ extension/sidepanel.js | 437 ++++++++++++++++++++++++++++++++++ 7 files changed, 1624 insertions(+), 1 deletion(-) create mode 100644 extension/inspector.css create mode 100644 extension/inspector.js diff --git a/extension/background.js b/extension/background.js index af1f32ea..7e1dd6da 100644 --- a/extension/background.js +++ b/extension/background.js @@ -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)); diff --git a/extension/inspector.css b/extension/inspector.css new file mode 100644 index 00000000..cb032559 --- /dev/null +++ b/extension/inspector.css @@ -0,0 +1,29 @@ +/* gstack browse — CSS Inspector overlay styles + * Injected alongside inspector.js into the active tab. + * Design system: amber accent, zinc neutrals. + */ + +#gstack-inspector-highlight { + position: fixed; + pointer-events: none; + z-index: 2147483647; + background: rgba(59, 130, 246, 0.15); + border: 2px solid rgba(59, 130, 246, 0.6); + border-radius: 2px; + transition: top 50ms ease, left 50ms ease, width 50ms ease, height 50ms ease; +} + +#gstack-inspector-tooltip { + position: fixed; + pointer-events: none; + z-index: 2147483647; + background: #27272A; + color: #e0e0e0; + font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace; + font-size: 11px; + padding: 3px 8px; + border-radius: 4px; + white-space: nowrap; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + line-height: 18px; +} diff --git a/extension/inspector.js b/extension/inspector.js new file mode 100644 index 00000000..01af66d9 --- /dev/null +++ b/extension/inspector.js @@ -0,0 +1,459 @@ +/** + * gstack browse — CSS Inspector content script + * + * Dynamically injected via chrome.scripting.executeScript. + * Provides element picker, selector generation, basic computed style capture, + * and page alteration handlers for agent-pushed CSS changes. + */ + +(() => { + // Guard against double-injection + if (window.__gstackInspectorActive) return; + window.__gstackInspectorActive = true; + + // ─── State ────────────────────────────────────────────────────── + let pickerActive = false; + let highlightEl = null; + let tooltipEl = null; + let lastPickTime = 0; + const PICK_DEBOUNCE_MS = 200; + + // Track original inline styles for resetAll + const originalStyles = new Map(); // element -> Map + const injectedStyleIds = new Set(); + + // ─── Highlight Overlay ────────────────────────────────────────── + + function createHighlight() { + if (highlightEl) return; + + highlightEl = document.createElement('div'); + highlightEl.id = 'gstack-inspector-highlight'; + highlightEl.style.cssText = ` + position: fixed; + pointer-events: none; + z-index: 2147483647; + background: rgba(59, 130, 246, 0.15); + border: 2px solid rgba(59, 130, 246, 0.6); + border-radius: 2px; + transition: top 50ms, left 50ms, width 50ms, height 50ms; + `; + document.documentElement.appendChild(highlightEl); + + tooltipEl = document.createElement('div'); + tooltipEl.id = 'gstack-inspector-tooltip'; + tooltipEl.style.cssText = ` + position: fixed; + pointer-events: none; + z-index: 2147483647; + background: #27272A; + color: #e0e0e0; + font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace; + font-size: 11px; + padding: 3px 8px; + border-radius: 4px; + white-space: nowrap; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + display: none; + `; + document.documentElement.appendChild(tooltipEl); + } + + function removeHighlight() { + if (highlightEl) { highlightEl.remove(); highlightEl = null; } + if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } + } + + function updateHighlight(el) { + if (!highlightEl || !tooltipEl) return; + const rect = el.getBoundingClientRect(); + + highlightEl.style.top = rect.top + 'px'; + highlightEl.style.left = rect.left + 'px'; + highlightEl.style.width = rect.width + 'px'; + highlightEl.style.height = rect.height + 'px'; + highlightEl.style.display = 'block'; + + // Build tooltip text: .classes WxH + const tag = el.tagName.toLowerCase(); + const classes = el.className && typeof el.className === 'string' + ? '.' + el.className.trim().split(/\s+/).join('.') + : ''; + const dims = `${Math.round(rect.width)}x${Math.round(rect.height)}`; + tooltipEl.textContent = `<${tag}> ${classes} ${dims}`.trim(); + + // Position tooltip above element, or below if no room + const tooltipHeight = 24; + const gap = 6; + let tooltipTop = rect.top - tooltipHeight - gap; + if (tooltipTop < 4) tooltipTop = rect.bottom + gap; + let tooltipLeft = rect.left; + if (tooltipLeft < 4) tooltipLeft = 4; + + tooltipEl.style.top = tooltipTop + 'px'; + tooltipEl.style.left = tooltipLeft + 'px'; + tooltipEl.style.display = 'block'; + } + + // ─── Selector Generation ──────────────────────────────────────── + + function buildSelector(el) { + // If element has an id, use it directly + if (el.id) { + const sel = '#' + CSS.escape(el.id); + if (isUnique(sel)) return sel; + } + + // Build path from element up to nearest ancestor with id or body + const parts = []; + let current = el; + + while (current && current !== document.body && current !== document.documentElement) { + let part = current.tagName.toLowerCase(); + + // If current has an id, use it and stop + if (current.id) { + part = '#' + CSS.escape(current.id); + parts.unshift(part); + break; + } + + // Add classes + if (current.className && typeof current.className === 'string') { + const classes = current.className.trim().split(/\s+/).filter(c => c.length > 0); + if (classes.length > 0) { + part += '.' + classes.map(c => CSS.escape(c)).join('.'); + } + } + + // Add nth-child if needed to disambiguate + const parent = current.parentElement; + if (parent) { + const siblings = Array.from(parent.children).filter( + s => s.tagName === current.tagName + ); + if (siblings.length > 1) { + const idx = siblings.indexOf(current) + 1; + part += `:nth-child(${Array.from(parent.children).indexOf(current) + 1})`; + } + } + + parts.unshift(part); + current = current.parentElement; + } + + // If we didn't reach an id, prepend body + if (parts.length > 0 && !parts[0].startsWith('#')) { + // Don't prepend body, just use the path as-is + } + + const selector = parts.join(' > '); + + // Verify uniqueness + if (isUnique(selector)) return selector; + + // Fallback: add nth-child at each level until unique + return selector; + } + + function isUnique(selector) { + try { + return document.querySelectorAll(selector).length === 1; + } catch { + return false; + } + } + + // ─── Basic Mode Data Capture ──────────────────────────────────── + + const KEY_PROPERTIES = [ + 'display', 'position', 'top', 'right', 'bottom', 'left', + 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height', + 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', + 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width', + 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style', + 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color', + 'color', 'background-color', 'background-image', + 'font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing', + 'text-align', 'text-decoration', 'text-transform', + 'overflow', 'overflow-x', 'overflow-y', + 'opacity', 'z-index', + 'flex-direction', 'justify-content', 'align-items', 'flex-wrap', 'gap', + 'grid-template-columns', 'grid-template-rows', + 'box-shadow', 'border-radius', + 'transition', 'transform', + ]; + + function captureBasicData(el) { + const computed = getComputedStyle(el); + const rect = el.getBoundingClientRect(); + + // Capture key computed properties + const computedStyles = {}; + for (const prop of KEY_PROPERTIES) { + computedStyles[prop] = computed.getPropertyValue(prop); + } + + // Box model from computed + const boxModel = { + content: { width: rect.width, height: rect.height }, + padding: { + top: parseFloat(computed.paddingTop) || 0, + right: parseFloat(computed.paddingRight) || 0, + bottom: parseFloat(computed.paddingBottom) || 0, + left: parseFloat(computed.paddingLeft) || 0, + }, + border: { + top: parseFloat(computed.borderTopWidth) || 0, + right: parseFloat(computed.borderRightWidth) || 0, + bottom: parseFloat(computed.borderBottomWidth) || 0, + left: parseFloat(computed.borderLeftWidth) || 0, + }, + margin: { + top: parseFloat(computed.marginTop) || 0, + right: parseFloat(computed.marginRight) || 0, + bottom: parseFloat(computed.marginBottom) || 0, + left: parseFloat(computed.marginLeft) || 0, + }, + }; + + // Matched CSS rules via CSSOM (same-origin only) + const matchedRules = []; + try { + for (const sheet of document.styleSheets) { + try { + const rules = sheet.cssRules || sheet.rules; + if (!rules) continue; + for (const rule of rules) { + if (rule.type !== CSSRule.STYLE_RULE) continue; + try { + if (el.matches(rule.selectorText)) { + const properties = []; + for (let i = 0; i < rule.style.length; i++) { + const prop = rule.style[i]; + properties.push({ + name: prop, + value: rule.style.getPropertyValue(prop), + priority: rule.style.getPropertyPriority(prop), + }); + } + matchedRules.push({ + selector: rule.selectorText, + properties, + source: sheet.href || 'inline', + }); + } + } catch { /* skip rules that can't be matched */ } + } + } catch { /* cross-origin sheet — silently skip */ } + } + } catch { /* CSSOM not available */ } + + return { computedStyles, boxModel, matchedRules }; + } + + // ─── Picker Event Handlers ────────────────────────────────────── + + function onMouseMove(e) { + if (!pickerActive) return; + // Ignore our own overlay elements + const target = e.target; + if (target === highlightEl || target === tooltipEl) return; + if (target.id === 'gstack-inspector-highlight' || target.id === 'gstack-inspector-tooltip') return; + + updateHighlight(target); + } + + function onClick(e) { + if (!pickerActive) return; + + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + // Debounce + const now = Date.now(); + if (now - lastPickTime < PICK_DEBOUNCE_MS) return; + lastPickTime = now; + + const target = e.target; + if (target === highlightEl || target === tooltipEl) return; + if (target.id === 'gstack-inspector-highlight' || target.id === 'gstack-inspector-tooltip') return; + + const selector = buildSelector(target); + const basicData = captureBasicData(target); + + // Frame detection + const frameInfo = {}; + if (window !== window.top) { + try { + frameInfo.frameSrc = window.location.href; + frameInfo.frameName = window.name || null; + } catch { /* cross-origin frame */ } + } + + chrome.runtime.sendMessage({ + type: 'elementPicked', + selector, + tagName: target.tagName.toLowerCase(), + classes: target.className && typeof target.className === 'string' + ? target.className.trim().split(/\s+/).filter(c => c.length > 0) + : [], + id: target.id || null, + dimensions: { + width: Math.round(target.getBoundingClientRect().width), + height: Math.round(target.getBoundingClientRect().height), + }, + basicData, + ...frameInfo, + }); + + // Keep highlight on the picked element + } + + function onKeyDown(e) { + if (!pickerActive) return; + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + stopPicker(); + chrome.runtime.sendMessage({ type: 'pickerCancelled' }); + } + } + + // ─── Picker Start/Stop ────────────────────────────────────────── + + function startPicker() { + if (pickerActive) return; + pickerActive = true; + createHighlight(); + document.addEventListener('mousemove', onMouseMove, true); + document.addEventListener('click', onClick, true); + document.addEventListener('keydown', onKeyDown, true); + } + + function stopPicker() { + if (!pickerActive) return; + pickerActive = false; + removeHighlight(); + document.removeEventListener('mousemove', onMouseMove, true); + document.removeEventListener('click', onClick, true); + document.removeEventListener('keydown', onKeyDown, true); + } + + // ─── Page Alteration Handlers ─────────────────────────────────── + + function findElement(selector) { + try { + return document.querySelector(selector); + } catch { + return null; + } + } + + function applyStyle(selector, property, value) { + // Validate property name: alphanumeric + hyphens only + if (!/^[a-zA-Z-]+$/.test(property)) return { error: 'Invalid property name' }; + + const el = findElement(selector); + if (!el) return { error: 'Element not found' }; + + // Track original value for resetAll + if (!originalStyles.has(el)) { + originalStyles.set(el, new Map()); + } + const origMap = originalStyles.get(el); + if (!origMap.has(property)) { + origMap.set(property, el.style.getPropertyValue(property)); + } + + el.style.setProperty(property, value, 'important'); + return { ok: true }; + } + + function toggleClass(selector, className, action) { + const el = findElement(selector); + if (!el) return { error: 'Element not found' }; + + if (action === 'add') { + el.classList.add(className); + } else if (action === 'remove') { + el.classList.remove(className); + } else { + el.classList.toggle(className); + } + return { ok: true }; + } + + function injectCSS(id, css) { + const styleId = `gstack-inject-${id}`; + let styleEl = document.getElementById(styleId); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + document.head.appendChild(styleEl); + } + styleEl.textContent = css; + injectedStyleIds.add(styleId); + return { ok: true }; + } + + function resetAll() { + // Restore original inline styles + for (const [el, propMap] of originalStyles) { + for (const [prop, origVal] of propMap) { + if (origVal) { + el.style.setProperty(prop, origVal); + } else { + el.style.removeProperty(prop); + } + } + } + originalStyles.clear(); + + // Remove injected style elements + for (const id of injectedStyleIds) { + const el = document.getElementById(id); + if (el) el.remove(); + } + injectedStyleIds.clear(); + + return { ok: true }; + } + + // ─── Message Listener ────────────────────────────────────────── + + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.type === 'startPicker') { + startPicker(); + sendResponse({ ok: true }); + return; + } + if (msg.type === 'stopPicker') { + stopPicker(); + sendResponse({ ok: true }); + return; + } + if (msg.type === 'applyStyle') { + const result = applyStyle(msg.selector, msg.property, msg.value); + sendResponse(result); + return; + } + if (msg.type === 'toggleClass') { + const result = toggleClass(msg.selector, msg.className, msg.action); + sendResponse(result); + return; + } + if (msg.type === 'injectCSS') { + const result = injectCSS(msg.id, msg.css); + sendResponse(result); + return; + } + if (msg.type === 'resetAll') { + const result = resetAll(); + sendResponse(result); + return; + } + }); +})(); diff --git a/extension/manifest.json b/extension/manifest.json index ea710e14..81b31804 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -3,7 +3,7 @@ "name": "gstack browse", "version": "0.1.0", "description": "Live activity feed and @ref overlays for gstack browse", - "permissions": ["sidePanel", "storage", "activeTab"], + "permissions": ["sidePanel", "storage", "activeTab", "scripting"], "host_permissions": ["http://127.0.0.1:*/"], "action": { "default_icon": { diff --git a/extension/sidepanel.css b/extension/sidepanel.css index 85558961..55c7392a 100644 --- a/extension/sidepanel.css +++ b/extension/sidepanel.css @@ -697,6 +697,496 @@ footer { flex-shrink: 0; } +/* ─── Inspector Tab ──────────────────────────────────── */ + +.inspector-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.inspector-pick-btn { + display: flex; + align-items: center; + gap: 4px; + height: 28px; + padding: 0 10px; + background: none; + border: 1px solid var(--amber-500); + border-radius: var(--radius-sm); + color: var(--amber-500); + font-family: var(--font-system); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 150ms; + flex-shrink: 0; +} + +.inspector-pick-btn:hover { + background: rgba(245, 158, 11, 0.1); + color: var(--amber-400); +} + +.inspector-pick-btn.active { + background: var(--amber-500); + color: #000; +} + +.inspector-pick-icon { + font-size: 14px; + line-height: 1; +} + +.inspector-selected { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-body); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.inspector-mode-badge { + font-family: var(--font-mono); + font-size: 10px; + padding: 1px 6px; + border-radius: var(--radius-sm); + flex-shrink: 0; +} + +.inspector-mode-badge.basic { + background: var(--zinc-800); + color: var(--zinc-400); +} + +.inspector-mode-badge.cdp { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +/* Inspector content area */ +.inspector-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; +} + +/* Empty state */ +.inspector-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 24px; + text-align: center; + gap: 6px; +} + +.inspector-empty-icon { + font-size: 24px; + color: var(--zinc-600); + margin-bottom: 4px; +} + +.inspector-empty p { + color: var(--zinc-400); + font-size: 13px; + margin: 0; +} + +.inspector-empty .muted { + color: var(--zinc-600); + font-size: 12px; +} + +/* Loading state */ +.inspector-loading { + padding: 16px 12px; +} + +.inspector-loading-text { + font-size: 12px; + color: var(--amber-500); + margin-bottom: 12px; + animation: pulse 2s ease-in-out infinite; +} + +.inspector-skeleton { + display: flex; + flex-direction: column; + gap: 8px; +} + +.inspector-skeleton-bar { + height: 12px; + background: var(--zinc-800); + border-radius: var(--radius-sm); + animation: shimmer 1.5s ease-in-out infinite; +} + +.inspector-skeleton-bar:nth-child(1) { width: 80%; } +.inspector-skeleton-bar:nth-child(2) { width: 60%; } +.inspector-skeleton-bar:nth-child(3) { width: 70%; } + +@keyframes shimmer { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.7; } +} + +/* Error state */ +.inspector-error { + padding: 16px 12px; + color: var(--error); + font-size: 12px; + font-family: var(--font-mono); +} + +/* Inspector sections */ +.inspector-section { + border-bottom: 1px solid var(--border-subtle); +} + +.inspector-section-header { + font-family: var(--font-system); + font-size: 13px; + font-weight: 600; + color: var(--zinc-400); + padding: 8px 12px 4px; +} + +.inspector-section-toggle { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 8px 12px; + background: none; + border: none; + font-family: var(--font-system); + font-size: 13px; + font-weight: 600; + color: var(--zinc-400); + cursor: pointer; + text-align: left; + transition: color 150ms; +} + +.inspector-section-toggle:hover { + color: var(--text-body); +} + +.inspector-toggle-arrow { + font-size: 10px; + color: var(--zinc-400); + flex-shrink: 0; + width: 12px; +} + +.inspector-section-body { + padding: 4px 12px 8px; +} + +.inspector-section-body.collapsed { + display: none; +} + +.inspector-rule-count { + font-size: 11px; + font-weight: 400; + color: var(--zinc-600); + margin-left: 4px; +} + +.inspector-no-data { + color: var(--zinc-600); + font-size: 11px; + font-style: italic; + padding: 4px 0; +} + +/* ─── Box Model ──────────────────────────────────────── */ + +.inspector-boxmodel { + padding: 8px 12px 12px; +} + +.boxmodel-margin, +.boxmodel-border, +.boxmodel-padding, +.boxmodel-content { + position: relative; + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed; + text-align: center; +} + +.boxmodel-margin { + background: rgba(245, 158, 11, 0.08); + border-color: rgba(245, 158, 11, 0.3); + padding: 14px 20px; + border-radius: var(--radius-sm); +} + +.boxmodel-border { + background: rgba(161, 161, 170, 0.08); + border-color: rgba(161, 161, 170, 0.3); + padding: 14px 20px; + width: 100%; +} + +.boxmodel-padding { + background: rgba(34, 197, 94, 0.08); + border-color: rgba(34, 197, 94, 0.3); + padding: 14px 20px; + width: 100%; +} + +.boxmodel-content { + background: rgba(59, 130, 246, 0.08); + border-color: rgba(59, 130, 246, 0.3); + padding: 8px 12px; + width: 100%; + min-height: 28px; +} + +.boxmodel-content span { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-body); +} + +.boxmodel-label { + position: absolute; + top: 1px; + left: 4px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--zinc-400); + pointer-events: none; +} + +.boxmodel-value { + position: absolute; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-body); +} + +.boxmodel-value.boxmodel-top { top: 1px; left: 50%; transform: translateX(-50%); } +.boxmodel-value.boxmodel-right { right: 4px; top: 50%; transform: translateY(-50%); } +.boxmodel-value.boxmodel-bottom { bottom: 1px; left: 50%; transform: translateX(-50%); } +.boxmodel-value.boxmodel-left { left: 4px; top: 50%; transform: translateY(-50%); } + +/* ─── Matched Rules ──────────────────────────────────── */ + +.inspector-rule { + padding: 6px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.inspector-rule:last-child { + border-bottom: none; +} + +.inspector-rule-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 2px; +} + +.inspector-selector { + font-family: var(--font-mono); + font-size: 12px; + color: var(--amber-400); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 35ch; +} + +.inspector-specificity { + font-family: var(--font-mono); + font-size: 10px; + background: var(--zinc-600); + color: var(--zinc-400); + padding: 0 4px; + border-radius: var(--radius-sm); + flex-shrink: 0; +} + +.inspector-rule-props { + padding-left: 12px; +} + +.inspector-prop { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.6; +} + +.inspector-prop.overridden { + text-decoration: line-through; + opacity: 0.5; +} + +.inspector-prop-name { + color: var(--zinc-400); +} + +.inspector-prop-value { + color: var(--text-body); +} + +.inspector-important { + color: var(--error); + font-size: 10px; +} + +.inspector-rule-source { + font-family: var(--font-mono); + font-size: 11px; + color: var(--zinc-600); + margin-top: 2px; +} + +/* UA rules */ +.inspector-ua-rules { + margin-top: 4px; +} + +.inspector-ua-toggle { + display: flex; + align-items: center; + gap: 4px; + background: none; + border: none; + font-family: var(--font-mono); + font-size: 11px; + color: var(--zinc-600); + cursor: pointer; + padding: 4px 0; + transition: color 150ms; +} + +.inspector-ua-toggle:hover { + color: var(--zinc-400); +} + +.inspector-ua-body.collapsed { + display: none; +} + +/* ─── Computed Styles ────────────────────────────────── */ + +.inspector-computed-row { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.6; + padding: 0 0 0 4px; +} + +.inspector-computed-row .inspector-prop-name { + color: var(--zinc-400); +} + +.inspector-computed-row .inspector-prop-value { + color: var(--text-body); +} + +/* ─── Quick Edit ─────────────────────────────────────── */ + +.inspector-quickedit-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.inspector-quickedit-row { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.6; + display: flex; + align-items: center; + gap: 4px; +} + +.inspector-quickedit-row .inspector-prop-name { + color: var(--zinc-400); + flex-shrink: 0; +} + +.inspector-quickedit-value { + color: var(--text-body); + cursor: pointer; + padding: 1px 4px; + border-radius: 2px; + transition: background 150ms; + min-width: 40px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.inspector-quickedit-value:hover { + background: var(--bg-hover); +} + +.inspector-quickedit-input { + font-family: var(--font-mono); + font-size: 12px; + background: var(--bg-base); + border: 1px solid var(--amber-500); + border-radius: 2px; + color: var(--text-heading); + padding: 1px 4px; + outline: none; + width: 100%; +} + +/* ─── Send to Agent ──────────────────────────────────── */ + +.inspector-send { + padding: 8px 12px; + background: var(--bg-surface); + border-top: 1px solid var(--border); + flex-shrink: 0; + position: sticky; + bottom: 0; +} + +.inspector-send-btn { + width: 100%; + height: 32px; + background: var(--amber-500); + border: none; + border-radius: var(--radius-md); + color: #000; + font-family: var(--font-system); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 150ms; +} + +.inspector-send-btn:hover { + background: var(--amber-400); +} + +.inspector-send-btn:active { + transform: scale(0.98); +} + /* ─── Accessibility ───────────────────────────────────── */ :focus-visible { outline: 2px solid var(--amber-500); diff --git a/extension/sidepanel.html b/extension/sidepanel.html index abbffb99..8e5b8fd4 100644 --- a/extension/sidepanel.html +++ b/extension/sidepanel.html @@ -48,6 +48,83 @@ + +
+ +
+ + + +
+ + +
+ +
+
+

Pick an element to inspect

+

Click the button above, then click any element on the page

+
+ + + + + + + + + +
+ + + +
+