@tanstack/pacer-devtools 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/pacer-devtools",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Devtools for Pacer.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -73,17 +73,17 @@
73
73
  "dependencies": {
74
74
  "@tanstack/devtools-ui": "0.5.1",
75
75
  "@tanstack/devtools-utils": "^0.4.0",
76
- "@tanstack/solid-store": "^0.8.1",
76
+ "@tanstack/solid-store": "^0.9.2",
77
77
  "clsx": "^2.1.1",
78
- "csstype": "^3.0.10",
79
- "dayjs": "^1.11.19",
78
+ "csstype": "^3.2.3",
79
+ "dayjs": "^1.11.20",
80
80
  "goober": "^2.1.18",
81
81
  "solid-js": "^1.9.11"
82
82
  },
83
83
  "devDependencies": {
84
84
  "rolldown-plugin-solid": "^0.2.1",
85
- "tsdown": "^0.21.1",
86
- "vite-plugin-solid": "^2.11.10"
85
+ "tsdown": "^0.21.4",
86
+ "vite-plugin-solid": "^2.11.11"
87
87
  },
88
88
  "scripts": {
89
89
  "clean": "premove ./build ./dist",
@@ -1,6 +1,9 @@
1
- import { createStore } from 'solid-js/store'
1
+ import { createStore, produce } from 'solid-js/store'
2
2
  import { createContext, createEffect, onCleanup, useContext } from 'solid-js'
3
- import { pacerEventClient } from '@tanstack/pacer/event-client'
3
+ import {
4
+ getPacerDevtoolsInstance,
5
+ pacerEventClient,
6
+ } from '@tanstack/pacer/event-client'
4
7
  import type {
5
8
  AsyncBatcher,
6
9
  AsyncDebouncer,
@@ -9,6 +12,7 @@ import type {
9
12
  AsyncThrottler,
10
13
  Batcher,
11
14
  Debouncer,
15
+ PacerEventName,
12
16
  Queuer,
13
17
  RateLimiter,
14
18
  Throttler,
@@ -49,18 +53,62 @@ const PacerDevtoolsContext = createContext<
49
53
  ]
50
54
  >([initialPacerDevtoolsStore, () => {}])
51
55
 
52
- const updateOrAddToArray = <T extends { key: string }>(
53
- oldArray: Array<T>,
54
- newItem: T,
55
- ) => {
56
- const index = oldArray.findIndex((item) => item.key === newItem.key)
57
- if (index !== -1) {
58
- // Update existing item
59
- return oldArray.map((item, i) => (i === index ? newItem : item))
60
- }
61
- // Add new item
62
- return [...oldArray, newItem]
63
- }
56
+ type UtilListKey = Exclude<keyof PacerDevtoolsContextType, 'lastUpdatedByKey'>
57
+
58
+ /**
59
+ * Match TanStack Form devtools: subscribe with {@link pacerEventClient.on} per
60
+ * event suffix so the same `pacer:${suffix}` channel the library emits on is
61
+ * observed. `onAllPluginEvents` only sees `tanstack-devtools-global`, which is
62
+ * not always relayed the same way by the shell.
63
+ *
64
+ * `d-*` suffixes are used when the devtools panel pushes state back (see
65
+ * ActionButtons); those must update the panel store too.
66
+ */
67
+ const PACER_DEVTOOLS_UTIL_EVENTS: Array<{
68
+ listKey: UtilListKey
69
+ suffixes: ReadonlyArray<PacerEventName>
70
+ }> = [
71
+ {
72
+ listKey: 'asyncBatchers',
73
+ suffixes: ['AsyncBatcher', 'd-AsyncBatcher'],
74
+ },
75
+ {
76
+ listKey: 'asyncDebouncers',
77
+ suffixes: ['AsyncDebouncer', 'd-AsyncDebouncer'],
78
+ },
79
+ {
80
+ listKey: 'asyncQueuers',
81
+ suffixes: ['AsyncQueuer', 'd-AsyncQueuer'],
82
+ },
83
+ {
84
+ listKey: 'asyncRateLimiters',
85
+ suffixes: ['AsyncRateLimiter', 'd-AsyncRateLimiter'],
86
+ },
87
+ {
88
+ listKey: 'asyncThrottlers',
89
+ suffixes: ['AsyncThrottler', 'd-AsyncThrottler'],
90
+ },
91
+ {
92
+ listKey: 'batchers',
93
+ suffixes: ['Batcher', 'd-Batcher'],
94
+ },
95
+ {
96
+ listKey: 'debouncers',
97
+ suffixes: ['Debouncer', 'd-Debouncer'],
98
+ },
99
+ {
100
+ listKey: 'queuers',
101
+ suffixes: ['Queuer', 'd-Queuer'],
102
+ },
103
+ {
104
+ listKey: 'rateLimiters',
105
+ suffixes: ['RateLimiter', 'd-RateLimiter'],
106
+ },
107
+ {
108
+ listKey: 'throttlers',
109
+ suffixes: ['Throttler', 'd-Throttler'],
110
+ },
111
+ ]
64
112
 
65
113
  export function PacerContextProvider(props: { children: any }) {
66
114
  const [store, setStore] = createStore<PacerDevtoolsContextType>(
@@ -68,121 +116,42 @@ export function PacerContextProvider(props: { children: any }) {
68
116
  )
69
117
 
70
118
  createEffect(() => {
71
- const cleanup = pacerEventClient.onAllPluginEvents((_e) => {
72
- const e = _e as unknown as { type: string; payload: any }
73
- switch (e.type) {
74
- case 'pacer:AsyncBatcher': {
75
- setStore({
76
- asyncBatchers: updateOrAddToArray(store.asyncBatchers, e.payload),
77
- lastUpdatedByKey: {
78
- ...store.lastUpdatedByKey,
79
- [e.payload.key]: Date.now(),
80
- },
81
- })
82
- break
83
- }
84
- case 'pacer:AsyncDebouncer': {
85
- setStore({
86
- asyncDebouncers: updateOrAddToArray(
87
- store.asyncDebouncers,
88
- e.payload,
89
- ),
90
- lastUpdatedByKey: {
91
- ...store.lastUpdatedByKey,
92
- [e.payload.key]: Date.now(),
93
- },
94
- })
95
- break
96
- }
97
- case 'pacer:AsyncQueuer': {
98
- setStore({
99
- asyncQueuers: updateOrAddToArray(store.asyncQueuers, e.payload),
100
- lastUpdatedByKey: {
101
- ...store.lastUpdatedByKey,
102
- [e.payload.key]: Date.now(),
103
- },
104
- })
105
- break
106
- }
107
- case 'pacer:AsyncRateLimiter': {
108
- setStore({
109
- asyncRateLimiters: updateOrAddToArray(
110
- store.asyncRateLimiters,
111
- e.payload,
112
- ),
113
- lastUpdatedByKey: {
114
- ...store.lastUpdatedByKey,
115
- [e.payload.key]: Date.now(),
116
- },
117
- })
118
- break
119
- }
120
- case 'pacer:AsyncThrottler': {
121
- setStore({
122
- asyncThrottlers: updateOrAddToArray(
123
- store.asyncThrottlers,
124
- e.payload,
125
- ),
126
- lastUpdatedByKey: {
127
- ...store.lastUpdatedByKey,
128
- [e.payload.key]: Date.now(),
129
- },
130
- })
131
- break
132
- }
133
- case 'pacer:Batcher': {
134
- setStore({
135
- batchers: updateOrAddToArray(store.batchers, e.payload),
136
- lastUpdatedByKey: {
137
- ...store.lastUpdatedByKey,
138
- [e.payload.key]: Date.now(),
139
- },
140
- })
141
- break
142
- }
143
- case 'pacer:Debouncer': {
144
- setStore({
145
- debouncers: updateOrAddToArray(store.debouncers, e.payload),
146
- lastUpdatedByKey: {
147
- ...store.lastUpdatedByKey,
148
- [e.payload.key]: Date.now(),
149
- },
150
- })
151
- break
152
- }
153
- case 'pacer:Queuer': {
154
- setStore({
155
- queuers: updateOrAddToArray(store.queuers, e.payload),
156
- lastUpdatedByKey: {
157
- ...store.lastUpdatedByKey,
158
- [e.payload.key]: Date.now(),
159
- },
160
- })
161
- break
162
- }
163
- case 'pacer:RateLimiter': {
164
- setStore({
165
- rateLimiters: updateOrAddToArray(store.rateLimiters, e.payload),
166
- lastUpdatedByKey: {
167
- ...store.lastUpdatedByKey,
168
- [e.payload.key]: Date.now(),
169
- },
170
- })
171
- break
172
- }
173
- case 'pacer:Throttler': {
174
- setStore({
175
- throttlers: updateOrAddToArray(store.throttlers, e.payload),
176
- lastUpdatedByKey: {
177
- ...store.lastUpdatedByKey,
178
- [e.payload.key]: Date.now(),
179
- },
180
- })
181
- break
182
- }
119
+ const cleanups: Array<() => void> = []
120
+
121
+ for (const { listKey, suffixes } of PACER_DEVTOOLS_UTIL_EVENTS) {
122
+ for (const suffix of suffixes) {
123
+ cleanups.push(
124
+ pacerEventClient.on(suffix, (e) => {
125
+ const payload = e.payload
126
+ const key = payload.key
127
+ if (!key) return
128
+
129
+ const instance = getPacerDevtoolsInstance(key)
130
+ if (!instance || typeof instance !== 'object') return
131
+
132
+ setStore(
133
+ produce((draft) => {
134
+ const list = draft[listKey] as Array<{ key: string }>
135
+ const index = list.findIndex((item) => item.key === key)
136
+ const inst = instance as { key: string }
137
+ if (index !== -1) {
138
+ list[index] = inst as (typeof list)[number]
139
+ } else {
140
+ list.push(inst as (typeof list)[number])
141
+ }
142
+ draft.lastUpdatedByKey[key] = Date.now()
143
+ }),
144
+ )
145
+ }),
146
+ )
147
+ }
148
+ }
149
+
150
+ onCleanup(() => {
151
+ for (const cleanup of cleanups) {
152
+ cleanup()
183
153
  }
184
154
  })
185
- onCleanup(cleanup)
186
155
  })
187
156
  return (
188
157
  <PacerDevtoolsContext.Provider value={[store, setStore]}>
@@ -1,5 +1,10 @@
1
- import { pacerEventClient } from '@tanstack/pacer'
1
+ import { emitChange } from '@tanstack/pacer'
2
+ import { useStore } from '@tanstack/solid-store'
2
3
  import { useStyles } from '../styles/use-styles'
4
+ import {
5
+ getPacerUtilStoreState,
6
+ isPacerUtilTanStackStore,
7
+ } from '../utils/read-pacer-store-state'
3
8
  import type { PacerEventName } from '@tanstack/pacer'
4
9
 
5
10
  type ActionButtonsProps = {
@@ -10,11 +15,30 @@ type ActionButtonsProps = {
10
15
  export function ActionButtons(props: ActionButtonsProps) {
11
16
  const styles = useStyles()
12
17
  const utilInstance = props.instance
13
- const utilState = utilInstance?.store?.state
14
- const hasPending = utilState && 'isPending' in utilState
15
- const hasEmpty = utilState && 'isEmpty' in utilState
16
- const isPending = hasPending ? !!utilState.isPending : false
17
- const isEmpty = hasEmpty ? utilState.isEmpty : undefined
18
+ const store = utilInstance?.store
19
+
20
+ const stateAccessor = isPacerUtilTanStackStore(store)
21
+ ? useStore(store as never, (s: unknown) =>
22
+ s !== null && s !== undefined && typeof s === 'object'
23
+ ? (s as Record<string, unknown>)
24
+ : {},
25
+ )
26
+ : () => getPacerUtilStoreState(utilInstance) as Record<string, unknown>
27
+
28
+ const getState = () => stateAccessor()
29
+
30
+ const hasPending = () => {
31
+ const u = getState()
32
+ return 'isPending' in u
33
+ }
34
+ const isPending = () => {
35
+ const u = getState()
36
+ return 'isPending' in u ? !!u.isPending : false
37
+ }
38
+ const isEmptyFlag = () => {
39
+ const u = getState()
40
+ return 'isEmpty' in u ? !!u.isEmpty : false
41
+ }
18
42
 
19
43
  const hasFlush = typeof utilInstance.flush === 'function'
20
44
  const hasCancel = typeof utilInstance.cancel === 'function'
@@ -24,9 +48,12 @@ export function ActionButtons(props: ActionButtonsProps) {
24
48
  const hasStop = typeof utilInstance.stop === 'function'
25
49
  const hasStartStop = hasStart && hasStop
26
50
 
27
- const isRunning = utilState?.isRunning ?? true
51
+ const isRunning = () => {
52
+ const u = getState()
53
+ return 'isRunning' in u ? !!u.isRunning : true
54
+ }
28
55
 
29
- if (!hasPending && !hasFlush && !hasCancel && !hasReset && !hasClear) {
56
+ if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) {
30
57
  return (
31
58
  <div class={styles().sectionEmpty}>
32
59
  No actions available for this util
@@ -35,23 +62,24 @@ export function ActionButtons(props: ActionButtonsProps) {
35
62
  }
36
63
 
37
64
  const emitName = `d-${props.utilName}` as PacerEventName
65
+ const utilNameLower = props.utilName.toLowerCase()
38
66
 
39
67
  return (
40
68
  <div class={styles().actionsRow}>
41
- {hasPending && (
69
+ {hasPending() && (
42
70
  <button
43
71
  class={styles().actionButton}
44
72
  onMouseDown={() => {
45
- const next = !isPending
73
+ const next = !isPending()
46
74
  utilInstance.store.setState((prev: any) => ({
47
75
  ...prev,
48
76
  isPending: next,
49
77
  }))
50
- pacerEventClient.emit(emitName, utilInstance)
78
+ emitChange(emitName, utilInstance)
51
79
  }}
52
80
  >
53
81
  <span class={styles().actionDotBlue} />
54
- {isPending ? 'Restore Pending' : 'Trigger Pending'}
82
+ {isPending() ? 'Restore Pending' : 'Trigger Pending'}
55
83
  </button>
56
84
  )}
57
85
  {hasFlush && (
@@ -59,15 +87,13 @@ export function ActionButtons(props: ActionButtonsProps) {
59
87
  class={styles().actionButton}
60
88
  onMouseDown={() => {
61
89
  utilInstance.flush()
62
- pacerEventClient.emit(emitName, utilInstance)
90
+ emitChange(emitName, utilInstance)
63
91
  }}
64
92
  disabled={
65
- (props.utilName.toLowerCase().includes('debouncer') &&
66
- !isPending) ||
67
- (props.utilName.toLowerCase().includes('throttler') &&
68
- !isPending) ||
69
- (props.utilName.toLowerCase().includes('batcher') && isEmpty) ||
70
- (props.utilName.toLowerCase().includes('queuer') && isEmpty)
93
+ (utilNameLower.includes('debouncer') && !isPending()) ||
94
+ (utilNameLower.includes('throttler') && !isPending()) ||
95
+ (utilNameLower.includes('batcher') && isEmptyFlag()) ||
96
+ (utilNameLower.includes('queuer') && isEmptyFlag())
71
97
  }
72
98
  >
73
99
  <span class={styles().actionDotGreen} />
@@ -79,15 +105,13 @@ export function ActionButtons(props: ActionButtonsProps) {
79
105
  class={styles().actionButton}
80
106
  onMouseDown={() => {
81
107
  utilInstance.cancel()
82
- pacerEventClient.emit(emitName, utilInstance)
108
+ emitChange(emitName, utilInstance)
83
109
  }}
84
110
  disabled={
85
- (props.utilName.toLowerCase().includes('debouncer') &&
86
- !isPending) ||
87
- (props.utilName.toLowerCase().includes('throttler') &&
88
- !isPending) ||
89
- (props.utilName.toLowerCase().includes('batcher') && isEmpty) ||
90
- (props.utilName.toLowerCase().includes('queuer') && isEmpty)
111
+ (utilNameLower.includes('debouncer') && !isPending()) ||
112
+ (utilNameLower.includes('throttler') && !isPending()) ||
113
+ (utilNameLower.includes('batcher') && isEmptyFlag()) ||
114
+ (utilNameLower.includes('queuer') && isEmptyFlag())
91
115
  }
92
116
  >
93
117
  <span class={styles().actionDotRed} />
@@ -99,7 +123,7 @@ export function ActionButtons(props: ActionButtonsProps) {
99
123
  class={styles().actionButton}
100
124
  onMouseDown={() => {
101
125
  utilInstance.reset()
102
- pacerEventClient.emit(emitName, utilInstance)
126
+ emitChange(emitName, utilInstance)
103
127
  }}
104
128
  >
105
129
  <span class={styles().actionDotYellow} />
@@ -111,11 +135,11 @@ export function ActionButtons(props: ActionButtonsProps) {
111
135
  class={styles().actionButton}
112
136
  onMouseDown={() => {
113
137
  utilInstance.clear()
114
- pacerEventClient.emit(emitName, utilInstance)
138
+ emitChange(emitName, utilInstance)
115
139
  }}
116
140
  disabled={
117
- (props.utilName.toLowerCase().includes('batcher') && isEmpty) ||
118
- (props.utilName.toLowerCase().includes('queuer') && isEmpty)
141
+ (utilNameLower.includes('batcher') && isEmptyFlag()) ||
142
+ (utilNameLower.includes('queuer') && isEmptyFlag())
119
143
  }
120
144
  >
121
145
  <span class={styles().actionDotOrange} />
@@ -126,16 +150,16 @@ export function ActionButtons(props: ActionButtonsProps) {
126
150
  <button
127
151
  class={styles().actionButton}
128
152
  onMouseDown={() => {
129
- if (isRunning) {
153
+ if (isRunning()) {
130
154
  utilInstance.stop()
131
155
  } else {
132
156
  utilInstance.start()
133
157
  }
134
- pacerEventClient.emit(emitName, utilInstance)
158
+ emitChange(emitName, utilInstance)
135
159
  }}
136
160
  >
137
161
  <span class={styles().actionDotPurple} />
138
- {isRunning ? 'Stop' : 'Start'}
162
+ {isRunning() ? 'Stop' : 'Start'}
139
163
  </button>
140
164
  )}
141
165
  </div>
@@ -2,6 +2,7 @@ import { JsonTree } from '@tanstack/devtools-ui'
2
2
  import { useStyles } from '../styles/use-styles'
3
3
  import { ActionButtons } from './ActionButtons'
4
4
  import { StateHeader } from './StateHeader'
5
+ import { UtilStateJsonTree } from './UtilStateJsonTree'
5
6
 
6
7
  type DetailsPanelProps = {
7
8
  selectedInstance: () => { instance: any; type: string } | null
@@ -42,7 +43,7 @@ export function DetailsPanel(props: DetailsPanelProps) {
42
43
  <div class={styles().detailSection}>
43
44
  <div class={styles().detailSectionHeader}>State</div>
44
45
  <div class={styles().stateContent}>
45
- <JsonTree value={entry.instance.store?.state as unknown} />
46
+ <UtilStateJsonTree instance={entry.instance} />
46
47
  </div>
47
48
  </div>
48
49
 
@@ -1,4 +1,10 @@
1
- import { createMemo, createSignal, onCleanup, onMount } from 'solid-js'
1
+ import {
2
+ createEffect,
3
+ createMemo,
4
+ createSignal,
5
+ onCleanup,
6
+ onMount,
7
+ } from 'solid-js'
2
8
  import { Header, HeaderLogo, MainPanel } from '@tanstack/devtools-ui'
3
9
  import { useStyles } from '../styles/use-styles'
4
10
  import { usePacerDevtoolsState } from '../PacerContextProvider'
@@ -7,6 +13,23 @@ import { UtilList } from './UtilList'
7
13
  import { DetailsPanel } from './DetailsPanel'
8
14
  import type { StateKey } from './util-groups'
9
15
 
16
+ function readResizeObserverBlockSize(entry: ResizeObserverEntry): number {
17
+ const [box] = entry.borderBoxSize
18
+ if (box !== undefined) return box.blockSize
19
+ return entry.contentRect.height
20
+ }
21
+
22
+ /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */
23
+ function resolvePluginHost(el: HTMLElement): HTMLElement | null {
24
+ const byId = el.closest<HTMLElement>('[id^="plugin-container-"]')
25
+ if (byId) return byId
26
+ return (
27
+ el.parentElement?.parentElement?.parentElement ??
28
+ el.parentElement?.parentElement ??
29
+ el.parentElement
30
+ )
31
+ }
32
+
10
33
  export function Shell() {
11
34
  const styles = useStyles()
12
35
  const state = usePacerDevtoolsState()
@@ -14,6 +37,12 @@ export function Shell() {
14
37
  const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
15
38
  const [leftPanelWidth, setLeftPanelWidth] = createSignal(300)
16
39
  const [isDragging, setIsDragging] = createSignal(false)
40
+ const [shellRootEl, setShellRootEl] = createSignal<HTMLDivElement | null>(
41
+ null,
42
+ )
43
+ const [slotHeightPx, setSlotHeightPx] = createSignal<number | undefined>(
44
+ undefined,
45
+ )
17
46
 
18
47
  const selectedInstance = createMemo(() => {
19
48
  const key = selectedKey()
@@ -65,43 +94,83 @@ export function Shell() {
65
94
  document.removeEventListener('mouseup', handleMouseUp)
66
95
  })
67
96
 
97
+ /**
98
+ * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex
99
+ * ancestors, so percentage height often never constrains our tree. Observing the
100
+ * outer `plugin-container-*` host (not the immediate React mount wrapper) and
101
+ * setting an explicit pixel height makes inner overflow-y regions scroll without
102
+ * capping to the mount div’s collapsed height.
103
+ */
104
+ createEffect(() => {
105
+ const el = shellRootEl()
106
+ if (!el || typeof ResizeObserver === 'undefined') return
107
+ const pluginSlot = resolvePluginHost(el)
108
+ if (!pluginSlot) return
109
+
110
+ const ro = new ResizeObserver((entries) => {
111
+ const entry = entries[0]
112
+ if (!entry) return
113
+ const target = entry.target as HTMLElement
114
+ const h =
115
+ target.clientHeight > 0
116
+ ? target.clientHeight
117
+ : readResizeObserverBlockSize(entry)
118
+ if (h > 0) setSlotHeightPx(Math.round(h))
119
+ })
120
+ ro.observe(pluginSlot)
121
+ onCleanup(() => ro.disconnect())
122
+ })
123
+
68
124
  return (
69
- <MainPanel>
70
- <Header>
71
- <HeaderLogo flavor={{ light: '#84cc16', dark: '#84cc16' }}>
72
- TanStack Pacer
73
- </HeaderLogo>
74
- </Header>
75
-
76
- <div class={styles().mainContainer}>
77
- <div
78
- class={styles().leftPanel}
79
- style={{
80
- width: `${leftPanelWidth()}px`,
81
- 'min-width': '150px',
82
- 'max-width': '800px',
83
- }}
84
- >
85
- <UtilList
86
- selectedKey={selectedKey}
87
- setSelectedKey={setSelectedKey}
88
- utilState={utilState}
89
- />
90
- </div>
125
+ <div
126
+ ref={setShellRootEl}
127
+ class={styles().shellRoot}
128
+ style={{
129
+ ...(slotHeightPx() !== undefined
130
+ ? {
131
+ height: `${slotHeightPx()}px`,
132
+ 'max-height': `${slotHeightPx()}px`,
133
+ }
134
+ : {}),
135
+ }}
136
+ >
137
+ <MainPanel class={styles().mainPanelShell}>
138
+ <Header>
139
+ <HeaderLogo flavor={{ light: '#84cc16', dark: '#84cc16' }}>
140
+ TanStack Pacer
141
+ </HeaderLogo>
142
+ </Header>
91
143
 
92
- <div
93
- class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
94
- onMouseDown={handleMouseDown}
95
- />
144
+ <div class={styles().mainContainer}>
145
+ <div
146
+ class={styles().leftPanel}
147
+ style={{
148
+ width: `${leftPanelWidth()}px`,
149
+ 'min-width': '150px',
150
+ 'max-width': '800px',
151
+ }}
152
+ >
153
+ <UtilList
154
+ selectedKey={selectedKey}
155
+ setSelectedKey={setSelectedKey}
156
+ utilState={utilState}
157
+ />
158
+ </div>
96
159
 
97
- <div class={styles().rightPanel} style={{ flex: 1 }}>
98
- <div class={styles().panelHeader}>Details</div>
99
- <DetailsPanel
100
- selectedInstance={selectedInstance}
101
- utilState={utilState}
160
+ <div
161
+ class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
162
+ onMouseDown={handleMouseDown}
102
163
  />
164
+
165
+ <div class={styles().rightPanel} style={{ flex: 1 }}>
166
+ <div class={styles().panelHeader}>Details</div>
167
+ <DetailsPanel
168
+ selectedInstance={selectedInstance}
169
+ utilState={utilState}
170
+ />
171
+ </div>
103
172
  </div>
104
- </div>
105
- </MainPanel>
173
+ </MainPanel>
174
+ </div>
106
175
  )
107
176
  }