Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Browser DevTools is excellent at inspecting one element, but finding why two sim
- **Difference-first table**: changed properties are shown by default, with a `Show all` toggle for the full computed style list.
- **Property search**: filter CSS properties by name while reviewing the comparison.
- **One-click copy**: click a value cell to copy `property: value;`.
- **Native page hover highlight**: hover a source/target DOM line in the diff table to highlight that DOM node in the inspected page, and remove the highlight immediately when the pointer leaves.
- **Cross-window/tab sync**: selected element data is broadcast to other open windows/tabs, which helps compare page states side by side.
- **Localized UI**: includes English and Simplified Chinese browser i18n messages.

Expand All @@ -31,15 +32,27 @@ Browser DevTools is excellent at inspecting one element, but finding why two sim

Download the packaged zip from [Releases](https://github.com/jevin98/css-diff-devtools/releases), then install or load it manually from your browser's extensions page.

## Chrome Debugger Permission

CSS-Diff requests Chrome's `debugger` permission so the diff table can use the Chrome DevTools Protocol `Overlay.highlightNode` and `Overlay.hideHighlight` commands. This is what makes DOM line hover highlighting appear and disappear immediately in the inspected page.

Chrome treats this permission as sensitive:

- Chrome may show a permission warning when the extension is installed or updated.
- Chrome may show a browser-level notice that CSS-Diff is debugging the current page while the extension is attached.
- CSS-Diff uses this permission only for temporary DOM hover highlighting from the DevTools sidebar.
- If the permission is denied, CSS comparison still works, but native hover highlighting in the inspected page is unavailable.

## Usage

1. Open the page you want to inspect.
2. Open DevTools and switch to the Elements panel.
3. Open the `CSS-Diff` sidebar.
4. Select the first DOM element, then select the second DOM element.
5. Review the highlighted differences, search for a property, or enable `Show all`.
6. Click a left/right value cell to copy the CSS declaration.
7. Click `Clear Selection` to start another comparison.
6. Hover a source/target DOM line in the diff table to highlight that DOM node in the inspected page.
7. Click a left/right value cell to copy the CSS declaration.
8. Click `Clear Selection` to start another comparison.

## Local Development

Expand Down
17 changes: 15 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- **差异优先展示**:默认只展示有差异的属性,也可以通过 `显示全部` 查看完整 computed style 列表。
- **属性搜索**:按 CSS 属性名过滤对比结果。
- **一键复制**:点击任意左右侧样式值单元格,即可复制 `property: value;`。
- **原生页面悬停高亮**:在差异表格中悬停源/目标 DOM 行时,会在被检查页面中高亮对应 DOM 节点,鼠标移出后立即取消高亮。
- **跨窗口/标签页同步**:选中的元素数据会同步广播到其他已打开的窗口/标签页,便于并排比较不同页面状态。
- **本地化界面**:内置英文和简体中文浏览器 i18n 文案。

Expand All @@ -31,15 +32,27 @@

从 [Releases](https://github.com/jevin98/css-diff-devtools/releases) 下载打包后的 zip 文件,然后在浏览器扩展程序页面中手动安装或加载。

## Chrome Debugger 权限

CSS-Diff 会请求 Chrome 的 `debugger` 权限,用于通过 Chrome DevTools Protocol 的 `Overlay.highlightNode` 和 `Overlay.hideHighlight` 命令实现差异表格 DOM 行的页面高亮,并在鼠标移出时立即取消高亮。

Chrome 会把该权限视为敏感权限:

- 安装或更新扩展时,Chrome 可能显示权限警告。
- 扩展附加到当前页面时,Chrome 可能显示“正在调试此浏览器/页面”的提示。
- CSS-Diff 仅将该权限用于 DevTools 侧边栏中的临时 DOM 悬停高亮。
- 如果未授予该权限,CSS 对比功能仍可使用,但被检查页面中的原生悬停高亮不可用。

## 使用方法

1. 打开需要检查的页面。
2. 打开 DevTools,并切换到 Elements 面板。
3. 打开 `CSS-Diff` 侧边栏。
4. 先选择第一个 DOM 元素,再选择第二个 DOM 元素。
5. 查看高亮的样式差异、搜索属性,或启用 `显示全部`。
6. 点击左侧或右侧的样式值单元格,复制对应 CSS 声明。
7. 点击 `清除选择` 开始新的比较。
6. 在差异表格中悬停源/目标 DOM 行,在被检查页面中高亮对应 DOM 节点。
7. 点击左侧或右侧的样式值单元格,复制对应 CSS 声明。
8. 点击 `清除选择` 开始新的比较。

## 本地开发

Expand Down
24 changes: 24 additions & 0 deletions entrypoints/devtools-panel/devtools-panel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const {
handleClearSelection,
handleRemoveSelectedElement,
handleCopyStyle,
handleInspectSelectedElement,
handleRestoreInspectedElement,
} = useDevToolsPanel()

const themePreference = ref<ThemePreference>('system')
Expand Down Expand Up @@ -149,6 +151,20 @@ function getStyleValueLabel(value: string) {
return value === UNDEFINED_STYLE_VALUE ? t('undefinedStyleValue') : value
}

function handleInspectPointerLeave(event: PointerEvent, element: SelectedElType) {
const relatedTarget = event.relatedTarget

if (relatedTarget instanceof Element) {
const nextInspectTarget = relatedTarget.closest('[data-css-diff-inspect-id]')

if (nextInspectTarget?.getAttribute('data-css-diff-inspect-id')) {
return
}
}

handleRestoreInspectedElement(element)
}

onMounted(() => {
initializeLocale(localStorage.getItem(LOCALE_STORAGE_KEY))
systemThemeQuery = window.matchMedia(THEME_MEDIA_QUERY)
Expand Down Expand Up @@ -403,6 +419,10 @@ const PropertyNode = defineComponent({
v-for="slot in selectionSlots"
:key="slot.key"
class="min-w-[280px] border-l border-border px-3 py-2 font-medium"
:data-css-diff-inspect-id="slot.element?.inspectId"
:title="slot.element ? t('nativeHoverTooltip') : undefined"
@pointerenter="slot.element && handleInspectSelectedElement(slot.element)"
@pointerleave="slot.element && handleInspectPointerLeave($event, slot.element)"
>
<div class="flex items-center justify-between gap-2">
<div class="min-w-0">
Expand Down Expand Up @@ -493,6 +513,10 @@ const PropertyNode = defineComponent({
:key="`${row.property}-${slot.key}`"
class="group min-w-[280px] cursor-copy border-l border-border px-3 py-2 align-top transition-colors hover:bg-accent hover:text-accent-foreground"
:class="row.isDiff ? 'bg-muted/30 font-medium text-foreground' : ''"
:data-css-diff-inspect-id="slot.element?.inspectId"
:title="slot.element ? t('nativeHoverTooltip') : undefined"
@pointerenter="slot.element && handleInspectSelectedElement(slot.element)"
@pointerleave="slot.element && handleInspectPointerLeave($event, slot.element)"
@click="slot.element && handleCopyStyle(row, slot.element.valueType)"
>
<div class="flex items-start justify-between gap-3">
Expand Down
80 changes: 59 additions & 21 deletions entrypoints/devtools-panel/hooks/useDevToolsPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useClipboard } from '@vueuse/core'
import { toast } from 'vue-sonner'
import { t } from '../lang'
import SM from '../message'
import { compareStyles, formatStyle, type FormatStyleValue, getVisibleCssDiffs, UNDEFINED_STYLE_VALUE } from '../utils'
import { type CaptureSelectedElementResult, compareStyles, createCaptureSelectedElementScript, devToolsOverlayHighlighter, getVisibleCssDiffs, UNDEFINED_STYLE_VALUE } from '../utils'

export function useDevToolsPanel() {
const inputValue = ref('')
Expand All @@ -12,31 +12,26 @@ export function useDevToolsPanel() {
const cssDiffs: Array<CssDiffsType> = reactive([])

const isAllProperty = ref(false)
const isLoadComplete = ref(false)

onMounted(() => {
browser.devtools.panels.elements.onSelectionChanged.addListener(() => {
browser.devtools.inspectedWindow.eval(
`(() => document.readyState)($0)`,
(readyState: Document['readyState']) => {
if (readyState === 'complete') {
isLoadComplete.value = true
}
else {
isLoadComplete.value = false
}
},
)
const valueType = getAvailableValueType()

if (!valueType) {
return
}

browser.devtools.inspectedWindow.eval(
`(${formatStyle.toString()})($0)`,
(result: FormatStyleValue, isException) => {
if (!isException && result != null && isLoadComplete.value) {
const valueType = getAvailableValueType()

if (valueType) {
saveSelectedEl({ ...result, valueType })
}
createCaptureSelectedElementScript(valueType),
(payload: CaptureSelectedElementResult, isException) => {
if (!isException && payload?.readyState === 'complete' && payload.result != null) {
saveSelectedEl({
...payload.result,
inspectId: payload.inspectId,
inspectPath: payload.inspectPath,
inspectTabId: browser.devtools.inspectedWindow.tabId,
valueType,
})
}
},
)
Expand All @@ -55,13 +50,18 @@ export function useDevToolsPanel() {

if (selectedEl.length === 2) {
compareSelectedEl()
prepareSelectedElementOverlay()
}
else {
cssDiffs.length = 0
}
})
})

onUnmounted(() => {
devToolsOverlayHighlighter.detach()
})

function saveSelectedEl(result: SelectedElType) {
if (!getAvailableValueType()) {
return
Expand All @@ -74,12 +74,14 @@ export function useDevToolsPanel() {

if (selectedEl.length === 2) {
compareSelectedEl()
prepareSelectedElementOverlay()
}
}

function handleClearSelection() {
selectedEl.length = 0
cssDiffs.length = 0
devToolsOverlayHighlighter.detach()

// Send selected data to other windows/tabs
SM.send([])
Expand All @@ -94,6 +96,7 @@ export function useDevToolsPanel() {

selectedEl.splice(index, 1)
cssDiffs.length = 0
devToolsOverlayHighlighter.detach()

// Send selected data to other windows/tabs
SM.send(selectedEl)
Expand Down Expand Up @@ -148,6 +151,39 @@ export function useDevToolsPanel() {
toast.success(`${t('copyInfo')} > ${source}`)
}

function prepareSelectedElementOverlay() {
const inspectedTabId = browser.devtools.inspectedWindow.tabId
const localElement = selectedEl.find(element => element.inspectTabId === inspectedTabId)

if (!localElement) {
return
}

devToolsOverlayHighlighter.prepare({
inspectTabId: localElement.inspectTabId,
})
}

function handleInspectSelectedElement(element: SelectedElType) {
if (!element.inspectId) {
return
}

devToolsOverlayHighlighter.highlight({
inspectId: element.inspectId,
inspectPath: element.inspectPath,
inspectTabId: element.inspectTabId,
})
}

function handleRestoreInspectedElement(element: SelectedElType) {
if (!element.inspectId) {
return
}

devToolsOverlayHighlighter.hide()
}

return {
inputValue,

Expand All @@ -158,5 +194,7 @@ export function useDevToolsPanel() {
handleClearSelection,
handleRemoveSelectedElement,
handleCopyStyle,
handleInspectSelectedElement,
handleRestoreInspectedElement,
}
}
1 change: 1 addition & 0 deletions entrypoints/devtools-panel/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type MessageKey =
| 'copyInfo'
| 'clearFilter'
| 'inputPlaceholder'
| 'nativeHoverTooltip'
| 'switchToDarkTheme'
| 'switchToLightTheme'
| 'waitingSelection'
Expand Down
3 changes: 3 additions & 0 deletions entrypoints/devtools-panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { FormatStyleValue } from './utils'

export type SelectedElType = {
valueType: 'left' | 'right'
inspectId?: string
inspectPath?: string
inspectTabId?: number
} & FormatStyleValue

export interface CssDiffsType {
Expand Down
Loading
Loading