Skip to content

Commit fc3b1da

Browse files
committed
ui improvements
1 parent a372a73 commit fc3b1da

File tree

8 files changed

+369
-88
lines changed

8 files changed

+369
-88
lines changed

packages/react-router-devtools/src/client/components/EditorButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useStyles } from "../styles/use-styles.js"
2+
import { Icon } from "./icon/Icon.js"
23

34
interface EditorButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
45
onClick: () => void
@@ -8,6 +9,7 @@ const EditorButton = ({ onClick, ...props }: EditorButtonProps) => {
89
const { styles } = useStyles()
910
return (
1011
<button onClick={onClick} type="button" className={styles.editorButton} {...props}>
12+
<Icon name="Send" size="sm" />
1113
Open in Editor
1214
</button>
1315
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { ReactNode } from "react"
2+
import { useStyles } from "../styles/use-styles.js"
3+
4+
interface RouteSegmentBodyProps {
5+
children: ReactNode
6+
}
7+
8+
const RouteSegmentBody = ({ children }: RouteSegmentBodyProps) => {
9+
const { styles } = useStyles()
10+
11+
return <div className={styles.routeSegmentCard.body}>{children}</div>
12+
}
13+
14+
export { RouteSegmentBody }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { ReactNode } from "react"
2+
import { useStyles } from "../styles/use-styles.js"
3+
4+
interface RouteSegmentCardProps {
5+
children: ReactNode
6+
onMouseEnter?: () => void
7+
onMouseLeave?: () => void
8+
"data-testid"?: string
9+
}
10+
11+
const RouteSegmentCard = ({
12+
children,
13+
onMouseEnter,
14+
onMouseLeave,
15+
"data-testid": dataTestId,
16+
}: RouteSegmentCardProps) => {
17+
const { styles } = useStyles()
18+
19+
return (
20+
<li
21+
data-testid={dataTestId}
22+
onMouseEnter={onMouseEnter}
23+
onMouseLeave={onMouseLeave}
24+
className={styles.routeSegmentCard.item}
25+
>
26+
{children}
27+
</li>
28+
)
29+
}
30+
31+
export { RouteSegmentCard }
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { ReactNode } from "react"
2+
import { cx, useStyles } from "../styles/use-styles.js"
3+
import { Icon } from "./icon/Icon.js"
4+
5+
type RouteColor = "green" | "blue" | "teal" | "red" | "purple"
6+
7+
interface RouteSegmentHeaderProps {
8+
icon: "Root" | "Layout" | "CornerDownRight"
9+
color: RouteColor
10+
title: string
11+
subtitle?: string
12+
rightContent?: ReactNode
13+
}
14+
15+
const RouteSegmentHeader = ({ icon, color, title, subtitle, rightContent }: RouteSegmentHeaderProps) => {
16+
const { styles } = useStyles()
17+
18+
return (
19+
<div className={styles.routeSegmentCard.header}>
20+
<div
21+
className={cx(
22+
styles.routeSegmentCard.icon,
23+
styles.routeSegmentCard[
24+
`icon${color.charAt(0).toUpperCase() + color.slice(1)}` as keyof typeof styles.routeSegmentCard
25+
]
26+
)}
27+
>
28+
<Icon name={icon} size="sm" />
29+
</div>
30+
<h2 className={styles.routeSegmentCard.title}>
31+
{title}
32+
{subtitle && <span className={styles.routeSegmentCard.subtitle}> ({subtitle})</span>}
33+
</h2>
34+
{rightContent && <div className={styles.routeSegmentCard.headerActions}>{rightContent}</div>}
35+
</div>
36+
)
37+
}
38+
39+
export { RouteSegmentHeader }
40+
export type { RouteColor }

packages/react-router-devtools/src/client/components/RouteSegmentInfo.tsx

Lines changed: 79 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import type { UIMatch } from "react-router"
22
import { parseCacheControlHeader } from "../../server/parser.js"
33
import { type ServerRouteInfo, defaultServerRouteState } from "../context/rdtReducer.js"
44
import { useServerInfo, useSettingsContext } from "../context/useRDTContext.js"
5-
import { cx, useStyles } from "../styles/use-styles.js"
5+
import { useStyles } from "../styles/use-styles.js"
66
import { openSource } from "../utils/open-source.js"
77
import { isLayoutRoute } from "../utils/routing.js"
88
import { CacheInfo } from "./CacheInfo.js"
99
import { EditorButton } from "./EditorButton.js"
1010
import { InfoCard } from "./InfoCard.js"
11+
import { RouteSegmentBody } from "./RouteSegmentBody.js"
12+
import { RouteSegmentCard } from "./RouteSegmentCard.js"
13+
import { type RouteColor, RouteSegmentHeader } from "./RouteSegmentHeader.js"
14+
import { Tag } from "./Tag.js"
1115
import { Icon } from "./icon/Icon.js"
1216
import { JsonRenderer } from "./jsonRenderer.js"
1317

@@ -60,14 +64,6 @@ const cleanServerInfo = (routeInfo: ServerRouteInfo) => {
6064
}
6165
}
6266

63-
const ROUTE_COLORS = {
64-
GREEN: "green",
65-
BLUE: "blue",
66-
TEAL: "teal",
67-
RED: "red",
68-
PURPLE: "purple",
69-
} as const
70-
7167
export const RouteSegmentInfo = ({ route, i }: { route: UIMatch<unknown, unknown>; i: number }) => {
7268
const { styles } = useStyles()
7369
const { server, setServerInfo } = useServerInfo()
@@ -98,83 +94,93 @@ export const RouteSegmentInfo = ({ route, i }: { route: UIMatch<unknown, unknown
9894
setServerInfo({ routes: newServerInfo })
9995
}
10096

101-
const routeColor = isRoot ? ROUTE_COLORS.PURPLE : isLayout ? ROUTE_COLORS.BLUE : ROUTE_COLORS.GREEN
97+
const routeColor: RouteColor = isRoot ? "purple" : isLayout ? "blue" : "green"
98+
const iconName: "Root" | "Layout" | "CornerDownRight" = isRoot ? "Root" : isLayout ? "Layout" : "CornerDownRight"
99+
100+
const headerActions = (
101+
<div className={styles.routeSegmentCard.headerActions}>
102+
{isDev && import.meta.env.DEV && entryRoute?.module && (
103+
<EditorButton
104+
data-testid={`${route.id}-open-source`}
105+
onClick={() => {
106+
openSource(`${entryRoute.module}`)
107+
}}
108+
/>
109+
)}
110+
{settings.showRouteBoundariesOn === "click" && (
111+
<button
112+
type="button"
113+
data-testid={`${route.id}-show-route-boundaries`}
114+
className={styles.routeSegmentInfo.showBoundaryButton}
115+
onClick={() => {
116+
const routeId = route.id === "root" ? "root" : i.toString()
117+
if (routeId !== settings.hoveredRoute) {
118+
// Remove the classes from the old hovered route
119+
setSettings({
120+
isHoveringRoute: false,
121+
})
122+
// Add the classes to the new hovered route
123+
setTimeout(() => {
124+
setSettings({
125+
hoveredRoute: routeId,
126+
isHoveringRoute: true,
127+
})
128+
})
129+
} else {
130+
// Just change the isHoveringRoute state
131+
setSettings({
132+
isHoveringRoute: !settings.isHoveringRoute,
133+
})
134+
}
135+
}}
136+
>
137+
<Icon name="Radio" size="sm" />
138+
Show Route Boundary
139+
</button>
140+
)}
141+
</div>
142+
)
102143

103144
return (
104-
<li
145+
<RouteSegmentCard
105146
data-testid={route.id}
106147
onMouseEnter={() => onHover(route.id === "root" ? "root" : i.toString(), "enter")}
107148
onMouseLeave={() => onHover(route.id === "root" ? "root" : i.toString(), "leave")}
108-
className={styles.routeSegmentInfo.listItem}
109149
>
110-
<div
111-
className={cx(
112-
styles.routeSegmentInfo.iconWrapper,
113-
styles.routeSegmentInfo[
114-
`iconWrapper${routeColor.charAt(0).toUpperCase() + routeColor.slice(1)}` as keyof typeof styles.routeSegmentInfo
115-
]
116-
)}
117-
>
118-
<Icon name={isRoot ? "Root" : isLayout ? "Layout" : "CornerDownRight"} size="sm" />
119-
</div>
120-
<h2 className={styles.routeSegmentInfo.header}>
121-
{route.pathname}
122-
123-
<div className={styles.routeSegmentInfo.headerActions}>
124-
{Boolean(cacheControl && serverInfo?.lastLoader.timestamp) && (
150+
<RouteSegmentHeader
151+
icon={iconName}
152+
color={routeColor}
153+
title={route.pathname}
154+
subtitle={route.id}
155+
rightContent={headerActions}
156+
/>
157+
<RouteSegmentBody>
158+
{/* Cache Info */}
159+
{Boolean(cacheControl && serverInfo?.lastLoader.timestamp) && (
160+
<div className={styles.routeSegmentCard.cacheSection}>
125161
<CacheInfo
126162
key={JSON.stringify(serverInfo?.lastLoader ?? "")}
127163
// biome-ignore lint/style/noNonNullAssertion: <explanation>
128164
cacheControl={cacheControl!}
129165
cacheDate={new Date(serverInfo?.lastLoader.timestamp ?? "")}
130166
/>
131-
)}
132-
<div className={styles.routeSegmentInfo.actionButtons}>
133-
{isDev && import.meta.env.DEV && entryRoute?.module && (
134-
<EditorButton
135-
data-testid={`${route.id}-open-source`}
136-
onClick={() => {
137-
openSource(`${entryRoute.module}`)
138-
}}
139-
/>
140-
)}
141-
{settings.showRouteBoundariesOn === "click" && (
142-
<button
143-
type="button"
144-
data-testid={`${route.id}-show-route-boundaries`}
145-
className={styles.routeSegmentInfo.showBoundaryButton}
146-
onClick={() => {
147-
const routeId = route.id === "root" ? "root" : i.toString()
148-
if (routeId !== settings.hoveredRoute) {
149-
// Remove the classes from the old hovered route
150-
setSettings({
151-
isHoveringRoute: false,
152-
})
153-
// Add the classes to the new hovered route
154-
setTimeout(() => {
155-
setSettings({
156-
hoveredRoute: routeId,
157-
isHoveringRoute: true,
158-
})
159-
})
160-
} else {
161-
// Just change the isHoveringRoute state
162-
setSettings({
163-
isHoveringRoute: !settings.isHoveringRoute,
164-
})
165-
}
166-
}}
167-
>
168-
Show Route Boundary
169-
</button>
170-
)}
167+
</div>
168+
)}
169+
170+
{/* Component Tags */}
171+
<div className={styles.routeSegmentCard.componentsSection}>
172+
<span className={styles.routeSegmentCard.componentsSectionLabel}>Components:</span>
173+
<div className={styles.routeSegmentCard.tagsContainer}>
174+
<Tag color={entryRoute?.hasLoader ? "GREEN" : "RED"}>Loader</Tag>
175+
<Tag color={entryRoute?.hasClientLoader ? "GREEN" : "RED"}>Client Loader</Tag>
176+
<Tag color={entryRoute?.hasClientAction ? "GREEN" : "RED"}>Client Action</Tag>
177+
<Tag color={entryRoute?.hasAction ? "GREEN" : "RED"}>Action</Tag>
178+
<Tag color={entryRoute?.hasErrorBoundary ? "GREEN" : "RED"}>ErrorBoundary</Tag>
171179
</div>
172180
</div>
173-
</h2>
174-
<div className={styles.routeSegmentInfo.content}>
175-
<p className={styles.routeSegmentInfo.routeFileLabel}>Route segment file: {route.id}</p>
176181

177-
<div className={styles.routeSegmentInfo.cardsContainer}>
182+
{/* Info Cards */}
183+
<div className={styles.routeSegmentCard.cardsContainer}>
178184
{loaderData && <InfoCard title="Returned loader data">{<JsonRenderer data={loaderData} />}</InfoCard>}
179185
{serverInfo && import.meta.env.DEV && (
180186
<InfoCard onClear={clearServerInfoForRoute} title="Server information">
@@ -193,7 +199,7 @@ export const RouteSegmentInfo = ({ route, i }: { route: UIMatch<unknown, unknown
193199
</InfoCard>
194200
)}
195201
</div>
196-
</div>
197-
</li>
202+
</RouteSegmentBody>
203+
</RouteSegmentCard>
198204
)
199205
}

0 commit comments

Comments
 (0)