Repository: vercel/next.js
Pull request: [devtools] fork devtools-indicator

Unlike the menu popover (legacy), the panel UI can be decoupled from the indicator and communicate with the dispatcher. We don't need those refs, animation, etc. anymore, so removed them as well.

Also, it feels good to move out of the `errors/` directory as it's no longer confined to the error overlay.

Changed files:
filename: packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/devtools-indicator.stories.tsx
patch: @@ -0,0 +1,69 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import type { OverlayState } from '../../shared'
+
+import { DevToolsIndicator } from './devtools-indicator'
+import { INITIAL_OVERLAY_STATE } from '../../shared'
+import { withShadowPortal } from '../../storybook/with-shadow-portal'
+
+const meta: Meta<typeof DevToolsIndicator> = {
+  component: DevToolsIndicator,
+  parameters: {
+    layout: 'centered',
+  },
+  argTypes: {},
+  decorators: [
+    withShadowPortal,
+    // Test for high z-index
+    (Story) => (
+      <div
+        style={{
+          position: 'fixed',
+          top: 0,
+          left: 0,
+          width: '100%',
+          height: '100%',
+          zIndex: 9999,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          background:
+            'linear-gradient(135deg, rgba(230,240,255,0.8) 0%, rgba(200,220,255,0.6) 100%)',
+        }}
+      >
+        <Story />
+      </div>
+    ),
+  ],
+}
+
+export default meta
+type Story = StoryObj<typeof DevToolsIndicator>
+
+const state: OverlayState = {
+  ...INITIAL_OVERLAY_STATE,
+  routerType: 'app',
+  isErrorOverlayOpen: false,
+}
+
+export const Default: Story = {
+  args: {
+    state,
+    dispatch: () => {},
+  },
+}
+
+export const SingleError: Story = {
+  args: {
+    errorCount: 1,
+    state,
+    dispatch: () => {},
+  },
+}
+
+export const MultipleErrors: Story = {
+  args: {
+    errorCount: 3,
+    state,
+    dispatch: () => {},
+  },
+}

filename: packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/devtools-indicator.tsx
patch: @@ -0,0 +1,87 @@
+import type { CSSProperties } from 'react'
+import type { OverlayState, OverlayDispatch } from '../../shared'
+import type { DevToolsScale } from '../errors/dev-tools-indicator/dev-tools-info/preferences'
+
+import { useState, useRef } from 'react'
+import { Toast } from '../toast'
+import { NextLogo } from '../errors/dev-tools-indicator/next-logo'
+import {
+  MENU_CURVE,
+  MENU_DURATION_MS,
+} from '../errors/dev-tools-indicator/utils'
+import { ACTION_ERROR_OVERLAY_TOGGLE, STORAGE_KEY_POSITION } from '../../shared'
+import { getInitialPosition } from '../errors/dev-tools-indicator/dev-tools-info/preferences'
+import { Draggable } from '../errors/dev-tools-indicator/draggable'
+
+const INDICATOR_PADDING = 20
+
+export function DevToolsIndicator({
+  state,
+  dispatch,
+  errorCount,
+  isBuildError,
+  scale,
+}: {
+  state: OverlayState
+  dispatch: OverlayDispatch
+  errorCount: number
+  isBuildError: boolean
+  scale: DevToolsScale
+}) {
+  const [open, setOpen] = useState(false)
+  const [position, setPosition] = useState(getInitialPosition())
+
+  const triggerRef = useRef<HTMLButtonElement | null>(null)
+
+  const [vertical, horizontal] = position.split('-', 2)
+
+  const toggleErrorOverlay = () => {
+    dispatch({ type: ACTION_ERROR_OVERLAY_TOGGLE })
+  }
+
+  const onTriggerClick = () => {}
+
+  return (
+    <Toast
+      data-nextjs-toast
+      style={
+        {
+          '--animate-out-duration-ms': `${MENU_DURATION_MS}ms`,
+          '--animate-out-timing-function': MENU_CURVE,
+          boxShadow: 'none',
+          zIndex: 2147483647,
+          [vertical]: `${INDICATOR_PADDING}px`,
+          [horizontal]: `${INDICATOR_PADDING}px`,
+        } as CSSProperties
+      }
+    >
+      <Draggable
+        padding={INDICATOR_PADDING}
+        onDragStart={() => setOpen(false)}
+        position={position}
+        setPosition={(p) => {
+          localStorage.setItem(STORAGE_KEY_POSITION, p)
+          setPosition(p)
+        }}
+      >
+        {/* Trigger */}
+        <NextLogo
+          ref={triggerRef}
+          aria-haspopup="menu"
+          aria-expanded={open}
+          aria-controls="nextjs-dev-tools-menu"
+          aria-label={`${open ? 'Close' : 'Open'} Next.js Dev Tools`}
+          data-nextjs-dev-tools-button
+          disabled={state.disableDevIndicator}
+          issueCount={errorCount}
+          onTriggerClick={onTriggerClick}
+          toggleErrorOverlay={toggleErrorOverlay}
+          isDevBuilding={state.buildingIndicator}
+          isDevRendering={state.renderingIndicator}
+          isBuildError={isBuildError}
+          scale={scale}
+        />
+      </Draggable>
+    </Toast>
+  )
+}

filename: packages/next/src/next-devtools/dev-overlay/dev-overlay.tsx
patch: @@ -11,6 +11,7 @@ import { RenderError } from './container/runtime-error/render-error'
 import { DarkTheme } from './styles/dark-theme'
 import { useDevToolsScale } from './components/errors/dev-tools-indicator/dev-tools-info/preferences'
 import type { HydrationErrorState } from '../shared/hydration-error'
+import { DevToolsIndicator as DevToolsIndicatorNew } from './components/devtools-indicator/devtools-indicator'
 
 export function DevOverlay({
   state,
@@ -35,16 +36,25 @@ export function DevOverlay({
           const isBuildError = state.buildError !== null
           return (
             <>
-              {state.showIndicator && (
-                <DevToolsIndicator
-                  scale={scale}
-                  setScale={setScale}
-                  state={state}
-                  dispatch={dispatch}
-                  errorCount={totalErrorCount}
-                  isBuildError={isBuildError}
-                />
-              )}
+              {state.showIndicator &&
+                (process.env.__NEXT_DEVTOOL_NEW_PANEL_UI ? (
+                  <DevToolsIndicatorNew
+                    scale={scale}
+                    state={state}
+                    dispatch={dispatch}
+                    errorCount={totalErrorCount}
+                    isBuildError={isBuildError}
+                  />
+                ) : (
+                  <DevToolsIndicator
+                    scale={scale}
+                    setScale={setScale}
+                    state={state}
+                    dispatch={dispatch}
+                    errorCount={totalErrorCount}
+                    isBuildError={isBuildError}
+                  />
+                ))}
 
               <ErrorOverlay
                 state={state}