@tanstack/router-devtools 1.20.1 → 1.20.3-alpha.1

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 (57) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/index.cjs +12 -3
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/cjs/index.d.cts +2 -1
  5. package/dist/esm/index.d.ts +2 -1
  6. package/dist/esm/index.js +6 -3
  7. package/dist/esm/index.js.map +1 -1
  8. package/package.json +34 -34
  9. package/src/index.tsx +6 -1
  10. package/dist/cjs/Explorer.cjs +0 -306
  11. package/dist/cjs/Explorer.cjs.map +0 -1
  12. package/dist/cjs/Explorer.d.cts +0 -46
  13. package/dist/cjs/devtools.cjs +0 -1181
  14. package/dist/cjs/devtools.cjs.map +0 -1
  15. package/dist/cjs/devtools.d.cts +0 -65
  16. package/dist/cjs/logo.cjs +0 -1012
  17. package/dist/cjs/logo.cjs.map +0 -1
  18. package/dist/cjs/logo.d.cts +0 -2
  19. package/dist/cjs/theme.d.cts +0 -34
  20. package/dist/cjs/tokens.cjs +0 -302
  21. package/dist/cjs/tokens.cjs.map +0 -1
  22. package/dist/cjs/tokens.d.cts +0 -298
  23. package/dist/cjs/useLocalStorage.cjs +0 -45
  24. package/dist/cjs/useLocalStorage.cjs.map +0 -1
  25. package/dist/cjs/useLocalStorage.d.cts +0 -1
  26. package/dist/cjs/useMediaQuery.d.cts +0 -1
  27. package/dist/cjs/utils.cjs +0 -82
  28. package/dist/cjs/utils.cjs.map +0 -1
  29. package/dist/cjs/utils.d.cts +0 -23
  30. package/dist/esm/Explorer.d.ts +0 -46
  31. package/dist/esm/Explorer.js +0 -289
  32. package/dist/esm/Explorer.js.map +0 -1
  33. package/dist/esm/devtools.d.ts +0 -65
  34. package/dist/esm/devtools.js +0 -1181
  35. package/dist/esm/devtools.js.map +0 -1
  36. package/dist/esm/logo.d.ts +0 -2
  37. package/dist/esm/logo.js +0 -1012
  38. package/dist/esm/logo.js.map +0 -1
  39. package/dist/esm/theme.d.ts +0 -34
  40. package/dist/esm/tokens.d.ts +0 -298
  41. package/dist/esm/tokens.js +0 -302
  42. package/dist/esm/tokens.js.map +0 -1
  43. package/dist/esm/useLocalStorage.d.ts +0 -1
  44. package/dist/esm/useLocalStorage.js +0 -46
  45. package/dist/esm/useLocalStorage.js.map +0 -1
  46. package/dist/esm/useMediaQuery.d.ts +0 -1
  47. package/dist/esm/utils.d.ts +0 -23
  48. package/dist/esm/utils.js +0 -82
  49. package/dist/esm/utils.js.map +0 -1
  50. package/src/Explorer.tsx +0 -357
  51. package/src/devtools.tsx +0 -1401
  52. package/src/logo.tsx +0 -817
  53. package/src/theme.tsx +0 -31
  54. package/src/tokens.ts +0 -305
  55. package/src/useLocalStorage.ts +0 -52
  56. package/src/useMediaQuery.ts +0 -39
  57. package/src/utils.ts +0 -183
