Skip to content

Commit 47d2ef6

Browse files
feat(devtools): add source inspector with element highlighting (#214)
* feat(devtools): add source inspector with element highlighting Extract the source opening logic into a new SourceInspector component that highlights elements on Shift+Ctrl hover, improving the devtools user experience for inspecting sources. * feat(devtools): enhance source inspector with improved highlighting and name tags Refactor the SourceInspector component to use createStore for state management, integrate @solid-primitives for resize observer, keyboard, mouse, and event listener to improve performance and accuracy. Add a dynamic name tag displaying the file name near highlighted elements, with smart positioning to avoid screen edges. * refactor(devtools): switch highlight state init to function-based approach in source inspector * refactor(devtools): replace mouse position tracking with custom implementation - Replace @solid-primitives/mouse with custom event listener using createEventListener - Use e.clientX and e.clientY for client position instead of page position - Explanation: @solid-primitives/mouse returns page position, but we need client position for accurate element highlighting in source inspector * refactor(devtools): clear text selection on click in source inspector * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 22b038b commit 47d2ef6

4 files changed

Lines changed: 218 additions & 28 deletions

File tree

packages/devtools/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@
5959
"build": "tsup"
6060
},
6161
"dependencies": {
62+
"@solid-primitives/event-listener": "^2.4.3",
6263
"@solid-primitives/keyboard": "^1.3.3",
64+
"@solid-primitives/resize-observer": "^2.1.3",
6365
"@tanstack/devtools-event-bus": "workspace:*",
6466
"@tanstack/devtools-ui": "workspace:*",
6567
"clsx": "^2.1.1",
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { createEffect, createMemo, createSignal } from 'solid-js'
2+
import { createStore } from 'solid-js/store'
3+
import { createElementSize } from '@solid-primitives/resize-observer'
4+
import { useKeyDownList } from '@solid-primitives/keyboard'
5+
import { createEventListener } from '@solid-primitives/event-listener'
6+
7+
export const SourceInspector = () => {
8+
const highlightStateInit = () => ({
9+
element: null as HTMLElement | null,
10+
bounding: { width: 0, height: 0, left: 0, top: 0 },
11+
dataSource: '',
12+
})
13+
14+
const [highlightState, setHighlightState] = createStore(highlightStateInit())
15+
const resetHighlight = () => {
16+
setHighlightState(highlightStateInit())
17+
}
18+
19+
const [nameTagRef, setNameTagRef] = createSignal<HTMLDivElement | null>(null)
20+
const nameTagSize = createElementSize(() => nameTagRef())
21+
22+
const [mousePosition, setMousePosition] = createStore({ x: 0, y: 0 })
23+
createEventListener(document, 'mousemove', (e) => {
24+
setMousePosition({ x: e.clientX, y: e.clientY })
25+
})
26+
27+
const downList = useKeyDownList()
28+
const isHighlightingKeysHeld = createMemo(() => {
29+
const keys = downList()
30+
const isShiftHeld = keys.includes('SHIFT')
31+
const isCtrlHeld = keys.includes('CONTROL')
32+
const isMetaHeld = keys.includes('META')
33+
return isShiftHeld && (isCtrlHeld || isMetaHeld)
34+
})
35+
36+
createEffect(() => {
37+
if (!isHighlightingKeysHeld()) {
38+
resetHighlight()
39+
return
40+
}
41+
42+
const target = document.elementFromPoint(mousePosition.x, mousePosition.y)
43+
44+
if (!(target instanceof HTMLElement)) {
45+
resetHighlight()
46+
return
47+
}
48+
49+
if (target === highlightState.element) {
50+
return
51+
}
52+
53+
const dataSource = target.getAttribute('data-tsd-source')
54+
if (!dataSource) {
55+
resetHighlight()
56+
return
57+
}
58+
59+
const rect = target.getBoundingClientRect()
60+
const bounding = {
61+
width: rect.width,
62+
height: rect.height,
63+
left: rect.left,
64+
top: rect.top,
65+
}
66+
67+
setHighlightState({
68+
element: target,
69+
bounding,
70+
dataSource,
71+
})
72+
})
73+
74+
createEventListener(document, 'click', (e) => {
75+
if (!highlightState.element) return
76+
77+
window.getSelection()?.removeAllRanges()
78+
e.preventDefault()
79+
e.stopPropagation()
80+
81+
fetch(
82+
`${location.origin}/__tsd/open-source?source=${encodeURIComponent(
83+
highlightState.dataSource,
84+
)}`,
85+
).catch(() => {})
86+
})
87+
88+
const currentElementBoxStyles = createMemo(() => {
89+
if (highlightState.element) {
90+
return {
91+
display: 'block',
92+
width: `${highlightState.bounding.width}px`,
93+
height: `${highlightState.bounding.height}px`,
94+
left: `${highlightState.bounding.left}px`,
95+
top: `${highlightState.bounding.top}px`,
96+
97+
'background-color': 'oklch(55.4% 0.046 257.417 /0.25)',
98+
transition: 'all 0.05s linear',
99+
position: 'fixed' as const,
100+
'z-index': 9999,
101+
}
102+
}
103+
return {
104+
display: 'none',
105+
}
106+
})
107+
108+
const fileNameStyles = createMemo(() => {
109+
if (highlightState.element && nameTagRef()) {
110+
const windowWidth = window.innerWidth
111+
const nameTagHeight = nameTagSize.height || 26
112+
const nameTagWidth = nameTagSize.width || 0
113+
let left = highlightState.bounding.left
114+
let top = highlightState.bounding.top - nameTagHeight - 4
115+
116+
if (top < 0) {
117+
top = highlightState.bounding.top + highlightState.bounding.height + 4
118+
}
119+
120+
if (left + nameTagWidth > windowWidth) {
121+
left = windowWidth - nameTagWidth - 4
122+
}
123+
124+
if (left < 0) {
125+
left = 4
126+
}
127+
128+
return {
129+
position: 'fixed' as const,
130+
left: `${left}px`,
131+
top: `${top}px`,
132+
'background-color': 'oklch(55.4% 0.046 257.417 /0.80)',
133+
color: 'white',
134+
padding: '2px 4px',
135+
fontSize: '12px',
136+
'border-radius': '2px',
137+
'z-index': 10000,
138+
visibility: 'visible' as const,
139+
transition: 'all 0.05s linear',
140+
}
141+
}
142+
return {
143+
display: 'none',
144+
}
145+
})
146+
147+
return (
148+
<>
149+
<div
150+
ref={setNameTagRef}
151+
style={{ ...fileNameStyles(), 'pointer-events': 'none' }}
152+
>
153+
{highlightState.dataSource.split(':')[0]}
154+
</div>
155+
<div style={{ ...currentElementBoxStyles(), 'pointer-events': 'none' }} />
156+
</>
157+
)
158+
}

packages/devtools/src/devtools.tsx

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
1+
import { Show, createEffect, createSignal } from 'solid-js'
22
import { createShortcut } from '@solid-primitives/keyboard'
33
import { Portal } from 'solid-js/web'
44
import { ThemeContextProvider } from '@tanstack/devtools-ui'
@@ -18,6 +18,7 @@ import { TabContent } from './components/tab-content'
1818
import { keyboardModifiers } from './context/devtools-store'
1919
import { getAllPermutations } from './utils/sanitize'
2020
import { usePiPWindow } from './context/pip-context'
21+
import { SourceInspector } from './components/source-inspector'
2122

2223
export default function DevTools() {
2324
const { settings } = useDevtoolsSettings()
@@ -159,33 +160,6 @@ export default function DevTools() {
159160
}
160161
})
161162

