first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
'use client'
import { RefreshCw, AlertTriangle } from 'lucide-react'
import { NetworkError } from '@/lib/errors'
interface ErrorStateProps {
error: unknown
onRetry?: () => void
}
export function ErrorState({ error }: ErrorStateProps) {
const isNetwork = error instanceof NetworkError
const title = isNetwork ? 'Server Connection Lost' : 'Error'
const message = isNetwork
? 'Unable to connect to the bundle analyzer server. Please ensure the server is running and try again.'
: ((error as any)?.message ?? 'An unexpected error occurred.')
return (
<div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-4 max-w-md px-6">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-destructive/10 text-destructive mb-2">
<AlertTriangle className="w-8 h-8" />
</div>
<div>
<h3 className="text-lg font-semibold text-foreground mb-2">
{title}
</h3>
<p className="text-sm text-muted-foreground mb-4">{message}</p>
<button
onClick={() => {
window.location.reload()
}}
className="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors text-sm font-medium"
>
<RefreshCw className="w-4 h-4" />
Retry Connection
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,63 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Input } from '@/components/ui/input'
import { Kbd } from '@/components/ui/kbd'
interface FileSearchProps {
value: string
onChange: (value: string) => void
}
export function FileSearch({ value, onChange }: FileSearchProps) {
const [focused, setFocused] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (
e.key === '/' &&
!['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)
) {
e.preventDefault()
inputRef.current?.focus()
} else if (
e.key === 'Escape' &&
document.activeElement === inputRef.current
) {
e.preventDefault()
onChange('')
inputRef.current?.blur()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onChange])
const handleFocus = () => {
setFocused(true)
}
const handleBlur = () => {
setFocused(false)
}
return (
<div className="relative">
<Input
ref={inputRef}
type="search"
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="Search files..."
className="w-48 focus:w-80 transition-all duration-200 pr-8"
/>
{!value && !focused && (
<Kbd className="absolute right-2 top-1/2 -translate-y-1/2">/</Kbd>
)}
</div>
)
}

View File

@@ -0,0 +1,538 @@
'use client'
import {
ArrowUp,
ChevronLeft,
ChevronRight,
Box,
File,
PanelTop,
SquareFunction,
Server,
Globe,
MessageCircleQuestion,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import type {
AnalyzeData,
ModuleIndex,
ModulesData,
SourceIndex,
} from '@/lib/analyze-data'
import { splitIdent } from '@/lib/utils'
import clsx from 'clsx'
import { Button } from '@/components/ui/button'
interface ImportChainProps {
startFileId: number
analyzeData: AnalyzeData
modulesData: ModulesData
depthMap: Map<ModuleIndex, number>
environmentFilter: 'client' | 'server'
}
interface ChainLevel {
moduleIndex: ModuleIndex
sourceIndex: SourceIndex | undefined
path: string
depth: number
fullPath?: string
templateArgs?: string
layer?: string
moduleType?: string
treeShaking?: string
selectedIndex: number
totalCount: number
// Info about this level's relationship to parent (undefined for root)
info?: DependentInfo
}
interface DependentInfo {
moduleIndex: number
sourceIndex: number | undefined
ident: string
isAsync: boolean
depth: number
}
type PathPart = {
segment: string
isCommon: boolean
isLastCommon: boolean
isPackageName: boolean
isInfrastructure: boolean
}
function spitPathSegments(path: string): string[] {
return Array.from(path.matchAll(/(.+?(?:\/|$))/g)).map(([i]) => i)
}
function getPathParts(
currentPath: string,
previousPath: string | null
): PathPart[] {
const currentSegments = spitPathSegments(currentPath)
let commonCount = 0
if (previousPath) {
const previousSegments = spitPathSegments(previousPath)
const minLength = Math.min(currentSegments.length, previousSegments.length)
for (let i = 0; i < minLength; i++) {
if (currentSegments[i] === previousSegments[i]) {
commonCount++
} else {
break
}
}
}
let infrastructureCount = 0
let packageNameCount = 0
let nodeModulesIndex = currentSegments.lastIndexOf('node_modules/')
if (nodeModulesIndex === -1) {
nodeModulesIndex = currentSegments.length
} else {
infrastructureCount = nodeModulesIndex + 1
if (currentSegments[nodeModulesIndex + 1]?.startsWith('@')) {
packageNameCount = 2
} else {
packageNameCount = 1
}
}
return currentSegments.map((segment, i) => ({
segment,
isCommon: i < commonCount,
isLastCommon: i === commonCount - 1,
isInfrastructure: i < infrastructureCount,
isPackageName:
i >= infrastructureCount && i < infrastructureCount + packageNameCount,
}))
}
function getTitle(level: ChainLevel) {
const parts = []
if (level.fullPath) parts.push(`Full Path: ${level.fullPath}`)
else parts.push(`Path: ${level.path}`)
if (level.layer) parts.push(`Layer: ${level.layer}`)
if (level.moduleType) parts.push(`Module Type: ${level.moduleType}`)
if (level.treeShaking) parts.push(`Tree Shaking: ${level.treeShaking}`)
if (level.templateArgs) parts.push(`Template Args: ${level.templateArgs}`)
return parts.join('\n')
}
export function ImportChain({
startFileId,
analyzeData,
modulesData,
depthMap,
environmentFilter,
}: ImportChainProps) {
// Filter to include only the current route
const [currentRouteOnly, setCurrentRouteOnly] = useState(true)
// Track which dependent is selected at each level
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
// Helper function to get module indices from source path
const getModuleIndicesFromSourceIndex = (sourceIndex: number) => {
const path = analyzeData.getFullSourcePath(sourceIndex)
return modulesData.getModuleIndiciesFromPath(path)
}
// Helper function to get source index from module path
const getSourceIndexFromModuleIndex = (moduleIndex: number) => {
const module = modulesData.module(moduleIndex)
if (!module) return undefined
// Search through all sources to find one with matching path
const modulePath = module.path
for (let i = 0; i < analyzeData.sourceCount(); i++) {
if (analyzeData.getFullSourcePath(i) === modulePath) {
return i
}
}
return undefined
}
// Build the import chain based on current selections
const chain = useMemo(() => {
const result: ChainLevel[] = []
const visitedModules = new Set<number>()
const startPath = analyzeData.getFullSourcePath(startFileId)
if (!startPath) return result
// Get all module indices for the starting source
const startModuleIndices = getModuleIndicesFromSourceIndex(
startFileId
).filter((moduleIndex) => {
if (currentRouteOnly && !depthMap.has(moduleIndex)) {
return false
}
let module = modulesData.module(moduleIndex)
let layer = splitIdent(module?.ident || '').layer
if (layer) {
if (environmentFilter === 'client' && /ssr|rsc|route|api/.test(layer)) {
return false
}
if (environmentFilter === 'server' && /client/.test(layer)) {
return false
}
}
return true
})
if (startModuleIndices.length === 0) return result
// Get the selected index for the start modules (default to 0)
const selectedStartIdx = selectedIndices[0] ?? 0
const actualStartIdx = Math.min(
selectedStartIdx,
startModuleIndices.length - 1
)
const startModuleIndex = startModuleIndices[actualStartIdx]
const startIdent = modulesData.module(startModuleIndex)?.ident ?? ''
result.push({
moduleIndex: startModuleIndex,
sourceIndex: startFileId,
path: startPath,
...splitIdent(startIdent),
depth: depthMap.get(startModuleIndex) ?? Infinity,
selectedIndex: actualStartIdx,
totalCount: startModuleIndices.length,
})
visitedModules.add(startModuleIndex)
// Build chain by following selected dependents
let levelIndex = 1
let currentModuleIndex = startModuleIndex
while (true) {
// Get dependents at the module level (sync and async)
const dependentModuleIndices = [
...modulesData
.moduleDependents(currentModuleIndex)
.map((index: number) => ({
index,
async: false,
depth: depthMap.get(index) ?? Infinity,
})),
...modulesData
.asyncModuleDependents(currentModuleIndex)
.map((index: number) => ({
index,
async: true,
depth: depthMap.get(index) ?? Infinity,
})),
]
// Filter out dependents that would create a cycle
const validDependents = dependentModuleIndices.filter(
({ index, depth }) =>
!visitedModules.has(index) && (isFinite(depth) || !currentRouteOnly)
)
if (validDependents.length === 0) {
// No more dependents or all would create cycles
break
}
// Build info for each dependent
const dependentsInfo: DependentInfo[] = validDependents.map(
({ index: moduleIndex, async: isAsync, depth }) => {
const sourceIndex = getSourceIndexFromModuleIndex(moduleIndex)
let ident = modulesData.module(moduleIndex)?.ident || ''
return {
moduleIndex,
sourceIndex,
ident,
isAsync,
depth,
}
}
)
// Sort: sync first, async second, then by source presence, then by depth
dependentsInfo.sort((a, b) => {
// Sort by depth (smallest first)
if (a.depth !== b.depth) {
return a.depth - b.depth
}
// Sort by ident length (shortest first)
if (a.ident.length !== b.ident.length) {
return a.ident.length - b.ident.length
}
// Sort by ident
return a.ident.localeCompare(b.ident)
})
// Get the selected index for this level (default to 0)
const selectedIdx = selectedIndices[levelIndex] ?? 0
const actualIdx = Math.min(selectedIdx, dependentsInfo.length - 1)
const selectedDepInfo = dependentsInfo[actualIdx]
const selectedDepModule = modulesData.module(selectedDepInfo.moduleIndex)
if (!selectedDepModule || selectedDepModule.ident == null) break
result.push({
moduleIndex: selectedDepInfo.moduleIndex,
sourceIndex: selectedDepInfo.sourceIndex,
path: selectedDepModule.path,
depth: depthMap.get(selectedDepInfo.moduleIndex) ?? Infinity,
...splitIdent(selectedDepModule.ident),
selectedIndex: actualIdx,
totalCount: dependentsInfo.length,
info: selectedDepInfo,
})
visitedModules.add(selectedDepInfo.moduleIndex)
currentModuleIndex = selectedDepInfo.moduleIndex
levelIndex++
// Safety check to prevent infinite loops
if (levelIndex > 100) break
}
return result
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
startFileId,
analyzeData,
modulesData,
selectedIndices,
currentRouteOnly,
depthMap,
environmentFilter,
])
const handlePrevious = (levelIndex: number) => {
setSelectedIndices((prev) => {
const newIndices = [...prev]
const currentIdx = newIndices[levelIndex] ?? 0
const level = chain[levelIndex]
newIndices[levelIndex] =
currentIdx > 0 ? currentIdx - 1 : level.totalCount - 1
return newIndices.slice(0, levelIndex + 1)
})
}
const handleNext = (levelIndex: number) => {
setSelectedIndices((prev) => {
const newIndices = [...prev]
const currentIdx = newIndices[levelIndex] ?? 0
const level = chain[levelIndex]
newIndices[levelIndex] =
currentIdx < level.totalCount - 1 ? currentIdx + 1 : 0
return newIndices.slice(0, levelIndex + 1)
})
}
const startPath = analyzeData.getFullSourcePath(startFileId)
if (!startPath) return null
return (
<div className="space-y-2">
<div className="flex items-center space-x-2">
<h3 className="text-xs font-semibold text-foreground flex-1">
Import Chain
</h3>
<label
className="inline-flex items-center space-x-2 text-xs cursor-pointer"
title="Only include dependent modules that are part of the current route's bundle"
>
<span>Current route only</span>
<input
type="checkbox"
className="form-checkbox h-4 w-4 text-primary"
checked={currentRouteOnly}
onChange={() => setCurrentRouteOnly((prev) => !prev)}
/>
</label>
</div>
<div className="space-y-0">
{chain.map((level, index) => {
const previousPath = index > 0 ? chain[index - 1].path : null
const parts = getPathParts(level.path, previousPath)
const flags =
level.sourceIndex !== undefined
? analyzeData.getSourceFlags(level.sourceIndex)
: undefined
// Get the current item's info from the level itself
const currentItemInfo = level.info
return (
<div key={`${level.path}-${index}`}>
{currentItemInfo?.isAsync && <div className="h-8" />}
<div className="flex items-center justify-center gap-2 py-1">
{currentItemInfo?.isAsync && (
<span className="text-xs text-muted-foreground italic">
(async)
</span>
)}
{index > 0 ? (
<ArrowUp className="w-4 h-4 text-muted-foreground" />
) : undefined}
{level.totalCount > 1 && (
<div className="flex items-center gap-1 flex-none">
<button
type="button"
onClick={() => handlePrevious(index)}
className="p-0.5 hover:bg-accent rounded transition-colors cursor-pointer"
title="Previous dependent"
>
<ChevronLeft className="w-3 h-3" />
</button>
<span className="text-muted-foreground text-xs min-w-[3ch] text-center">
{level.selectedIndex + 1}/{level.totalCount}
</span>
<button
type="button"
onClick={() => handleNext(index)}
className="p-0.5 hover:bg-accent rounded transition-colors cursor-pointer"
title="Next dependent"
>
<ChevronRight className="w-3 h-3" />
</button>
</div>
)}
</div>
{currentItemInfo?.isAsync && <div className="h-8" />}
<div className="flex items-center gap-2">
<div className="flex flex-col gap-1 items-center">
{!level.layer ? (
<div title="Unknown">
<MessageCircleQuestion className="w-3 h-3 text-gray-500" />
</div>
) : /app/.test(level.layer || '') ? (
<div title="App Router">
<Box className="w-3 h-3 text-green-500" />
</div>
) : (
<div title="Pages Router">
<File className="w-3 h-3 text-purple-500" />
</div>
)}
</div>
<div className="flex-1 border border-border rounded px-2 py-1 bg-background">
<span
className="font-mono text-xs text-foreground text-center block break-words"
title={getTitle(level)}
>
{parts.map(
(
{
segment,
isCommon,
isLastCommon,
isInfrastructure,
isPackageName,
},
i
) => (
<span
key={i}
className={clsx(
segment.length > 20 && 'break-all',
isCommon &&
!isLastCommon &&
!isPackageName &&
!isInfrastructure &&
'text-muted-foreground/80',
!isCommon && !isInfrastructure && 'font-bold',
isInfrastructure && 'text-muted-foreground/50',
isPackageName && 'text-orange-500'
)}
>
{segment}
<wbr />
</span>
)
)}
</span>
</div>
{/* Show icons for current item if we have flag info or no source */}
<div className="flex flex-col gap-1 items-center">
{/client/.test(level.layer || '') &&
(flags?.client ? (
<div title="Included on client-side of this route">
<PanelTop className="w-3 h-3 text-green-500" />
</div>
) : (
<div title="On client layer, but not included on client-side of this route (might be optimized away)">
<PanelTop className="w-3 h-3 text-gray-500" />
</div>
))}
{/ssr/.test(level.layer || '') &&
(flags?.server ? (
<div title="Included on server-side of this route for server-side rendering">
<Globe className="w-3 h-3 text-blue-500" />
</div>
) : (
<div title="On server-side rendering layer, but not included on server-side of this route (might be optimized away)">
<Globe className="w-3 h-3 text-gray-500" />
</div>
))}
{/rsc/.test(level.layer || '') &&
(flags?.server ? (
<div title="Included on server-side of this route as Server Component">
<Server className="w-3 h-3 text-orange-500" />
</div>
) : (
<div title="On Server Component layer, but not included on server-side of this route (might be optimized away)">
<Server className="w-3 h-3 text-gray-500" />
</div>
))}
{/route|api/.test(level.layer || '') &&
(flags?.server ? (
<div title="Included on server-side of this route for API routes">
<SquareFunction className="w-3 h-3 text-red-500" />
</div>
) : (
<div title="On API route layer, but not included on server-side of this route (might be optimized away)">
<SquareFunction className="w-3 h-3 text-gray-500" />
</div>
))}
</div>
</div>
<div className="block text-center">
{level.treeShaking && (
<div className="text-xs text-foreground text-center">
{level.treeShaking === 'locals'
? '(local declarations only)'
: level.treeShaking === 'module evaluation'
? '(only the module evaluation part of the module)'
: `(${level.treeShaking})`}
</div>
)}
</div>
</div>
)
})}
{chain.length === 0 && (
<p className="text-muted-foreground italic text-xs">
No dependents found.{' '}
{currentRouteOnly ? (
<Button
variant="link"
className="inline p-0 text-xs h-auto"
onClick={() => setCurrentRouteOnly(false)}
>
Show all routes.
</Button>
) : null}
</p>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,4 @@
export { ImportChain } from './import-chain'
export { RouteTypeahead } from './route-typeahead'
export { TreemapVisualizer } from './treemap-visualizer'
export { ErrorState } from './error-state'

View File

@@ -0,0 +1,156 @@
'use client'
import useSWR from 'swr'
import { Check, ChevronsUpDown, Loader, Route } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { cn, jsonFetcher } from '@/lib/utils'
import { NetworkError } from '@/lib/errors'
import { Kbd } from '@/components/ui/kbd'
interface RouteTypeaheadProps {
selectedRoute: string | null
onRouteSelected: (routeName: string) => void
}
export function RouteTypeahead({
selectedRoute,
onRouteSelected,
}: RouteTypeaheadProps) {
const [open, setOpen] = useState(false)
const [shortcutLabel, setShortcutLabel] = useState<string | null>(null)
useEffect(() => {
const isAppleDevice = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
setShortcutLabel(isAppleDevice ? '⌘K' : 'Ctrl+K')
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement
const isInputFocused =
activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)
if (isInputFocused) return
const isShortcutPressed = isAppleDevice
? e.metaKey && e.key === 'k'
: e.ctrlKey && e.key === 'k'
if (isShortcutPressed) {
e.preventDefault()
setOpen(true)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
const {
data: routes,
isLoading,
error,
} = useSWR<string[]>('data/routes.json', jsonFetcher, {
onSuccess: (routeNames) => {
// Auto-select first route if none is selected
if (routeNames.length > 0 && selectedRoute == null) {
onRouteSelected(routeNames[0])
}
},
})
if (error) {
return (
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm max-w-full">
<span className="font-medium"></span>
<span className="truncate">
{error instanceof NetworkError
? 'Unable to connect to server'
: error.message}
</span>
</div>
)
}
let ctaText
if (isLoading) {
ctaText = 'Loading routes...'
} else if (selectedRoute != null) {
ctaText = selectedRoute
} else {
ctaText = 'Select route...'
}
return (
<div className="flex items-center gap-2 min-w-64 max-w-full">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
disabled={isLoading}
className="flex-grow-1 w-full justify-between font-mono text-sm"
>
<div className="flex items-center">
{isLoading ? (
<Loader className="mr-2 inline animate-spin" />
) : (
<Route className="inline mr-2" />
)}
{ctaText}
</div>
<div className="flex items-center gap-2">
{shortcutLabel && <Kbd>{shortcutLabel}</Kbd>}
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-96 p-0">
<Command>
<CommandInput placeholder="Search routes..." className="h-9" />
<CommandList>
<CommandEmpty>No route found.</CommandEmpty>
<CommandGroup>
{(routes || []).map((route) => {
return (
<CommandItem
key={route}
value={route}
onSelect={() => {
onRouteSelected(route)
setOpen(false)
}}
className="font-mono"
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedRoute === route ? 'opacity-100' : 'opacity-0'
)}
/>
{route}
</CommandItem>
)
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)
}

View File

@@ -0,0 +1,237 @@
'use client'
import type React from 'react'
import { CircleHelp } from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from './ui/tooltip'
import { ImportChain } from '@/components/import-chain'
import { Skeleton } from '@/components/ui/skeleton'
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
import { SpecialModule } from '@/lib/types'
import { getSpecialModuleType } from '@/lib/utils'
import { Badge } from './ui/badge'
interface SidebarProps {
sidebarWidth: number
analyzeData: AnalyzeData | null
modulesData: ModulesData | null
selectedSourceIndex: number | null
moduleDepthMap: Map<number, number>
environmentFilter: 'client' | 'server'
filterSource?: (sourceIndex: number) => boolean
isLoading?: boolean
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
}
export function Sidebar({
sidebarWidth,
analyzeData,
modulesData,
selectedSourceIndex,
moduleDepthMap,
environmentFilter,
filterSource,
isLoading = false,
}: SidebarProps) {
filterSource = filterSource ?? (() => true)
if (isLoading || !analyzeData) {
return (
<div
className="flex-none bg-muted border-l border-border overflow-y-auto"
style={{ width: `${sidebarWidth}%` }}
>
<div className="flex-1 p-3 space-y-4 overflow-y-auto">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<div className="mt-4 space-y-2">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-4/5" />
</div>
</div>
</div>
)
}
return (
<div
className="flex-none bg-muted border-l border-border overflow-y-auto"
style={{ width: `${sidebarWidth}%` }}
>
{selectedSourceIndex != null ? (
<SelectionDetails
analyzeData={analyzeData}
modulesData={modulesData}
selectedSourceIndex={selectedSourceIndex}
filterSource={filterSource}
moduleDepthMap={moduleDepthMap}
environmentFilter={environmentFilter}
/>
) : null}
</div>
)
}
function SelectionDetails({
analyzeData,
modulesData,
selectedSourceIndex,
filterSource,
moduleDepthMap,
environmentFilter,
}: {
analyzeData: AnalyzeData
modulesData: ModulesData | null
selectedSourceIndex: number
moduleDepthMap: Map<number, number>
environmentFilter: 'client' | 'server'
filterSource: (sourceIndex: number) => boolean
}) {
const specialModuleType = getSpecialModuleType(
analyzeData,
selectedSourceIndex
)
const selectedSource =
selectedSourceIndex != null
? analyzeData.source(selectedSourceIndex)
: undefined
const hasChildModules =
selectedSourceIndex != null &&
analyzeData.sourceChildren(selectedSourceIndex).length > 0
const childModuleCount =
hasChildModules && selectedSourceIndex != null
? analyzeData.getRecursiveModuleCount(selectedSourceIndex, filterSource)
: null
const { size, compressedSize } = analyzeData.getRecursiveSizes(
selectedSourceIndex,
filterSource
)
const chunks =
selectedSourceIndex != null
? analyzeData.sourceChunks(selectedSourceIndex)
: []
return (
<div className="flex-1 p-3 space-y-8 overflow-y-auto">
<div className="space-y-2">
<h2 className="text-s font-semibold mb-1 text-foreground truncate">
{selectedSource?.path || 'All Route Modules'}
</h2>
{selectedSourceIndex != null &&
analyzeData.source(selectedSourceIndex) ? (
<div className="text-xs">
<div>
<span>{formatBytes(compressedSize)}</span>
<span className="text-muted-foreground ml-1">
compressed (estimated)
</span>
<InlineHelpTooltip>
Estimated compressed size. Modules are compressed in isolation
which may differ from their size in the final chunk.
</InlineHelpTooltip>
</div>
<div>
<span>{formatBytes(size)}</span>{' '}
<span className="text-muted-foreground">uncompressed</span>
<InlineHelpTooltip>
Uncompressed modules may still be minified, tree-shaken, and
dead-code eliminated. They just don't account for general
compression like gzip.
</InlineHelpTooltip>
</div>
{hasChildModules && childModuleCount != null ? (
<div>
<span>{childModuleCount} </span>
<span className="text-muted-foreground">
{childModuleCount === 1 ? 'module' : 'modules'}
</span>
</div>
) : null}
</div>
) : null}
</div>
{selectedSourceIndex != null &&
analyzeData.source(selectedSourceIndex) &&
(specialModuleType === SpecialModule.POLYFILL_MODULE ||
specialModuleType === SpecialModule.POLYFILL_NOMODULE) && (
<dl className="flex items-center gap-2">
<dt className="inline-flex items-center">
<Badge variant="polyfill">Polyfill</Badge>
</dt>
<dd className="text-xs text-muted-foreground">
Next.js built-in polyfills
</dd>
</dl>
)}
{selectedSourceIndex != null &&
analyzeData.source(selectedSourceIndex) &&
!hasChildModules && (
<>
{modulesData && (
<ImportChain
startFileId={selectedSourceIndex}
analyzeData={analyzeData}
modulesData={modulesData}
depthMap={moduleDepthMap}
environmentFilter={environmentFilter}
/>
)}
{chunks.length > 0 ? (
<div className="mt-2">
<p className="text-xs font-semibold text-foreground">
Output Chunks
</p>
<ul className="text-xs text-muted-foreground font-mono mt-1 space-y-1">
{chunks.map((chunk) => (
<li key={chunk} className="break-all">
{chunk}
</li>
))}
</ul>
</div>
) : null}
</>
)}
</div>
)
}
function InlineHelpTooltip({ children }: { children: React.ReactNode }) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<CircleHelp
size={14}
className="inline-block ml-1 text-muted-foreground"
aria-hidden="true"
/>
</TooltipTrigger>
<TooltipContent className="max-w-xs" side="top" align="center">
{children}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -0,0 +1,10 @@
'use client'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
client:
'border-transparent bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-300/20',
server:
'border-transparent bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-700/10 dark:ring-purple-300/20',
polyfill:
'border-transparent bg-polyfill/10 dark:bg-polyfill/30 text-polyfill dark:text-polyfill-foreground ring-1 ring-inset ring-polyfill/20',
},
},
defaultVariants: {
variant: 'default',
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<span className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,59 @@
'use client'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }

View File

@@ -0,0 +1,184 @@
'use client'
import * as React from 'react'
import { Command as CommandPrimitive } from 'cmdk'
import { SearchIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className
)}
{...props}
/>
)
}
function CommandDialog({
title = 'Command Palette',
description = 'Search for a command to run...',
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn('overflow-hidden p-0', className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn('bg-border -mx-1 h-px', className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="command-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,143 @@
'use client'
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-header"
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-footer"
className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn('text-lg leading-none font-semibold', className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,25 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }

View File

@@ -0,0 +1,28 @@
import { cn } from '@/lib/utils'
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
<kbd
data-slot="kbd"
className={cn(
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
"[&_svg:not([class*='size-'])]:size-3",
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<kbd
data-slot="kbd-group"
className={cn('inline-flex items-center gap-1', className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }

View File

@@ -0,0 +1,187 @@
'use client'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { ChevronDown, CheckIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
export interface MultiSelectPropOption {
value: string
label: string
icon?: React.ReactNode
}
interface MultiSelectProps {
options: MultiSelectPropOption[]
value: string[]
onValueChange: (value: string[]) => void
placeholder?: string
emptyMessage?: string
selectionName?: {
singular: string
plural: string
}
className?: string
triggerClassName?: string
triggerIcon?: React.ReactNode
'aria-label'?: string
size?: 'sm' | 'default'
}
export function MultiSelect({
options,
value,
onValueChange,
placeholder = 'Select items...',
selectionName = {
singular: 'item',
plural: 'items',
},
className,
triggerClassName,
triggerIcon,
'aria-label': ariaLabel,
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false)
function handleToggle(optionValue: string, checked: boolean) {
const newValue = checked
? [...value, optionValue]
: value.filter((v) => v !== optionValue)
if (newValue.length > 0) {
onValueChange(newValue)
}
}
let displayText: string
if (value.length === options.length) {
displayText = `All ${selectionName.plural}`
} else if (value.length === 0) {
displayText = placeholder
} else {
displayText = `${value.length} ${value.length === 1 ? selectionName.singular : selectionName.plural}`
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'justify-between border focus-visible:ring-[3px] focus-visible:ring-ring/50 font-normal px-3 py-2 h-9 text-xs',
triggerClassName
)}
role="combobox"
aria-expanded={open}
aria-haspopup="dialog"
aria-label={ariaLabel}
onKeyDown={(e) => {
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !open) {
e.preventDefault()
setOpen(true)
}
}}
>
<div className="flex items-center gap-1">
{triggerIcon && (
<span
className="shrink-0 text-muted-foreground"
aria-hidden="true"
>
{triggerIcon}
</span>
)}
<span>{displayText}</span>
</div>
<ChevronDown className="h-3.5 w-3.5 opacity-50" aria-hidden="true" />
</Button>
</PopoverTrigger>
<PopoverContent
className={cn('w-48 p-2', className)}
align="end"
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
setOpen(false)
return
}
const checkboxes = Array.from(
e.currentTarget.querySelectorAll('input[type="checkbox"]')
) as HTMLInputElement[]
const currentIndex = checkboxes.indexOf(
document.activeElement as HTMLInputElement
)
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
if (currentIndex === -1) {
checkboxes[0]?.focus()
} else {
const nextIndex =
e.key === 'ArrowDown'
? (currentIndex + 1) % checkboxes.length
: (currentIndex - 1 + checkboxes.length) % checkboxes.length
checkboxes[nextIndex]?.focus()
}
}
}}
>
<fieldset className="space-y-2">
<legend className="sr-only">{ariaLabel || placeholder}</legend>
{options.map((option) => (
<MultiSelectOption
key={option.value}
label={option.label}
icon={option.icon}
checked={value.includes(option.value)}
onChange={(checked) => handleToggle(option.value, checked)}
/>
))}
</fieldset>
</PopoverContent>
</Popover>
)
}
function MultiSelectOption({
label,
icon,
checked,
onChange,
}: {
label: string
icon?: React.ReactNode
checked: boolean
onChange: (checked: boolean) => void
}) {
return (
<label
className={cn(
"text-xs hover:bg-accent hover:text-accent-foreground has-[input:focus-visible]:bg-accent has-[input:focus-visible]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 select-none outline-hidden [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
)}
>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="sr-only peer focus:outline-hidden focus-visible:outline-2"
aria-label={label}
/>
{icon && (
<span className="shrink-0" aria-hidden="true">
{icon}
</span>
)}
<span className="flex items-center gap-2">{label}</span>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
{checked && <CheckIcon className="size-4" />}
</span>
</label>
)
}

View File

@@ -0,0 +1,33 @@
'use client'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import * as React from 'react'
import { cn } from '@/lib/utils'
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,190 @@
'use client'
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default'
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = 'item-aligned',
align = 'center',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
'flex cursor-default items-center justify-center py-1',
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
'flex cursor-default items-center justify-center py-1',
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,33 @@
import { cn } from '@/lib/utils'
export function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-muted', className)}
{...props}
/>
)
}
export function TreemapSkeleton() {
return (
<div className="h-full w-full grid grid-cols-12 grid-rows-8 gap-2">
{/* Simulate treemap blocks with varying sizes */}
<Skeleton className="col-span-5 row-span-4" />
<Skeleton className="col-span-4 row-span-3" />
<Skeleton className="col-span-3 row-span-3" />
<Skeleton className="col-span-4 row-span-1" />
<Skeleton className="col-span-3 row-span-2" />
<Skeleton className="col-span-3 row-span-4" />
<Skeleton className="col-span-2 row-span-2" />
<Skeleton className="col-span-2 row-span-2" />
<Skeleton className="col-span-3 row-span-2" />
<Skeleton className="col-span-4 row-span-2" />
<Skeleton className="col-span-2 row-span-2" />
<Skeleton className="col-span-3 row-span-2" />
</div>
)
}

View File

@@ -0,0 +1,66 @@
'use client'
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
import * as React from 'react'
import { cn } from '@/lib/utils'
const ToggleGroupContext = React.createContext<{
size?: 'default' | 'sm' | 'lg'
variant?: 'default' | 'outline'
}>({
size: 'default',
variant: 'default',
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & {
size?: 'default' | 'sm' | 'lg'
variant?: 'default' | 'outline'
}
>(
(
{ className, variant = 'default', size = 'default', children, ...props },
ref
) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('flex items-center justify-center gap-1', className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
)
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>
>(({ className, children, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
context.size === 'default' && 'h-10 px-3',
context.size === 'sm' && 'h-8 px-2 text-xs',
context.size === 'lg' && 'h-11 px-5',
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View File

@@ -0,0 +1,30 @@
'use client'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import * as React from 'react'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }