Skip to content

Commit 30b10c9

Browse files
committed
feat(core): plugin split pane functionality
1 parent 3aca2ec commit 30b10c9

File tree

10 files changed

+196
-59
lines changed

10 files changed

+196
-59
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"size-limit": [
4949
{
5050
"path": "packages/devtools/dist/esm/index.js",
51-
"limit": "30 KB"
51+
"limit": "33 KB"
5252
},
5353
{
5454
"path": "packages/event-bus-client/dist/esm/plugin.js",

packages/devtools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"build": "vite build"
4949
},
5050
"dependencies": {
51+
"@neodrag/solid": "3.0.0-next.8",
5152
"@solid-primitives/keyboard": "^1.2.8",
5253
"@tanstack/devtools-event-bus": "workspace:*",
5354
"@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
@@ -78,7 +78,10 @@ const getSettings = () => {
7878
}
7979
}
8080

81-
const generatePluginId = (plugin: TanStackDevtoolsPlugin, index: number) => {
81+
export const generatePluginId = (
82+
plugin: TanStackDevtoolsPlugin,
83+
index: number,
84+
) => {
8285
// if set by user, return the plugin id
8386
if (plugin.id) {
8487
return plugin.id

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export type DevtoolsStore = {
6060
state: {
6161
activeTab: TabName
6262
height: number
63-
activePlugin?: string | undefined
63+
activePlugins: Array<string>
6464
persistOpen: boolean
6565
}
6666
plugins?: Array<TanStackDevtoolsPlugin>
@@ -79,7 +79,7 @@ export const initialState: DevtoolsStore = {
7979
state: {
8080
activeTab: 'plugins',
8181
height: 400,
82-
activePlugin: undefined,
82+
activePlugins: [],
8383
persistOpen: false,
8484
},
8585
}
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
@@ -23,27 +23,35 @@ export const usePlugins = () => {
2323
const { setForceExpand } = useDrawContext()
2424

2525
const plugins = createMemo(() => store.plugins)
26-
const activePlugin = createMemo(() => store.state.activePlugin)
26+
const activePlugins = createMemo(() => store.state.activePlugins)
2727

2828
createEffect(() => {
29-
if (activePlugin() == null) {
29+
if (activePlugins().length > 0) {
3030
setForceExpand(false)
3131
} else {
3232
setForceExpand(true)
3333
}
3434
})
3535

36-
const setActivePlugin = (pluginId: string) => {
37-
setStore((prev) => ({
38-
...prev,
39-
state: {
40-
...prev.state,
41-
activePlugin: pluginId,
42-
},
43-
}))
36+
const toggleActivePlugins = (pluginId: string) => {
37+
setStore((prev) => {
38+
const isActive = prev.state.activePlugins.includes(pluginId)
39+
40+
const updatedPlugins = isActive
41+
? prev.state.activePlugins.filter((id) => id !== pluginId)
42+
: [...prev.state.activePlugins, pluginId]
43+
44+
return {
45+
...prev,
46+
state: {
47+
...prev.state,
48+
activePlugins: updatedPlugins,
49+
},
50+
}
51+
})
4452
}
4553

46-
return { plugins, setActivePlugin, activePlugin }
54+
return { plugins, toggleActivePlugins, activePlugins }
4755
}
4856

4957
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: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
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 } 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
createEffect(() => {
14-
const currentActivePlugin = plugins()?.find(
15-
(plugin) => plugin.id === activePlugin(),
19+
const currentActivePlugins = plugins()?.filter((plugin) =>
20+
activePlugins().includes(plugin.id!),
1621
)
17-
if (activePluginRef && currentActivePlugin) {
18-
currentActivePlugin.render(activePluginRef)
19-
}
22+
23+
currentActivePlugins?.forEach((plugin) => {
24+
const ref = pluginRefs().get(plugin.id!)
25+
26+
if (ref) {
27+
plugin.render(ref)
28+
}
29+
})
2030
})
21-
const styles = useStyles()
2231

2332
return (
2433
<div class={styles().pluginsTabPanel}>
@@ -30,12 +39,8 @@ export const PluginsTab = () => {
3039
},
3140
styles().pluginsTabDrawTransition(animationMs),
3241
)}
33-
onMouseEnter={() => {
34-
hoverUtils.enter()
35-
}}
36-
onMouseLeave={() => {
37-
hoverUtils.leave()
38-
}}
42+
onMouseEnter={() => hoverUtils.enter()}
43+
onMouseLeave={() => hoverUtils.leave()}
3944
>
4045
<div
4146
class={clsx(
@@ -49,33 +54,54 @@ export const PluginsTab = () => {
4954
<For each={plugins()}>
5055
{(plugin) => {
5156
let pluginHeading: HTMLHeadingElement | undefined
57+
5258
createEffect(() => {
5359
if (pluginHeading) {
5460
typeof plugin.name === 'string'
5561
? (pluginHeading.textContent = plugin.name)
5662
: plugin.name(pluginHeading)
5763
}
5864
})
65+
66+
const isActive = createMemo(() =>
67+
activePlugins().includes(plugin.id!),
68+
)
69+
5970
return (
6071
<div
61-
onClick={() => setActivePlugin(plugin.id!)}
72+
onClick={() => {
73+
toggleActivePlugins(plugin.id!)
74+
}}
6275
class={clsx(styles().pluginName, {
63-
active: activePlugin() === plugin.id,
76+
active: isActive(),
6477
})}
6578
>
66-
<h3 id={PLUGIN_TITLE_CONTAINER_ID} ref={pluginHeading} />
79+
<h3
80+
id={`${PLUGIN_TITLE_CONTAINER_ID}-${plugin.id}`}
81+
ref={pluginHeading}
82+
/>
6783
</div>
6884
)
6985
}}
7086
</For>
7187
</div>
7288
</div>
7389

74-
<div
75-
id={PLUGIN_CONTAINER_ID}
76-
ref={activePluginRef}
77-
class={styles().pluginsTabContent}
78-
></div>
90+
<For each={activePlugins()}>
91+
{(pluginId) => (
92+
<div
93+
id={`${PLUGIN_CONTAINER_ID}-${pluginId}`}
94+
ref={(el) => {
95+
setPluginRefs((prev) => {
96+
const updated = new Map(prev)
97+
updated.set(pluginId, el)
98+
return updated
99+
})
100+
}}
101+
class={styles().pluginsTabContent}
102+
/>
103+
)}
104+
</For>
79105
</div>
80106
)
81107
}

packages/react-devtools/src/devtools.tsx

Lines changed: 40 additions & 21 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'
@@ -103,47 +104,59 @@ export const TanStackDevtools = ({
103104
eventBusConfig,
104105
}: TanStackDevtoolsReactInit): ReactElement | null => {
105106
const devToolRef = useRef<HTMLDivElement>(null)
106-
const [pluginContainer, setPluginContainer] = useState<HTMLElement | null>(
107-
null,
107+
const [pluginContainers, setPluginContainers] = useState<Array<HTMLElement>>(
108+
[],
108109
)
109-
const [titleContainer, setTitleContainer] = useState<HTMLElement | null>(null)
110-
const [PluginComponent, setPluginComponent] = useState<JSX.Element | null>(
111-
null,
110+
const [titleContainers, setTitleContainers] = useState<Array<HTMLElement>>([])
111+
const [PluginComponents, setPluginComponents] = useState<Array<JSX.Element>>(
112+
[],
112113
)
113-
const [TitleComponent, setTitleComponent] = useState<JSX.Element | null>(null)
114+
const [TitleComponents, setTitleComponents] = useState<Array<JSX.Element>>([])
115+
114116
const [devtools] = useState(
115117
() =>
116118
new TanStackDevtoolsCore({
117119
config,
118120
eventBusConfig,
119-
plugins: plugins?.map((plugin) => {
121+
plugins: plugins?.map((plugin, index) => {
120122
return {
121123
...plugin,
122124
name:
123125
typeof plugin.name === 'string'
124126
? plugin.name
125127
: // The check above confirms that `plugin.name` is of Render type
126128
(e) => {
127-
setTitleContainer(
128-
e.ownerDocument.getElementById(
129-
PLUGIN_TITLE_CONTAINER_ID,
130-
) || null,
129+
const target = e.ownerDocument.getElementById(
130+
// @ts-ignore just testing
131+
`${PLUGIN_TITLE_CONTAINER_ID}-${generatePluginId(plugin, index)}`,
131132
)
132-
convertRender(
133-
plugin.name as PluginRender,
134-
setTitleComponent,
133+
if (target) {
134+
setTitleContainers((prev) => [...prev, target])
135+
}
136+
convertRender(plugin.name as PluginRender, (newVal) =>
137+
// @ts-ignore just testing
138+
setTitleComponents((prev) => [...prev, newVal]),
135139
)
136140
},
137141
render: (e) => {
138-
setPluginContainer(
139-
e.ownerDocument.getElementById(PLUGIN_CONTAINER_ID) || null,
142+
const target = e.ownerDocument.getElementById(
143+
// @ts-ignore just testing
144+
`${PLUGIN_CONTAINER_ID}-${generatePluginId(plugin, index)}`,
145+
)
146+
if (target) {
147+
setPluginContainers((prev) => [...prev, target])
148+
}
149+
150+
convertRender(plugin.render, (newVal) =>
151+
// @ts-ignore just testing
152+
setPluginComponents((prev) => [...prev, newVal]),
140153
)
141-
convertRender(plugin.render, setPluginComponent)
142154
},
143155
}
144156
}),
145157
}),
146158
)
159+
147160
useEffect(() => {
148161
if (devToolRef.current) {
149162
devtools.mount(devToolRef.current)
@@ -155,11 +168,17 @@ export const TanStackDevtools = ({
155168
return (
156169
<>
157170
<div style={{ position: 'absolute' }} ref={devToolRef} />
158-
{pluginContainer && PluginComponent
159-
? createPortal(<>{PluginComponent}</>, pluginContainer)
171+
172+
{pluginContainers.length > 0 && PluginComponents.length > 0
173+
? pluginContainers.map((pluginContainer, index) =>
174+
createPortal(<>{PluginComponents[index]}</>, pluginContainer),
175+
)
160176
: null}
161-
{titleContainer && TitleComponent
162-
? createPortal(<>{TitleComponent}</>, titleContainer)
177+
178+
{titleContainers.length > 0 && TitleComponents.length > 0
179+
? titleContainers.map((titleContainer, index) =>
180+
createPortal(<>{TitleComponents[index]}</>, titleContainer),
181+
)
163182
: null}
164183
</>
165184
)

0 commit comments

Comments
 (0)