@tanstack/router-devtools 1.111.11 → 1.112.4

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.
Files changed (48) hide show
  1. package/dist/cjs/AgeTicker.cjs +58 -0
  2. package/dist/cjs/AgeTicker.cjs.map +1 -0
  3. package/dist/cjs/AgeTicker.d.cts +5 -0
  4. package/dist/cjs/BaseTanStackRouterDevtoolsPanel.cjs +421 -0
  5. package/dist/cjs/BaseTanStackRouterDevtoolsPanel.cjs.map +1 -0
  6. package/dist/cjs/BaseTanStackRouterDevtoolsPanel.d.cts +3 -0
  7. package/dist/cjs/TanStackRouterDevtools.cjs +177 -0
  8. package/dist/cjs/TanStackRouterDevtools.cjs.map +1 -0
  9. package/dist/cjs/{devtools.d.cts → TanStackRouterDevtools.d.cts} +0 -31
  10. package/dist/cjs/TanStackRouterDevtoolsPanel.cjs +21 -0
  11. package/dist/cjs/TanStackRouterDevtoolsPanel.cjs.map +1 -0
  12. package/dist/cjs/TanStackRouterDevtoolsPanel.d.cts +33 -0
  13. package/dist/cjs/index.cjs +4 -3
  14. package/dist/cjs/index.cjs.map +1 -1
  15. package/dist/cjs/index.d.cts +2 -1
  16. package/dist/cjs/useStyles.cjs +570 -0
  17. package/dist/cjs/useStyles.cjs.map +1 -0
  18. package/dist/cjs/useStyles.d.cts +52 -0
  19. package/dist/esm/AgeTicker.d.ts +5 -0
  20. package/dist/esm/AgeTicker.js +58 -0
  21. package/dist/esm/AgeTicker.js.map +1 -0
  22. package/dist/esm/BaseTanStackRouterDevtoolsPanel.d.ts +3 -0
  23. package/dist/esm/BaseTanStackRouterDevtoolsPanel.js +421 -0
  24. package/dist/esm/BaseTanStackRouterDevtoolsPanel.js.map +1 -0
  25. package/dist/esm/{devtools.d.ts → TanStackRouterDevtools.d.ts} +0 -31
  26. package/dist/esm/TanStackRouterDevtools.js +177 -0
  27. package/dist/esm/TanStackRouterDevtools.js.map +1 -0
  28. package/dist/esm/TanStackRouterDevtoolsPanel.d.ts +33 -0
  29. package/dist/esm/TanStackRouterDevtoolsPanel.js +21 -0
  30. package/dist/esm/TanStackRouterDevtoolsPanel.js.map +1 -0
  31. package/dist/esm/index.d.ts +2 -1
  32. package/dist/esm/index.js +2 -1
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/useStyles.d.ts +52 -0
  35. package/dist/esm/useStyles.js +553 -0
  36. package/dist/esm/useStyles.js.map +1 -0
  37. package/package.json +2 -2
  38. package/src/AgeTicker.tsx +73 -0
  39. package/src/BaseTanStackRouterDevtoolsPanel.tsx +488 -0
  40. package/src/TanStackRouterDevtools.tsx +250 -0
  41. package/src/TanStackRouterDevtoolsPanel.tsx +54 -0
  42. package/src/index.tsx +2 -1
  43. package/src/useStyles.tsx +589 -0
  44. package/dist/cjs/devtools.cjs +0 -1212
  45. package/dist/cjs/devtools.cjs.map +0 -1
  46. package/dist/esm/devtools.js +0 -1195
  47. package/dist/esm/devtools.js.map +0 -1
  48. package/src/devtools.tsx +0 -1443