162-
createEffect(() => {
163-
// this will only work with the Vite plugin
164-
const openSourceHandler = (e: Event) => {
165-
const isShiftHeld = (e as KeyboardEvent).shiftKey
166-
const isCtrlHeld =
167-
(e as KeyboardEvent).ctrlKey || (e as KeyboardEvent).metaKey
168-
if (!isShiftHeld || !isCtrlHeld) return
169-
170-
if (e.target instanceof HTMLElement) {
171-
const dataSource = e.target.getAttribute('data-tsd-source')
172-
window.getSelection()?.removeAllRanges()
173-
if (dataSource) {
174-
e.preventDefault()
175-
e.stopPropagation()
176-
fetch(
177-
`${location.origin}/__tsd/open-source?source=${encodeURIComponent(
178-
dataSource,
179-
)}`,
180-
).catch(() => {})
181-
}
182-
}
183-
}
184-
window.addEventListener('click', openSourceHandler)
185-
onCleanup(() => {
186-
window.removeEventListener('click', openSourceHandler)
187-
})
188-
})
189163
const { theme } = useTheme()
190164

191165
return (
@@ -216,6 +190,7 @@ export default function DevTools() {
216190
</ContentPanel>
217191
</MainPanel>
218192
</Show>
193+
<SourceInspector />
219194
</div>
220195
</Portal>
221196
</ThemeContextProvider>

0 commit comments

Comments
 (0)