@tanstack/react-router-devtools 0.0.1-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 (40) hide show
  1. package/README.md +3 -0
  2. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +49 -0
  3. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  4. package/build/cjs/packages/react-router-devtools/src/Explorer.js +245 -0
  5. package/build/cjs/packages/react-router-devtools/src/Explorer.js.map +1 -0
  6. package/build/cjs/packages/react-router-devtools/src/Logo.js +73 -0
  7. package/build/cjs/packages/react-router-devtools/src/Logo.js.map +1 -0
  8. package/build/cjs/packages/react-router-devtools/src/devtools.js +654 -0
  9. package/build/cjs/packages/react-router-devtools/src/devtools.js.map +1 -0
  10. package/build/cjs/packages/react-router-devtools/src/index.js +21 -0
  11. package/build/cjs/packages/react-router-devtools/src/index.js.map +1 -0
  12. package/build/cjs/packages/react-router-devtools/src/styledComponents.js +107 -0
  13. package/build/cjs/packages/react-router-devtools/src/styledComponents.js.map +1 -0
  14. package/build/cjs/packages/react-router-devtools/src/theme.js +54 -0
  15. package/build/cjs/packages/react-router-devtools/src/theme.js.map +1 -0
  16. package/build/cjs/packages/react-router-devtools/src/useLocalStorage.js +65 -0
  17. package/build/cjs/packages/react-router-devtools/src/useLocalStorage.js.map +1 -0
  18. package/build/cjs/packages/react-router-devtools/src/useMediaQuery.js +57 -0
  19. package/build/cjs/packages/react-router-devtools/src/useMediaQuery.js.map +1 -0
  20. package/build/cjs/packages/react-router-devtools/src/utils.js +117 -0
  21. package/build/cjs/packages/react-router-devtools/src/utils.js.map +1 -0
  22. package/build/esm/index.js +2013 -0
  23. package/build/esm/index.js.map +1 -0
  24. package/build/stats-html.html +4034 -0
  25. package/build/stats-react.json +9681 -0
  26. package/build/types/index.d.ts +76 -0
  27. package/build/umd/index.development.js +2043 -0
  28. package/build/umd/index.development.js.map +1 -0
  29. package/build/umd/index.production.js +12 -0
  30. package/build/umd/index.production.js.map +1 -0
  31. package/package.json +49 -0
  32. package/src/Explorer.tsx +288 -0
  33. package/src/Logo.tsx +37 -0
  34. package/src/devtools.tsx +960 -0
  35. package/src/index.tsx +1 -0
  36. package/src/styledComponents.ts +106 -0
  37. package/src/theme.tsx +31 -0
  38. package/src/useLocalStorage.ts +52 -0
  39. package/src/useMediaQuery.ts +36 -0
  40. package/src/utils.ts +151 -0