@@ -0,0 +1,488 @@
1
+ import React from 'react'
2
+ import { clsx as cx } from 'clsx'
3
+ import {
4
+ invariant,
5
+ rootRouteId,
6
+ trimPath,
7
+ useRouter,
8
+ useRouterState,
9
+ } from '@tanstack/react-router'
10
+ import { useDevtoolsOnClose } from './context'
11
+ import { useStyles } from './useStyles'
12
+ import useLocalStorage from './useLocalStorage'
13
+ import Explorer from './Explorer'
14
+ import { getRouteStatusColor, getStatusColor, multiSortBy } from './utils'
15
+ import { AgeTicker } from './AgeTicker'
16
+ import type { DevtoolsPanelOptions } from './TanStackRouterDevtoolsPanel'
17
+
18
+ import type {
19
+ AnyRootRoute,
20
+ AnyRoute,
21
+ AnyRouter,
22
+ Route,
23
+ } from '@tanstack/react-router'
24
+
25
+ function Logo(props: React.HTMLAttributes<HTMLButtonElement>) {
26
+ const { className, ...rest } = props
27
+ const styles = useStyles()
28
+ return (
29
+ <button {...rest} className={cx(styles.logo, className)}>
30
+ <div className={styles.tanstackLogo}>TANSTACK</div>
31
+ <div className={styles.routerLogo}>React Router v1</div>
32
+ </button>
33
+ )
34
+ }
35
+
36
+ function RouteComp({
37
+ router,
38
+ route,
39
+ isRoot,
40
+ activeId,
41
+ setActiveId,
42
+ }: {
43
+ router: AnyRouter
44
+ route: AnyRootRoute | AnyRoute
45
+ isRoot?: boolean
46
+ activeId: string | undefined
47
+ setActiveId: (id: string) => void
48
+ }) {
49
+ const routerState = useRouterState({
50
+ router,
51
+ } as any)
52
+ const styles = useStyles()
53
+ const matches = routerState.pendingMatches || routerState.matches
54
+ const match = routerState.matches.find((d) => d.routeId === route.id)
55
+
56
+ const param = React.useMemo(() => {
57
+ try {
58
+ if (match?.params) {
59
+ const p = match.params
60
+ const r: string = route.path || trimPath(route.id)
61
+ if (r.startsWith('$')) {
62
+ const trimmed = r.slice(1)
63
+ if (p[trimmed]) {
64
+ return `(${p[trimmed]})`
65
+ }
66
+ }
67
+ }
68
+ return ''
69
+ } catch (error) {
70
+ return ''
71
+ }
72
+ }, [match, route])
73
+
74
+ return (
75
+ <div>
76
+ <div
77
+ role="button"
78
+ aria-label={`Open match details for ${route.id}`}
79
+ onClick={() => {
80
+ if (match) {
81
+ setActiveId(activeId === route.id ? '' : route.id)
82
+ }
83
+ }}
84
+ className={cx(
85
+ styles.routesRowContainer(route.id === activeId, !!match),
86
+ )}
87
+ >
88
+ <div
89
+ className={cx(
90
+ styles.matchIndicator(getRouteStatusColor(matches, route)),
91
+ )}
92
+ />
93
+ <div className={cx(styles.routesRow(!!match))}>
94
+ <div>
95
+ <code className={styles.code}>
96
+ {isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '}
97
+ </code>
98
+ <code className={styles.routeParamInfo}>{param}</code>
99
+ </div>
100
+ <AgeTicker match={match} router={router} />
101
+ </div>
102
+ </div>
103
+ {route.children?.length ? (
104
+ <div className={styles.nestedRouteRow(!!isRoot)}>
105
+ {[...(route.children as Array<Route>)]
106
+ .sort((a, b) => {
107
+ return a.rank - b.rank
108
+ })
109
+ .map((r) => (
110
+ <RouteComp
111
+ key={r.id}
112
+ router={router}
113
+ route={r}
114
+ activeId={activeId}
115
+ setActiveId={setActiveId}
116
+ />
117
+ ))}
118
+ </div>
119
+ ) : null}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ export const BaseTanStackRouterDevtoolsPanel = React.forwardRef<
125
+ HTMLDivElement,
126
+ DevtoolsPanelOptions
127
+ >(function BaseTanStackRouterDevtoolsPanel(props, ref): React.ReactElement {
128
+ const {
129
+ isOpen = true,
130
+ setIsOpen,
131
+ handleDragStart,
132
+ router: userRouter,
133
+ shadowDOMTarget,
134
+ ...panelProps
135
+ } = props
136
+
137
+ const { onCloseClick } = useDevtoolsOnClose()
138
+ const styles = useStyles()
139
+ const { className, ...otherPanelProps } = panelProps
140
+
141
+ const contextRouter = useRouter({ warn: false })
142
+ const router = userRouter ?? contextRouter
143
+ const routerState = useRouterState({
144
+ router,
145
+ } as any)
146
+
147
+ invariant(
148
+ router,
149
+ 'No router was found for the TanStack Router Devtools. Please place the devtools in the <RouterProvider> component tree or pass the router instance to the devtools manually.',
150
+ )
151
+
152
+ // useStore(router.__store)
153
+
154
+ const [showMatches, setShowMatches] = useLocalStorage(
155
+ 'tanstackRouterDevtoolsShowMatches',
156
+ true,
157
+ )
158
+
159
+ const [activeId, setActiveId] = useLocalStorage(
160
+ 'tanstackRouterDevtoolsActiveRouteId',
161
+ '',
162
+ )
163
+
164
+ const activeMatch = React.useMemo(() => {
165
+ const matches = [
166
+ ...(routerState.pendingMatches ?? []),
167
+ ...routerState.matches,
168
+ ...routerState.cachedMatches,
169
+ ]
170
+ return matches.find((d) => d.routeId === activeId || d.id === activeId)
171
+ }, [
172
+ activeId,
173
+ routerState.cachedMatches,
174
+ routerState.matches,
175
+ routerState.pendingMatches,
176
+ ])
177
+
178
+ const hasSearch = Object.keys(routerState.location.search).length
179
+
180
+ const explorerState = {
181
+ ...router,
182
+ state: router.state,
183
+ }
184
+
185
+ return (
186
+ <div
187
+ ref={ref}
188
+ className={cx(
189
+ styles.devtoolsPanel,
190
+ 'TanStackRouterDevtoolsPanel',
191
+ className,
192
+ )}
193
+ {...otherPanelProps}
194
+ >
195
+ {handleDragStart ? (
196
+ <div className={styles.dragHandle} onMouseDown={handleDragStart}></div>
197
+ ) : null}
198
+ <button
199
+ className={styles.panelCloseBtn}
200
+ onClick={(e) => {
201
+ setIsOpen(false)
202
+ onCloseClick(e)
203
+ }}
204
+ >
205
+ <svg
206
+ xmlns="http://www.w3.org/2000/svg"
207
+ width="10"
208
+ height="6"
209
+ fill="none"
210
+ viewBox="0 0 10 6"
211
+ className={styles.panelCloseBtnIcon}
212
+ >
213
+ <path
214
+ stroke="currentColor"
215
+ strokeLinecap="round"
216
+ strokeLinejoin="round"
217
+ strokeWidth="1.667"
218
+ d="M1 1l4 4 4-4"
219
+ ></path>
220
+ </svg>
221
+ </button>
222
+ <div className={styles.firstContainer}>
223
+ <div className={styles.row}>
224
+ <Logo
225
+ aria-hidden
226
+ onClick={(e) => {
227
+ setIsOpen(false)
228
+ onCloseClick(e)
229
+ }}
230
+ />
231
+ </div>
232
+ <div className={styles.routerExplorerContainer}>
233
+ <div className={styles.routerExplorer}>
234
+ <Explorer
235
+ label="Router"
236
+ value={Object.fromEntries(
237
+ multiSortBy(
238
+ Object.keys(explorerState),
239
+ (
240
+ [
241
+ 'state',
242
+ 'routesById',
243
+ 'routesByPath',
244
+ 'flatRoutes',
245
+ 'options',
246
+ 'manifest',
247
+ ] as const
248
+ ).map((d) => (dd) => dd !== d),
249
+ )
250
+ .map((key) => [key, (explorerState as any)[key]])
251
+ .filter(
252
+ (d) =>
253
+ typeof d[1] !== 'function' &&
254
+ ![
255
+ '__store',
256
+ 'basepath',
257
+ 'injectedHtml',
258
+ 'subscribers',
259
+ 'latestLoadPromise',
260
+ 'navigateTimeout',
261
+ 'resetNextScroll',
262
+ 'tempLocationKey',
263
+ 'latestLocation',
264
+ 'routeTree',
265
+ 'history',
266
+ ].includes(d[0]),
267
+ ),
268
+ )}
269
+ defaultExpanded={{
270
+ state: {} as any,
271
+ context: {} as any,
272
+ options: {} as any,
273
+ }}
274
+ filterSubEntries={(subEntries) => {
275
+ return subEntries.filter((d) => typeof d.value !== 'function')
276
+ }}
277
+ />
278
+ </div>
279
+ </div>
280
+ </div>
281
+ <div className={styles.secondContainer}>
282
+ <div className={styles.matchesContainer}>
283
+ <div className={styles.detailsHeader}>
284
+ <span>Pathname</span>
285
+ {routerState.location.maskedLocation ? (
286
+ <div className={styles.maskedBadgeContainer}>
287
+ <span className={styles.maskedBadge}>masked</span>
288
+ </div>
289
+ ) : null}
290
+ </div>
291
+ <div className={styles.detailsContent}>
292
+ <code>{routerState.location.pathname}</code>
293
+ {routerState.location.maskedLocation ? (
294
+ <code className={styles.maskedLocation}>
295
+ {routerState.location.maskedLocation.pathname}
296
+ </code>
297
+ ) : null}
298
+ </div>
299
+ <div className={styles.detailsHeader}>
300
+ <div className={styles.routeMatchesToggle}>
301
+ <button
302
+ type="button"
303
+ onClick={() => {
304
+ setShowMatches(false)
305
+ }}
306
+ disabled={!showMatches}
307
+ className={cx(styles.routeMatchesToggleBtn(!showMatches, true))}
308
+ >
309
+ Routes
310
+ </button>
311
+ <button
312
+ type="button"
313
+ onClick={() => {
314
+ setShowMatches(true)
315
+ }}
316
+ disabled={showMatches}
317
+ className={cx(
318
+ styles.routeMatchesToggleBtn(!!showMatches, false),
319
+ )}
320
+ >
321
+ Matches
322
+ </button>
323
+ </div>
324
+ <div className={styles.detailsHeaderInfo}>
325
+ <div>age / staleTime / gcTime</div>
326
+ </div>
327
+ </div>
328
+ <div className={cx(styles.routesContainer)}>
329
+ {!showMatches ? (
330
+ <RouteComp
331
+ router={router}
332
+ route={router.routeTree}
333
+ isRoot
334
+ activeId={activeId}
335
+ setActiveId={setActiveId}
336
+ />
337
+ ) : (
338
+ <div>
339
+ {(routerState.pendingMatches?.length
340
+ ? routerState.pendingMatches
341
+ : routerState.matches
342
+ ).map((match, i) => {
343
+ return (
344
+ <div
345
+ key={match.id || i}
346
+ role="button"
347
+ aria-label={`Open match details for ${match.id}`}
348
+ onClick={() =>
349
+ setActiveId(activeId === match.id ? '' : match.id)
350
+ }
351
+ className={cx(styles.matchRow(match === activeMatch))}
352
+ >
353
+ <div
354
+ className={cx(
355
+ styles.matchIndicator(getStatusColor(match)),
356
+ )}
357
+ />
358
+
359
+ <code
360
+ className={styles.matchID}
361
+ >{`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}</code>
362
+ <AgeTicker match={match} router={router} />
363
+ </div>
364
+ )
365
+ })}
366
+ </div>
367
+ )}
368
+ </div>
369
+ </div>
370
+ {routerState.cachedMatches.length ? (
371
+ <div className={styles.cachedMatchesContainer}>
372
+ <div className={styles.detailsHeader}>
373
+ <div>Cached Matches</div>
374
+ <div className={styles.detailsHeaderInfo}>
375
+ age / staleTime / gcTime
376
+ </div>
377
+ </div>
378
+ <div>
379
+ {routerState.cachedMatches.map((match) => {
380
+ return (
381
+ <div
382
+ key={match.id}
383
+ role="button"
384
+ aria-label={`Open match details for ${match.id}`}
385
+ onClick={() =>
386
+ setActiveId(activeId === match.id ? '' : match.id)
387
+ }
388
+ className={cx(styles.matchRow(match === activeMatch))}
389
+ >
390
+ <div
391
+ className={cx(
392
+ styles.matchIndicator(getStatusColor(match)),
393
+ )}
394
+ />
395
+
396
+ <code className={styles.matchID}>{`${match.id}`}</code>
397
+
398
+ <AgeTicker match={match} router={router} />
399
+ </div>
400
+ )
401
+ })}
402
+ </div>
403
+ </div>
404
+ ) : null}
405
+ </div>
406
+ {activeMatch ? (
407
+ <div className={styles.thirdContainer}>
408
+ <div className={styles.detailsHeader}>Match Details</div>
409
+ <div>
410
+ <div className={styles.matchDetails}>
411
+ <div
412
+ className={styles.matchStatus(
413
+ activeMatch.status,
414
+ activeMatch.isFetching,
415
+ )}
416
+ >
417
+ <div>
418
+ {activeMatch.status === 'success' && activeMatch.isFetching
419
+ ? 'fetching'
420
+ : activeMatch.status}
421
+ </div>
422
+ </div>
423
+ <div className={styles.matchDetailsInfoLabel}>
424
+ <div>ID:</div>
425
+ <div className={styles.matchDetailsInfo}>
426
+ <code>{activeMatch.id}</code>
427
+ </div>
428
+ </div>
429
+ <div className={styles.matchDetailsInfoLabel}>
430
+ <div>State:</div>
431
+ <div className={styles.matchDetailsInfo}>
432
+ {routerState.pendingMatches?.find(
433
+ (d) => d.id === activeMatch.id,
434
+ )
435
+ ? 'Pending'
436
+ : routerState.matches.find((d) => d.id === activeMatch.id)
437
+ ? 'Active'
438
+ : 'Cached'}
439
+ </div>
440
+ </div>
441
+ <div className={styles.matchDetailsInfoLabel}>
442
+ <div>Last Updated:</div>
443
+ <div className={styles.matchDetailsInfo}>
444
+ {activeMatch.updatedAt
445
+ ? new Date(activeMatch.updatedAt).toLocaleTimeString()
446
+ : 'N/A'}
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ {activeMatch.loaderData ? (
452
+ <>
453
+ <div className={styles.detailsHeader}>Loader Data</div>
454
+ <div className={styles.detailsContent}>
455
+ <Explorer
456
+ label="loaderData"
457
+ value={activeMatch.loaderData}
458
+ defaultExpanded={{}}
459
+ />
460
+ </div>
461
+ </>
462
+ ) : null}
463
+ <div className={styles.detailsHeader}>Explorer</div>
464
+ <div className={styles.detailsContent}>
465
+ <Explorer label="Match" value={activeMatch} defaultExpanded={{}} />
466
+ </div>
467
+ </div>
468
+ ) : null}
469
+ {hasSearch ? (
470
+ <div className={styles.fourthContainer}>
471
+ <div className={styles.detailsHeader}>Search Params</div>
472
+ <div className={styles.detailsContent}>
473
+ <Explorer
474
+ value={routerState.location.search}
475
+ defaultExpanded={Object.keys(routerState.location.search).reduce(
476
+ (obj: any, next) => {
477
+ obj[next] = {}
478
+ return obj
479
+ },
480
+ {},
481
+ )}
482
+ />
483
+ </div>
484
+ </div>
485
+ ) : null}
486
+ </div>
487
+ )
488
+ })