Skip to content

Commit 6be26fb

Browse files
committed
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.
1 parent 422acf3 commit 6be26fb

3 files changed

Lines changed: 197 additions & 80 deletions

File tree

packages/devtools/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
"build": "tsup"
6060
},
6161
"dependencies": {
62+
"@solid-primitives/event-listener": "^2.4.3",
6263
"@solid-primitives/keyboard": "^1.3.3",
64+
"@solid-primitives/mouse": "^2.1.4",
65+
"@solid-primitives/resize-observer": "^2.1.3",
6366
"@tanstack/devtools-event-bus": "workspace:*",
6467
"@tanstack/devtools-ui": "workspace:*",
6568
"clsx": "^2.1.1",
Lines changed: 123 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,96 @@
1-
import { createEffect, createSignal, onCleanup } from 'solid-js'
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 { createMousePosition } from '@solid-primitives/mouse'
6+
import { createEventListener } from '@solid-primitives/event-listener'
27

38
export const SourceInspector = () => {
4-
const [isHighlighting, setIsHighlighting] = createSignal(false)
5-
const [currentElement, setCurrentElement] = createSignal<HTMLElement | null>(
6-
null,
7-
)
8-
const [currentElementBounding, setCurrentElementBounding] = createSignal({
9-
width: 0,
10-
height: 0,
11-
left: 0,
12-
top: 0,
9+
const highlightStateInit = {
10+
element: null as HTMLElement | null,
11+
bounding: { width: 0, height: 0, left: 0, top: 0 },
12+
dataSource: '',
13+
}
14+
15+
const [highlightState, setHighlightState] = createStore({
16+
...highlightStateInit,
1317
})
18+
const resetHighlight = () => {
19+
setHighlightState({ ...highlightStateInit })
20+
}
1421

15-
createEffect(() => {
16-
const handleKeyDown = (e: KeyboardEvent) => {
17-
const isShiftHeld = e.shiftKey
18-
const isCtrlHeld = e.ctrlKey || e.metaKey
19-
if (isShiftHeld && isCtrlHeld) {
20-
setIsHighlighting(true)
21-
}
22-
}
22+
const [nameTagRef, setNameTagRef] = createSignal<HTMLDivElement | null>(null)
23+
const nameTagSize = createElementSize(() => nameTagRef())
2324

24-
const handleKeyUp = (e: KeyboardEvent) => {
25-
const isShiftHeld = e.shiftKey
26-
const isCtrlHeld = e.ctrlKey || e.metaKey
27-
if (!isShiftHeld || !isCtrlHeld) {
28-
setIsHighlighting(false)
29-
setCurrentElement(null)
30-
}
25+
const mousePosition = createMousePosition()
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
3140
}
32-
const handleMouseMove = (e: MouseEvent) => {
33-
if (!isHighlighting()) return
3441

35-
const target = document.elementFromPoint(e.clientX, e.clientY)
42+
const target = document.elementFromPoint(mousePosition.x, mousePosition.y)
3643

37-
if (!(target instanceof HTMLElement)) {
38-
return
39-
}
44+
if (!(target instanceof HTMLElement)) {
45+
resetHighlight()
46+
return
47+
}
4048

41-
if (target === currentElement()) {
42-
return
43-
}
49+
if (target === highlightState.element) {
50+
return
51+
}
4452

45-
const dataSource = target.getAttribute('data-tsd-source')
46-
if (!dataSource) return
47-
48-
setCurrentElement(target)
49-
const rect = target.getBoundingClientRect()
50-
setCurrentElementBounding({
51-
width: rect.width,
52-
height: rect.height,
53-
left: rect.left,
54-
top: rect.top,
55-
})
53+
const dataSource = target.getAttribute('data-tsd-source')
54+
if (!dataSource) {
55+
resetHighlight()
56+
return
5657
}
5758

58-
const openSourceHandler = (e: Event) => {
59-
if (!isHighlighting()) return
60-
61-
if (e.target instanceof HTMLElement) {
62-
const dataSource = e.target.getAttribute('data-tsd-source')
63-
window.getSelection()?.removeAllRanges()
64-
if (dataSource) {
65-
e.preventDefault()
66-
e.stopPropagation()
67-
fetch(
68-
`${location.origin}/__tsd/open-source?source=${encodeURIComponent(
69-
dataSource,
70-
)}`,
71-
).catch(() => {})
72-
}
73-
}
59+
const rect = target.getBoundingClientRect()
60+
const bounding = {
61+
width: rect.width,
62+
height: rect.height,
63+
left: rect.left,
64+
top: rect.top,
7465
}
7566

76-
window.addEventListener('keydown', handleKeyDown)
77-
window.addEventListener('keyup', handleKeyUp)
78-
window.addEventListener('mousemove', handleMouseMove)
79-
window.addEventListener('click', openSourceHandler)
80-
onCleanup(() => {
81-
window.removeEventListener('keydown', handleKeyDown)
82-
window.removeEventListener('keyup', handleKeyUp)
83-
window.removeEventListener('mousemove', handleMouseMove)
84-
window.removeEventListener('click', openSourceHandler)
67+
setHighlightState({
68+
element: target,
69+
bounding,
70+
dataSource,
8571
})
8672
})
8773

88-
const currentElementBoxStyles = () => {
89-
const element = currentElement()
90-
if (element) {
91-
const bounding = currentElementBounding()
74+
createEventListener(window, 'click', (e: Event) => {
75+
if (!highlightState.element) return
76+
77+
e.preventDefault()
78+
e.stopPropagation()
79+
fetch(
80+
`${location.origin}/__tsd/open-source?source=${encodeURIComponent(
81+
highlightState.dataSource,
82+
)}`,
83+
).catch(() => {})
84+
})
85+
86+
const currentElementBoxStyles = createMemo(() => {
87+
if (highlightState.element) {
9288
return {
9389
display: 'block',
94-
width: `${bounding.width}px`,
95-
height: `${bounding.height}px`,
96-
left: `${bounding.left}px`,
97-
top: `${bounding.top}px`,
90+
width: `${highlightState.bounding.width}px`,
91+
height: `${highlightState.bounding.height}px`,
92+
left: `${highlightState.bounding.left}px`,
93+
top: `${highlightState.bounding.top}px`,
9894

9995
'background-color': 'oklch(55.4% 0.046 257.417 /0.25)',
10096
transition: 'all 0.05s linear',
@@ -105,9 +101,56 @@ export const SourceInspector = () => {
105101
return {
106102
display: 'none',
107103
}
108-
}
104+
})
105+
106+
const fileNameStyles = createMemo(() => {
107+
if (highlightState.element && nameTagRef()) {
108+
const windowWidth = window.innerWidth
109+
const nameTagHeight = nameTagSize.height || 26
110+
const nameTagWidth = nameTagSize.width || 0
111+
let left = highlightState.bounding.left
112+
let top = highlightState.bounding.top - nameTagHeight - 4
113+
114+
if (top < 0) {
115+
top = highlightState.bounding.top + highlightState.bounding.height + 4
116+
}
117+
118+
if (left + nameTagWidth > windowWidth) {
119+
left = windowWidth - nameTagWidth - 4
120+
}
121+
122+
if (left < 0) {
123+
left = 4
124+
}
125+
126+
return {
127+
position: 'fixed' as const,
128+
left: `${left}px`,
129+
top: `${top}px`,
130+
'background-color': 'oklch(55.4% 0.046 257.417 /0.80)',
131+
color: 'white',
132+
padding: '2px 4px',
133+
fontSize: '12px',
134+
'border-radius': '2px',
135+
'z-index': 10000,
136+
visibility: 'visible' as const,
137+
transition: 'all 0.05s linear',
138+
}
139+
}
140+
return {
141+
display: 'none',
142+
}
143+
})
109144

110145
return (
111-
<div style={{ ...currentElementBoxStyles(), 'pointer-events': 'none' }} />
146+
<>
147+
<div
148+
ref={setNameTagRef}
149+
style={{ ...fileNameStyles(), 'pointer-events': 'none' }}
150+
>
151+
{highlightState.dataSource.split(':')[0]}
152+
</div>
153+
<div style={{ ...currentElementBoxStyles(), 'pointer-events': 'none' }} />
154+
</>
112155
)
113156
}

0 commit comments

Comments
 (0)