package/src/devtools.tsx DELETED
@@ -1,1401 +0,0 @@
1
- import React from 'react'
2
- import {
3
- invariant,
4
- AnyRouter,
5
- Route,
6
- AnyRoute,
7
- AnyRootRoute,
8
- trimPath,
9
- useRouter,
10
- useRouterState,
11
- AnyRouteMatch,
12
- rootRouteId,
13
- } from '@tanstack/react-router'
14
-
15
- import useLocalStorage from './useLocalStorage'
16
- import {
17
- getRouteStatusColor,
18
- getStatusColor,
19
- multiSortBy,
20
- useIsMounted,
21
- useSafeState,
22
- } from './utils'
23
- import { css } from 'goober'
24
- import { clsx as cx } from 'clsx'
25
- import Explorer from './Explorer'
26
- import { tokens } from './tokens'
27
- import { TanStackLogo } from './logo'
28
-
29
- export type PartialKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
30
-
31
- interface DevtoolsOptions {
32
- /**
33
- * Set this true if you want the dev tools to default to being open
34
- */
35
- initialIsOpen?: boolean
36
- /**
37
- * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc.
38
- */
39
- panelProps?: React.DetailedHTMLProps<
40
- React.HTMLAttributes<HTMLDivElement>,
41
- HTMLDivElement
42
- >
43
- /**
44
- * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
45
- */
46
- closeButtonProps?: React.DetailedHTMLProps<
47
- React.ButtonHTMLAttributes<HTMLButtonElement>,
48
- HTMLButtonElement
49
- >
50
- /**
51
- * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
52
- */
53
- toggleButtonProps?: React.DetailedHTMLProps<
54
- React.ButtonHTMLAttributes<HTMLButtonElement>,
55
- HTMLButtonElement
56
- >
57
- /**
58
- * The position of the TanStack Router logo to open and close the devtools panel.
59
- * Defaults to 'bottom-left'.
60
- */
61
- position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
62
- /**
63
- * Use this to render the devtools inside a different type of container element for a11y purposes.
64
- * Any string which corresponds to a valid intrinsic JSX element is allowed.
65
- * Defaults to 'footer'.
66
- */
67
- containerElement?: string | any
68
- /**
69
- * A boolean variable indicating if the "lite" version of the library is being used
70
- */
71
- router?: AnyRouter
72
- }
73
-
74
- interface DevtoolsPanelOptions {
75
- /**
76
- * The standard React style object used to style a component with inline styles
77
- */
78
- style?: React.CSSProperties
79
- /**
80
- * The standard React className property used to style a component with classes
81
- */
82
- className?: string
83
- /**
84
- * A boolean variable indicating whether the panel is open or closed
85
- */
86
- isOpen?: boolean
87
- /**
88
- * A function that toggles the open and close state of the panel
89
- */
90
- setIsOpen: (isOpen: boolean) => void
91
- /**
92
- * Handles the opening and closing the devtools panel
93
- */
94
- handleDragStart?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
95
- /**
96
- * A boolean variable indicating if the "lite" version of the library is being used
97
- */
98
- router?: AnyRouter
99
- }
100
-
101
- const isServer = typeof window === 'undefined'
102
-
103
- function Logo(props: React.HTMLAttributes<HTMLButtonElement>) {
104
- const { className, ...rest } = props
105
- return (
106
- <button {...rest} className={cx(getStyles().logo, className)}>
107
- <div className={getStyles().tanstackLogo}>TANSTACK</div>
108
- <div className={getStyles().routerLogo}>React Router v1</div>
109
- </button>
110
- )
111
- }
112
-
113
- const DevtoolsOnCloseContext = React.createContext<
114
- | {
115
- onCloseClick: (e: React.MouseEvent<HTMLButtonElement>) => void
116
- }
117
- | undefined
118
- >(undefined)
119
-
120
- const useDevtoolsOnClose = () => {
121
- const context = React.useContext(DevtoolsOnCloseContext)
122
- if (!context) {
123
- throw new Error(
124
- 'useDevtoolsOnClose must be used within a TanStackRouterDevtools component',
125
- )
126
- }
127
- return context
128
- }
129
-
130
- export function TanStackRouterDevtools({
131
- initialIsOpen,
132
- panelProps = {},
133
- closeButtonProps = {},
134
- toggleButtonProps = {},
135
- position = 'bottom-left',
136
- containerElement: Container = 'footer',
137
- router,
138
- }: DevtoolsOptions): React.ReactElement | null {
139
- const [rootEl, setRootEl] = React.useState<HTMLDivElement>(null!)
140
- const panelRef = React.useRef<HTMLDivElement>(null)
141
- const [isOpen, setIsOpen] = useLocalStorage(
142
- 'tanstackRouterDevtoolsOpen',
143
- initialIsOpen,
144
- )
145
- const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage<number | null>(
146
- 'tanstackRouterDevtoolsHeight',
147
- null,
148
- )
149
- const [isResolvedOpen, setIsResolvedOpen] = useSafeState(false)
150
- const [isResizing, setIsResizing] = useSafeState(false)
151
- const isMounted = useIsMounted()
152
-
153
- const handleDragStart = (
154
- panelElement: HTMLDivElement | null,
155
- startEvent: React.MouseEvent<HTMLDivElement, MouseEvent>,
156
- ) => {
157
- if (startEvent.button !== 0) return // Only allow left click for drag
158
-
159
- setIsResizing(true)
160
-
161
- const dragInfo = {
162
- originalHeight: panelElement?.getBoundingClientRect().height ?? 0,
163
- pageY: startEvent.pageY,
164
- }
165
-
166
- const run = (moveEvent: MouseEvent) => {
167
- const delta = dragInfo.pageY - moveEvent.pageY
168
- const newHeight = dragInfo?.originalHeight + delta
169
-
170
- setDevtoolsHeight(newHeight)
171
-
172
- if (newHeight < 70) {
173
- setIsOpen(false)
174
- } else {
175
- setIsOpen(true)
176
- }
177
- }
178
-
179
- const unsub = () => {
180
- setIsResizing(false)
181
- document.removeEventListener('mousemove', run)
182
- document.removeEventListener('mouseUp', unsub)
183
- }
184
-
185
- document.addEventListener('mousemove', run)
186
- document.addEventListener('mouseup', unsub)
187
- }
188
-
189
- const isButtonClosed = isOpen ?? false
190
-
191
- React.useEffect(() => {
192
- setIsResolvedOpen(isOpen ?? false)
193
- }, [isOpen, isResolvedOpen, setIsResolvedOpen])
194
-
195
- React.useEffect(() => {
196
- if (isResolvedOpen) {
197
- const previousValue = rootEl?.parentElement?.style.paddingBottom
198
-
199
- const run = () => {
200
- const containerHeight = panelRef.current?.getBoundingClientRect().height
201
- if (rootEl?.parentElement) {
202
- rootEl.parentElement.style.paddingBottom = `${containerHeight}px`
203
- }
204
- }
205
-
206
- run()
207
-
208
- if (typeof window !== 'undefined') {
209
- window.addEventListener('resize', run)
210
-
211
- return () => {
212
- window.removeEventListener('resize', run)
213
- if (rootEl?.parentElement && typeof previousValue === 'string') {
214
- rootEl.parentElement.style.paddingBottom = previousValue
215
- }
216
- }
217
- }
218
- }
219
- return
220
- }, [isResolvedOpen])
221
-
222
- React.useEffect(() => {
223
- if (rootEl) {
224
- const el = rootEl
225
- const fontSize = getComputedStyle(el).fontSize
226
- el.style.setProperty('--tsrd-font-size', fontSize)
227
- }
228
- }, [rootEl])
229
-
230
- const { style: panelStyle = {}, ...otherPanelProps } = panelProps
231
-
232
- const {
233
- style: closeButtonStyle = {},
234
- onClick: onCloseClick,
235
- ...otherCloseButtonProps
236
- } = closeButtonProps
237
-
238
- const {
239
- style: toggleButtonStyle = {},
240
- onClick: onToggleClick,
241
- ...otherToggleButtonProps
242
- } = toggleButtonProps
243
-
244
- // Do not render on the server
245
- if (!isMounted()) return null
246
-
247
- const resolvedHeight = devtoolsHeight ?? 500
248
-
249
- return (
250
- <Container ref={setRootEl} className="TanStackRouterDevtools">
251
- <DevtoolsOnCloseContext.Provider
252
- value={{
253
- onCloseClick: onCloseClick ?? (() => {}),
254
- }}
255
- >
256
- <TanStackRouterDevtoolsPanel
257
- ref={panelRef as any}
258
- {...otherPanelProps}
259
- router={router}
260
- className={cx(
261
- getStyles().devtoolsPanelContainer,
262
- getStyles().devtoolsPanelContainerVisibility(!!isOpen),
263
- getStyles().devtoolsPanelContainerResizing(isResizing),
264
- getStyles().devtoolsPanelContainerAnimation(
265
- isResolvedOpen,
266
- resolvedHeight + 16,
267
- ),
268
- )}
269
- style={{
270
- height: resolvedHeight,
271
- ...panelStyle,
272
- }}
273
- isOpen={isResolvedOpen}
274
- setIsOpen={setIsOpen}
275
- handleDragStart={(e) => handleDragStart(panelRef.current, e)}
276
- />
277
- </DevtoolsOnCloseContext.Provider>
278
-
279
- <button
280
- type="button"
281
- {...otherToggleButtonProps}
282
- aria-label="Open TanStack Router Devtools"
283
- onClick={(e) => {
284
- setIsOpen(true)
285
- onToggleClick && onToggleClick(e)
286
- }}
287
- className={cx(
288
- getStyles().mainCloseBtn,
289
- getStyles().mainCloseBtnPosition(position),
290
- getStyles().mainCloseBtnAnimation(!isButtonClosed),
291
- )}
292
- >
293
- <div className={getStyles().mainCloseBtnIconContainer}>
294
- <div className={getStyles().mainCloseBtnIconOuter}>
295
- <TanStackLogo />
296
- </div>
297
- <div className={getStyles().mainCloseBtnIconInner}>
298
- <TanStackLogo />
299
- </div>
300
- </div>
301
- <div className={getStyles().mainCloseBtnDivider}>-</div>
302
- <div className={getStyles().routerLogoCloseButton}>TanStack Router</div>
303
- </button>
304
- </Container>
305
- )
306
- }
307
-
308
- function RouteComp({
309
- route,
310
- isRoot,
311
- activeId,
312
- setActiveId,
313
- }: {
314
- route: AnyRootRoute | AnyRoute
315
- isRoot?: boolean
316
- activeId: string | undefined
317
- setActiveId: (id: string) => void
318
- }) {
319
- const routerState = useRouterState()
320
- const matches =
321
- routerState.status === 'pending'
322
- ? routerState.pendingMatches ?? []
323
- : routerState.matches
324
-
325
- const match = routerState.matches.find((d) => d.routeId === route.id)
326
-
327
- const param = React.useMemo(() => {
328
- try {
329
- if (match?.params) {
330
- const p = match.params
331
- const r: string = route.path || trimPath(route.id)
332
- if (r.startsWith('$')) {
333
- const trimmed = r.slice(1)
334
- if (p[trimmed]) {
335
- return `(${p[trimmed]})`
336
- }
337
- }
338
- }
339
- return ''
340
- } catch (error) {
341
- return ''
342
- }
343
- }, [match, route])
344
-
345
- return (
346
- <div>
347
- <div
348
- role="button"
349
- aria-label={`Open match details for ${route.id}`}
350
- onClick={() => {
351
- if (match) {
352
- setActiveId(activeId === route.id ? '' : route.id)
353
- }
354
- }}
355
- className={cx(
356
- getStyles().routesRowContainer(route.id === activeId, !!match),
357
- )}
358
- >
359
- <div
360
- className={cx(
361
- getStyles().matchIndicator(getRouteStatusColor(matches, route)),
362
- )}
363
- />
364
- <div className={cx(getStyles().routesRow(!!match))}>
365
- <div>
366
- <code className={getStyles().code}>
367
- {isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '}
368
- </code>
369
- <code className={getStyles().routeParamInfo}>{param}</code>
370
- </div>
371
- <AgeTicker match={match} />
372
- </div>
373
- </div>
374
- {(route.children as Route[])?.length ? (
375
- <div className={getStyles().nestedRouteRow(!!isRoot)}>
376
- {[...(route.children as Route[])]
377
- .sort((a, b) => {
378
- return a.rank - b.rank
379
- })
380
- .map((r) => (
381
- <RouteComp
382
- key={r.id}
383
- route={r}
384
- activeId={activeId}
385
- setActiveId={setActiveId}
386
- />
387
- ))}
388
- </div>
389
- ) : null}
390
- </div>
391
- )
392
- }
393
-
394
- export const TanStackRouterDevtoolsPanel = React.forwardRef<
395
- HTMLDivElement,
396
- DevtoolsPanelOptions
397
- >(function TanStackRouterDevtoolsPanel(props, ref): React.ReactElement {
398
- const {
399
- isOpen = true,
400
- setIsOpen,
401
- handleDragStart,
402
- router: userRouter,
403
- ...panelProps
404
- } = props
405
-
406
- const { onCloseClick } = useDevtoolsOnClose()
407
- const { className, ...otherPanelProps } = panelProps
408
-
409
- const contextRouter = useRouter({ warn: false })
410
- const router = userRouter ?? contextRouter
411
- const routerState = useRouterState({
412
- router,
413
- } as any)
414
-
415
- const matches = [
416
- ...(routerState.pendingMatches ?? []),
417
- ...routerState.matches,
418
- ...routerState.cachedMatches,
419
- ]
420
-
421
- invariant(
422
- router,
423
- '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.',
424
- )
425
-
426
- // useStore(router.__store)
427
-
428
- const [showMatches, setShowMatches] = useLocalStorage(
429
- 'tanstackRouterDevtoolsShowMatches',
430
- true,
431
- )
432
-
433
- const [activeId, setActiveId] = useLocalStorage(
434
- 'tanstackRouterDevtoolsActiveRouteId',
435
- '',
436
- )
437
-
438
- const activeMatch = React.useMemo(
439
- () => matches.find((d) => d.routeId === activeId || d.id === activeId),
440
- [matches, activeId],
441
- )
442
-
443
- const hasSearch = Object.keys(routerState.location.search || {}).length
444
-
445
- const explorerState = {
446
- ...router,
447
- state: router.state,
448
- }
449
-
450
- return (
451
- <div
452
- ref={ref}
453
- className={cx(
454
- getStyles().devtoolsPanel,
455
- 'TanStackRouterDevtoolsPanel',
456
- className,
457
- )}
458
- {...otherPanelProps}
459
- >
460
- {handleDragStart ? (
461
- <div
462
- className={getStyles().dragHandle}
463
- onMouseDown={handleDragStart}
464
- ></div>
465
- ) : null}
466
- <button
467
- className={getStyles().panelCloseBtn}
468
- onClick={(e) => {
469
- setIsOpen(false)
470
- onCloseClick(e)
471
- }}
472
- >
473
- <svg
474
- xmlns="http://www.w3.org/2000/svg"
475
- width="10"
476
- height="6"
477
- fill="none"
478
- viewBox="0 0 10 6"
479
- className={getStyles().panelCloseBtnIcon}
480
- >
481
- <path
482
- stroke="currentColor"
483
- strokeLinecap="round"
484
- strokeLinejoin="round"
485
- strokeWidth="1.667"
486
- d="M1 1l4 4 4-4"
487
- ></path>
488
- </svg>
489
- </button>
490
- <div className={getStyles().firstContainer}>
491
- <div className={getStyles().row}>
492
- <Logo
493
- aria-hidden
494
- onClick={(e) => {
495
- setIsOpen(false)
496
- onCloseClick(e)
497
- }}
498
- />
499
- </div>
500
- <div className={getStyles().routerExplorerContainer}>
501
- <div className={getStyles().routerExplorer}>
502
- <Explorer
503
- label="Router"
504
- value={Object.fromEntries(
505
- multiSortBy(
506
- Object.keys(explorerState),
507
- (
508
- [
509
- 'state',
510
- 'routesById',
511
- 'routesByPath',
512
- 'flatRoutes',
513
- 'options',
514
- ] as const
515
- ).map((d) => (dd) => dd !== d),
516
- )
517
- .map((key) => [key, (explorerState as any)[key]])
518
- .filter(
519
- (d) =>
520
- typeof d[1] !== 'function' &&
521
- ![
522
- '__store',
523
- 'basepath',
524
- 'injectedHtml',
525
- 'subscribers',
526
- 'latestLoadPromise',
527
- 'navigateTimeout',
528
- 'resetNextScroll',
529
- 'tempLocationKey',
530
- 'latestLocation',
531
- 'routeTree',
532
- 'history',
533
- ].includes(d[0]),
534
- ),
535
- )}
536
- defaultExpanded={{
537
- state: {} as any,
538
- context: {} as any,
539
- options: {} as any,
540
- }}
541
- filterSubEntries={(subEntries) => {
542
- return subEntries.filter((d) => typeof d.value !== 'function')
543
- }}
544
- />
545
- </div>
546
- </div>
547
- </div>
548
- <div className={getStyles().secondContainer}>
549
- <div className={getStyles().matchesContainer}>
550
- <div className={getStyles().detailsHeader}>
551
- <span>Pathname</span>
552
- {routerState.location.maskedLocation ? (
553
- <div className={getStyles().maskedBadgeContainer}>
554
- <span className={getStyles().maskedBadge}>masked</span>
555
- </div>
556
- ) : null}
557
- </div>
558
- <div className={getStyles().detailsContent}>
559
- <code>{routerState.location.pathname}</code>
560
- {routerState.location.maskedLocation ? (
561
- <code className={getStyles().maskedLocation}>
562
- {routerState.location.maskedLocation.pathname}
563
- </code>
564
- ) : null}
565
- </div>
566
- <div className={getStyles().detailsHeader}>
567
- <div className={getStyles().routeMatchesToggle}>
568
- <button
569
- type="button"
570
- onClick={() => {
571
- setShowMatches(false)
572
- }}
573
- disabled={!showMatches}
574
- className={cx(
575
- getStyles().routeMatchesToggleBtn(!showMatches, true),
576
- )}
577
- >
578
- Routes
579
- </button>
580
- <button
581
- type="button"
582
- onClick={() => {
583
- setShowMatches(true)
584
- }}
585
- disabled={showMatches}
586
- className={cx(
587
- getStyles().routeMatchesToggleBtn(!!showMatches, false),
588
- )}
589
- >
590
- Matches
591
- </button>
592
- </div>
593
- <div className={getStyles().detailsHeaderInfo}>
594
- <div>age / staleTime / gcTime</div>
595
- </div>
596
- </div>
597
- <div className={cx(getStyles().routesContainer)}>
598
- {!showMatches ? (
599
- <RouteComp
600
- route={router.routeTree}
601
- isRoot
602
- activeId={activeId}
603
- setActiveId={setActiveId}
604
- />
605
- ) : (
606
- <div>
607
- {(routerState.status === 'pending'
608
- ? routerState.pendingMatches ?? []
609
- : routerState.matches
610
- ).map((match, i) => {
611
- return (
612
- <div
613
- key={match.id || i}
614
- role="button"
615
- aria-label={`Open match details for ${match.id}`}
616
- onClick={() =>
617
- setActiveId(activeId === match.id ? '' : match.id)
618
- }
619
- className={cx(
620
- getStyles().matchRow(match === activeMatch),
621
- )}
622
- >
623
- <div
624
- className={cx(
625
- getStyles().matchIndicator(getStatusColor(match)),
626
- )}
627
- />
628
-
629
- <code
630
- className={getStyles().matchID}
631
- >{`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}</code>
632
- <AgeTicker match={match} />
633
- </div>
634
- )
635
- })}
636
- </div>
637
- )}
638
- </div>
639
- </div>
640
- {routerState.cachedMatches?.length ? (
641
- <div className={getStyles().cachedMatchesContainer}>
642
- <div className={getStyles().detailsHeader}>
643
- <div>Cached Matches</div>
644
- <div className={getStyles().detailsHeaderInfo}>
645
- age / staleTime / gcTime
646
- </div>
647
- </div>
648
- <div>
649
- {routerState.cachedMatches.map((match) => {
650
- return (
651
- <div
652
- key={match.id}
653
- role="button"
654
- aria-label={`Open match details for ${match.id}`}
655
- onClick={() =>
656
- setActiveId(activeId === match.id ? '' : match.id)
657
- }
658
- className={cx(getStyles().matchRow(match === activeMatch))}
659
- >
660
- <div
661
- className={cx(
662
- getStyles().matchIndicator(getStatusColor(match)),
663
- )}
664
- />
665
-
666
- <code className={getStyles().matchID}>{`${match.id}`}</code>
667
-
668
- <AgeTicker match={match} />
669
- </div>
670
- )
671
- })}
672
- </div>
673
- </div>
674
- ) : null}
675
- </div>
676
- {activeMatch ? (
677
- <div className={getStyles().thirdContainer}>
678
- <div className={getStyles().detailsHeader}>Match Details</div>
679
- <div>
680
- <div className={getStyles().matchDetails}>
681
- <div
682
- className={getStyles().matchStatus(
683
- activeMatch.status,
684
- activeMatch.isFetching,
685
- )}
686
- >
687
- <div>
688
- {activeMatch.status === 'success' && activeMatch.isFetching
689
- ? 'fetching'
690
- : activeMatch.status}
691
- </div>
692
- </div>
693
- <div className={getStyles().matchDetailsInfoLabel}>
694
- <div>ID:</div>
695
- <div className={getStyles().matchDetailsInfo}>
696
- <code>{activeMatch.id}</code>
697
- </div>
698
- </div>
699
- <div className={getStyles().matchDetailsInfoLabel}>
700
- <div>State:</div>
701
- <div className={getStyles().matchDetailsInfo}>
702
- {routerState.pendingMatches?.find(
703
- (d) => d.id === activeMatch.id,
704
- )
705
- ? 'Pending'
706
- : routerState.matches?.find((d) => d.id === activeMatch.id)
707
- ? 'Active'
708
- : 'Cached'}
709
- </div>
710
- </div>
711
- <div className={getStyles().matchDetailsInfoLabel}>
712
- <div>Last Updated:</div>
713
- <div className={getStyles().matchDetailsInfo}>
714
- {activeMatch.updatedAt
715
- ? new Date(
716
- activeMatch.updatedAt as number,
717
- ).toLocaleTimeString()
718
- : 'N/A'}
719
- </div>
720
- </div>
721
- </div>
722
- </div>
723
- {activeMatch.loaderData ? (
724
- <>
725
- <div className={getStyles().detailsHeader}>Loader Data</div>
726
- <div className={getStyles().detailsContent}>
727
- <Explorer
728
- label="loaderData"
729
- value={activeMatch.loaderData}
730
- defaultExpanded={{}}
731
- />
732
- </div>
733
- </>
734
- ) : null}
735
- <div className={getStyles().detailsHeader}>Explorer</div>
736
- <div className={getStyles().detailsContent}>
737
- <Explorer label="Match" value={activeMatch} defaultExpanded={{}} />
738
- </div>
739
- </div>
740
- ) : null}
741
- {hasSearch ? (
742
- <div className={getStyles().fourthContainer}>
743
- <div className={getStyles().detailsHeader}>Search Params</div>
744
- <div className={getStyles().detailsContent}>
745
- <Explorer
746
- value={routerState.location.search || {}}
747
- defaultExpanded={Object.keys(
748
- (routerState.location.search as {}) || {},
749
- ).reduce((obj: any, next) => {
750
- obj[next] = {}
751
- return obj
752
- }, {})}
753
- />
754
- </div>
755
- </div>
756
- ) : null}
757
- </div>
758
- )
759
- })
760
-
761
- function AgeTicker({ match }: { match?: AnyRouteMatch }) {
762
- const router = useRouter()
763
-
764
- const rerender = React.useReducer(
765
- () => ({}),
766
- () => ({}),
767
- )[1]
768
-
769
- React.useEffect(() => {
770
- const interval = setInterval(() => {
771
- rerender()
772
- }, 1000)
773
-
774
- return () => {
775
- clearInterval(interval)
776
- }
777
- }, [])
778
-
779
- if (!match) {
780
- return null
781
- }
782
-
783
- const route = router.looseRoutesById[match?.routeId]!
784
-
785
- if (!route.options.loader) {
786
- return null
787
- }
788
-
789
- const age = Date.now() - match?.updatedAt
790
- const staleTime =
791
- route.options.staleTime ?? router.options.defaultStaleTime ?? 0
792
- const gcTime =
793
- route.options.gcTime ?? router.options.defaultGcTime ?? 30 * 60 * 1000
794
-
795
- return (
796
- <div className={cx(getStyles().ageTicker(age > staleTime))}>
797
- <div>{formatTime(age)}</div>
798
- <div>/</div>
799
- <div>{formatTime(staleTime)}</div>
800
- <div>/</div>
801
- <div>{formatTime(gcTime)}</div>
802
- </div>
803
- )
804
- }
805
-
806
- function formatTime(ms: number) {
807
- const units = ['s', 'min', 'h', 'd']
808
- const values = [ms / 1000, ms / 60000, ms / 3600000, ms / 86400000]
809
-
810
- let chosenUnitIndex = 0
811
- for (let i = 1; i < values.length; i++) {
812
- if (values[i]! < 1) break
813
- chosenUnitIndex = i
814
- }
815
-
816
- const formatter = new Intl.NumberFormat(navigator.language, {
817
- compactDisplay: 'short',
818
- notation: 'compact',
819
- maximumFractionDigits: 0,
820
- })
821
-
822
- return formatter.format(values[chosenUnitIndex]!) + units[chosenUnitIndex]
823
- }
824
-
825
- const stylesFactory = () => {
826
- const { colors, font, size, alpha, shadow, border } = tokens
827
- const { fontFamily, lineHeight, size: fontSize } = font
828
-
829
- return {
830
- devtoolsPanelContainer: css`
831
- direction: ltr;
832
- position: fixed;
833
- bottom: 0;
834
- right: 0;
835
- z-index: 99999;
836
- width: 100%;
837
- max-height: 90%;
838
- border-top: 1px solid ${colors.gray[700]};
839
- transform-origin: top;
840
- `,
841
- devtoolsPanelContainerVisibility: (isOpen: boolean) => {
842
- return css`
843
- visibility: ${isOpen ? 'visible' : 'hidden'};
844
- `
845
- },
846
- devtoolsPanelContainerResizing: (isResizing: boolean) => {
847
- if (isResizing) {
848
- return css`
849
- transition: none;
850
- `
851
- }
852
-
853
- return css`
854
- transition: all 0.4s ease;
855
- `
856
- },
857
- devtoolsPanelContainerAnimation: (isOpen: boolean, height: number) => {
858
- if (isOpen) {
859
- return css`
860
- pointer-events: auto;
861
- transform: translateY(0);
862
- `
863
- }
864
- return css`
865
- pointer-events: none;
866
- transform: translateY(${height}px);
867
- `
868
- },
869
- logo: css`
870
- cursor: pointer;
871
- display: flex;
872
- flex-direction: column;
873
- background-color: transparent;
874
- border: none;
875
- font-family: ${fontFamily.sans};
876
- gap: ${tokens.size[0.5]};
877
- padding: 0px;
878
- &:hover {
879
- opacity: 0.7;
880
- }
881
- &:focus-visible {
882
- outline-offset: 4px;
883
- border-radius: ${border.radius.xs};
884
- outline: 2px solid ${colors.blue[800]};
885
- }
886
- `,
887
- tanstackLogo: css`
888
- font-size: ${font.size.md};
889
- font-weight: ${font.weight.bold};
890
- line-height: ${font.lineHeight.xs};
891
- white-space: nowrap;
892
- color: ${colors.gray[300]};
893
- `,
894
- routerLogo: css`
895
- font-weight: ${font.weight.semibold};
896
- font-size: ${font.size.xs};
897
- background: linear-gradient(to right, #84cc16, #10b981);
898
- background-clip: text;
899
- -webkit-background-clip: text;
900
- line-height: 1;
901
- -webkit-text-fill-color: transparent;
902
- white-space: nowrap;
903
- `,
904
- devtoolsPanel: css`
905
- display: flex;
906
- font-size: ${fontSize.sm};
907
- font-family: ${fontFamily.sans};
908
- background-color: ${colors.darkGray[700]};
909
- color: ${colors.gray[300]};
910
-
911
- @media (max-width: 700px) {
912
- flex-direction: column;
913
- }
914
- @media (max-width: 600px) {
915
- font-size: ${fontSize.xs};
916
- }
917
- `,
918
- dragHandle: css`
919
- position: absolute;
920
- left: 0;
921
- top: 0;
922
- width: 100%;
923
- height: 4px;
924
- cursor: row-resize;
925
- z-index: 100000;
926
- &:hover {
927
- background-color: ${colors.purple[400]}${alpha[90]};
928
- }
929
- `,
930
- firstContainer: css`
931
- flex: 1 1 500px;
932
- min-height: 40%;
933
- max-height: 100%;
934
- overflow: auto;
935
- border-right: 1px solid ${colors.gray[700]};
936
- display: flex;
937
- flex-direction: column;
938
- `,
939
- routerExplorerContainer: css`
940
- overflow-y: auto;
941
- flex: 1;
942
- `,
943
- routerExplorer: css`
944
- padding: ${tokens.size[2]};
945
- `,
946
- row: css`
947
- display: flex;
948
- align-items: center;
949
- padding: ${tokens.size[2]} ${tokens.size[2.5]};
950
- gap: ${tokens.size[2.5]};
951
- border-bottom: ${colors.darkGray[500]} 1px solid;
952
- align-items: center;
953
- `,
954
- detailsHeader: css`
955
- font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif;
956
- position: sticky;
957
- top: 0;
958
- z-index: 2;
959
- background-color: ${colors.darkGray[600]};
960
- padding: 0px ${tokens.size[2]};
961
- font-weight: ${font.weight.medium};
962
- font-size: ${font.size.xs};
963
- min-height: ${tokens.size[8]};
964
- line-height: ${font.lineHeight.xs};
965
- text-align: left;
966
- display: flex;
967
- align-items: center;
968
- `,
969
- maskedBadge: css`
970
- background: ${colors.yellow[900]}${alpha[70]};
971
- color: ${colors.yellow[300]};
972
- display: inline-block;
973
- padding: ${tokens.size[0]} ${tokens.size[2.5]};
974
- border-radius: ${border.radius.full};
975
- font-size: ${font.size.xs};
976
- font-weight: ${font.weight.normal};
977
- border: 1px solid ${colors.yellow[300]};
978
- `,
979
- maskedLocation: css`
980
- color: ${colors.yellow[300]};
981
- `,
982
- detailsContent: css`
983
- padding: ${tokens.size[1.5]} ${tokens.size[2]};
984
- display: flex;
985
- align-items: center;
986
- font-size: ${font.size.xs};
987
- `,
988
- routeMatchesToggle: css`
989
- display: flex;
990
- align-items: center;
991
- border: 1px solid ${colors.gray[500]};
992
- border-radius: ${border.radius.sm};
993
- overflow: hidden;
994
- `,
995
- routeMatchesToggleBtn: (active: boolean, showBorder: boolean) => {
996
- const base = css`
997
- appearance: none;
998
- border: none;
999
- font-size: 12px;
1000
- padding: 4px 8px;
1001
- background: transparent;
1002
- cursor: pointer;
1003
- font-family: ${fontFamily.sans};
1004
- font-weight: ${font.weight.medium};
1005
- `
1006
- const classes = [base]
1007
-
1008
- if (active) {
1009
- const activeStyles = css`
1010
- background: ${colors.darkGray[400]};
1011
- color: ${colors.gray[300]};
1012
- `
1013
- classes.push(activeStyles)
1014
- } else {
1015
- const inactiveStyles = css`
1016
- color: ${colors.gray[500]};
1017
- background: ${colors.darkGray[800]}${alpha[20]};
1018
- `
1019
- classes.push(inactiveStyles)
1020
- }
1021
-
1022
- if (showBorder) {
1023
- const border = css`
1024
- border-right: 1px solid ${tokens.colors.gray[500]};
1025
- `
1026
- classes.push(border)
1027
- }
1028
-
1029
- return classes
1030
- },
1031
- detailsHeaderInfo: css`
1032
- flex: 1;
1033
- justify-content: flex-end;
1034
- display: flex;
1035
- align-items: center;
1036
- font-weight: ${font.weight.normal};
1037
- color: ${colors.gray[400]};
1038
- `,
1039
- matchRow: (active: boolean) => {
1040
- const base = css`
1041
- display: flex;
1042
- border-bottom: 1px solid ${colors.darkGray[400]};
1043
- cursor: pointer;
1044
- align-items: center;
1045
- padding: ${size[1]} ${size[2]};
1046
- gap: ${size[2]};
1047
- font-size: ${fontSize.xs};
1048
- color: ${colors.gray[300]};
1049
- `
1050
- const classes = [base]
1051
-
1052
- if (active) {
1053
- const activeStyles = css`
1054
- background: ${colors.darkGray[500]};
1055
- `
1056
- classes.push(activeStyles)
1057
- }
1058
-
1059
- return classes
1060
- },
1061
- matchIndicator: (color: 'green' | 'red' | 'yellow' | 'gray' | 'blue') => {
1062
- const base = css`
1063
- flex: 0 0 auto;
1064
- width: ${size[3]};
1065
- height: ${size[3]};
1066
- background: ${colors[color][900]};
1067
- border: 1px solid ${colors[color][500]};
1068
- border-radius: ${border.radius.full};
1069
- transition: all 0.25s ease-out;
1070
- box-sizing: border-box;
1071
- `
1072
- const classes = [base]
1073
-
1074
- if (color === 'gray') {
1075
- const grayStyles = css`
1076
- background: ${colors.gray[700]};
1077
- border-color: ${colors.gray[400]};
1078
- `
1079
- classes.push(grayStyles)
1080
- }
1081
-
1082
- return classes
1083
- },
1084
- matchID: css`
1085
- flex: 1;
1086
- line-height: ${lineHeight['xs']};
1087
- `,
1088
- ageTicker: (showWarning: boolean) => {
1089
- const base = css`
1090
- display: flex;
1091
- gap: ${size[1]};
1092
- font-size: ${fontSize.xs};
1093
- color: ${colors.gray[400]};
1094
- font-variant-numeric: tabular-nums;
1095
- line-height: ${lineHeight['xs']};
1096
- `
1097
-
1098
- const classes = [base]
1099
-
1100
- if (showWarning) {
1101
- const warningStyles = css`
1102
- color: ${colors.yellow[400]};
1103
- `
1104
- classes.push(warningStyles)
1105
- }
1106
-
1107
- return classes
1108
- },
1109
- secondContainer: css`
1110
- flex: 1 1 500px;
1111
- min-height: 40%;
1112
- max-height: 100%;
1113
- overflow: auto;
1114
- border-right: 1px solid ${colors.gray[700]};
1115
- display: flex;
1116
- flex-direction: column;
1117
- `,
1118
- thirdContainer: css`
1119
- flex: 1 1 500px;
1120
- overflow: auto;
1121
- display: flex;
1122
- flex-direction: column;
1123
- height: 100%;
1124
- border-right: 1px solid ${colors.gray[700]};
1125
-
1126
- @media (max-width: 700px) {
1127
- border-top: 2px solid ${colors.gray[700]};
1128
- }
1129
- `,
1130
- fourthContainer: css`
1131
- flex: 1 1 500px;
1132
- min-height: 40%;
1133
- max-height: 100%;
1134
- overflow: auto;
1135
- display: flex;
1136
- flex-direction: column;
1137
- `,
1138
- routesContainer: css`
1139
- overflow-x: auto;
1140
- overflow-y: visible;
1141
- `,
1142
- routesRowContainer: (active: boolean, isMatch: boolean) => {
1143
- const base = css`
1144
- display: flex;
1145
- border-bottom: 1px solid ${colors.darkGray[400]};
1146
- align-items: center;
1147
- padding: ${size[1]} ${size[2]};
1148
- gap: ${size[2]};
1149
- font-size: ${fontSize.xs};
1150
- color: ${colors.gray[300]};
1151
- cursor: ${isMatch ? 'pointer' : 'default'};
1152
- line-height: ${lineHeight['xs']};
1153
- `
1154
- const classes = [base]
1155
-
1156
- if (active) {
1157
- const activeStyles = css`
1158
- background: ${colors.darkGray[500]};
1159
- `
1160
- classes.push(activeStyles)
1161
- }
1162
-
1163
- return classes
1164
- },
1165
- routesRow: (isMatch: boolean) => {
1166
- const base = css`
1167
- flex: 1 0 auto;
1168
- display: flex;
1169
- justify-content: space-between;
1170
- align-items: center;
1171
- font-size: ${fontSize.xs};
1172
- line-height: ${lineHeight['xs']};
1173
- `
1174
-
1175
- const classes = [base]
1176
-
1177
- if (!isMatch) {
1178
- const matchStyles = css`
1179
- color: ${colors.gray[400]};
1180
- `
1181
- classes.push(matchStyles)
1182
- }
1183
-
1184
- return classes
1185
- },
1186
- routeParamInfo: css`
1187
- color: ${colors.gray[400]};
1188
- font-size: ${fontSize.xs};
1189
- line-height: ${lineHeight['xs']};
1190
- `,
1191
- nestedRouteRow: (isRoot: boolean) => {
1192
- const base = css`
1193
- margin-left: ${isRoot ? 0 : size[3.5]};
1194
- border-left: ${isRoot ? '' : `solid 1px ${colors.gray[700]}`};
1195
- `
1196
- return base
1197
- },
1198
- code: css`
1199
- font-size: ${fontSize.xs};
1200
- line-height: ${lineHeight['xs']};
1201
- `,
1202
- matchesContainer: css`
1203
- flex: 1 1 auto;
1204
- overflow-y: auto;
1205
- `,
1206
- cachedMatchesContainer: css`
1207
- flex: 1 1 auto;
1208
- overflow-y: auto;
1209
- max-height: 50%;
1210
- `,
1211
- maskedBadgeContainer: css`
1212
- flex: 1;
1213
- justify-content: flex-end;
1214
- display: flex;
1215
- `,
1216
- matchDetails: css`
1217
- display: flex;
1218
- flex-direction: column;
1219
- padding: ${tokens.size[2]};
1220
- font-size: ${tokens.font.size.xs};
1221
- color: ${tokens.colors.gray[300]};
1222
- line-height: ${tokens.font.lineHeight.sm};
1223
- `,
1224
- matchStatus: (
1225
- status: 'pending' | 'success' | 'error' | 'notFound' | 'redirected',
1226
- isFetching: boolean,
1227
- ) => {
1228
- const colorMap = {
1229
- pending: 'yellow',
1230
- success: 'green',
1231
- error: 'red',
1232
- notFound: 'purple',
1233
- redirected: 'gray',
1234
- } as const
1235
-
1236
- const color =
1237
- isFetching && status === 'success' ? 'blue' : colorMap[status]
1238
-
1239
- return css`
1240
- display: flex;
1241
- justify-content: center;
1242
- align-items: center;
1243
- height: 40px;
1244
- border-radius: ${tokens.border.radius.sm};
1245
- font-weight: ${tokens.font.weight.normal};
1246
- background-color: ${tokens.colors[color][900]}${tokens.alpha[90]};
1247
- color: ${tokens.colors[color][300]};
1248
- border: 1px solid ${tokens.colors[color][600]};
1249
- margin-bottom: ${tokens.size[2]};
1250
- transition: all 0.25s ease-out;
1251
- `
1252
- },
1253
- matchDetailsInfo: css`
1254
- display: flex;
1255
- justify-content: flex-end;
1256
- flex: 1;
1257
- `,
1258
- matchDetailsInfoLabel: css`
1259
- display: flex;
1260
- `,
1261
- mainCloseBtn: css`
1262
- background: ${colors.darkGray[700]};
1263
- padding: ${size[1]} ${size[2]} ${size[1]} ${size[1.5]};
1264
- border-radius: ${border.radius.md};
1265
- position: fixed;
1266
- z-index: 99999;
1267
- display: inline-flex;
1268
- width: fit-content;
1269
- cursor: pointer;
1270
- appearance: none;
1271
- border: 0;
1272
- gap: 8px;
1273
- align-items: center;
1274
- border: 1px solid ${colors.gray[500]};
1275
- font-size: ${font.size.xs};
1276
- cursor: pointer;
1277
- transition: all 0.25s ease-out;
1278
-
1279
- &:hover {
1280
- background: ${colors.darkGray[500]};
1281
- }
1282
- `,
1283
- mainCloseBtnPosition: (
1284
- position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right',
1285
- ) => {
1286
- const base = css`
1287
- ${position === 'top-left' ? `top: ${size[2]}; left: ${size[2]};` : ''}
1288
- ${position === 'top-right' ? `top: ${size[2]}; right: ${size[2]};` : ''}
1289
- ${position === 'bottom-left'
1290
- ? `bottom: ${size[2]}; left: ${size[2]};`
1291
- : ''}
1292
- ${position === 'bottom-right'
1293
- ? `bottom: ${size[2]}; right: ${size[2]};`
1294
- : ''}
1295
- `
1296
- return base
1297
- },
1298
- mainCloseBtnAnimation: (isOpen: boolean) => {
1299
- if (isOpen) {
1300
- return css`
1301
- opacity: 1;
1302
- pointer-events: auto;
1303
- visibility: visible;
1304
- `
1305
- }
1306
- return css`
1307
- opacity: 0;
1308
- pointer-events: none;
1309
- visibility: hidden;
1310
- `
1311
- },
1312
- routerLogoCloseButton: css`
1313
- font-weight: ${font.weight.semibold};
1314
- font-size: ${font.size.xs};
1315
- background: linear-gradient(to right, #98f30c, #00f4a3);
1316
- background-clip: text;
1317
- -webkit-background-clip: text;
1318
- line-height: 1;
1319
- -webkit-text-fill-color: transparent;
1320
- white-space: nowrap;
1321
- `,
1322
- mainCloseBtnDivider: css`
1323
- width: 1px;
1324
- background: ${tokens.colors.gray[600]};
1325
- height: 100%;
1326
- border-radius: 999999px;
1327
- color: transparent;
1328
- `,
1329
- mainCloseBtnIconContainer: css`
1330
- position: relative;
1331
- width: ${size[5]};
1332
- height: ${size[5]};
1333
- background: pink;
1334
- border-radius: 999999px;
1335
- overflow: hidden;
1336
- `,
1337
- mainCloseBtnIconOuter: css`
1338
- width: ${size[5]};
1339
- height: ${size[5]};
1340
- position: absolute;
1341
- top: 50%;
1342
- left: 50%;
1343
- transform: translate(-50%, -50%);
1344
- filter: blur(3px) saturate(1.8) contrast(2);
1345
- `,
1346
- mainCloseBtnIconInner: css`
1347
- width: ${size[4]};
1348
- height: ${size[4]};
1349
- position: absolute;
1350
- top: 50%;
1351
- left: 50%;
1352
- transform: translate(-50%, -50%);
1353
- `,
1354
- panelCloseBtn: css`
1355
- position: absolute;
1356
- cursor: pointer;
1357
- z-index: 100001;
1358
- display: flex;
1359
- align-items: center;
1360
- justify-content: center;
1361
- outline: none;
1362
- background-color: ${colors.darkGray[700]};
1363
- &:hover {
1364
- background-color: ${colors.darkGray[500]};
1365
- }
1366
-
1367
- top: 0;
1368
- right: ${size[2]};
1369
- transform: translate(0, -100%);
1370
- border-right: ${colors.darkGray[300]} 1px solid;
1371
- border-left: ${colors.darkGray[300]} 1px solid;
1372
- border-top: ${colors.darkGray[300]} 1px solid;
1373
- border-bottom: none;
1374
- border-radius: ${border.radius.sm} ${border.radius.sm} 0px 0px;
1375
- padding: ${size[1]} ${size[1.5]} ${size[0.5]} ${size[1.5]};
1376
-
1377
- &::after {
1378
- content: ' ';
1379
- position: absolute;
1380
- top: 100%;
1381
- left: -${size[2.5]};
1382
- height: ${size[1.5]};
1383
- width: calc(100% + ${size[5]});
1384
- }
1385
- `,
1386
- panelCloseBtnIcon: css`
1387
- color: ${colors.gray[400]};
1388
- width: ${size[2]};
1389
- height: ${size[2]};
1390
- `,
1391
- }
1392
- }
1393
-
1394
- let _styles: ReturnType<typeof stylesFactory> | null = null
1395
-
1396
- function getStyles() {
1397
- if (_styles) return _styles
1398
- _styles = stylesFactory()
1399
-
1400
- return _styles
1401
- }