@@ -0,0 +1,960 @@
1
+ import React from 'react'
2
+ import { Router, useRouter, last } from '@tanstack/react-router'
3
+ import { formatDistanceStrict } from 'date-fns'
4
+
5
+ import useLocalStorage from './useLocalStorage'
6
+ import { getStatusColor, useIsMounted, useSafeState } from './utils'
7
+ import { Panel, Button, Code, ActivePanel } from './styledComponents'
8
+ import { ThemeProvider, defaultTheme as theme } from './theme'
9
+ // import { getQueryStatusLabel, getQueryStatusColor } from './utils'
10
+ import Explorer from './Explorer'
11
+ import Logo from './Logo'
12
+
13
+ export type PartialKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
14
+
15
+ interface DevtoolsOptions {
16
+ /**
17
+ * Set this true if you want the dev tools to default to being open
18
+ */
19
+ initialIsOpen?: boolean
20
+ /**
21
+ * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc.
22
+ */
23
+ panelProps?: React.DetailedHTMLProps<
24
+ React.HTMLAttributes<HTMLDivElement>,
25
+ HTMLDivElement
26
+ >
27
+ /**
28
+ * 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.
29
+ */
30
+ closeButtonProps?: React.DetailedHTMLProps<
31
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
32
+ HTMLButtonElement
33
+ >
34
+ /**
35
+ * 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.
36
+ */
37
+ toggleButtonProps?: React.DetailedHTMLProps<
38
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ HTMLButtonElement
40
+ >
41
+ /**
42
+ * The position of the TanStack Router logo to open and close the devtools panel.
43
+ * Defaults to 'bottom-left'.
44
+ */
45
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
46
+ /**
47
+ * Use this to render the devtools inside a different type of container element for a11y purposes.
48
+ * Any string which corresponds to a valid intrinsic JSX element is allowed.
49
+ * Defaults to 'footer'.
50
+ */
51
+ containerElement?: string | any
52
+ /**
53
+ * A boolean variable indicating if the "lite" version of the library is being used
54
+ */
55
+ useRouter?: () => unknown
56
+ }
57
+
58
+ interface DevtoolsPanelOptions {
59
+ /**
60
+ * The standard React style object used to style a component with inline styles
61
+ */
62
+ style?: React.CSSProperties
63
+ /**
64
+ * The standard React className property used to style a component with classes
65
+ */
66
+ className?: string
67
+ /**
68
+ * A boolean variable indicating whether the panel is open or closed
69
+ */
70
+ isOpen?: boolean
71
+ /**
72
+ * A function that toggles the open and close state of the panel
73
+ */
74
+ setIsOpen: (isOpen: boolean) => void
75
+ /**
76
+ * Handles the opening and closing the devtools panel
77
+ */
78
+ handleDragStart: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
79
+ /**
80
+ * A boolean variable indicating if the "lite" version of the library is being used
81
+ */
82
+ useRouter: () => unknown
83
+ }
84
+
85
+ const isServer = typeof window === 'undefined'
86
+
87
+ export function TanStackRouterDevtools({
88
+ initialIsOpen,
89
+ panelProps = {},
90
+ closeButtonProps = {},
91
+ toggleButtonProps = {},
92
+ position = 'bottom-left',
93
+ containerElement: Container = 'footer',
94
+ useRouter: useRouterImpl = useRouter,
95
+ }: DevtoolsOptions): React.ReactElement | null {
96
+ const rootRef = React.useRef<HTMLDivElement>(null)
97
+ const panelRef = React.useRef<HTMLDivElement>(null)
98
+ const [isOpen, setIsOpen] = useLocalStorage(
99
+ 'tanstackRouterDevtoolsOpen',
100
+ initialIsOpen,
101
+ )
102
+ const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage<number | null>(
103
+ 'tanstackRouterDevtoolsHeight',
104
+ null,
105
+ )
106
+ const [isResolvedOpen, setIsResolvedOpen] = useSafeState(false)
107
+ const [isResizing, setIsResizing] = useSafeState(false)
108
+ const isMounted = useIsMounted()
109
+
110
+ const handleDragStart = (
111
+ panelElement: HTMLDivElement | null,
112
+ startEvent: React.MouseEvent<HTMLDivElement, MouseEvent>,
113
+ ) => {
114
+ if (startEvent.button !== 0) return // Only allow left click for drag
115
+
116
+ setIsResizing(true)
117
+
118
+ const dragInfo = {
119
+ originalHeight: panelElement?.getBoundingClientRect().height ?? 0,
120
+ pageY: startEvent.pageY,
121
+ }
122
+
123
+ const run = (moveEvent: MouseEvent) => {
124
+ const delta = dragInfo.pageY - moveEvent.pageY
125
+ const newHeight = dragInfo?.originalHeight + delta
126
+
127
+ setDevtoolsHeight(newHeight)
128
+
129
+ if (newHeight < 70) {
130
+ setIsOpen(false)
131
+ } else {
132
+ setIsOpen(true)
133
+ }
134
+ }
135
+
136
+ const unsub = () => {
137
+ setIsResizing(false)
138
+ document.removeEventListener('mousemove', run)
139
+ document.removeEventListener('mouseUp', unsub)
140
+ }
141
+
142
+ document.addEventListener('mousemove', run)
143
+ document.addEventListener('mouseup', unsub)
144
+ }
145
+
146
+ React.useEffect(() => {
147
+ setIsResolvedOpen(isOpen ?? false)
148
+ }, [isOpen, isResolvedOpen, setIsResolvedOpen])
149
+
150
+ // Toggle panel visibility before/after transition (depending on direction).
151
+ // Prevents focusing in a closed panel.
152
+ React.useEffect(() => {
153
+ const ref = panelRef.current
154
+
155
+ if (ref) {
156
+ const handlePanelTransitionStart = () => {
157
+ if (ref && isResolvedOpen) {
158
+ ref.style.visibility = 'visible'
159
+ }
160
+ }
161
+
162
+ const handlePanelTransitionEnd = () => {
163
+ if (ref && !isResolvedOpen) {
164
+ ref.style.visibility = 'hidden'
165
+ }
166
+ }
167
+
168
+ ref.addEventListener('transitionstart', handlePanelTransitionStart)
169
+ ref.addEventListener('transitionend', handlePanelTransitionEnd)
170
+
171
+ return () => {
172
+ ref.removeEventListener('transitionstart', handlePanelTransitionStart)
173
+ ref.removeEventListener('transitionend', handlePanelTransitionEnd)
174
+ }
175
+ }
176
+
177
+ return
178
+ }, [isResolvedOpen])
179
+
180
+ React[isServer ? 'useEffect' : 'useLayoutEffect'](() => {
181
+ if (isResolvedOpen) {
182
+ const previousValue = rootRef.current?.parentElement?.style.paddingBottom
183
+
184
+ const run = () => {
185
+ const containerHeight = panelRef.current?.getBoundingClientRect().height
186
+ if (rootRef.current?.parentElement) {
187
+ rootRef.current.parentElement.style.paddingBottom = `${containerHeight}px`
188
+ }
189
+ }
190
+
191
+ run()
192
+
193
+ if (typeof window !== 'undefined') {
194
+ window.addEventListener('resize', run)
195
+
196
+ return () => {
197
+ window.removeEventListener('resize', run)
198
+ if (
199
+ rootRef.current?.parentElement &&
200
+ typeof previousValue === 'string'
201
+ ) {
202
+ rootRef.current.parentElement.style.paddingBottom = previousValue
203
+ }
204
+ }
205
+ }
206
+ }
207
+ return
208
+ }, [isResolvedOpen])
209
+
210
+ const { style: panelStyle = {}, ...otherPanelProps } = panelProps
211
+
212
+ const {
213
+ style: closeButtonStyle = {},
214
+ onClick: onCloseClick,
215
+ ...otherCloseButtonProps
216
+ } = closeButtonProps
217
+
218
+ const {
219
+ style: toggleButtonStyle = {},
220
+ onClick: onToggleClick,
221
+ ...otherToggleButtonProps
222
+ } = toggleButtonProps
223
+
224
+ // Do not render on the server
225
+ if (!isMounted()) return null
226
+
227
+ return (
228
+ <Container ref={rootRef} className="TanStackRouterDevtools">
229
+ <ThemeProvider theme={theme}>
230
+ <TanStackRouterDevtoolsPanel
231
+ ref={panelRef as any}
232
+ {...otherPanelProps}
233
+ useRouter={useRouterImpl}
234
+ style={{
235
+ position: 'fixed',
236
+ bottom: '0',
237
+ right: '0',
238
+ zIndex: 99999,
239
+ width: '100%',
240
+ height: devtoolsHeight ?? 500,
241
+ maxHeight: '90%',
242
+ boxShadow: '0 0 20px rgba(0,0,0,.3)',
243
+ borderTop: `1px solid ${theme.gray}`,
244
+ transformOrigin: 'top',
245
+ // visibility will be toggled after transitions, but set initial state here
246
+ visibility: isOpen ? 'visible' : 'hidden',
247
+ ...panelStyle,
248
+ ...(isResizing
249
+ ? {
250
+ transition: `none`,
251
+ }
252
+ : { transition: `all .2s ease` }),
253
+ ...(isResolvedOpen
254
+ ? {
255
+ opacity: 1,
256
+ pointerEvents: 'all',
257
+ transform: `translateY(0) scale(1)`,
258
+ }
259
+ : {
260
+ opacity: 0,
261
+ pointerEvents: 'none',
262
+ transform: `translateY(15px) scale(1.02)`,
263
+ }),
264
+ }}
265
+ isOpen={isResolvedOpen}
266
+ setIsOpen={setIsOpen}
267
+ handleDragStart={(e) => handleDragStart(panelRef.current, e)}
268
+ />
269
+ {isResolvedOpen ? (
270
+ <Button
271
+ type="button"
272
+ aria-label="Close TanStack Router Devtools"
273
+ {...(otherCloseButtonProps as any)}
274
+ onClick={(e) => {
275
+ setIsOpen(false)
276
+ onCloseClick && onCloseClick(e)
277
+ }}
278
+ style={{
279
+ position: 'fixed',
280
+ zIndex: 99999,
281
+ margin: '.5em',
282
+ bottom: 0,
283
+ ...(position === 'top-right'
284
+ ? {
285
+ right: '0',
286
+ }
287
+ : position === 'top-left'
288
+ ? {
289
+ left: '0',
290
+ }
291
+ : position === 'bottom-right'
292
+ ? {
293
+ right: '0',
294
+ }
295
+ : {
296
+ left: '0',
297
+ }),
298
+ ...closeButtonStyle,
299
+ }}
300
+ >
301
+ Close
302
+ </Button>
303
+ ) : null}
304
+ </ThemeProvider>
305
+ {!isResolvedOpen ? (
306
+ <button
307
+ type="button"
308
+ {...otherToggleButtonProps}
309
+ aria-label="Open TanStack Router Devtools"
310
+ onClick={(e) => {
311
+ setIsOpen(true)
312
+ onToggleClick && onToggleClick(e)
313
+ }}
314
+ style={{
315
+ appearance: 'none',
316
+ background: 'none',
317
+ border: 0,
318
+ padding: 0,
319
+ position: 'fixed',
320
+ zIndex: 99999,
321
+ display: 'inline-flex',
322
+ fontSize: '1.5em',
323
+ margin: '.5em',
324
+ cursor: 'pointer',
325
+ width: 'fit-content',
326
+ ...(position === 'top-right'
327
+ ? {
328
+ top: '0',
329
+ right: '0',
330
+ }
331
+ : position === 'top-left'
332
+ ? {
333
+ top: '0',
334
+ left: '0',
335
+ }
336
+ : position === 'bottom-right'
337
+ ? {
338
+ bottom: '0',
339
+ right: '0',
340
+ }
341
+ : {
342
+ bottom: '0',
343
+ left: '0',
344
+ }),
345
+ ...toggleButtonStyle,
346
+ }}
347
+ >
348
+ <Logo aria-hidden />
349
+ </button>
350
+ ) : null}
351
+ </Container>
352
+ )
353
+ }
354
+
355
+ export const TanStackRouterDevtoolsPanel = React.forwardRef<
356
+ HTMLDivElement,
357
+ DevtoolsPanelOptions
358
+ >(function TanStackRouterDevtoolsPanel(props, ref): React.ReactElement {
359
+ const {
360
+ isOpen = true,
361
+ setIsOpen,
362
+ handleDragStart,
363
+ useRouter,
364
+ ...panelProps
365
+ } = props
366
+
367
+ const router = useRouter() as Router
368
+
369
+ React.useEffect(() => {
370
+ let interval = setInterval(() => {
371
+ router.cleanPreloadCache()
372
+ router.notify()
373
+ }, 1000)
374
+
375
+ return () => {
376
+ clearInterval(interval)
377
+ }
378
+ }, [])
379
+
380
+ const [activeRouteId, setActiveRouteId] = useLocalStorage(
381
+ 'tanstackRouterDevtoolsActiveRouteId',
382
+ '',
383
+ )
384
+
385
+ const activeMatch = router.state.matches?.find(
386
+ (d) => d.routeId === activeRouteId,
387
+ )
388
+
389
+ return (
390
+ <ThemeProvider theme={theme}>
391
+ <Panel ref={ref} className="TanStackRouterDevtoolsPanel" {...panelProps}>
392
+ <style
393
+ dangerouslySetInnerHTML={{
394
+ __html: `
395
+
396
+ .TanStackRouterDevtoolsPanel * {
397
+ scrollbar-color: ${theme.backgroundAlt} ${theme.gray};
398
+ }
399
+
400
+ .TanStackRouterDevtoolsPanel *::-webkit-scrollbar, .TanStackRouterDevtoolsPanel scrollbar {
401
+ width: 1em;
402
+ height: 1em;
403
+ }
404
+
405
+ .TanStackRouterDevtoolsPanel *::-webkit-scrollbar-track, .TanStackRouterDevtoolsPanel scrollbar-track {
406
+ background: ${theme.backgroundAlt};
407
+ }
408
+
409
+ .TanStackRouterDevtoolsPanel *::-webkit-scrollbar-thumb, .TanStackRouterDevtoolsPanel scrollbar-thumb {
410
+ background: ${theme.gray};
411
+ border-radius: .5em;
412
+ border: 3px solid ${theme.backgroundAlt};
413
+ }
414
+
415
+ .TanStackRouterDevtoolsPanel table {
416
+ width: 100%;
417
+ }
418
+
419
+ .TanStackRouterDevtoolsPanel table tr {
420
+ border-bottom: 2px dotted rgba(255, 255, 255, .2);
421
+ }
422
+
423
+ .TanStackRouterDevtoolsPanel table tr:last-child {
424
+ border-bottom: none
425
+ }
426
+
427
+ .TanStackRouterDevtoolsPanel table td {
428
+ padding: .25rem .5rem;
429
+ border-right: 2px dotted rgba(255, 255, 255, .05);
430
+ }
431
+
432
+ .TanStackRouterDevtoolsPanel table td:last-child {
433
+ border-right: none
434
+ }
435
+
436
+ `,
437
+ }}
438
+ />
439
+ <div
440
+ style={{
441
+ position: 'absolute',
442
+ left: 0,
443
+ top: 0,
444
+ width: '100%',
445
+ height: '4px',
446
+ marginBottom: '-4px',
447
+ cursor: 'row-resize',
448
+ zIndex: 100000,
449
+ }}
450
+ onMouseDown={handleDragStart}
451
+ ></div>
452
+ <div
453
+ style={{
454
+ flex: '1 1 500px',
455
+ minHeight: '40%',
456
+ maxHeight: '100%',
457
+ overflow: 'auto',
458
+ borderRight: `1px solid ${theme.grayAlt}`,
459
+ display: 'flex',
460
+ flexDirection: 'column',
461
+ }}
462
+ >
463
+ <div
464
+ style={{
465
+ padding: '.5em',
466
+ background: theme.backgroundAlt,
467
+ display: 'flex',
468
+ justifyContent: 'space-between',
469
+ alignItems: 'center',
470
+ }}
471
+ >
472
+ <Logo
473
+ aria-hidden
474
+ style={{
475
+ marginRight: '.5em',
476
+ }}
477
+ />
478
+ <div
479
+ style={{
480
+ marginRight: 'auto',
481
+ fontSize: 'clamp(.8rem, 2vw, 1.3rem)',
482
+ fontWeight: 'bold',
483
+ }}
484
+ >
485
+ TanStack Router{' '}
486
+ <span
487
+ style={{
488
+ fontWeight: 100,
489
+ }}
490
+ >
491
+ Devtools
492
+ </span>
493
+ </div>
494
+ </div>
495
+ <div
496
+ style={{
497
+ overflowY: 'auto',
498
+ flex: '1',
499
+ }}
500
+ >
501
+ <div
502
+ style={{
503
+ padding: '.5em',
504
+ }}
505
+ >
506
+ <Explorer
507
+ label="Router"
508
+ value={(() => {
509
+ const {
510
+ listeners,
511
+ buildLocation,
512
+ mount,
513
+ update,
514
+ buildNext,
515
+ navigate,
516
+ cancelMatches,
517
+ loadLocation,
518
+ cleanPreloadCache,
519
+ loadRoute,
520
+ matchRoutes,
521
+ loadMatches,
522
+ invalidateRoute,
523
+ resolvePath,
524
+ matchRoute,
525
+ buildLink,
526
+ __experimental__createSnapshot,
527
+ destroy,
528
+ ...rest
529
+ } = router
530
+ return rest
531
+ })()}
532
+ defaultExpanded={{}}
533
+ />
534
+ </div>
535
+ </div>
536
+ </div>
537
+ <div
538
+ style={{
539
+ flex: '1 1 500px',
540
+ minHeight: '40%',
541
+ maxHeight: '100%',
542
+ overflow: 'auto',
543
+ borderRight: `1px solid ${theme.grayAlt}`,
544
+ display: 'flex',
545
+ flexDirection: 'column',
546
+ }}
547
+ >
548
+ <div
549
+ style={{
550
+ padding: '.5em',
551
+ background: theme.backgroundAlt,
552
+ position: 'sticky',
553
+ top: 0,
554
+ zIndex: 1,
555
+ }}
556
+ >
557
+ Current Matches
558
+ </div>
559
+ {router.state.matches.map((match, i) => {
560
+ return (
561
+ <div
562
+ key={match.routeId || i}
563
+ role="button"
564
+ aria-label={`Open match details for ${match.routeId}`}
565
+ onClick={() =>
566
+ setActiveRouteId(
567
+ activeRouteId === match.routeId ? '' : match.routeId,
568
+ )
569
+ }
570
+ style={{
571
+ display: 'flex',
572
+ borderBottom: `solid 1px ${theme.grayAlt}`,
573
+ cursor: 'pointer',
574
+ alignItems: 'center',
575
+ background:
576
+ match === activeMatch ? 'rgba(255,255,255,.1)' : undefined,
577
+ }}
578
+ >
579
+ <div
580
+ style={{
581
+ flex: '0 0 auto',
582
+ width: '1.3rem',
583
+ height: '1.3rem',
584
+ marginLeft: '.25rem',
585
+ background: getStatusColor(match, theme),
586
+ alignItems: 'center',
587
+ justifyContent: 'center',
588
+ fontWeight: 'bold',
589
+ borderRadius: '.25rem',
590
+ transition: 'all .2s ease-out',
591
+ }}
592
+ />
593
+
594
+ <Code
595
+ style={{
596
+ padding: '.5em',
597
+ }}
598
+ >
599
+ {`${match.matchId}`}
600
+ </Code>
601
+ </div>
602
+ )
603
+ })}
604
+ <div
605
+ style={{
606
+ marginTop: '2rem',
607
+ padding: '.5em',
608
+ background: theme.backgroundAlt,
609
+ position: 'sticky',
610
+ top: 0,
611
+ zIndex: 1,
612
+ }}
613
+ >
614
+ Pending Matches
615
+ </div>
616
+ {router.state.pending?.matches.map((match, i) => {
617
+ return (
618
+ <div
619
+ key={match.routeId || i}
620
+ role="button"
621
+ aria-label={`Open match details for ${match.routeId}`}
622
+ onClick={() =>
623
+ setActiveRouteId(
624
+ activeRouteId === match.routeId ? '' : match.routeId,
625
+ )
626
+ }
627
+ style={{
628
+ display: 'flex',
629
+ borderBottom: `solid 1px ${theme.grayAlt}`,
630
+ cursor: 'pointer',
631
+ background:
632
+ match === activeMatch ? 'rgba(255,255,255,.1)' : undefined,
633
+ }}
634
+ >
635
+ <div
636
+ style={{
637
+ flex: '0 0 auto',
638
+ width: '1.3rem',
639
+ height: '1.3rem',
640
+ marginLeft: '.25rem',
641
+ background: getStatusColor(match, theme),
642
+ alignItems: 'center',
643
+ justifyContent: 'center',
644
+ fontWeight: 'bold',
645
+ borderRadius: '.25rem',
646
+ transition: 'all .2s ease-out',
647
+ }}
648
+ />
649
+
650
+ <Code
651
+ style={{
652
+ padding: '.5em',
653
+ }}
654
+ >
655
+ {`${match.matchId}`}
656
+ </Code>
657
+ </div>
658
+ )
659
+ })}
660
+ <div
661
+ style={{
662
+ marginTop: '2rem',
663
+ padding: '.5em',
664
+ background: theme.backgroundAlt,
665
+ position: 'sticky',
666
+ top: 0,
667
+ zIndex: 1,
668
+ }}
669
+ >
670
+ Preloading Matches
671
+ </div>
672
+ {Object.keys(router.preloadCache)
673
+ .filter((key) => {
674
+ const cacheEntry = router.preloadCache[key]!
675
+ return (
676
+ (cacheEntry.match.updatedAt ?? Date.now()) + cacheEntry.maxAge >
677
+ Date.now()
678
+ )
679
+ })
680
+ .map((key, i) => {
681
+ const { match, maxAge } = router.preloadCache[key]!
682
+
683
+ return (
684
+ <div
685
+ key={match.matchId || i}
686
+ role="button"
687
+ aria-label={`Open match details for ${match.matchId}`}
688
+ onClick={() =>
689
+ setActiveRouteId(
690
+ activeRouteId === match.routeId ? '' : match.routeId,
691
+ )
692
+ }
693
+ style={{
694
+ display: 'flex',
695
+ borderBottom: `solid 1px ${theme.grayAlt}`,
696
+ cursor: 'pointer',
697
+ background:
698
+ match === activeMatch
699
+ ? 'rgba(255,255,255,.1)'
700
+ : undefined,
701
+ }}
702
+ >
703
+ <div
704
+ style={{
705
+ display: 'flex',
706
+ flexDirection: 'column',
707
+ padding: '.5rem',
708
+ gap: '.3rem',
709
+ }}
710
+ >
711
+ <div
712
+ style={{
713
+ display: 'flex',
714
+ alignItems: 'center',
715
+ gap: '.5rem',
716
+ }}
717
+ >
718
+ <div
719
+ style={{
720
+ flex: '0 0 auto',
721
+ width: '1.3rem',
722
+ height: '1.3rem',
723
+ background: getStatusColor(match, theme),
724
+ alignItems: 'center',
725
+ justifyContent: 'center',
726
+ fontWeight: 'bold',
727
+ borderRadius: '.25rem',
728
+ transition: 'all .2s ease-out',
729
+ }}
730
+ />
731
+ <Code>{`${match.matchId}`}</Code>
732
+ </div>
733
+ <span
734
+ style={{
735
+ opacity: '.5',
736
+ }}
737
+ >
738
+ Expires{' '}
739
+ {formatDistanceStrict(
740
+ new Date(),
741
+ new Date((match.updatedAt ?? Date.now()) + maxAge),
742
+ {
743
+ addSuffix: true,
744
+ },
745
+ )}
746
+ </span>
747
+ </div>
748
+ </div>
749
+ )
750
+ })}
751
+ </div>
752
+
753
+ {activeMatch ? (
754
+ <ActivePanel>
755
+ <div
756
+ style={{
757
+ padding: '.5em',
758
+ background: theme.backgroundAlt,
759
+ position: 'sticky',
760
+ top: 0,
761
+ zIndex: 1,
762
+ }}
763
+ >
764
+ Match Details
765
+ </div>
766
+ <div>
767
+ <table>
768
+ <tbody>
769
+ <tr>
770
+ <td style={{ opacity: '.5' }}>ID</td>
771
+ <td>
772
+ <Code
773
+ style={{
774
+ lineHeight: '1.8em',
775
+ }}
776
+ >
777
+ {JSON.stringify(activeMatch.matchId, null, 2)}
778
+ </Code>
779
+ </td>
780
+ </tr>
781
+ <tr>
782
+ <td style={{ opacity: '.5' }}>Status</td>
783
+ <td>{activeMatch.status}</td>
784
+ </tr>
785
+ <tr>
786
+ <td style={{ opacity: '.5' }}>Pending</td>
787
+ <td>{activeMatch.isPending.toString()}</td>
788
+ </tr>
789
+ <tr>
790
+ <td style={{ opacity: '.5' }}>Invalid</td>
791
+ <td>{activeMatch.isInvalid.toString()}</td>
792
+ </tr>
793
+ <tr>
794
+ <td style={{ opacity: '.5' }}>Last Updated</td>
795
+ <td>
796
+ {activeMatch.updatedAt
797
+ ? new Date(
798
+ activeMatch.updatedAt as number,
799
+ ).toLocaleTimeString()
800
+ : 'N/A'}
801
+ </td>
802
+ </tr>
803
+ </tbody>
804
+ </table>
805
+ </div>
806
+ <div
807
+ style={{
808
+ background: theme.backgroundAlt,
809
+ padding: '.5em',
810
+ position: 'sticky',
811
+ top: 0,
812
+ zIndex: 1,
813
+ }}
814
+ >
815
+ Actions
816
+ </div>
817
+ <div
818
+ style={{
819
+ padding: '0.5em',
820
+ }}
821
+ >
822
+ <Button
823
+ type="button"
824
+ onClick={() => {
825
+ router.invalidateRoute(activeMatch as any)
826
+ router.notify()
827
+ }}
828
+ style={{
829
+ background: theme.warning,
830
+ color: theme.inputTextColor,
831
+ }}
832
+ >
833
+ Invalidate
834
+ </Button>{' '}
835
+ <Button
836
+ type="button"
837
+ onClick={() => router.reload()}
838
+ style={{
839
+ background: theme.gray,
840
+ }}
841
+ >
842
+ Reload
843
+ </Button>
844
+ </div>
845
+ <div
846
+ style={{
847
+ background: theme.backgroundAlt,
848
+ padding: '.5em',
849
+ position: 'sticky',
850
+ top: 0,
851
+ zIndex: 1,
852
+ }}
853
+ >
854
+ Explorer
855
+ </div>
856
+ <div
857
+ style={{
858
+ padding: '.5em',
859
+ }}
860
+ >
861
+ <Explorer
862
+ label="Match"
863
+ value={(() => {
864
+ const {
865
+ cancel,
866
+ load,
867
+ router,
868
+ Link,
869
+ MatchRoute,
870
+ buildLink,
871
+ linkProps,
872
+ matchRoute,
873
+ navigate,
874
+ ...rest
875
+ } = activeMatch
876
+
877
+ return rest
878
+ })()}
879
+ defaultExpanded={{}}
880
+ />
881
+ </div>
882
+ </ActivePanel>
883
+ ) : null}
884
+ <div
885
+ style={{
886
+ flex: '1 1 500px',
887
+ minHeight: '40%',
888
+ maxHeight: '100%',
889
+ overflow: 'auto',
890
+ borderRight: `1px solid ${theme.grayAlt}`,
891
+ display: 'flex',
892
+ flexDirection: 'column',
893
+ }}
894
+ >
895
+ <div
896
+ style={{
897
+ padding: '.5em',
898
+ background: theme.backgroundAlt,
899
+ position: 'sticky',
900
+ top: 0,
901
+ zIndex: 1,
902
+ }}
903
+ >
904
+ Loader Data
905
+ </div>
906
+ <div
907
+ style={{
908
+ padding: '.5em',
909
+ }}
910
+ >
911
+ {Object.keys(last(router.state.matches)?.loaderData || {})
912
+ .length ? (
913
+ <Explorer
914
+ value={last(router.state.matches)?.loaderData || {}}
915
+ defaultExpanded={Object.keys(
916
+ (last(router.state.matches)?.loaderData as {}) || {},
917
+ ).reduce((obj: any, next) => {
918
+ obj[next] = {}
919
+ return obj
920
+ }, {})}
921
+ />
922
+ ) : (
923
+ <em style={{ opacity: 0.5 }}>{'{ }'}</em>
924
+ )}
925
+ </div>
926
+ <div
927
+ style={{
928
+ padding: '.5em',
929
+ background: theme.backgroundAlt,
930
+ position: 'sticky',
931
+ top: 0,
932
+ zIndex: 1,
933
+ }}
934
+ >
935
+ Search Params
936
+ </div>
937
+ <div
938
+ style={{
939
+ padding: '.5em',
940
+ }}
941
+ >
942
+ {Object.keys(last(router.state.matches)?.search || {}).length ? (
943
+ <Explorer
944
+ value={last(router.state.matches)?.search || {}}
945
+ defaultExpanded={Object.keys(
946
+ (last(router.state.matches)?.search as {}) || {},
947
+ ).reduce((obj: any, next) => {
948
+ obj[next] = {}
949
+ return obj
950
+ }, {})}
951
+ />
952
+ ) : (
953
+ <em style={{ opacity: 0.5 }}>{'{ }'}</em>
954
+ )}
955
+ </div>
956
+ </div>
957
+ </Panel>
958
+ </ThemeProvider>
959
+ )
960
+ })