@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.
@@ -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: { value: any; copyable?: boolean }) {
7
- return <JsonValue isRoot value={props.value} copyable={props.copyable} />
8
- }
9
- type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
10
-
11
- const CopyButton = (props: { value: unknown }) => {
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
- <span
69
- class={clsx(
70
- styles().tree.expander,
71
- css`
72
- transform: rotate(${props.expanded ? 90 : 0}deg);
73
- `,
74
- props.expanded &&
75
- css`
76
- & svg {
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 { value, keyName, isRoot = false, isLastKey, copyable } = props
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 copyable={copyable} keyName={keyName} value={value} />
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 copyable={copyable} keyName={keyName} value={value} />
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
- const [expanded, setExpanded] = createSignal(true)
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
+ &quot;{keyName}&quot;:{' '}
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 expanded={expanded()} />
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
- const [expanded, setExpanded] = createSignal(true)
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
+ &quot;{keyName}&quot;:{' '}
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 && <Expander expanded={expanded()} />}
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
+ }
@@ -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: 300px;
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: 300px;
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