Skip to content

[devtools] panel ui footer #80515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { OverlayState } from '../../shared'

import { DevToolsPanelVersionInfo } from './devtools-panel-version-info'
import { QuestionIcon } from '../../icons/question'
import { BugIcon } from '../../icons/bug'
import { css } from '../../utils/css'

export function DevToolsPanelFooter({
versionInfo,
}: {
versionInfo: OverlayState['versionInfo']
}) {
const bundlerName = (
process.env.__NEXT_BUNDLER || 'WEBPACK'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just stick with __NEXT_BUNDLER values instead of creating a new uppercase values?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's based on design

).toUpperCase() as 'WEBPACK' | 'TURBOPACK' | 'RSPACK'
return (
<div data-nextjs-devtools-panel-footer>
<div data-nextjs-devtools-panel-footer-tab-group>
<DevToolsPanelVersionInfo versionInfo={versionInfo} />
<div data-nextjs-devtools-panel-footer-tab>
{/* TODO: The details may change, follow up. */}
<span
data-nextjs-devtools-panel-footer-tab-bundler-name={bundlerName}
>
{bundlerName}
</span>
<span data-nextjs-devtools-panel-footer-tab-bundler-status>
enabled
</span>
</div>
</div>
<div data-nextjs-devtools-panel-footer-action-button-group>
{/* TODO: Add help feature, details TBD */}
<button data-nextjs-devtools-panel-footer-action-button>
<QuestionIcon width={16} height={16} />
</button>
{/* TODO: Add debugging/report GitHub issue feature, details TBD */}
<button data-nextjs-devtools-panel-footer-action-button>
<BugIcon width={16} height={16} />
</button>
</div>
</div>
)
}

