Skip to content

Commit 299d849

Browse files
committed
feat(core): plugin split pane functionality
1 parent ab9770b commit 299d849

File tree

9 files changed

+200
-56
lines changed

9 files changed

+200
-56
lines changed

packages/devtools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"build": "tsup"
5656
},
5757
"dependencies": {
58+
"@neodrag/solid": "3.0.0-next.8",
5859
"@solid-primitives/keyboard": "^1.2.8",
5960
"@tanstack/devtools-event-bus": "workspace:*",
6061
"@tanstack/devtools-ui": "workspace:*",

packages/devtools/src/context/devtools-context.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ const getSettings = () => {
8686
}
8787
}
8888

89-
const generatePluginId = (plugin: TanStackDevtoolsPlugin, index: number) => {
89+
export const generatePluginId = (
90+
plugin: TanStackDevtoolsPlugin,
91+
index: number,
92+
) => {
9093
// if set by user, return the plugin id
9194
if (plugin.id) {
9295
return plugin.id

packages/devtools/src/context/devtools-store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export type DevtoolsStore = {
6565
state: {
6666
activeTab: TabName
6767
height: number
68-
activePlugin?: string | undefined
68+
activePlugins: Array<string>
6969
persistOpen: boolean
7070
}
7171
plugins?: Array<TanStackDevtoolsPlugin>
@@ -89,7 +89,7 @@ export const initialState: DevtoolsStore = {
8989
state: {
9090
activeTab: 'plugins',
9191
height: 400,
92-
activePlugin: undefined,
92+
activePlugins: [],
9393
persistOpen: false,
9494
},
9595
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { createContext, createSignal, useContext } from 'solid-js'
2+
3+
import type { ParentComponent } from 'solid-js'
4+
5+
type DropZone = {
6+
id: string
7+
name: string
8+
ref: HTMLElement
9+
}
10+
11+
const useDropzone = () => {
12+
const [isDragging, setDragging] = createSignal(false)
13+
const dropZones: Array<DropZone> = []
14+
15+
const registerDropZone = (zone: DropZone) => {
16+
dropZones.push(zone)
17+
}
18+
19+
const checkDrop = (dragEl: HTMLElement): string | null => {
20+
const dragRect = dragEl.getBoundingClientRect()
21+
for (const { ref, name } of dropZones) {
22+
const dropRect = ref.getBoundingClientRect()
23+
const isInside =
24+
dragRect.left >= dropRect.left &&
25+
dragRect.right <= dropRect.right &&
26+
dragRect.top >= dropRect.top &&
27+
dragRect.bottom <= dropRect.bottom
28+
if (isInside) return name
29+
}
30+
return null
31+
}
32+
33+
return { isDragging, setDragging, checkDrop, registerDropZone }
34+
}
35+
36+
type ContextType = ReturnType<typeof useDropzone>
37+
38+
const DropzoneContext = createContext<ContextType | undefined>(undefined)
39+
40+
export const DropzoneProvider: ParentComponent = (props) => {
41+
const value = useDropzone()
42+
43+
return (
44+
<DropzoneContext.Provider value={value}>
45+
{props.children}
46+
</DropzoneContext.Provider>
47+
)
48+
}
49+
50+
export function useDropzoneContext() {
51+
const context = useContext(DropzoneContext)
52+
53+
if (context === undefined) {
54+
throw new Error(
55+
`useDropzoneContext must be used within a DropzoneClientProvider`,
56+
)
57+
}
58+
59+
return context
60+
}

packages/devtools/src/context/use-devtools-context.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,35 @@ export const usePlugins = () => {
3333
const { setForceExpand } = useDrawContext()
3434

3535
const plugins = createMemo(() => store.plugins)
36-
const activePlugin = createMemo(() => store.state.activePlugin)
36+
const activePlugins = createMemo(() => store.state.activePlugins)
3737

3838
createEffect(() => {
39-
if (activePlugin() == null) {
39+
if (activePlugins().length > 0) {
4040
setForceExpand(false)
4141
} else {
4242
setForceExpand(true)
4343
}
4444
})
4545

46-
const setActivePlugin = (pluginId: string) => {
47-
setStore((prev) => ({
48-
...prev,
49-
state: {
50-
...prev.state,
51-
activePlugin: pluginId,
52-
},
53-
}))
46+
const toggleActivePlugins = (pluginId: string) => {
47+
setStore((prev) => {
48+
const isActive = prev.state.activePlugins.includes(pluginId)
49+
50+
const updatedPlugins = isActive
51+
? prev.state.activePlugins.filter((id) => id !== pluginId)
52+
: [...prev.state.activePlugins, pluginId]
53+
54+
return {
55+
...prev,
56+
state: {
57+
...prev.state,
58+
activePlugins: updatedPlugins,
59+
},
60+
}
61+
})
5462
}
5563

56-
return { plugins, setActivePlugin, activePlugin }
64+
return { plugins, toggleActivePlugins, activePlugins }
5765
}
5866

5967
export const useDevtoolsState = () => {

packages/devtools/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export type {
55
TanStackDevtoolsPlugin,
66
TanStackDevtoolsConfig,
77
} from './context/devtools-context'
8+
export { generatePluginId } from './context/devtools-context'
Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
1-
import { For, createEffect } from 'solid-js'
1+
import { For, createEffect, createMemo, createSignal } from 'solid-js'
22
import clsx from 'clsx'
33
import { useDrawContext } from '../context/draw-context'
44
import { usePlugins, useTheme } from '../context/use-devtools-context'
55
import { useStyles } from '../styles/use-styles'
66
import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from '../constants'
77

88
export const PluginsTab = () => {
9-
const { plugins, activePlugin, setActivePlugin } = usePlugins()
9+
const { plugins, activePlugins, toggleActivePlugins } = usePlugins()
1010
const { expanded, hoverUtils, animationMs } = useDrawContext()
11-
let activePluginRef: HTMLDivElement | undefined
11+
12+
const [pluginRefs, setPluginRefs] = createSignal(
13+
new Map<string, HTMLDivElement>(),
14+
)
15+
16+
const styles = useStyles()
1217

1318
const { theme } = useTheme()
1419
createEffect(() => {
15-
const currentActivePlugin = plugins()?.find(
16-
(plugin) => plugin.id === activePlugin(),
20+
const currentActivePlugins = plugins()?.filter((plugin) =>
21+
activePlugins().includes(plugin.id!),
1722
)
18-
if (activePluginRef && currentActivePlugin) {
19-
currentActivePlugin.render(activePluginRef, theme())
20-
}
23+
24+
currentActivePlugins?.forEach((plugin) => {
25+
const ref = pluginRefs().get(plugin.id!)
26+
27+
if (ref) {
28+
plugin.render(ref, theme())
29+
}
30+
})
2131
})
22-
const styles = useStyles()
32+
2333
return (
2434
<div class={styles().pluginsTabPanel}>
2535
<div
@@ -30,12 +40,8 @@ export const PluginsTab = () => {
3040
},
3141
styles().pluginsTabDrawTransition(animationMs),
3242
)}
33-
onMouseEnter={() => {
34-
hoverUtils.enter()
35-
}}
36-
onMouseLeave={() => {
37-
hoverUtils.leave()
38-
}}
43+
onMouseEnter={() => hoverUtils.enter()}
44+
onMouseLeave={() => hoverUtils.leave()}
3945
>
4046
<div
4147
class={clsx(
@@ -46,33 +52,54 @@ export const PluginsTab = () => {
4652
<For each={plugins()}>
4753
{(plugin) => {
4854
let pluginHeading: HTMLHeadingElement | undefined
55+
4956
createEffect(() => {
5057
if (pluginHeading) {
5158
typeof plugin.name === 'string'
5259
? (pluginHeading.textContent = plugin.name)
5360
: plugin.name(pluginHeading, theme())
5461
}
5562
})
63+
64+
const isActive = createMemo(() =>
65+
activePlugins().includes(plugin.id!),
66+
)
67+
5668
return (
5769
<div
58-
onClick={() => setActivePlugin(plugin.id!)}
70+
onClick={() => {
71+
toggleActivePlugins(plugin.id!)
72+
}}
5973
class={clsx(styles().pluginName, {
60-
active: activePlugin() === plugin.id,
74+
active: isActive(),
6175
})}
6276
>
63-
<h3 id={PLUGIN_TITLE_CONTAINER_ID} ref={pluginHeading} />
77+
<h3
78+
id={`${PLUGIN_TITLE_CONTAINER_ID}-${plugin.id}`}
79+
ref={pluginHeading}
80+
/>
6481
</div>
6582
)
6683
}}
6784
</For>
6885
</div>
6986
</div>
7087

71-
<div
72-
id={PLUGIN_CONTAINER_ID}
73-
ref={activePluginRef}
74-
class={styles().pluginsTabContent}
75-
></div>
88+
<For each={activePlugins()}>
89+
{(pluginId) => (
90+
<div
91+
id={`${PLUGIN_CONTAINER_ID}-${pluginId}`}
92+
ref={(el) => {
93+
setPluginRefs((prev) => {
94+
const updated = new Map(prev)
95+
updated.set(pluginId, el)
96+
return updated
97+
})
98+
}}
99+
class={styles().pluginsTabContent}
100+
/>
101+
)}
102+
</For>
76103
</div>
77104
)
78105
}

packages/react-devtools/src/devtools.tsx

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
PLUGIN_CONTAINER_ID,
44
PLUGIN_TITLE_CONTAINER_ID,
55
TanStackDevtoolsCore,
6+
generatePluginId,
67
} from '@tanstack/devtools'
78
import { createPortal } from 'react-dom'
89
import type { JSX, ReactElement } from 'react'
@@ -109,49 +110,67 @@ export const TanStackDevtools = ({
109110
eventBusConfig,
110111
}: TanStackDevtoolsReactInit): ReactElement | null => {
111112
const devToolRef = useRef<HTMLDivElement>(null)
112-
const [pluginContainer, setPluginContainer] = useState<HTMLElement | null>(
113-
null,
113+
const [pluginContainers, setPluginContainers] = useState<Array<HTMLElement>>(
114+
[],
114115
)
115-
const [titleContainer, setTitleContainer] = useState<HTMLElement | null>(null)
116-
const [PluginComponent, setPluginComponent] = useState<JSX.Element | null>(
117-
null,
116+
const [titleContainers, setTitleContainers] = useState<Array<HTMLElement>>([])
117+
const [PluginComponents, setPluginComponents] = useState<Array<JSX.Element>>(
118+
[],
118119
)
119-
const [TitleComponent, setTitleComponent] = useState<JSX.Element | null>(null)
120+
const [TitleComponents, setTitleComponents] = useState<Array<JSX.Element>>([])
121+
120122
const [devtools] = useState(
121123
() =>
122124
new TanStackDevtoolsCore({
123125
config,
124126
eventBusConfig,
125-
plugins: plugins?.map((plugin) => {
127+
plugins: plugins?.map((plugin, index) => {
126128
return {
127129
...plugin,
128130
name:
129131
typeof plugin.name === 'string'
130132
? plugin.name
131133
: // The check above confirms that `plugin.name` is of Render type
132134
(e, theme) => {
133-
setTitleContainer(
134-
e.ownerDocument.getElementById(
135-
PLUGIN_TITLE_CONTAINER_ID,
136-
) || null,
135+
const target = e.ownerDocument.getElementById(
136+
// @ts-ignore just testing
137+
`${PLUGIN_TITLE_CONTAINER_ID}-${generatePluginId(plugin, index)}`,
137138
)
139+
if (target) {
140+
setTitleContainers((prev) => [...prev, target])
141+
}
138142
convertRender(
139143
plugin.name as PluginRender,
140-
setTitleComponent,
144+
(newVal) =>
145+
// @ts-ignore just testing
146+
setTitleComponents((prev) => [...prev, newVal]),
141147
e,
142148
theme,
143149
)
144150
},
145151
render: (e, theme) => {
146-
setPluginContainer(
147-
e.ownerDocument.getElementById(PLUGIN_CONTAINER_ID) || null,
152+
const target = e.ownerDocument.getElementById(
153+
// @ts-ignore just testing
154+
`${PLUGIN_CONTAINER_ID}-${generatePluginId(plugin, index)}`,
155+
)
156+
if (target) {
157+
setPluginContainers((prev) => [...prev, target])
158+
}
159+
160+
convertRender(
161+
plugin.render,
162+
(newVal) =>
163+
// @ts-ignore just testing
164+
setPluginComponents((prev) => [...prev, newVal]),
165+
e,
166+
theme,
148167
)
149-
convertRender(plugin.render, setPluginComponent, e, theme)
150168
},
151169
}
152170
}),
153171
}),
154172
)
173+
155174
useEffect(() => {
156175
if (devToolRef.current) {
157176
devtools.mount(devToolRef.current)
@@ -163,11 +182,17 @@ export const TanStackDevtools = ({
163182
return (
164183
<>
165184
<div style={{ position: 'absolute' }} ref={devToolRef} />
166-
{pluginContainer && PluginComponent
167-
? createPortal(<>{PluginComponent}</>, pluginContainer)
185+
186+
{pluginContainers.length > 0 && PluginComponents.length > 0
187+
? pluginContainers.map((pluginContainer, index) =>
188+
createPortal(<>{PluginComponents[index]}</>, pluginContainer),
189+
)
168190
: null}
169-
{titleContainer && TitleComponent
170-
? createPortal(<>{TitleComponent}</>, titleContainer)
191+
192+
{titleContainers.length > 0 && TitleComponents.length > 0
193+
? titleContainers.map((titleContainer, index) =>
194+
createPortal(<>{TitleComponents[index]}</>, titleContainer),
195+
)
171196
: null}
172197
</>
173198
)

0 commit comments

Comments
 (0)