@tanstack/devtools-ui 0.3.4 → 0.4.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/dist/esm/components/tree.d.ts +5 -2
- package/dist/esm/components/tree.js +266 -181
- package/dist/esm/components/tree.js.map +1 -1
- package/dist/esm/styles/use-styles.js +5 -2
- package/dist/esm/styles/use-styles.js.map +1 -1
- package/dist/esm/utils/deep-keys.d.ts +5 -0
- package/package.json +3 -3
- package/src/components/tree.tsx +223 -97
- package/src/styles/use-styles.ts +6 -2
- package/src/utils/deep-keys.ts +18 -0
package/src/components/tree.tsx
CHANGED
|
@@ -2,98 +2,24 @@ import { For, Match, Show, Switch, createSignal } from 'solid-js'
|
|
|
2
2
|
import clsx from 'clsx'
|
|
3
3
|
import { css, useStyles } from '../styles/use-styles'
|
|
4
4
|
import { CopiedCopier, Copier, ErrorCopier } from './icons'
|
|
5
|
+
import type { CollapsiblePaths } from '../utils/deep-keys'
|
|
5
6
|
|
|
6
|
-
export function JsonTree(props: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const styles = useStyles()
|
|
13
|
-
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<button
|
|
17
|
-
class={styles().tree.actionButton}
|
|
18
|
-
title="Copy object to clipboard"
|
|
19
|
-
aria-label={`${
|
|
20
|
-
copyState() === 'NoCopy'
|
|
21
|
-
? 'Copy object to clipboard'
|
|
22
|
-
: copyState() === 'SuccessCopy'
|
|
23
|
-
? 'Object copied to clipboard'
|
|
24
|
-
: 'Error copying object to clipboard'
|
|
25
|
-
}`}
|
|
26
|
-
onClick={
|
|
27
|
-
copyState() === 'NoCopy'
|
|
28
|
-
? () => {
|
|
29
|
-
navigator.clipboard
|
|
30
|
-
.writeText(JSON.stringify(props.value, null, 2))
|
|
31
|
-
.then(
|
|
32
|
-
() => {
|
|
33
|
-
setCopyState('SuccessCopy')
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
setCopyState('NoCopy')
|
|
36
|
-
}, 1500)
|
|
37
|
-
},
|
|
38
|
-
(err) => {
|
|
39
|
-
console.error('Failed to copy: ', err)
|
|
40
|
-
setCopyState('ErrorCopy')
|
|
41
|
-
setTimeout(() => {
|
|
42
|
-
setCopyState('NoCopy')
|
|
43
|
-
}, 1500)
|
|
44
|
-
},
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
: undefined
|
|
48
|
-
}
|
|
49
|
-
>
|
|
50
|
-
<Switch>
|
|
51
|
-
<Match when={copyState() === 'NoCopy'}>
|
|
52
|
-
<Copier />
|
|
53
|
-
</Match>
|
|
54
|
-
<Match when={copyState() === 'SuccessCopy'}>
|
|
55
|
-
<CopiedCopier theme={'dark'} />
|
|
56
|
-
</Match>
|
|
57
|
-
<Match when={copyState() === 'ErrorCopy'}>
|
|
58
|
-
<ErrorCopier />
|
|
59
|
-
</Match>
|
|
60
|
-
</Switch>
|
|
61
|
-
</button>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const Expander = (props: { expanded: boolean }) => {
|
|
66
|
-
const styles = useStyles()
|
|
7
|
+
export function JsonTree<TData, TName extends CollapsiblePaths<TData>>(props: {
|
|
8
|
+
value: TData
|
|
9
|
+
copyable?: boolean
|
|
10
|
+
defaultExpansionDepth?: number
|
|
11
|
+
collapsePaths?: Array<TName>
|
|
12
|
+
}) {
|
|
67
13
|
return (
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
top: -1px;
|
|
78
|
-
}
|
|
79
|
-
`,
|
|
80
|
-
)}
|
|
81
|
-
>
|
|
82
|
-
<svg
|
|
83
|
-
width="16"
|
|
84
|
-
height="16"
|
|
85
|
-
viewBox="0 0 16 16"
|
|
86
|
-
fill="none"
|
|
87
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
88
|
-
>
|
|
89
|
-
<path
|
|
90
|
-
d="M6 12L10 8L6 4"
|
|
91
|
-
stroke-width="2"
|
|
92
|
-
stroke-linecap="round"
|
|
93
|
-
stroke-linejoin="round"
|
|
94
|
-
/>
|
|
95
|
-
</svg>
|
|
96
|
-
</span>
|
|
14
|
+
<JsonValue
|
|
15
|
+
isRoot
|
|
16
|
+
value={props.value}
|
|
17
|
+
copyable={props.copyable}
|
|
18
|
+
depth={0}
|
|
19
|
+
defaultExpansionDepth={props.defaultExpansionDepth ?? 1}
|
|
20
|
+
path=""
|
|
21
|
+
collapsePaths={props.collapsePaths}
|
|
22
|
+
/>
|
|
97
23
|
)
|
|
98
24
|
}
|
|
99
25
|
|
|
@@ -103,8 +29,24 @@ function JsonValue(props: {
|
|
|
103
29
|
isRoot?: boolean
|
|
104
30
|
isLastKey?: boolean
|
|
105
31
|
copyable?: boolean
|
|
32
|
+
|
|
33
|
+
defaultExpansionDepth: number
|
|
34
|
+
depth: number
|
|
35
|
+
|
|
36
|
+
collapsePaths?: Array<string>
|
|
37
|
+
path: string
|
|
106
38
|
}) {
|
|
107
|
-
const {
|
|
39
|
+
const {
|
|
40
|
+
value,
|
|
41
|
+
keyName,
|
|
42
|
+
isRoot = false,
|
|
43
|
+
isLastKey,
|
|
44
|
+
copyable,
|
|
45
|
+
defaultExpansionDepth,
|
|
46
|
+
depth,
|
|
47
|
+
collapsePaths,
|
|
48
|
+
path,
|
|
49
|
+
} = props
|
|
108
50
|
const styles = useStyles()
|
|
109
51
|
|
|
110
52
|
return (
|
|
@@ -137,12 +79,28 @@ function JsonValue(props: {
|
|
|
137
79
|
}
|
|
138
80
|
if (Array.isArray(value)) {
|
|
139
81
|
return (
|
|
140
|
-
<ArrayValue
|
|
82
|
+
<ArrayValue
|
|
83
|
+
defaultExpansionDepth={defaultExpansionDepth}
|
|
84
|
+
depth={depth}
|
|
85
|
+
copyable={copyable}
|
|
86
|
+
keyName={keyName}
|
|
87
|
+
value={value}
|
|
88
|
+
collapsePaths={collapsePaths}
|
|
89
|
+
path={path}
|
|
90
|
+
/>
|
|
141
91
|
)
|
|
142
92
|
}
|
|
143
93
|
if (typeof value === 'object') {
|
|
144
94
|
return (
|
|
145
|
-
<ObjectValue
|
|
95
|
+
<ObjectValue
|
|
96
|
+
defaultExpansionDepth={defaultExpansionDepth}
|
|
97
|
+
depth={depth}
|
|
98
|
+
copyable={copyable}
|
|
99
|
+
keyName={keyName}
|
|
100
|
+
value={value}
|
|
101
|
+
collapsePaths={collapsePaths}
|
|
102
|
+
path={path}
|
|
103
|
+
/>
|
|
146
104
|
)
|
|
147
105
|
}
|
|
148
106
|
return <span />
|
|
@@ -161,16 +119,45 @@ const ArrayValue = ({
|
|
|
161
119
|
value,
|
|
162
120
|
keyName,
|
|
163
121
|
copyable,
|
|
122
|
+
defaultExpansionDepth,
|
|
123
|
+
depth,
|
|
124
|
+
collapsePaths,
|
|
125
|
+
path,
|
|
164
126
|
}: {
|
|
165
127
|
value: Array<any>
|
|
166
128
|
copyable?: boolean
|
|
167
129
|
keyName?: string
|
|
130
|
+
defaultExpansionDepth: number
|
|
131
|
+
depth: number
|
|
132
|
+
collapsePaths?: Array<string>
|
|
133
|
+
path: string
|
|
168
134
|
}) => {
|
|
169
135
|
const styles = useStyles()
|
|
170
|
-
|
|
136
|
+
|
|
137
|
+
const [expanded, setExpanded] = createSignal(
|
|
138
|
+
depth <= defaultExpansionDepth && !collapsePaths?.includes(path),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (value.length === 0) {
|
|
142
|
+
return (
|
|
143
|
+
<span class={styles().tree.expanderContainer}>
|
|
144
|
+
{keyName && (
|
|
145
|
+
<span class={clsx(styles().tree.valueKey, styles().tree.collapsible)}>
|
|
146
|
+
"{keyName}":{' '}
|
|
147
|
+
</span>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
<span class={styles().tree.valueBraces}>[]</span>
|
|
151
|
+
</span>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
171
154
|
return (
|
|
172
155
|
<span class={styles().tree.expanderContainer}>
|
|
173
|
-
<Expander
|
|
156
|
+
<Expander
|
|
157
|
+
onClick={() => setExpanded(!expanded())}
|
|
158
|
+
expanded={expanded()}
|
|
159
|
+
/>
|
|
160
|
+
|
|
174
161
|
{keyName && (
|
|
175
162
|
<span
|
|
176
163
|
onclick={(e) => {
|
|
@@ -184,7 +171,9 @@ const ArrayValue = ({
|
|
|
184
171
|
<span class={styles().tree.info}>{value.length} items</span>
|
|
185
172
|
</span>
|
|
186
173
|
)}
|
|
174
|
+
|
|
187
175
|
<span class={styles().tree.valueBraces}>[</span>
|
|
176
|
+
|
|
188
177
|
<Show when={expanded()}>
|
|
189
178
|
<span class={styles().tree.expandedLine(Boolean(keyName))}>
|
|
190
179
|
<For each={value}>
|
|
@@ -195,12 +184,17 @@ const ArrayValue = ({
|
|
|
195
184
|
copyable={copyable}
|
|
196
185
|
value={item}
|
|
197
186
|
isLastKey={isLastKey}
|
|
187
|
+
defaultExpansionDepth={defaultExpansionDepth}
|
|
188
|
+
depth={depth + 1}
|
|
189
|
+
collapsePaths={collapsePaths}
|
|
190
|
+
path={path ? `${path}[${i()}]` : `[${i()}]`}
|
|
198
191
|
/>
|
|
199
192
|
)
|
|
200
193
|
}}
|
|
201
194
|
</For>
|
|
202
195
|
</span>
|
|
203
196
|
</Show>
|
|
197
|
+
|
|
204
198
|
<Show when={!expanded()}>
|
|
205
199
|
<span
|
|
206
200
|
onClick={(e) => {
|
|
@@ -222,19 +216,51 @@ const ObjectValue = ({
|
|
|
222
216
|
value,
|
|
223
217
|
keyName,
|
|
224
218
|
copyable,
|
|
219
|
+
defaultExpansionDepth,
|
|
220
|
+
depth,
|
|
221
|
+
collapsePaths,
|
|
222
|
+
path,
|
|
225
223
|
}: {
|
|
226
224
|
value: Record<string, any>
|
|
227
225
|
keyName?: string
|
|
228
226
|
copyable?: boolean
|
|
227
|
+
defaultExpansionDepth: number
|
|
228
|
+
depth: number
|
|
229
|
+
collapsePaths?: Array<string>
|
|
230
|
+
path: string
|
|
229
231
|
}) => {
|
|
230
232
|
const styles = useStyles()
|
|
231
|
-
|
|
233
|
+
|
|
234
|
+
const [expanded, setExpanded] = createSignal(
|
|
235
|
+
depth <= defaultExpansionDepth && !collapsePaths?.includes(path),
|
|
236
|
+
)
|
|
237
|
+
|
|
232
238
|
const keys = Object.keys(value)
|
|
233
239
|
const lastKeyName = keys[keys.length - 1]
|
|
234
240
|
|
|
241
|
+
if (keys.length === 0) {
|
|
242
|
+
return (
|
|
243
|
+
<span class={styles().tree.expanderContainer}>
|
|
244
|
+
{keyName && (
|
|
245
|
+
<span class={clsx(styles().tree.valueKey, styles().tree.collapsible)}>
|
|
246
|
+
"{keyName}":{' '}
|
|
247
|
+
</span>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
<span class={styles().tree.valueBraces}>{'{}'}</span>
|
|
251
|
+
</span>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
return (
|
|
236
256
|
<span class={styles().tree.expanderContainer}>
|
|
237
|
-
{keyName &&
|
|
257
|
+
{keyName && (
|
|
258
|
+
<Expander
|
|
259
|
+
onClick={() => setExpanded(!expanded())}
|
|
260
|
+
expanded={expanded()}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
|
|
238
264
|
{keyName && (
|
|
239
265
|
<span
|
|
240
266
|
onClick={(e) => {
|
|
@@ -248,7 +274,9 @@ const ObjectValue = ({
|
|
|
248
274
|
<span class={styles().tree.info}>{keys.length} items</span>
|
|
249
275
|
</span>
|
|
250
276
|
)}
|
|
277
|
+
|
|
251
278
|
<span class={styles().tree.valueBraces}>{'{'}</span>
|
|
279
|
+
|
|
252
280
|
<Show when={expanded()}>
|
|
253
281
|
<span class={styles().tree.expandedLine(Boolean(keyName))}>
|
|
254
282
|
<For each={keys}>
|
|
@@ -259,12 +287,17 @@ const ObjectValue = ({
|
|
|
259
287
|
keyName={k}
|
|
260
288
|
isLastKey={lastKeyName === k}
|
|
261
289
|
copyable={copyable}
|
|
290
|
+
defaultExpansionDepth={defaultExpansionDepth}
|
|
291
|
+
depth={depth + 1}
|
|
292
|
+
collapsePaths={collapsePaths}
|
|
293
|
+
path={`${path}${path ? '.' : ''}${k}`}
|
|
262
294
|
/>
|
|
263
295
|
</>
|
|
264
296
|
)}
|
|
265
297
|
</For>
|
|
266
298
|
</span>
|
|
267
299
|
</Show>
|
|
300
|
+
|
|
268
301
|
<Show when={!expanded()}>
|
|
269
302
|
<span
|
|
270
303
|
onClick={(e) => {
|
|
@@ -277,7 +310,100 @@ const ObjectValue = ({
|
|
|
277
310
|
{`...`}
|
|
278
311
|
</span>
|
|
279
312
|
</Show>
|
|
313
|
+
|
|
280
314
|
<span class={styles().tree.valueBraces}>{'}'}</span>
|
|
281
315
|
</span>
|
|
282
316
|
)
|
|
283
317
|
}
|
|
318
|
+
|
|
319
|
+
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
|
|
320
|
+
|
|
321
|
+
const CopyButton = (props: { value: unknown }) => {
|
|
322
|
+
const styles = useStyles()
|
|
323
|
+
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<button
|
|
327
|
+
class={styles().tree.actionButton}
|
|
328
|
+
title="Copy object to clipboard"
|
|
329
|
+
aria-label={`${
|
|
330
|
+
copyState() === 'NoCopy'
|
|
331
|
+
? 'Copy object to clipboard'
|
|
332
|
+
: copyState() === 'SuccessCopy'
|
|
333
|
+
? 'Object copied to clipboard'
|
|
334
|
+
: 'Error copying object to clipboard'
|
|
335
|
+
}`}
|
|
336
|
+
onClick={
|
|
337
|
+
copyState() === 'NoCopy'
|
|
338
|
+
? () => {
|
|
339
|
+
navigator.clipboard
|
|
340
|
+
.writeText(JSON.stringify(props.value, null, 2))
|
|
341
|
+
.then(
|
|
342
|
+
() => {
|
|
343
|
+
setCopyState('SuccessCopy')
|
|
344
|
+
setTimeout(() => {
|
|
345
|
+
setCopyState('NoCopy')
|
|
346
|
+
}, 1500)
|
|
347
|
+
},
|
|
348
|
+
(err) => {
|
|
349
|
+
console.error('Failed to copy: ', err)
|
|
350
|
+
setCopyState('ErrorCopy')
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
setCopyState('NoCopy')
|
|
353
|
+
}, 1500)
|
|
354
|
+
},
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
: undefined
|
|
358
|
+
}
|
|
359
|
+
>
|
|
360
|
+
<Switch>
|
|
361
|
+
<Match when={copyState() === 'NoCopy'}>
|
|
362
|
+
<Copier />
|
|
363
|
+
</Match>
|
|
364
|
+
<Match when={copyState() === 'SuccessCopy'}>
|
|
365
|
+
<CopiedCopier theme={'dark'} />
|
|
366
|
+
</Match>
|
|
367
|
+
<Match when={copyState() === 'ErrorCopy'}>
|
|
368
|
+
<ErrorCopier />
|
|
369
|
+
</Match>
|
|
370
|
+
</Switch>
|
|
371
|
+
</button>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const Expander = (props: { expanded: boolean; onClick: () => void }) => {
|
|
376
|
+
const styles = useStyles()
|
|
377
|
+
return (
|
|
378
|
+
<span
|
|
379
|
+
onClick={props.onClick}
|
|
380
|
+
class={clsx(
|
|
381
|
+
styles().tree.expander,
|
|
382
|
+
css`
|
|
383
|
+
transform: rotate(${props.expanded ? 90 : 0}deg);
|
|
384
|
+
`,
|
|
385
|
+
props.expanded &&
|
|
386
|
+
css`
|
|
387
|
+
& svg {
|
|
388
|
+
top: -1px;
|
|
389
|
+
}
|
|
390
|
+
`,
|
|
391
|
+
)}
|
|
392
|
+
>
|
|
393
|
+
<svg
|
|
394
|
+
width="16"
|
|
395
|
+
height="16"
|
|
396
|
+
viewBox="0 0 16 16"
|
|
397
|
+
fill="none"
|
|
398
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
399
|
+
>
|
|
400
|
+
<path
|
|
401
|
+
d="M6 12L10 8L6 4"
|
|
402
|
+
stroke-width="2"
|
|
403
|
+
stroke-linecap="round"
|
|
404
|
+
stroke-linejoin="round"
|
|
405
|
+
/>
|
|
406
|
+
</svg>
|
|
407
|
+
</span>
|
|
408
|
+
)
|
|
409
|
+
}
|
package/src/styles/use-styles.ts
CHANGED
|
@@ -59,6 +59,8 @@ const stylesFactory = (theme: Theme = 'dark') => {
|
|
|
59
59
|
|
|
60
60
|
const t = (light: string, dark: string) => (theme === 'light' ? light : dark)
|
|
61
61
|
|
|
62
|
+
const wrapperSize = 320
|
|
63
|
+
|
|
62
64
|
return {
|
|
63
65
|
logo: css`
|
|
64
66
|
cursor: pointer;
|
|
@@ -78,7 +80,7 @@ const stylesFactory = (theme: Theme = 'dark') => {
|
|
|
78
80
|
|
|
79
81
|
selectWrapper: css`
|
|
80
82
|
width: 100%;
|
|
81
|
-
max-width:
|
|
83
|
+
max-width: ${wrapperSize}px;
|
|
82
84
|
display: flex;
|
|
83
85
|
flex-direction: column;
|
|
84
86
|
gap: 0.375rem;
|
|
@@ -129,7 +131,7 @@ const stylesFactory = (theme: Theme = 'dark') => {
|
|
|
129
131
|
`,
|
|
130
132
|
inputWrapper: css`
|
|
131
133
|
width: 100%;
|
|
132
|
-
max-width:
|
|
134
|
+
max-width: ${wrapperSize}px;
|
|
133
135
|
display: flex;
|
|
134
136
|
flex-direction: column;
|
|
135
137
|
gap: 0.375rem;
|
|
@@ -152,6 +154,7 @@ const stylesFactory = (theme: Theme = 'dark') => {
|
|
|
152
154
|
`,
|
|
153
155
|
input: css`
|
|
154
156
|
appearance: none;
|
|
157
|
+
box-sizing: border-box;
|
|
155
158
|
width: 100%;
|
|
156
159
|
padding: 0.75rem;
|
|
157
160
|
border-radius: 0.5rem;
|
|
@@ -393,6 +396,7 @@ const stylesFactory = (theme: Theme = 'dark') => {
|
|
|
393
396
|
`,
|
|
394
397
|
expander: css`
|
|
395
398
|
position: absolute;
|
|
399
|
+
cursor: pointer;
|
|
396
400
|
left: -16px;
|
|
397
401
|
top: 3px;
|
|
398
402
|
& path {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type CollapsibleKeys<T, TPrefix extends string = ''> =
|
|
2
|
+
T extends ReadonlyArray<infer U>
|
|
3
|
+
?
|
|
4
|
+
| (TPrefix extends '' ? '' : TPrefix)
|
|
5
|
+
| CollapsibleKeys<U, `${TPrefix}[${number}]`>
|
|
6
|
+
: T extends object
|
|
7
|
+
?
|
|
8
|
+
| (TPrefix extends '' ? '' : TPrefix)
|
|
9
|
+
| {
|
|
10
|
+
[K in Extract<keyof T, string>]: CollapsibleKeys<
|
|
11
|
+
T[K],
|
|
12
|
+
TPrefix extends '' ? `${K}` : `${TPrefix}.${K}`
|
|
13
|
+
>
|
|
14
|
+
}[Extract<keyof T, string>]
|
|
15
|
+
: never
|
|
16
|
+
|
|
17
|
+
export type CollapsiblePaths<T> =
|
|
18
|
+
CollapsibleKeys<T> extends string ? CollapsibleKeys<T> : never
|