Skip to content

[segment explorer] single row layout segment #80576

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 8 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
Expand Up @@ -336,7 +336,7 @@ function DevToolsPopover({
setScale={setScale}
/>

{/* Page Segment Explorer */}
{/* Page Routes Info */}
{process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER ? (
<SegmentsExplorer
isOpen={isSegmentExplorerOpen}
Expand Down Expand Up @@ -414,7 +414,7 @@ function DevToolsPopover({
{process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER ? (
<MenuItem
data-segment-explorer
label="Segment Explorer"
label="Routes Info"
value={<ChevronRight />}
onClick={() => setOpen(OVERLAYS.SegmentExplorer)}
index={isTurbopack ? 3 : 4}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,33 @@ import { css } from '../../utils/css'
import type { DevToolsInfoPropsCore } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
import { DevToolsInfo } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
import { useSegmentTree, type SegmentTrieNode } from '../../segment-explorer'
import { cx } from '../../utils/cx'

const isFileNode = (node: SegmentTrieNode) => {
return !!node.value?.type && !!node.value?.pagePath
}

function PageSegmentTree({ tree }: { tree: SegmentTrieNode }) {
return (
<div
className="segment-explorer-content"
data-nextjs-devtool-segment-explorer
>
<PageSegmentTreeLayerPresentation
node={tree}
level={0}
segment=""
parentSegment=""
/>
<PageSegmentTreeLayerPresentation node={tree} level={0} segment="" />
</div>
)
}

function PageSegmentTreeLayerPresentation({
segment,
parentSegment,
node,
level,
}: {
segment: string
parentSegment: string
node: SegmentTrieNode
level: number
}) {
const pagePath = node.value?.pagePath || ''
const nodeName = node.value?.type
const isFile = !!nodeName

const segments = pagePath.split('/') || []
const fileName = isFile ? segments.pop() || '' : ''
const childrenKeys = Object.keys(node.children)

const sortedChildrenKeys = childrenKeys.sort((a, b) => {
Expand All @@ -47,31 +40,61 @@ function PageSegmentTreeLayerPresentation({
if (aHasExt && !bHasExt) return -1
if (!aHasExt && bHasExt) return 1
// Otherwise sort alphabetically
return a.localeCompare(b)
})

// check if it has file children
const hasFileChildren = sortedChildrenKeys.some((key) => {
const childNode = node.children[key]
return !!childNode?.value?.type
// If it's file, sort by order: layout > template > page
if (aHasExt && bHasExt) {
const aType = node.children[a]?.value?.type
const bType = node.children[b]?.value?.type

if (aType === 'layout' && bType !== 'layout') return -1
if (aType !== 'layout' && bType === 'layout') return 1
if (aType === 'template' && bType !== 'template') return -1
if (aType !== 'template' && bType === 'template') return 1

// If both are the same type, sort by pagePath
const aFilePath = node.children[a]?.value?.pagePath || ''
const bFilePath = node.children[b]?.value?.pagePath || ''
return aFilePath.localeCompare(bFilePath)
}

return a.localeCompare(b)
})

// If it's the 1st level and contains a file, use 'app' as the folder name
const folderName = level === 1 && isFile ? 'app' : parentSegment
const folderName = level === 0 && !segment ? 'app' : segment

const folderChildrenKeys: string[] = []
const filesChildrenKeys: string[] = []

for (const childKey of sortedChildrenKeys) {
const childNode = node.children[childKey]
if (!childNode) continue

// If it's a file node, add it to filesChildrenKeys
if (isFileNode(childNode)) {
filesChildrenKeys.push(childKey)
continue
}

// Otherwise, it's a folder node, add it to folderChildrenKeys
folderChildrenKeys.push(childKey)
}

const hasFilesChildren = filesChildrenKeys.length > 0

return (
<>
{isFile ? (
{hasFilesChildren && (
<div
className="segment-explorer-item"
data-nextjs-devtool-segment-explorer-segment={segment}
data-nextjs-devtool-segment-explorer-segment={segment + '-' + level}
>
<div
className="segment-explorer-item-row"
style={{
// If it's children levels, show indents if there's any file at that level.
// Otherwise it's empty folder, no need to show indents.
...(level > 0 && isFile && { paddingLeft: `${level * 8}px` }),
...{ paddingLeft: `${(level + 1) * 8}px` },
}}
>
<div className="segment-explorer-line">
Expand All @@ -84,27 +107,54 @@ function PageSegmentTreeLayerPresentation({
<small>{'/'}</small>
</span>
)}
<span className="segment-explorer-filename--name">
{fileName}
</span>
{/* display all the file segments in this level */}
{filesChildrenKeys.length > 0 && (
<span className="segment-explorer-files">
{filesChildrenKeys.map((fileChildSegment) => {
const childNode = node.children[fileChildSegment]
if (!childNode || !childNode.value) {
return null
}
const fileName =
childNode.value.pagePath.split('/').pop() || ''
return (
<span
key={fileChildSegment}
className={cx(
'segment-explorer-file-label',
`segment-explorer-file-label--${childNode.value.type}`
)}
>
{fileName}
</span>
)
})}
</span>
)}
</div>
</div>
</div>
</div>
</div>
) : null}
{sortedChildrenKeys.map((childSegment) => {
)}

{folderChildrenKeys.map((childSegment) => {
const child = node.children[childSegment]
if (!child) {
return null
}

// If it's an folder segment without any files under it,
// merge it with the segment in the next level.
const nextSegment = hasFilesChildren
? childSegment
: segment + ' / ' + childSegment
return (
<PageSegmentTreeLayerPresentation
key={childSegment}
segment={childSegment}
parentSegment={segment}
segment={nextSegment}
node={child}
level={hasFileChildren ? level + 1 : level}
level={hasFilesChildren ? level + 1 : level}
/>
)
})}
Expand All @@ -118,7 +168,7 @@ export function SegmentsExplorer(
const tree = useSegmentTree()

return (
<DevToolsInfo title="Segment Explorer" {...props}>
<DevToolsInfo title="Routes Info" {...props}>
<PageSegmentTree tree={tree} />
</DevToolsInfo>
)
Expand All @@ -133,28 +183,36 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`

.segment-explorer-item {
margin: 4px 0;
border-radius: 6px;
}

.segment-explorer-item:nth-child(odd) {
background-color: var(--color-gray-100);
background-color: var(--color-background-200);
}

.segment-explorer-item-row {
display: flex;
align-items: center;
padding: 4px 24px;
border-radius: 6px;
padding-top: 10px;
padding-bottom: 10px;
padding-right: 4px;
}

.segment-explorer-children--intended {
padding-left: 16px;
}

.segment-explorer-filename {
display: inline-flex;
}

.segment-explorer-filename--path {
margin-right: 8px;
}
.segment-explorer-filename--path small {
display: inline-block;
width: 0;
opacity: 0;
}
.segment-explorer-filename--name {
color: var(--color-gray-800);
Expand All @@ -169,7 +227,26 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
color: var(--color-gray-1000);
}

[data-nextjs-devtool-segment-explorer-level='odd'] {
background-color: var(--color-gray-100);
.segment-explorer-files {
display: inline-flex;
gap: 8px;
}

.segment-explorer-file-label {
padding: 2px 6px;
border-radius: 16px;
font-size: var(--size-12);
line-height: 16px;
font-weight: 500;
user-select: none;
}
.segment-explorer-file-label--layout,
.segment-explorer-file-label--template {
background-color: var(--color-gray-300);
color: var(--color-gray-1000);
}
.segment-explorer-file-label--page {
background-color: var(--color-blue-300);
color: var(--color-blue-800);
}
`
Loading
Loading