Skip to content

Commit bf83dbc

Browse files
committed
v2.4.0
1 parent de26962 commit bf83dbc

21 files changed

+213
-93
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Remix Development Tools is an open-source package designed to enhance your devel
2222

2323
## What's new?
2424

25+
## v2.4.0
26+
- Route boundaries locked behind a feature flag
27+
- Panel position settings allow you to change the panel position to either top or bottom of the screen
28+
- ErrorBoundaries are more precise now
2529
## v2.3.0
2630
Route tree view support!
2731
View all your routes in a node tree! You can also open routes in your browser from the tree view.
@@ -53,7 +57,7 @@ Key features include:
5357
- **URL Parameters**: Easily view and analyze the URL parameters associated with the current page.
5458
- **Server Responses**: Inspect and review the server responses received by the application for the current page.
5559
- **Loader Data**: Monitor and track the loader data used by the application during page rendering.
56-
- **Outlet boundaries** Activate the **Show Route Boundaries** option to see each Outlet and route boundaries by coloring the background. This needs to make a GET request to the current route when the dev tools are mounted for the first time to work properly and hence it is locked behind a flag. You can enable it by passing `showRouteBoundaries` prop to `true` in the `RemixDevTools` component.
60+
- **Outlet boundaries** Activate the **Show Route Boundaries** option to see each Outlet and route boundaries by coloring the background. It is locked behind a flag. You can enable it by passing `useRouteBoundaries` prop to `true` in the `RemixDevTools`,first parameter of `initClient` set to `true` and second parameter of `initServer` set to `true`. This feature is experimental and can cause issues in certain scenarios. It will be considered stable with v3.0 release but until then use at your own risk.
5761

5862
### Routes Tab
5963

