11'use client'
22
33import type React from 'react'
4- import { useEffect , useMemo , useRef , useState } from 'react'
4+ import { useEffect , useMemo , useState } from 'react'
55import useSWR from 'swr'
6- import { ImportChain } from '@/components/import-chain'
76import { ErrorState } from '@/components/error-state'
8- import {
9- RouteTypeahead ,
10- type RouteTypeaheadRef ,
11- } from '@/components/route-typeahead'
7+ import { FileSearch } from '@/components/file-search'
8+ import { RouteTypeahead } from '@/components/route-typeahead'
9+ import { Sidebar } from '@/components/sidebar'
1210import { TreemapVisualizer } from '@/components/treemap-visualizer'
1311
14- import { Input } from '@/components/ui/input '
15- import { Skeleton , TreemapSkeleton } from '@/components/ui/skeleton'
12+ import { Badge } from '@/components/ui/badge '
13+ import { TreemapSkeleton } from '@/components/ui/skeleton'
1614import { ToggleGroup , ToggleGroupItem } from '@/components/ui/toggle-group'
1715import { AnalyzeData , ModulesData } from '@/lib/analyze-data'
1816import { computeActiveEntries , computeModuleDepthMap } from '@/lib/module-graph'
19- import { SpecialModule } from '@/lib/types'
20- import { getSpecialModuleType , fetchStrict } from '@/lib/utils'
21-
22- function formatBytes ( bytes : number ) : string {
23- if ( bytes === 0 ) return '0 B'
24- if ( bytes < 1024 ) return `${ bytes } B`
25- if ( bytes < 1024 * 1024 ) return `${ ( bytes / 1024 ) . toFixed ( 2 ) } KB`
26- if ( bytes < 1024 * 1024 * 1024 )
27- return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 2 ) } MB`
28- return `${ ( bytes / ( 1024 * 1024 * 1024 ) ) . toFixed ( 2 ) } GB`
29- }
17+ import { fetchStrict } from '@/lib/utils'
18+ import { formatBytes } from '@/lib/utils'
3019
3120export default function Home ( ) {
3221 const [ selectedRoute , setSelectedRoute ] = useState < string | null > ( null )
@@ -75,35 +64,32 @@ export default function Home() {
7564 const [ isMouseInTreemap , setIsMouseInTreemap ] = useState ( false )
7665 const [ hoveredNodeInfo , setHoveredNodeInfo ] = useState < {
7766 name : string
67+ size : number
7868 server ?: boolean
7969 client ?: boolean
8070 } | null > ( null )
8171 const [ searchQuery , setSearchQuery ] = useState ( '' )
82- const [ searchFocused , setSearchFocused ] = useState ( false )
83-
84- const routeTypeaheadRef = useRef < RouteTypeaheadRef > ( null )
85- const searchInputRef = useRef < HTMLInputElement > ( null )
8672
8773 useEffect ( ( ) => {
8874 const handleKeyDown = ( e : KeyboardEvent ) => {
89- // Cmd+K or Ctrl+K to focus route filter
90- if ( ( e . metaKey || e . ctrlKey ) && e . key === 'k ' ) {
91- e . preventDefault ( )
92- routeTypeaheadRef . current ?. focus ( )
93- }
94- // / to focus search (only if not already in an input)
95- else if (
96- e . key === '/' &&
97- ! [ 'INPUT' , 'TEXTAREA' ] . includes ( ( e . target as HTMLElement ) . tagName )
98- ) {
99- e . preventDefault ( )
100- searchInputRef . current ?. focus ( )
75+ // esc clears current treemap source selection
76+ if ( e . key === 'Escape ' ) {
77+ const activeElement = document . activeElement
78+ const isInputFocused =
79+ activeElement && [ 'INPUT' , 'TEXTAREA' ] . includes ( activeElement . tagName )
80+
81+ if ( ! isInputFocused ) {
82+ e . preventDefault ( )
83+ const rootSourceIndex = getRootSourceIndex ( analyzeData )
84+ setSelectedSourceIndex ( rootSourceIndex )
85+ setFocusedSourceIndex ( rootSourceIndex )
86+ }
10187 }
10288 }
10389
10490 window . addEventListener ( 'keydown' , handleKeyDown )
10591 return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown )
106- } , [ ] )
92+ } , [ analyzeData ] )
10793
10894 // Compute module depth map from active entries
10995 const moduleDepthMap = useMemo ( ( ) => {
@@ -153,11 +139,6 @@ export default function Home() {
153139 const isAnyLoading = isAnalyzeLoading || isModulesLoading
154140 const rootSourceIndex = getRootSourceIndex ( analyzeData )
155141
156- const specialModuleType = getSpecialModuleType (
157- analyzeData ,
158- selectedSourceIndex
159- )
160-
161142 return (
162143 < main
163144 className = "h-screen flex flex-col bg-background"
@@ -167,7 +148,6 @@ export default function Home() {
167148 < div className = "flex-none px-4 py-2 border-b border-border flex items-center gap-4" >
168149 < div className = "basis-1/3 flex" >
169150 < RouteTypeahead
170- ref = { routeTypeaheadRef }
171151 selectedRoute = { selectedRoute }
172152 onRouteSelected = { ( route ) => {
173153 setSelectedRoute ( route )
@@ -208,27 +188,7 @@ export default function Home() {
208188 < ToggleGroupItem value = "asset" > Asset</ ToggleGroupItem >
209189 </ ToggleGroup >
210190
211- { ! searchFocused && (
212- < div className = "flex items-center gap-4 text-xs" >
213- < p className = "text-muted-foreground" >
214- {
215- analyzeData . source ( focusedSourceIndex ?? rootSourceIndex )
216- ?. path
217- }
218- </ p >
219- </ div >
220- ) }
221-
222- < Input
223- ref = { searchInputRef }
224- type = "search"
225- value = { searchQuery }
226- onChange = { ( e ) => setSearchQuery ( e . target . value ) }
227- onFocus = { ( ) => setSearchFocused ( true ) }
228- onBlur = { ( ) => setSearchFocused ( false ) }
229- placeholder = "Search files..."
230- className = "w-48 focus:w-80 transition-all duration-200"
231- />
191+ < FileSearch value = { searchQuery } onChange = { setSearchQuery } />
232192 </ >
233193 ) }
234194 </ div >
@@ -250,24 +210,15 @@ export default function Home() {
250210 aria-label = "Resize sidebar"
251211 />
252212
253- < div
254- className = "flex-none bg-muted border-l border-border overflow-y-auto"
255- style = { { width : `${ sidebarWidth } %` } }
256- >
257- < div className = "flex-1 p-3 space-y-4 overflow-y-auto" >
258- < h2 className = "text-xs font-semibold mb-2 text-foreground" >
259- Selected Source
260- </ h2 >
261- < Skeleton className = "h-4 w-3/4" />
262- < Skeleton className = "h-4 w-full" />
263- < Skeleton className = "h-4 w-5/6" />
264- < div className = "mt-4 space-y-2" >
265- < Skeleton className = "h-3 w-full" />
266- < Skeleton className = "h-3 w-full" />
267- < Skeleton className = "h-3 w-4/5" />
268- </ div >
269- </ div >
270- </ div >
213+ < Sidebar
214+ sidebarWidth = { sidebarWidth }
215+ analyzeData = { null }
216+ modulesData = { null }
217+ selectedSourceIndex = { null }
218+ moduleDepthMap = { new Map ( ) }
219+ environmentFilter = { environmentFilter }
220+ isLoading = { true }
221+ />
271222 </ >
272223 ) : analyzeData ? (
273224 < >
@@ -294,116 +245,44 @@ export default function Home() {
294245 aria-label = "Resize sidebar"
295246 />
296247
297- < div
298- className = "flex-none bg-muted border-l border-border overflow-y-auto"
299- style = { { width : `${ sidebarWidth } %` } }
300- >
301- < div className = "flex-1 p-3 space-y-4 overflow-y-auto" >
302- < h2 className = "text-xs font-semibold mb-2 text-foreground" >
303- Selected Source
304- </ h2 >
305-
306- { selectedSourceIndex != null &&
307- analyzeData . source ( selectedSourceIndex ) && (
308- < >
309- < dl className = "space-y-2" >
310- < div >
311- < dt className = "text-xs text-muted-foreground inline" >
312- Output Size:{ ' ' }
313- </ dt >
314- < dd className = "text-xs text-muted-foreground inline" >
315- { formatBytes (
316- analyzeData . getSourceOutputSize (
317- selectedSourceIndex
318- )
319- ) }
320- </ dd >
321- </ div >
322- { ( specialModuleType === SpecialModule . POLYFILL_MODULE ||
323- specialModuleType ===
324- SpecialModule . POLYFILL_NOMODULE ) && (
325- < div className = "flex items-center gap-2" >
326- < dt className = "inline-flex items-center rounded-md bg-polyfill/10 dark:bg-polyfill/30 px-2 py-1 text-xs font-medium text-polyfill dark:text-polyfill-foreground ring-1 ring-inset ring-polyfill/20 shrink-0" >
327- Polyfill
328- </ dt >
329- < dd className = "text-xs text-muted-foreground" >
330- Next.js built-in polyfills
331- { specialModuleType ===
332- SpecialModule . POLYFILL_NOMODULE ? (
333- < >
334- . < code > polyfill-nomodule.js</ code > is only
335- sent to legacy browsers.
336- </ >
337- ) : null }
338- </ dd >
339- </ div >
340- ) }
341- </ dl >
342- { modulesData && (
343- < ImportChain
344- key = { selectedSourceIndex }
345- startFileId = { selectedSourceIndex }
346- analyzeData = { analyzeData }
347- modulesData = { modulesData }
348- depthMap = { moduleDepthMap }
349- environmentFilter = { environmentFilter }
350- />
351- ) }
352- { ( ( ) => {
353- const chunks =
354- analyzeData . sourceChunks ( selectedSourceIndex )
355- if ( chunks . length > 0 ) {
356- return (
357- < div className = "mt-2" >
358- < p className = "text-xs font-semibold text-foreground" >
359- Output Chunks:
360- </ p >
361- < ul className = "text-xs text-muted-foreground font-mono mt-1 space-y-1" >
362- { chunks . map ( ( chunk ) => (
363- < li key = { chunk } className = "break-all" >
364- { chunk }
365- </ li >
366- ) ) }
367- </ ul >
368- </ div >
369- )
370- }
371- return null
372- } ) ( ) }
373- </ >
374- ) }
375- </ div >
376- </ div >
248+ < Sidebar
249+ sidebarWidth = { sidebarWidth }
250+ analyzeData = { analyzeData ?? null }
251+ modulesData = { modulesData ?? null }
252+ selectedSourceIndex = { selectedSourceIndex }
253+ moduleDepthMap = { moduleDepthMap }
254+ environmentFilter = { environmentFilter }
255+ />
377256 </ >
378257 ) : null }
379258 </ div >
380259
381260 { analyzeData && (
382261 < div className = "flex-none border-t border-border bg-background px-4 py-2 h-10" >
383- < p className = "text-sm text-muted-foreground" >
262+ < div className = "text-sm text-muted-foreground" >
384263 { hoveredNodeInfo ? (
385264 < >
386265 < span className = "font-medium text-foreground" >
387266 { hoveredNodeInfo . name }
388267 </ span >
268+ < span className = "ml-2 text-muted-foreground" >
269+ { formatBytes ( hoveredNodeInfo . size ) }
270+ </ span >
389271 { ( hoveredNodeInfo . server || hoveredNodeInfo . client ) && (
390- < span className = "ml-2 text-xs " >
272+ < span className = "ml-2 inline-flex gap-1 " >
391273 { hoveredNodeInfo . client && (
392- < span className = "text-primary" > [client]</ span >
393- ) }
394- { hoveredNodeInfo . server && hoveredNodeInfo . client && (
395- < span > </ span >
274+ < Badge variant = "client" > client</ Badge >
396275 ) }
397276 { hoveredNodeInfo . server && (
398- < span className = "text-primary" > [ server] </ span >
277+ < Badge variant = "server" > server</ Badge >
399278 ) }
400279 </ span >
401280 ) }
402281 </ >
403282 ) : (
404283 'Hover over a file to see details'
405284 ) }
406- </ p >
285+ </ div >
407286 </ div >
408287 ) }
409288 </ main >
0 commit comments