export const DEVTOOLS_PANEL_FOOTER_STYLES = css`
[data-nextjs-devtools-panel-footer] {
background-color: var(--color-background-200);
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
border-top: 1px solid var(--color-gray-400);
border-radius: 0 0 var(--rounded-xl) var(--rounded-xl);
}

[data-nextjs-devtools-panel-footer-tab-group] {
display: flex;
align-items: center;
}

[data-nextjs-devtools-panel-footer-tab] {
display: flex;
align-items: center;
padding: 12px;
gap: 8px;
align-self: stretch;
border-right: 1px solid var(--color-gray-400);

color: var(--color-gray-900);
font-size: 12px;
font-family: var(--font-stack-monospace);
}

[data-nextjs-devtools-panel-footer-tab-bundler-name='TURBOPACK'] {
background: linear-gradient(
to right,
var(--color-turbopack-text-red) 0%,
var(--color-turbopack-text-blue) 100%
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}

[data-nextjs-devtools-panel-footer-action-button-group] {
display: flex;
align-items: center;
gap: 8px;
padding-right: 8px;
}

[data-nextjs-devtools-panel-footer-action-button] {
display: flex;
justify-content: center;
align-items: center;

padding: 4px;
background: var(--color-background-100);
background-clip: padding-box;
border: 1px solid var(--color-gray-alpha-400);
box-shadow: var(--shadow-small);
border-radius: var(--rounded-full);
color: var(--color-gray-800);

&:focus {
outline: var(--focus-ring);
}

&:not(:disabled):hover {
background: var(--color-gray-alpha-100);
}

&:not(:disabled):active {
background: var(--color-gray-alpha-200);
}

&:disabled {
background-color: var(--color-gray-100);
cursor: not-allowed;
}
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Meta, StoryObj } from '@storybook/react'
import { DevToolsPanelVersionInfo } from './devtools-panel-version-info'
import { withShadowPortal } from '../../storybook/with-shadow-portal'

const meta: Meta<typeof DevToolsPanelVersionInfo> = {
component: DevToolsPanelVersionInfo,
parameters: {
layout: 'centered',
},
decorators: [withShadowPortal],
}

export default meta
type Story = StoryObj<typeof DevToolsPanelVersionInfo>

// Mock version info for different scenarios
const mockVersionInfo = {
fresh: {
installed: '15.0.0',
expected: '15.0.0',
staleness: 'fresh' as const,
},
stalePatch: {
installed: '15.0.0',
expected: '15.0.1',
staleness: 'stale-patch' as const,
},
staleMinor: {
installed: '15.0.0',
expected: '15.1.0',
staleness: 'stale-minor' as const,
},
staleMajor: {
installed: '14.0.0',
expected: '15.0.0',
staleness: 'stale-major' as const,
},
stalePrerelease: {
installed: '15.0.0-canary.0',
expected: '15.0.0-canary.1',
staleness: 'stale-prerelease' as const,
},
newerThanNpm: {
installed: '15.0.0-canary.1',
expected: '15.0.0-canary.0',
staleness: 'newer-than-npm' as const,
},
unknown: {
installed: '15.0.0',
expected: '15.0.0',
staleness: 'unknown' as const,
},
}

export const Fresh: Story = {
args: {
versionInfo: mockVersionInfo.fresh,
},
}

export const StalePatch: Story = {
args: {
versionInfo: mockVersionInfo.stalePatch,
},
}

export const StaleMinor: Story = {
args: {
versionInfo: mockVersionInfo.staleMinor,
},
}

export const StaleMajor: Story = {
args: {
versionInfo: mockVersionInfo.staleMajor,
},
}

export const StalePrerelease: Story = {
args: {
versionInfo: mockVersionInfo.stalePrerelease,
},
}

export const NewerThanNpm: Story = {
args: {
versionInfo: mockVersionInfo.newerThanNpm,
},
}

export const Unknown: Story = {
args: {
versionInfo: mockVersionInfo.unknown,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { OverlayState } from '../../shared'

import { EclipseIcon } from '../../icons/eclipse'
import { getStaleness } from '../../../shared/version-staleness'
import { css } from '../../utils/css'

export function DevToolsPanelVersionInfo({
versionInfo,
}: {
versionInfo: OverlayState['versionInfo']
}) {
const { staleness } = versionInfo
const { text, indicatorClass, title } = getStaleness(versionInfo)
const shouldBeLink = staleness.startsWith('stale')

if (shouldBeLink) {
return (
// TODO: Make it button-like and display radix-ui Tooltip on hover.
<a
data-nextjs-devtools-panel-footer-tab
target="_blank"
rel="noopener noreferrer"
href="https://nextjs.org/docs/messages/version-staleness"
title={title}
>
<EclipseIcon data-nextjs-version-staleness-indicator={indicatorClass} />
<span>{text}</span>
</a>
)
}

return (
<div data-nextjs-devtools-panel-footer-tab title={title}>
<EclipseIcon data-nextjs-version-staleness-indicator={indicatorClass} />
<span>{text}</span>
</div>
)
}

export const DEVTOOLS_PANEL_VERSION_INFO_STYLES = css`
[data-nextjs-version-staleness-indicator='fresh'] {
fill: var(--color-green-800);
stroke: var(--color-green-300);
}
[data-nextjs-version-staleness-indicator='stale'] {
fill: var(--color-amber-800);
stroke: var(--color-amber-300);
}
[data-nextjs-version-staleness-indicator='outdated'] {
fill: var(--color-red-800);
stroke: var(--color-red-300);
}
[data-nextjs-version-staleness-indicator='unknown'] {
fill: var(--color-gray-800);
stroke: var(--color-gray-300);
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const state: OverlayState = {
routerType: 'app',
isErrorOverlayOpen: false,
isDevToolsPanelOpen: true,
versionInfo: {
installed: '15.0.0',
expected: '15.0.0',
staleness: 'fresh',
},
}

export const Default: Story = {
Expand All @@ -39,3 +44,35 @@ export const WithIssues: Story = {
issueCount: 3,
},
}

export const Turbopack: Story = {
beforeEach: () => {
process.env.__NEXT_BUNDLER = 'Turbopack'

// clean up callback function
return () => {
delete process.env.__NEXT_BUNDLER
}
},
args: {
state,
dispatch: () => {},
issueCount: 0,
},
}

export const Rspack: Story = {
beforeEach: () => {
process.env.__NEXT_BUNDLER = 'Rspack'

// clean up callback function
return () => {
delete process.env.__NEXT_BUNDLER
}
},
args: {
state,
dispatch: () => {},
issueCount: 0,
},
}
Loading
Loading