Skip to content

Commit 39c6cbb

Browse files
authored
bundle-analyzer: use <Select> and multiselect for top bar (#87254)
The top bar is getting quite wide with all of its button toggle groups. This: - Converts the Compressed/Uncompressed and Client/Server toggles to shadcn `<Select>`s - Adds a custom `<MultiSelect>` implementation since this is missing in shadcn. This composes shadcn’s popovers and a series of checkboxes, along with accessibility attributes and keyboard shortcuts. - Uses this `<MultiSelect>` for the type filter - Removes the divider between these since there aren’t any groups left to divide <img width="1547" height="719" alt="image" src="https://github.com/user-attachments/assets/4041f6b7-2a47-4e17-b020-448ac0103740" />
1 parent affb52d commit 39c6cbb

File tree

5 files changed

+591
-138
lines changed

5 files changed

+591
-138
lines changed

apps/bundle-analyzer/app/page.tsx

Lines changed: 164 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,28 @@ import { TreemapVisualizer } from '@/components/treemap-visualizer'
1313

1414
import { Badge } from '@/components/ui/badge'
1515
import { TreemapSkeleton } from '@/components/ui/skeleton'
16-
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
16+
import {
17+
Select,
18+
SelectContent,
19+
SelectItem,
20+
SelectTrigger,
21+
SelectValue,
22+
} from '@/components/ui/select'
23+
import { MultiSelect } from '@/components/ui/multi-select'
1724
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
1825
import { computeActiveEntries, computeModuleDepthMap } from '@/lib/module-graph'
1926
import { fetchStrict } from '@/lib/utils'
2027
import { formatBytes } from '@/lib/utils'
28+
import {
29+
File,
30+
FileArchive,
31+
Monitor,
32+
Server,
33+
FileCode,
34+
FileJson,
35+
Palette,
36+
Package,
37+
} from 'lucide-react'
2138

2239
enum Environment {
2340
Client = 'client',
@@ -153,78 +170,21 @@ export default function Home() {
153170
onMouseMove={handleMouseMove}
154171
onMouseUp={handleMouseUp}
155172
>
156-
<div className="flex-none px-4 py-2 border-b border-border flex items-center gap-4">
157-
<div className="basis-1/3 flex">
158-
<RouteTypeahead
159-
selectedRoute={selectedRoute}
160-
onRouteSelected={(route) => {
161-
setSelectedRoute(route)
162-
setSelectedSourceIndex(null)
163-
setFocusedSourceIndex(null)
164-
}}
165-
/>
166-
</div>
167-
168-
<div className="basis-2/3 flex justify-end items-center space-x-4">
169-
{analyzeData && (
170-
<>
171-
<ToggleGroup
172-
type="single"
173-
value={sizeMode}
174-
onValueChange={(value) => {
175-
if (value) setSizeMode(value as SizeMode)
176-
}}
177-
size="sm"
178-
>
179-
<ToggleGroupItem value={SizeMode.Uncompressed}>
180-
Uncompressed
181-
</ToggleGroupItem>
182-
<ToggleGroupItem value={SizeMode.Compressed}>
183-
Compressed
184-
</ToggleGroupItem>
185-
</ToggleGroup>
186-
187-
<ControlDivider />
188-
189-
<ToggleGroup
190-
type="single"
191-
value={environmentFilter}
192-
onValueChange={(value) => {
193-
if (value) setEnvironmentFilter(value as Environment)
194-
}}
195-
size="sm"
196-
>
197-
<ToggleGroupItem value={Environment.Client}>
198-
Client
199-
</ToggleGroupItem>
200-
<ToggleGroupItem value={Environment.Server}>
201-
Server
202-
</ToggleGroupItem>
203-
</ToggleGroup>
204-
205-
<ControlDivider />
206-
207-
<ToggleGroup
208-
type="multiple"
209-
value={typeFilter}
210-
onValueChange={(value) => {
211-
if (value.length > 0) setTypeFilter(value)
212-
}}
213-
size="sm"
214-
>
215-
<ToggleGroupItem value="js">JS</ToggleGroupItem>
216-
<ToggleGroupItem value="css">CSS</ToggleGroupItem>
217-
<ToggleGroupItem value="json">JSON</ToggleGroupItem>
218-
<ToggleGroupItem value="asset">Asset</ToggleGroupItem>
219-
</ToggleGroup>
220-
221-
<ControlDivider />
222-
223-
<FileSearch value={searchQuery} onChange={setSearchQuery} />
224-
</>
225-
)}
226-
</div>
227-
</div>
173+
<TopBar
174+
analyzeData={analyzeData}
175+
selectedRoute={selectedRoute}
176+
setSelectedRoute={setSelectedRoute}
177+
sizeMode={sizeMode}
178+
setSizeMode={setSizeMode}
179+
environmentFilter={environmentFilter}
180+
setEnvironmentFilter={setEnvironmentFilter}
181+
setSelectedSourceIndex={setSelectedSourceIndex}
182+
setFocusedSourceIndex={setFocusedSourceIndex}
183+
typeFilter={typeFilter}
184+
setTypeFilter={setTypeFilter}
185+
searchQuery={searchQuery}
186+
setSearchQuery={setSearchQuery}
187+
/>
228188

229189
<div className="flex-1 flex min-h-0">
230190
{error && !analyzeData ? (
@@ -323,6 +283,137 @@ export default function Home() {
323283
)
324284
}
325285

286+
const typeFilterOptions = [
287+
{
288+
value: 'js',
289+
label: 'JavaScript',
290+
icon: <FileCode className="h-3.5 w-3.5" />,
291+
},
292+
{ value: 'css', label: 'CSS', icon: <Palette className="h-3.5 w-3.5" /> },
293+
{
294+
value: 'json',
295+
label: 'JSON',
296+
icon: <FileJson className="h-3.5 w-3.5" />,
297+
},
298+
{
299+
value: 'asset',
300+
label: 'Asset',
301+
icon: <Package className="h-3.5 w-3.5" />,
302+
},
303+
]
304+
305+
function TopBar({
306+
analyzeData,
307+
selectedRoute,
308+
setSelectedRoute,
309+
sizeMode,
310+
setSizeMode,
311+
environmentFilter,
312+
setEnvironmentFilter,
313+
setSelectedSourceIndex,
314+
setFocusedSourceIndex,
315+
typeFilter,
316+
setTypeFilter,
317+
searchQuery,
318+
setSearchQuery,
319+
}: {
320+
analyzeData: AnalyzeData | undefined
321+
selectedRoute: string | null
322+
setSelectedRoute: (route: string | null) => void
323+
sizeMode: SizeMode
324+
setSizeMode: (mode: SizeMode) => void
325+
environmentFilter: Environment
326+
setEnvironmentFilter: (env: Environment) => void
327+
setSelectedSourceIndex: (index: number | null) => void
328+
setFocusedSourceIndex: (index: number | null) => void
329+
typeFilter: string[]
330+
setTypeFilter: (types: string[]) => void
331+
searchQuery: string
332+
setSearchQuery: (query: string) => void
333+
}) {
334+
return (
335+
<div className="flex-none px-4 py-2 border-b border-border flex items-center gap-3">
336+
<div className="flex-1 flex">
337+
<RouteTypeahead
338+
selectedRoute={selectedRoute}
339+
onRouteSelected={(route) => {
340+
setSelectedRoute(route)
341+
setSelectedSourceIndex(null)
342+
setFocusedSourceIndex(null)
343+
}}
344+
/>
345+
</div>
346+
347+
<div className="flex items-center gap-2">
348+
{analyzeData && (
349+
<>
350+
<Select
351+
value={sizeMode}
352+
onValueChange={(value: SizeMode) => setSizeMode(value)}
353+
>
354+
<SelectTrigger className="w-fit min-w-[120px] h-8 text-xs">
355+
<SelectValue />
356+
</SelectTrigger>
357+
<SelectContent>
358+
<SelectItem value={SizeMode.Uncompressed} className="text-xs">
359+
<div className="flex items-center gap-1.5">
360+
<File className="h-3.5 w-3.5" />
361+
<span className="text-xs">Uncompressed</span>
362+
</div>
363+
</SelectItem>
364+
<SelectItem value={SizeMode.Compressed} className="text-xs">
365+
<div className="flex items-center gap-1.5">
366+
<FileArchive className="h-3.5 w-3.5" />
367+
<span className="text-xs">Compressed</span>
368+
</div>
369+
</SelectItem>
370+
</SelectContent>
371+
</Select>
372+
373+
<Select
374+
value={environmentFilter}
375+
onValueChange={(value: Environment) =>
376+
setEnvironmentFilter(value)
377+
}
378+
>
379+
<SelectTrigger className="w-fit min-w-[100px]">
380+
<SelectValue />
381+
</SelectTrigger>
382+
<SelectContent>
383+
<SelectItem value={Environment.Client}>
384+
<div className="flex items-center gap-1.5">
385+
<Monitor className="h-3.5 w-3.5" />
386+
<span className="text-xs">Client</span>
387+
</div>
388+
</SelectItem>
389+
<SelectItem value={Environment.Server}>
390+
<div className="flex items-center gap-1.5">
391+
<Server className="h-3.5 w-3.5" />
392+
<span className="text-xs">Server</span>
393+
</div>
394+
</SelectItem>
395+
</SelectContent>
396+
</Select>
397+
398+
<MultiSelect
399+
options={typeFilterOptions}
400+
value={typeFilter}
401+
onValueChange={setTypeFilter}
402+
selectionName={{ singular: 'file type', plural: 'file types' }}
403+
triggerIcon={<FileCode className="h-3.5 w-3.5" />}
404+
aria-label="Filter by file type"
405+
/>
406+
407+
<ControlDivider />
408+
409+
<FileSearch value={searchQuery} onChange={setSearchQuery} />
410+
</>
411+
)}
412+
</div>
413+
</div>
414+
)
415+
}
416+
326417
function ControlDivider() {
327418
return <span className="h-6 w-px bg-muted-foreground/30" />
328419
}

0 commit comments

Comments
 (0)