Skip to content

Commit 56fe93b

Browse files
authored
[dev-overlay]: add preference panel (#76522)
Adds user preferences for: - Toggling theme (localstorage) - Position (localstorage) - Hiding the overlay (existing dev session heuristic) https://github.com/user-attachments/assets/0f08ce66-4107-4751-80d3-a42d3315966a ![CleanShot 2025-02-25 at 17 59 11@2x](https://github.com/user-attachments/assets/e65fdfb0-23b3-4c47-b8af-f55dd60e7b95) ![CleanShot 2025-02-25 at 17 59 15@2x](https://github.com/user-attachments/assets/86d33590-18f7-44f6-acd2-9639c098476a)
1 parent 6bba4ba commit 56fe93b

File tree

16 files changed

+575
-153
lines changed

16 files changed

+575
-153
lines changed

packages/next/src/client/components/react-dev-overlay/shared.ts

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection'
3838
export const ACTION_DEBUG_INFO = 'debug-info'
3939
export const ACTION_DEV_INDICATOR = 'dev-indicator'
4040

41+
export const STORAGE_KEY_THEME = '__nextjs-dev-tools-theme'
42+
export const STORAGE_KEY_POSITION = '__nextjs-dev-tools-position'
43+
4144
interface StaticIndicatorAction {
4245
type: typeof ACTION_STATIC_INDICATOR
4346
staticIndicator: boolean

packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.stories.tsx

+1-7
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@ const meta: Meta<typeof DevToolsIndicator> = {
99
parameters: {
1010
layout: 'centered',
1111
},
12-
argTypes: {
13-
position: {
14-
control: 'select',
15-
options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
16-
defaultValue: 'bottom-left',
17-
},
18-
},
12+
argTypes: {},
1913
decorators: [
2014
withShadowPortal,
2115
// Test for high z-index

packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.tsx

+61-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Dispatch, SetStateAction } from 'react'
2-
import type { OverlayState } from '../../../../shared'
2+
import { STORAGE_KEY_POSITION, type OverlayState } from '../../../../shared'
33

44
import { useState, useEffect, useRef, createContext, useContext } from 'react'
55
import { Toast } from '../../toast'
@@ -9,7 +9,8 @@ import { useIsDevRendering } from '../../../../utils/dev-indicator/dev-render-in
99
import { useDelayedRender } from '../../../hooks/use-delayed-render'
1010
import { TurbopackInfo } from './dev-tools-info/turbopack-info'
1111
import { RouteInfo } from './dev-tools-info/route-info'
12-
import { StopIcon } from '../../../icons/stop-icon'
12+
import GearIcon from '../../../icons/gear-icon'
13+
import { UserPreferences } from './dev-tools-info/user-preferences'
1314

1415
// TODO: add E2E tests to cover different scenarios
1516

@@ -18,23 +19,20 @@ const INDICATOR_POSITION =
1819
.__NEXT_DEV_INDICATOR_POSITION as typeof window.__NEXT_DEV_INDICATOR_POSITION) ||
1920
'bottom-left'
2021

21-
type DevToolsIndicatorPosition = typeof INDICATOR_POSITION
22+
export type DevToolsIndicatorPosition = typeof INDICATOR_POSITION
2223

2324
export function DevToolsIndicator({
2425
state,
2526
errorCount,
2627
isBuildError,
2728
setIsErrorOverlayOpen,
28-
position = INDICATOR_POSITION,
2929
}: {
3030
state: OverlayState
3131
errorCount: number
3232
isBuildError: boolean
3333
setIsErrorOverlayOpen: (
3434
isErrorOverlayOpen: boolean | ((prev: boolean) => boolean)
3535
) => void
36-
// Technically this prop isn't needed, but useful for testing.
37-
position?: DevToolsIndicatorPosition
3836
}) {
3937
const [isDevToolsIndicatorVisible, setIsDevToolsIndicatorVisible] =
4038
useState(true)
@@ -53,7 +51,6 @@ export function DevToolsIndicator({
5351
}}
5452
setIsErrorOverlayOpen={setIsErrorOverlayOpen}
5553
isTurbopack={!!process.env.TURBOPACK}
56-
position={position}
5754
disabled={state.disableDevIndicator || !isDevToolsIndicatorVisible}
5855
isBuildError={isBuildError}
5956
/>
@@ -73,13 +70,25 @@ interface C {
7370

7471
const Context = createContext({} as C)
7572

73+
function getInitialPosition() {
74+
if (
75+
typeof localStorage !== 'undefined' &&
76+
localStorage.getItem(STORAGE_KEY_POSITION)
77+
) {
78+
return localStorage.getItem(
79+
STORAGE_KEY_POSITION
80+
) as DevToolsIndicatorPosition
81+
}
82+
83+
return INDICATOR_POSITION
84+
}
85+
7686
function DevToolsPopover({
7787
routerType,
7888
disabled,
7989
issueCount,
8090
isStaticRoute,
8191
isTurbopack,
82-
position,
8392
isBuildError,
8493
hide,
8594
setIsErrorOverlayOpen,
@@ -90,7 +99,6 @@ function DevToolsPopover({
9099
isStaticRoute: boolean
91100
semver: string | undefined
92101
isTurbopack: boolean
93-
position: DevToolsIndicatorPosition
94102
isBuildError: boolean
95103
hide: () => void
96104
setIsErrorOverlayOpen: (
@@ -103,10 +111,14 @@ function DevToolsPopover({
103111
const triggerTurbopackRef = useRef<HTMLButtonElement | null>(null)
104112
const routeInfoRef = useRef<HTMLElement>(null)
105113
const triggerRouteInfoRef = useRef<HTMLButtonElement | null>(null)
114+
const preferencesRef = useRef<HTMLElement>(null)
115+
const triggerPreferencesRef = useRef<HTMLButtonElement | null>(null)
106116
const [isMenuOpen, setIsMenuOpen] = useState(false)
107117
const [isTurbopackInfoOpen, setIsTurbopackInfoOpen] = useState(false)
108118
const [isRouteInfoOpen, setIsRouteInfoOpen] = useState(false)
119+
const [isPreferencesOpen, setIsPreferencesOpen] = useState(false)
109120
const [selectedIndex, setSelectedIndex] = useState(-1)
121+
const [position, setPosition] = useState(getInitialPosition())
110122

111123
// This hook lets us do an exit animation before unmounting the component
112124
const { mounted: menuMounted, rendered: menuRendered } = useDelayedRender(
@@ -128,6 +140,11 @@ function DevToolsPopover({
128140
enterDelay: 0,
129141
exitDelay: ANIMATE_OUT_DURATION_MS,
130142
})
143+
const { mounted: preferencesMounted, rendered: preferencesRendered } =
144+
useDelayedRender(isPreferencesOpen, {
145+
enterDelay: 0,
146+
exitDelay: ANIMATE_OUT_DURATION_MS,
147+
})
131148

132149
// Features to make the menu accessible
133150
useFocusTrap(menuRef, triggerRef, isMenuOpen)
@@ -146,7 +163,13 @@ function DevToolsPopover({
146163
isRouteInfoOpen,
147164
closeRouteInfo
148165
)
149-
166+
useFocusTrap(preferencesRef, triggerPreferencesRef, isPreferencesOpen)
167+
useClickOutside(
168+
preferencesRef,
169+
triggerPreferencesRef,
170+
isPreferencesOpen,
171+
closePreferences
172+
)
150173
function select(index: number | 'first' | 'last') {
151174
if (index === 'first') {
152175
const all = menuRef.current?.querySelectorAll('[role="menuitem"]')
@@ -254,6 +277,15 @@ function DevToolsPopover({
254277
setIsRouteInfoOpen(false)
255278
}
256279

280+
function closePreferences() {
281+
setIsPreferencesOpen(false)
282+
}
283+
284+
function handleHideDevtools() {
285+
closePreferences()
286+
hide()
287+
}
288+
257289
const [vertical, horizontal] = position.split('-', 2)
258290

259291
return (
@@ -316,6 +348,21 @@ function DevToolsPopover({
316348
/>
317349
)}
318350

351+
{preferencesMounted && (
352+
<UserPreferences
353+
ref={preferencesRef}
354+
isOpen={isPreferencesOpen}
355+
hide={handleHideDevtools}
356+
setPosition={setPosition}
357+
position={position}
358+
style={{
359+
[vertical]: 'calc(100% + 8px)',
360+
[horizontal]: 0,
361+
}}
362+
data-rendered={preferencesRendered}
363+
/>
364+
)}
365+
319366
{menuMounted && (
320367
<div
321368
ref={menuRef}
@@ -381,11 +428,10 @@ function DevToolsPopover({
381428

382429
<div className="dev-tools-indicator-footer">
383430
<MenuItem
384-
data-hide-dev-tools
385-
title="Hide Dev Tools for the current server session or a day."
386-
label="Hide for Dev Session"
387-
value={<StopIcon />}
388-
onClick={hide}
431+
data-preferences
432+
label="Preferences"
433+
value={<GearIcon />}
434+
onClick={() => setIsPreferencesOpen(true)}
389435
index={isTurbopack ? 2 : 3}
390436
/>
391437
</div>

packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/dev-tools-info.tsx

+31-25
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,40 @@ export function DevToolsInfo({
88
}: {
99
title: string
1010
children: React.ReactNode
11-
learnMoreLink: string
12-
setIsOpen: (isOpen: boolean) => void
13-
setPreviousOpen: (isOpen: boolean) => void
11+
learnMoreLink?: string
12+
setIsOpen?: (isOpen: boolean) => void
13+
setPreviousOpen?: (isOpen: boolean) => void
1414
}) {
15+
const hasActionButtons = Boolean(
16+
learnMoreLink && setIsOpen && setPreviousOpen
17+
)
18+
1519
return (
1620
<div data-info-popover {...props}>
1721
<div className="dev-tools-info-container">
1822
<h1 className="dev-tools-info-title">{title}</h1>
1923
{children}
20-
<div className="dev-tools-info-button-container">
21-
<button
22-
className="dev-tools-info-close-button"
23-
onClick={() => {
24-
setIsOpen(false)
25-
setPreviousOpen(true)
26-
}}
27-
>
28-
Close
29-
</button>
30-
<a
31-
className="dev-tools-info-learn-more-button"
32-
href={learnMoreLink}
33-
target="_blank"
34-
rel="noreferrer noopener"
35-
>
36-
Learn More
37-
</a>
38-
</div>
24+
{hasActionButtons && (
25+
<div className="dev-tools-info-button-container">
26+
<button
27+
className="dev-tools-info-close-button"
28+
onClick={() => {
29+
setIsOpen?.(false)
30+
setPreviousOpen?.(true)
31+
}}
32+
>
33+
Close
34+
</button>
35+
<a
36+
className="dev-tools-info-learn-more-button"
37+
href={learnMoreLink}
38+
target="_blank"
39+
rel="noreferrer noopener"
40+
>
41+
Learn More
42+
</a>
43+
</div>
44+
)}
3945
</div>
4046
</div>
4147
)
@@ -69,14 +75,14 @@ export const DEV_TOOLS_INFO_STYLES = `
6975
}
7076
7177
.dev-tools-info-container {
72-
padding: 6px;
78+
padding: 12px;
7379
}
7480
7581
.dev-tools-info-title {
7682
padding: 8px 6px;
7783
color: var(--color-gray-1000);
78-
font-size: var(--size-14);
79-
font-weight: 500;
84+
font-size: var(--size-16);
85+
font-weight: 600;
8086
line-height: var(--size-20);
8187
margin: 0;
8288
}

packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/route-info.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,4 @@ export function RouteInfo({
146146
)
147147
}
148148

149-
export const DEV_TOOLS_INFO_ROUTE_INFO_STYLES = `
150-
.dev-tools-info-link {
151-
}
152-
`
149+
export const DEV_TOOLS_INFO_ROUTE_INFO_STYLES = ``

0 commit comments

Comments
 (0)