Repository: vercel/next.js
Pull request: [segment-explorer] optimize tree view

### What

Optimize the display of segment explorer tree views, show the filename along with parent folder. This way it's easier to capture where roughly the file locate.

😌
| parallel routes | nested routes |
|:--|:--|
| ![image](https://github.com/user-attachments/assets/08af9465-6c7b-4040-bdb2-2a4b5d442deb) | ![image](https://github.com/user-attachments/assets/4bbeb7e3-ce1c-4c40-aa19-b7eb21f87e1b) |

This is intended to improve cases where the tree is extremely large and complex. 
See this bad base ❌

<img height="400" alt="image" src="https://github.com/user-attachments/assets/cf458755-6707-48c2-b51c-8b9d5ecd79ab">

This also improves the indentation and styling:

Indentation: when there's no file children under the folder, but only other folders, no need to give extra indentation. This wil avoid too many indentation when multi folders nested together.
Styling: Show the route path and route file in different colors, getting closer to what we want to have.


Changed files:
filename: packages/next/src/next-devtools/dev-overlay/components/overview/segment-explorer.tsx
patch: @@ -2,53 +2,12 @@ import type { HTMLProps } from 'react'
 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 { cx } from '../../utils/cx'
 import {
   type SegmentNode,
   useSegmentTreeClientState,
 } from '../../../../shared/lib/devtool/app-segment-tree'
 import type { Trie, TrieNode } from '../../../../shared/lib/devtool/trie'
 
-const IconLayout = (props: React.SVGProps<SVGSVGElement>) => {
-  return (
-    <svg
-      {...props}
-      viewBox="0 0 16 16"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-    >
-      <path
-        d="M16 12.5L15.9873 12.7559C15.8677 13.9323 14.9323 14.8677 13.7559 14.9873L13.5 15H2.5L2.24414 14.9873C1.06772 14.8677 0.132274 13.9323 0.0126953 12.7559L0 12.5V1H16V12.5ZM1.5 6.25488V12.5C1.5 13.0523 1.94772 13.5 2.5 13.5H4.99512V6.25488H1.5ZM6.24512 6.25488V13.5H13.5C14.0523 13.5 14.5 13.0523 14.5 12.5V6.25488H6.24512ZM1.5 5.00488H14.5V2.5H1.5V5.00488Z"
-        fill="currentColor"
-      />
-    </svg>
-  )
-}
-
-const IconPage = (props: React.SVGProps<SVGSVGElement>) => {
-  return (
-    <svg
-      {...props}
-      viewBox="0 0 16 16"
-      fill="none"
-      strokeLinejoin="round"
-      xmlns="http://www.w3.org/2000/svg"
-    >
-      <path
-        fillRule="evenodd"
-        clipRule="evenodd"
-        d="M14.5 6.5V13.5C14.5 14.8807 13.3807 16 12 16H4C2.61929 16 1.5 14.8807 1.5 13.5V1.5V0H3H8H9.08579C9.351 0 9.60536 0.105357 9.79289 0.292893L14.2071 4.70711C14.3946 4.89464 14.5 5.149 14.5 5.41421V6.5ZM13 6.5V13.5C13 14.0523 12.5523 14.5 12 14.5H4C3.44772 14.5 3 14.0523 3 13.5V1.5H8V5V6.5H9.5H13ZM9.5 2.12132V5H12.3787L9.5 2.12132Z"
-        fill="currentColor"
-      />
-    </svg>
-  )
-}
-
-const ICONS = {
-  layout: <IconLayout width={16} />,
-  page: <IconPage width={16} />,
-}
-
 function PageSegmentTree({ tree }: { tree: Trie<SegmentNode> | undefined }) {
   if (!tree) {
     return null
@@ -63,6 +22,7 @@ function PageSegmentTree({ tree }: { tree: Trie<SegmentNode> | undefined }) {
         node={tree.getRoot()}
         level={0}
         segment=""
+        parentSegment=""
       />
     </div>
   )
@@ -71,19 +31,22 @@ function PageSegmentTree({ tree }: { tree: Trie<SegmentNode> | undefined }) {
 function PageSegmentTreeLayerPresentation({
   tree,
   segment,
+  parentSegment,
   node,
   level,
 }: {
   tree: Trie<SegmentNode>
   segment: string
+  parentSegment: string
   node: TrieNode<SegmentNode>
   level: number
 }) {
   const pagePath = node.value?.pagePath || ''
   const nodeName = node.value?.type
+  const isFile = !!nodeName
 
   const segments = pagePath.split('/') || []
-  const fileName = segments.pop() || ''
+  const fileName = isFile ? segments.pop() || '' : ''
   const childrenKeys = Object.keys(node.children)
 
   const sortedChildrenKeys = childrenKeys.sort((a, b) => {
@@ -97,65 +60,64 @@ function PageSegmentTreeLayerPresentation({
     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 the 1st level and contains a file, use 'app' as the folder name
+  const folderName = level === 1 && isFile ? 'app' : parentSegment
+
   return (
-    <div
-      className="segment-explorer-item"
-      data-nextjs-devtool-segment-explorer-segment={segment}
-    >
-      {fileName ? (
-        <div className={cx('segment-explorer-item-row')}>
-          <div className="segment-explorer-line">
-            <div className={`segment-explorer-line-text-${nodeName}`}>
-              <span
-                className={cx(
-                  'segment-explorer-line-icon',
-                  `segment-explorer-line-icon-${nodeName}`
-                )}
-              >
-                {nodeName === 'layout' ? ICONS.layout : ICONS.page}
-              </span>
-              <span className="segment-explorer-filename-path">{fileName}</span>
-            </div>
-          </div>
-        </div>
-      ) : segment ? (
-        <div className={'segment-explorer-item-row'}>
-          <div className="segment-explorer-line">
-            <div className={`segment-explorer-line-text-${nodeName}`}>
-              <span
-                className={cx(
-                  'segment-explorer-line-icon',
-                  `segment-explorer-line-icon-${nodeName}`
-                )}
-              ></span>
-              <span className="segment-explorer-filename-path">
-                {`${segment}/`}
-              </span>
+    <>
+      {isFile ? (
+        <div
+          className="segment-explorer-item"
+          data-nextjs-devtool-segment-explorer-segment={segment}
+        >
+          <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` }),
+            }}
+          >
+            <div className="segment-explorer-line">
+              <div className={`segment-explorer-line-text-${nodeName}`}>
+                <div className="segment-explorer-filename">
+                  {folderName && (
+                    <span className="segment-explorer-filename--path">
+                      {folderName}
+                    </span>
+                  )}
+                  <span className="segment-explorer-filename--name">
+                    {fileName}
+                  </span>
+                </div>
+              </div>
             </div>
           </div>
         </div>
       ) : null}
-
-      <div
-        className="segment-explorer-segment-children"
-        data-nextjs-devtool-segment-explorer-level={level}
-      >
-        {sortedChildrenKeys.map((childSegment) => {
-          const child = node.children[childSegment]
-          return (
-            child && (
-              <PageSegmentTreeLayerPresentation
-                key={childSegment}
-                segment={childSegment}
-                tree={tree}
-                node={child}
-                level={level + 1}
-              />
-            )
-          )
-        })}
-      </div>
-    </div>
+      {sortedChildrenKeys.map((childSegment) => {
+        const child = node.children[childSegment]
+        if (!child) {
+          return null
+        }
+        return (
+          <PageSegmentTreeLayerPresentation
+            key={childSegment}
+            segment={childSegment}
+            parentSegment={segment}
+            tree={tree}
+            node={child}
+            level={hasFileChildren ? level + 1 : level}
+          />
+        )
+      })}
+    </>
   )
 }
 
@@ -177,47 +139,46 @@ export function SegmentsExplorer(
 export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
   .segment-explorer-content {
     overflow-y: auto;
-    padding: 0 12px;
     font-size: var(--size-14);
+    margin: -12px -8px;
+  }
+
+  .segment-explorer-item {
+    margin: 4px 0;
+  }
+
+  .segment-explorer-item:nth-child(odd) {
+    background-color: var(--color-gray-100);
   }
 
   .segment-explorer-item-row {
     display: flex;
     align-items: center;
-    gap: 8px;
-    padding: 2px 0;
+    padding: 4px 24px;
+    border-radius: 6px;
   }
 
-  [data-nextjs-devtool-segment-explorer-level].segment-explorer-segment-children {
-    padding-left: 20px;
-  }
-  [data-nextjs-devtool-segment-explorer-level='0'].segment-explorer-segment-children {
-    padding-left: 0px;
+  .segment-explorer-children--intended {
+    padding-left: 16px;
   }
 
-  .segment-explorer-filename-path {
-    display: inline-block;
+  .segment-explorer-filename--path {
+    margin-right: 8px;
   }
-
-  .segment-explorer-filename-path a {
-    color: inherit;
-    text-decoration: inherit;
+  .segment-explorer-filename--name {
+    color: var(--color-gray-800);
   }
 
   .segment-explorer-line {
     white-space: pre;
     cursor: default;
   }
 
-  .segment-explorer-line-icon {
-    margin-right: 4px;
-  }
-  .segment-explorer-line-icon-page {
-    color: inherit;
+  .segment-explorer-line {
+    color: var(--color-gray-1000);
   }
 
-  .segment-explorer-line-text-page {
-    color: var(--color-gray-1000);
-    font-weight: 500;
+  [data-nextjs-devtool-segment-explorer-level='odd'] {
+    background-color: var(--color-gray-100);
   }
 `

filename: test/development/app-dir/segment-explorer/segment-explorer.test.ts
patch: @@ -26,35 +26,24 @@ describe('segment-explorer', () => {
   it('should render the segment explorer for parallel routes', async () => {
     const browser = await next.browser('/parallel-routes')
     expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
-     "app/
-     layout.tsx
-     parallel-routes/
-     layout.tsx
-     page.tsx
-     @bar/
-     layout.tsx
-     page.tsx
-     @foo/
-     layout.tsx
-     page.tsx"
+     "applayout.tsx
+     parallel-routeslayout.tsx
+     parallel-routespage.tsx
+     @barlayout.tsx
+     @barpage.tsx
+     @foolayout.tsx
+     @foopage.tsx"
     `)
   })
 
   it('should render the segment explorer for nested routes', async () => {
     const browser = await next.browser('/blog/~/grid')
     expect(await getSegmentExplorerContent(browser)).toMatchInlineSnapshot(`
-     "app/
-     layout.tsx
-     (v2)/
-     layout.tsx
-     blog/
-     (team)/
-     layout.tsx
-     ~/
-     (overview)/
-     layout.tsx
-     grid/
-     page.tsx"
+     "applayout.tsx
+     (v2)layout.tsx
+     (team)layout.tsx
+     (overview)layout.tsx
+     gridpage.tsx"
     `)
   })
 })