Repository: vercel/next.js
Pull request: [devtools] panel ui header

https://github.com/user-attachments/assets/512cd9d4-4f72-4ed3-a9dc-4efb823651c3

Closes NEXT-4528

Changed files:
filename: packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel.stories.tsx
patch: @@ -28,5 +28,14 @@ export const Default: Story = {
   args: {
     state,
     dispatch: () => {},
+    issueCount: 0,
+  },
+}
+
+export const WithIssues: Story = {
+  args: {
+    state,
+    dispatch: () => {},
+    issueCount: 3,
   },
 }

filename: packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel.tsx
patch: @@ -1,5 +1,7 @@
 import type { OverlayDispatch, OverlayState } from '../../shared'
 
+import { useState } from 'react'
+
 import { Dialog, DialogContent, DialogHeader, DialogBody } from '../dialog'
 import { Overlay } from '../overlay/overlay'
 import {
@@ -11,14 +13,21 @@ import { css } from '../../utils/css'
 import { OverlayBackdrop } from '../overlay'
 import { Draggable } from '../errors/dev-tools-indicator/draggable'
 import { INDICATOR_PADDING } from '../devtools-indicator/devtools-indicator'
+import { FullScreenIcon } from '../../icons/fullscreen'
+import { Cross } from '../../icons/cross'
 
 export function DevToolsPanel({
   state,
   dispatch,
+  issueCount,
 }: {
   state: OverlayState
   dispatch: OverlayDispatch
+  issueCount: number
 }) {
+  const [activeTab, setActiveTab] = useState<'issues' | 'route' | 'settings'>(
+    'settings'
+  )
   const [vertical, horizontal] = state.devToolsPosition.split('-', 2)
 
   const onClose = () => {
@@ -59,7 +68,51 @@ export function DevToolsPanel({
           onClose={onClose}
         >
           <DialogContent>
-            <DialogHeader></DialogHeader>
+            <DialogHeader>
+              <div data-nextjs-devtools-panel-header>
+                <div data-nextjs-devtools-panel-header-tab-group>
+                  <button
+                    data-nextjs-devtools-panel-header-tab={
+                      activeTab === 'issues'
+                    }
+                    onClick={() => setActiveTab('issues')}
+                  >
+                    Issues
+                    <span data-nextjs-devtools-panel-header-tab-issues-badge>
+                      {issueCount}
+                    </span>
+                  </button>
+                  <button
+                    data-nextjs-devtools-panel-header-tab={
+                      activeTab === 'route'
+                    }
+                    onClick={() => setActiveTab('route')}
+                  >
+                    Route Info
+                  </button>
+                  <button
+                    data-nextjs-devtools-panel-header-tab={
+                      activeTab === 'settings'
+                    }
+                    onClick={() => setActiveTab('settings')}
+                  >
+                    Settings
+                  </button>
+                </div>
+                <div data-nextjs-devtools-panel-header-action-button-group>
+                  {/* TODO: Currently no-op, will add fullscreen toggle. */}
+                  <button data-nextjs-devtools-panel-header-action-button>
+                    <FullScreenIcon width={16} height={16} />
+                  </button>
+                  <button
+                    data-nextjs-devtools-panel-header-action-button
+                    onClick={onClose}
+                  >
+                    <Cross width={16} height={16} />
+                  </button>
+                </div>
+              </div>
+            </DialogHeader>
             <DialogBody></DialogBody>
           </DialogContent>
         </Dialog>
@@ -100,4 +153,85 @@ export const DEVTOOLS_PANEL_STYLES = css`
     /* This is handled from dialog/styles.ts */
     max-width: var(--next-dialog-max-width);
   }
+
+  [data-nextjs-devtools-panel-header] {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid var(--color-gray-400);
+  }
+
+  [data-nextjs-devtools-panel-header-tab-group] {
+    display: flex;
+    align-items: center;
+    padding: 8px;
+    gap: 6px;
+  }
+
+  [data-nextjs-devtools-panel-header-tab] {
+    display: flex;
+    align-items: center;
+    color: var(--color-gray-900);
+    border-radius: var(--rounded-md-2);
+    padding: 4px 12px;
+    font-size: 14px;
+    font-weight: 500;
+
+    transition: all 0.2s ease;
+
+    &:hover {
+      background-color: var(--color-gray-200);
+    }
+
+    &:active {
+      background-color: var(--color-gray-300);
+    }
+  }
+
+  [data-nextjs-devtools-panel-header-tab='true'] {
+    color: var(--color-gray-1000);
+    background-color: var(--color-gray-100);
+  }
+
+  [data-nextjs-devtools-panel-header-tab-issues-badge] {
+    display: inline-block;
+    margin-left: 8px;
+    background-color: var(--color-red-400);
+    color: var(--color-red-900);
+    font-size: 11px;
+    border-radius: var(--rounded-full);
+    padding: 2px 6px;
+    width: 20px;
+    height: 20px;
+    font-weight: 500;
+  }
+
+  [data-nextjs-devtools-panel-header-action-button-group] {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    padding-right: 8px;
+  }
+
+  [data-nextjs-devtools-panel-header-action-button] {
+    background: transparent;
+    border: none;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 6px;
+    color: var(--color-gray-600);
+    border-radius: 4px;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background-color: var(--color-gray-200);
+      color: var(--color-gray-900);
+    }
+
+    &:active {
+      background-color: var(--color-gray-300);
+    }
+  }
 `

filename: packages/next/src/next-devtools/dev-overlay/dev-overlay.tsx
patch: @@ -49,7 +49,11 @@ export function DevOverlay({
                     />
 
                     {state.isDevToolsPanelOpen && (
-                      <DevToolsPanel state={state} dispatch={dispatch} />
+                      <DevToolsPanel
+                        state={state}
+                        dispatch={dispatch}
+                        issueCount={totalErrorCount}
+                      />
                     )}
                   </>
                 ) : (

filename: packages/next/src/next-devtools/dev-overlay/icons/fullscreen.tsx
patch: @@ -0,0 +1,17 @@
+export function FullScreenIcon(props: React.SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="16"
+      height="16"
+      viewBox="0 0 16 16"
+      fill="none"
+      {...props}
+    >
+      <path
+        d="M15 14C15 14.5523 14.5523 15 14 15H10V13.5H13.5V10H15V14ZM2.5 13.499H6V14.999H2C1.44772 14.999 1 14.5513 1 13.999V9.99902H2.5V13.499ZM6 2.5H2.5V6H1V2C1 1.44772 1.44772 1 2 1H6V2.5ZM14 1C14.5523 1 15 1.44772 15 2V6H13.5V2.5H10V1H14Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}