@@ -218,6 +222,7 @@ export default function App() {
218222
The `RemixDevTools` component accepts the following props:
219223
- `requireUrlFlag`: Requires rdt=true to be present in the URL search to open the Remix Development Tools. Defaults to `false`.
220224
- `plugins`: Allows you to provide additional tabs (plugins) to the Remix Development Tools. Defaults to `[]`.
225+
- `useRouteBoundaries`: Allows you to enable the route boundaries feature. Defaults to `false`. This feature is experimental and can cause issues in certain scenarios. It will be considered stable with v3.0 release but until then use at your own risk.
221226

222227

223228
## Plugins

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "remix-development-tools",
33
"description": "Remix development tools.",
44
"author": "Alem Tuzlak",
5-
"version": "2.3.1",
5+
"version": "2.4.0",
66
"license": "MIT",
77
"keywords": [
88
"remix",

src/RemixDevTools/RemixDevTools.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,21 @@ function useHydrated() {
8080
export interface RemixDevToolsProps {
8181
// Whether the dev tools require a url flag to be shown
8282
requireUrlFlag?: boolean;
83-
83+
// Whether to use route boundaries to show them on the UI
84+
useRouteBoundaries?: boolean;
8485
// Additional tabs to add to the dev tools
8586
plugins?: Tab[];
8687
}
8788

88-
const RemixDevTools = ({ requireUrlFlag, plugins }: RemixDevToolsProps) => {
89+
const RemixDevTools = ({ requireUrlFlag, plugins, useRouteBoundaries }: RemixDevToolsProps) => {
8990
const hydrated = useHydrated();
9091
const url = useLocation().search;
9192

9293
if (!hydrated) return null;
9394
if (requireUrlFlag && !url.includes("rdt=true")) return null;
9495

9596
return (
96-
<RDTContextProvider>
97+
<RDTContextProvider useRouteBoundaries={useRouteBoundaries}>
9798
<InjectedStyles />
9899
<DevTools plugins={plugins} />
99100
</RDTContextProvider>

src/RemixDevTools/components/RouteInfo.tsx

+24-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const RouteInfo = ({ route, className, openNewRoute, onClose }: RouteInfo
1818
const { routeWildcards, routeViewMode } = settings;
1919
const { hasWildcard, path, pathToOpen } = constructRoutePath(route, routeWildcards);
2020
const isTreeView = routeViewMode === "tree";
21+
const hasParentErrorBoundary =
22+
route.errorBoundary.errorBoundaryId && route.errorBoundary.errorBoundaryId !== route.id;
23+
const hasErrorBoundary = route.errorBoundary.hasErrorBoundary;
2124
return (
2225
<div className={clsx(className, "rdt-relative")}>
2326
{isTreeView && (
@@ -42,7 +45,27 @@ export const RouteInfo = ({ route, className, openNewRoute, onClose }: RouteInfo
4245
<div className="rdt-flex rdt-gap-2">
4346
<Tag color={route.hasLoader ? "GREEN" : "RED"}>Loader</Tag>
4447
<Tag color={route.hasAction ? "GREEN" : "RED"}>Action</Tag>
45-
<Tag color={route.hasErrorBoundary ? "GREEN" : "RED"}>ErrorBoundary</Tag>
48+
49+
<div
50+
className={clsx(
51+
"rdt-flex rdt-gap-2 rdt-rounded-md rdt-border",
52+
!hasErrorBoundary ? "rdt-border-red-500" : "rdt-border-green-500"
53+
)}
54+
>
55+
<Tag
56+
className={clsx(hasErrorBoundary && "rdt-rounded-br-none rdt-rounded-tr-none")}
57+
color={hasErrorBoundary ? "GREEN" : "RED"}
58+
>
59+
ErrorBoundary
60+
</Tag>
61+
{hasErrorBoundary ? (
62+
<div className="rdt-mr-2">
63+
{hasParentErrorBoundary
64+
? `Covered by parent ErrorBoundary: ${route.errorBoundary.errorBoundaryId}`
65+
: "Contains ErrorBoundary"}
66+
</div>
67+
) : null}
68+
</div>
4669
</div>
4770
</div>
4871
{hasWildcard && (

src/RemixDevTools/components/RouteNode.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const RouteNode = ({
1717
const parent = hierarchyPointNode.parent?.data;
1818
const parentName = parent && parent?.name !== "/" ? parent.name : "";
1919
const name = nodeDatum.name.replace(parentName, "") ?? "/";
20-
const route = nodeDatum.attributes as any as ExtendedRoute;
20+
const route = { ...nodeDatum, ...nodeDatum.attributes } as any as ExtendedRoute;
2121
return (
2222
<g className="rdt-flex">
2323
<circle

src/RemixDevTools/components/Select.tsx

+4-5
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,7 @@ const SelectLabel = React.forwardRef<
6868
>(({ className, ...props }, ref) => (
6969
<SelectPrimitive.Label
7070
ref={ref}
71-
className={cn(
72-
"rdt-py-1.5 rdt-pl-8 rdt-pr-2 rdt-font-sans rdt-text-sm",
73-
className
74-
)}
71+
className={cn("rdt-py-1.5 rdt-pl-8 rdt-pr-2 rdt-font-sans rdt-text-sm", className)}
7572
{...props}
7673
/>
7774
));
@@ -119,16 +116,18 @@ const SelectWithOptions = <T extends string>({
119116
onSelect,
120117
hint,
121118
value,
119+
className,
122120
}: {
123121
placeholder?: string;
124122
value?: T;
125123
label?: string;
126124
hint?: string;
127125
options: { value: T; label: string }[];
128126
onSelect: (value: T) => void;
127+
className?: string;
129128
}) => {
130129
return (
131-
<Stack gap="sm">
130+
<Stack className={className} gap="sm">
132131
{label && <Label>{label}</Label>}
133132
<Select value={value} onValueChange={onSelect}>
134133
<SelectTrigger className="rdt-w-full">

src/RemixDevTools/context/RDTContext.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ RDTContext.displayName = "RDTContext";
2424

2525
interface ContextProps {
2626
children: React.ReactNode;
27+
useRouteBoundaries?: boolean;
2728
}
2829

2930
export const setIsDetachedIfRequired = () => {
@@ -66,21 +67,24 @@ export const getSettings = () => {
6667
};
6768
};
6869

69-
export const getExistingStateFromStorage = (): RemixDevToolsState => {
70+
export const getExistingStateFromStorage = (useRouteBoundaries?: boolean) => {
7071
const existingState = getStorageItem(REMIX_DEV_TOOLS_STATE);
7172
const settings = getSettings();
7273
const { detachedWindow, detachedWindowOwner } = detachedModeSetup();
73-
return {
74+
const state: RemixDevToolsState = {
7475
...initialState,
7576
...(existingState ? JSON.parse(existingState) : {}),
7677
settings,
78+
useRouteBoundaries,
7779
detachedWindow,
7880
detachedWindowOwner,
7981
};
82+
83+
return state;
8084
};
8185

82-
export const RDTContextProvider = ({ children }: ContextProps) => {
83-
const [state, dispatch] = useReducer(rdtReducer, getExistingStateFromStorage());
86+
export const RDTContextProvider = ({ children, useRouteBoundaries }: ContextProps) => {
87+
const [state, dispatch] = useReducer<typeof rdtReducer>(rdtReducer, getExistingStateFromStorage(useRouteBoundaries));
8488
const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);
8589
useListenToRouteChange();
8690
useRemoveBody(state);

src/RemixDevTools/context/rdtReducer.ts

+4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ export type RemixDevToolsState = {
4040
hoveredRoute: string;
4141
isHoveringRoute: boolean;
4242
routeViewMode: "list" | "tree";
43+
panelLocation: "top" | "bottom";
4344
};
45+
useRouteBoundaries: boolean;
4446
persistOpen: boolean;
4547
detachedWindow: boolean;
4648
detachedWindowOwner: boolean;
@@ -65,10 +67,12 @@ export const initialState: RemixDevToolsState = {
6567
hoveredRoute: "",
6668
isHoveringRoute: false,
6769
routeViewMode: "tree",
70+
panelLocation: "bottom",
6871
},
6972
persistOpen: false,
7073
detachedWindow: false,
7174
detachedWindowOwner: false,
75+
useRouteBoundaries: false,
7276
};
7377

7478
export type ReducerActions = Pick<RemixDevToolsActions, "type">["type"];

src/RemixDevTools/hooks/useOutletAugment.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { useEffect } from "react";
22
import { InvisibleBoundary } from "../init/project";
3+
import { useRDTContext } from "../context/useRDTContext";
34

45
const isHooked = Symbol("isHooked") as any;
56

67
export function useOutletAugment() {
8+
// TODO Remove once stabilized
9+
const { state } = useRDTContext();
710
useEffect(() => {
11+
if (!state.useRouteBoundaries) return;
812
if (window.__remixRouteModules[isHooked]) return;
913

1014
window.__remixRouteModules = new Proxy(window.__remixRouteModules, {
@@ -35,5 +39,5 @@ export function useOutletAugment() {
3539
return value;
3640
},
3741
});
38-
}, []);
42+
}, [state.useRouteBoundaries]);
3943
}

src/RemixDevTools/hooks/useResize.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useSettingsContext } from "../context/useRDTContext";
33

44
const useResize = () => {
55
const { setSettings, settings } = useSettingsContext();
6-
const { height, maxHeight, minHeight } = settings;
6+
const { height, maxHeight, minHeight, panelLocation } = settings;
77
const [isResizing, setIsResizing] = useState(false);
88

99
const enableResize = useCallback(() => {
@@ -18,7 +18,7 @@ const useResize = () => {
1818
(e: MouseEvent) => {
1919
if (isResizing) {
2020
window.getSelection()?.removeAllRanges(); // Prevent text selection
21-
const newHeight = window.innerHeight - e.clientY; // Calculate the new height based on the mouse position
21+
const newHeight = panelLocation === "top" ? e.clientY : window.innerHeight - e.clientY; // Calculate the new height based on the mouse position
2222

2323
//const newHeight = e.clientY; // You may want to add some offset here from props
2424

@@ -35,7 +35,7 @@ const useResize = () => {
3535
setSettings({ height: newHeight });
3636
}
3737
},
38-
[isResizing, maxHeight, minHeight, setSettings]
38+
[isResizing, maxHeight, minHeight, setSettings, panelLocation]
3939
);
4040

4141
useEffect(() => {

src/RemixDevTools/hooks/useSetRouteBoundaries.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { useCallback, useEffect } from "react";
22
import { ROUTE_BOUNDARY_GRADIENTS } from "../context/rdtReducer";
3-
import { useSettingsContext, useDetachedWindowControls } from "../context/useRDTContext";
3+
import { useSettingsContext, useDetachedWindowControls, useRDTContext } from "../context/useRDTContext";
44
import { useAttachListener } from "./useAttachListener";
55

66
export const useSetRouteBoundaries = () => {
77
const { settings, setSettings } = useSettingsContext();
88
const { detachedWindow } = useDetachedWindowControls();
9+
// TODO Remove once stabilized
10+
const { state } = useRDTContext();
911
const applyOrRemoveClasses = useCallback(
1012
(isHovering?: boolean) => {
13+
// TODO Remove once stabilized
14+
if (!state.useRouteBoundaries) return;
1115
// Overrides the hovering so the classes are force removed if needed
1216
const hovering = isHovering ?? settings.isHoveringRoute;
1317
// Classes to apply/remove
@@ -32,10 +36,12 @@ export const useSetRouteBoundaries = () => {
3236
}
3337
}
3438
},
35-
[settings.hoveredRoute, settings.isHoveringRoute, settings.routeBoundaryGradient]
39+
[settings.hoveredRoute, state.useRouteBoundaries, settings.isHoveringRoute, settings.routeBoundaryGradient]
3640
);
3741
// Mouse left the document => remove classes => set isHovering to false so that detached mode removes as well
3842
useAttachListener("mouseleave", "document", () => {
43+
// TODO Remove once stabilized
44+
if (!state.useRouteBoundaries) return;
3945
applyOrRemoveClasses();
4046
if (!detachedWindow) {
4147
return;
@@ -46,6 +52,8 @@ export const useSetRouteBoundaries = () => {
4652
});
4753
// Mouse is scrolling => remove classes => set isHovering to false so that detached mode removes as well
4854
useAttachListener("wheel", "window", () => {
55+
// TODO Remove once stabilized
56+
if (!state.useRouteBoundaries) return;
4957
applyOrRemoveClasses(false);
5058
if (!detachedWindow) {
5159
return;
@@ -56,6 +64,8 @@ export const useSetRouteBoundaries = () => {
5664
});
5765
// We apply/remove classes on state change which happens in Page tab
5866
useEffect(() => {
67+
// TODO Remove once stabilized
68+
if (!state.useRouteBoundaries) return;
5969
if (!settings.isHoveringRoute && !settings.hoveredRoute) return;
6070
applyOrRemoveClasses();
6171
if (!settings.isHoveringRoute && !detachedWindow) {
@@ -65,6 +75,7 @@ export const useSetRouteBoundaries = () => {
6575
});
6676
}
6777
}, [
78+
state.useRouteBoundaries,
6879
settings.hoveredRoute,
6980
settings.isHoveringRoute,
7081
settings.routeBoundaryGradient,

src/RemixDevTools/init/project.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export const InvisibleBoundary = ({ path }: { path: string }) => {
55
return <div className={clsx("rdt-invisible rdt-absolute rdt-hidden rdt-h-0 rdt-w-0", path)} />;
66
};
77

8-
export const initServer = (context: EntryContext) => {
8+
export const initServer = (context: EntryContext, useRouteBoundaries = false) => {
9+
if (!useRouteBoundaries) return context;
910
return {
1011
...context,
1112
routeModules: Object.entries(context.routeModules).reduce((acc, [key, value]) => {
@@ -33,7 +34,8 @@ export const initServer = (context: EntryContext) => {
3334
};
3435
};
3536

36-
export const initClient = () => {
37+
export const initClient = (useRouteBoundaries = false) => {
38+
if (!useRouteBoundaries) return;
3739
window.__remixRouteModules = Object.keys(window.__remixRouteModules).reduce((acc, key) => {
3840
const value = window.__remixRouteModules[key];
3941
if (key === "root") {

src/RemixDevTools/layout/MainPanel.tsx

+26-10
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const useResizeDetachedPanel = () => {
2424
const MainPanel = ({ children, isOpen, isEmbedded = false, className }: MainPanelProps) => {
2525
const { settings } = useSettingsContext();
2626
const { detachedWindow } = useDetachedWindowControls();
27-
const { height } = settings;
27+
const { height, panelLocation } = settings;
2828
const { enableResize, disableResize, isResizing } = useResize();
2929
useResizeDetachedPanel();
3030

@@ -38,19 +38,35 @@ const MainPanel = ({ children, isOpen, isEmbedded = false, className }: MainPane
3838
"rdt-duration-600 rdt-box-border rdt-flex rdt-w-screen rdt-flex-col rdt-overflow-auto rdt-bg-[#212121] rdt-text-white rdt-opacity-0 rdt-transition-all",
3939
isOpen ? "rdt-opacity-100 rdt-drop-shadow-2xl" : "rdt-h-0",
4040
isResizing && "rdt-cursor-grabbing ",
41-
!isEmbedded && "rdt-fixed rdt-bottom-0 rdt-left-0",
41+
!isEmbedded
42+
? `rdt-fixed rdt-left-0 ${
43+
panelLocation === "bottom" ? "rdt-bottom-0" : "rdt-top-0 rdt-border-b-2 rdt-border-[#212121]"
44+
}`
45+
: "",
4246
className
4347
)}
4448
>
45-
<div
46-
onMouseDown={enableResize}
47-
onMouseUp={disableResize}
48-
className={clsx(
49-
"rdt-absolute rdt-z-50 rdt-h-1 rdt-w-full",
50-
isResizing ? "rdt-cursor-grabbing" : "rdt-cursor-ns-resize"
51-
)}
52-
/>
49+
{panelLocation === "bottom" && (
50+
<div
51+
onMouseDown={enableResize}
52+
onMouseUp={disableResize}
53+
className={clsx(
54+
"rdt-absolute rdt-z-50 rdt-h-1 rdt-w-full",
55+
isResizing ? "rdt-cursor-grabbing" : "rdt-cursor-ns-resize"
56+
)}
57+
/>
58+
)}
5359
{children}
60+
{panelLocation === "top" && (
61+
<div
62+
onMouseDown={enableResize}
63+
onMouseUp={disableResize}
64+
className={clsx(
65+
"rdt-absolute rdt-bottom-0 rdt-z-50 rdt-h-1 rdt-w-full",
66+
isResizing ? "rdt-cursor-grabbing" : "rdt-cursor-ns-resize"
67+
)}
68+
/>
69+
)}
5470
</div>
5571
);
5672
};

0 commit comments

Comments
 (0)