Skip to content

Commit c591da3

Browse files
feat(core): plugin split pane functionality (#90)
1 parent b842dea commit c591da3

File tree

5 files changed

+144
-69
lines changed

5 files changed

+144
-69
lines changed

.changeset/fair-crabs-sing.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@tanstack/react-devtools': patch
3+
'@tanstack/devtools': patch
4+
---
5+
6+
Adds split panel functionality to the devtools panel, allowing multiple instances of devtools to be shown.

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
}

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(true)
4141
} else {
4242
setForceExpand(false)
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+
if (updatedPlugins.length > 3) return prev
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 = () => {
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: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import React, { useEffect, useRef, useState } from 'react'
2-
import {
3-
PLUGIN_CONTAINER_ID,
4-
PLUGIN_TITLE_CONTAINER_ID,
5-
TanStackDevtoolsCore,
6-
} from '@tanstack/devtools'
2+
import { TanStackDevtoolsCore } from '@tanstack/devtools'
73
import { createPortal } from 'react-dom'
84
import type { JSX, ReactElement } from 'react'
95
import type {
@@ -94,13 +90,19 @@ export interface TanStackDevtoolsReactInit {
9490

9591
const convertRender = (
9692
Component: PluginRender,
97-
setComponent: React.Dispatch<React.SetStateAction<JSX.Element | null>>,
93+
setComponents: React.Dispatch<
94+
React.SetStateAction<Record<string, JSX.Element>>
95+
>,
9896
e: HTMLElement,
9997
theme: 'dark' | 'light',
10098
) => {
101-
setComponent(
102-
typeof Component === 'function' ? Component(e, theme) : Component,
103-
)
99+
const element =
100+
typeof Component === 'function' ? Component(e, theme) : Component
101+
102+
setComponents((prev) => ({
103+
...prev,
104+
[e.getAttribute('id') as string]: element,
105+
}))
104106
}
105107

106108
export const TanStackDevtools = ({
@@ -109,14 +111,21 @@ export const TanStackDevtools = ({
109111
eventBusConfig,
110112
}: TanStackDevtoolsReactInit): ReactElement | null => {
111113
const devToolRef = useRef<HTMLDivElement>(null)
112-
const [pluginContainer, setPluginContainer] = useState<HTMLElement | null>(
113-
null,
114-
)
115-
const [titleContainer, setTitleContainer] = useState<HTMLElement | null>(null)
116-
const [PluginComponent, setPluginComponent] = useState<JSX.Element | null>(
117-
null,
118-
)
119-
const [TitleComponent, setTitleComponent] = useState<JSX.Element | null>(null)
114+
115+
const [pluginContainers, setPluginContainers] = useState<
116+
Record<string, HTMLElement>
117+
>({})
118+
const [titleContainers, setTitleContainers] = useState<
119+
Record<string, HTMLElement>
120+
>({})
121+
122+
const [PluginComponents, setPluginComponents] = useState<
123+
Record<string, JSX.Element>
124+
>({})
125+
const [TitleComponents, setTitleComponents] = useState<
126+
Record<string, JSX.Element>
127+
>({})
128+
120129
const [devtools] = useState(
121130
() =>
122131
new TanStackDevtoolsCore({
@@ -128,30 +137,42 @@ export const TanStackDevtools = ({
128137
name:
129138
typeof plugin.name === 'string'
130139
? plugin.name
131-
: // The check above confirms that `plugin.name` is of Render type
132-
(e, theme) => {
133-
setTitleContainer(
134-
e.ownerDocument.getElementById(
135-
PLUGIN_TITLE_CONTAINER_ID,
136-
) || null,
137-
)
140+
: (e, theme) => {
141+
const id = e.getAttribute('id')!
142+
const target = e.ownerDocument.getElementById(id)
143+
144+
if (target) {
145+
setTitleContainers((prev) => ({
146+
...prev,
147+
[id]: e,
148+
}))
149+
}
150+
138151
convertRender(
139152
plugin.name as PluginRender,
140-
setTitleComponent,
153+
setTitleComponents,
141154
e,
142155
theme,
143156
)
144157
},
145158
render: (e, theme) => {
146-
setPluginContainer(
147-
e.ownerDocument.getElementById(PLUGIN_CONTAINER_ID) || null,
148-
)
149-
convertRender(plugin.render, setPluginComponent, e, theme)
159+
const id = e.getAttribute('id')!
160+
const target = e.ownerDocument.getElementById(id)
161+
162+
if (target) {
163+
setPluginContainers((prev) => ({
164+
...prev,
165+
[id]: e,
166+
}))
167+
}
168+
169+
convertRender(plugin.render, setPluginComponents, e, theme)
150170
},
151171
}
152172
}),
153173
}),
154174
)
175+
155176
useEffect(() => {
156177
if (devToolRef.current) {
157178
devtools.mount(devToolRef.current)
@@ -160,14 +181,27 @@ export const TanStackDevtools = ({
160181
return () => devtools.unmount()
161182
}, [devtools])
162183

184+
const hasPlugins =
185+
Object.values(pluginContainers).length > 0 &&
186+
Object.values(PluginComponents).length > 0
187+
const hasTitles =
188+
Object.values(titleContainers).length > 0 &&
189+
Object.values(TitleComponents).length > 0
190+
163191
return (
164192
<>
165193
<div style={{ position: 'absolute' }} ref={devToolRef} />
166-
{pluginContainer && PluginComponent
167-
? createPortal(<>{PluginComponent}</>, pluginContainer)
194+
195+
{hasPlugins
196+
? Object.entries(pluginContainers).map(([key, pluginContainer]) =>
197+
createPortal(<>{PluginComponents[key]}</>, pluginContainer),
198+
)
168199
: null}
169-
{titleContainer && TitleComponent
170-
? createPortal(<>{TitleComponent}</>, titleContainer)
200+
201+
{hasTitles
202+
? Object.entries(titleContainers).map(([key, titleContainer]) =>
203+
createPortal(<>{TitleComponents[key]}</>, titleContainer),
204+
)
171205
: null}
172206
</>
173207
)

0 commit comments

Comments
 (0)