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