11/**
22 * DirectoryPicker -- monorepo package selection before indexing.
33 *
4- * Shows an interactive card grid where each package is a clickable card
5- * sized proportionally to its file count. Users select which packages
6- * to index instead of the entire repo.
4+ * Shows a clean vertical list where each package is a row with
5+ * checkbox, name, file count, and function estimate . Users select
6+ * which packages to index instead of the entire repo.
77 */
88
99import { useState , useMemo } from 'react'
1010import { motion , AnimatePresence } from 'framer-motion'
11- import { FolderGit2 , X , Files , FunctionSquare } from 'lucide-react'
11+ import { FolderGit2 , X , Files , FunctionSquare , ArrowUpDown } from 'lucide-react'
1212import { Button } from '@/components/ui/button'
1313import { Checkbox } from '@/components/ui/checkbox'
14- import { ScrollArea } from '@/components/ui/scroll-area'
1514import { cn } from '@/lib/utils'
1615import type { AnalyzeResult , DirectoryEntry } from '@/types'
1716
17+ type SortKey = 'name' | 'files' | 'functions'
18+
1819interface DirectoryPickerProps {
1920 isOpen : boolean
2021 onClose : ( ) => void
@@ -33,11 +34,25 @@ export function DirectoryPicker({
3334 functionLimit,
3435} : DirectoryPickerProps ) {
3536 const [ selected , setSelected ] = useState < Set < string > > ( new Set ( ) )
37+ const [ sortBy , setSortBy ] = useState < SortKey > ( 'files' )
38+ const [ sortAsc , setSortAsc ] = useState ( false )
3639
37- const maxFiles = useMemo (
38- ( ) => Math . max ( ...repoInfo . directories . map ( ( d ) => d . file_count ) , 1 ) ,
39- [ repoInfo . directories ] ,
40- )
40+ const sortedDirs = useMemo ( ( ) => {
41+ const dirs = [ ...repoInfo . directories ]
42+ dirs . sort ( ( a , b ) => {
43+ let cmp = 0
44+ if ( sortBy === 'name' ) cmp = a . name . localeCompare ( b . name )
45+ else if ( sortBy === 'files' ) cmp = a . file_count - b . file_count
46+ else cmp = a . estimated_functions - b . estimated_functions
47+ return sortAsc ? cmp : - cmp
48+ } )
49+ return dirs
50+ } , [ repoInfo . directories , sortBy , sortAsc ] )
51+
52+ function toggleSort ( key : SortKey ) {
53+ if ( sortBy === key ) setSortAsc ( ( prev ) => ! prev )
54+ else { setSortBy ( key ) ; setSortAsc ( key === 'name' ) }
55+ }
4156
4257 const stats = useMemo ( ( ) => {
4358 const dirs = repoInfo . directories . filter ( ( d ) => selected . has ( d . path ) )
@@ -90,11 +105,8 @@ export function DirectoryPicker({
90105 loading = { loading }
91106 />
92107
93- < div className = "px-6 pb-3" >
94- < p className = "text-sm text-muted-foreground" >
95- Select the packages you need for faster indexing and more focused results.
96- </ p >
97- < div className = "flex items-center gap-2 mt-3" >
108+ < div className = "flex items-center justify-between px-6 py-2 border-b border-border" >
109+ < div className = "flex items-center gap-2" >
98110 < Checkbox
99111 checked = { allSelected }
100112 onCheckedChange = { toggleAll }
@@ -104,36 +116,45 @@ export function DirectoryPicker({
104116 { allSelected ? 'Deselect all' : 'Select all' }
105117 </ label >
106118 </ div >
119+ < span className = "text-xs text-muted-foreground" >
120+ { repoInfo . directories . length } packages
121+ </ span >
122+ </ div >
123+
124+ < div className = "flex items-center gap-3 px-6 py-1.5 border-b border-border text-xs text-muted-foreground bg-muted/30" >
125+ < span className = "w-4" />
126+ < SortButton label = "Package" sortKey = "name" current = { sortBy } asc = { sortAsc } onToggle = { toggleSort } className = "flex-1" />
127+ < SortButton label = "Files" sortKey = "files" current = { sortBy } asc = { sortAsc } onToggle = { toggleSort } className = "w-20 text-right" />
128+ < SortButton label = "Functions" sortKey = "functions" current = { sortBy } asc = { sortAsc } onToggle = { toggleSort } className = "w-24 text-right" />
107129 </ div >
108130
109- < ScrollArea className = "flex-1 min-h-0 px-6" >
131+ < div className = "overflow-y-auto" style = { { maxHeight : ' min(400px, 50vh)' } } >
110132 < motion . div
111- className = "flex flex-wrap gap-2 pb-4 "
133+ className = "divide-y divide-border "
112134 initial = "hidden"
113135 animate = "visible"
114136 variants = { {
115137 hidden : { } ,
116- visible : { transition : { staggerChildren : 0.04 } } ,
138+ visible : { transition : { staggerChildren : 0.03 } } ,
117139 } }
118140 >
119- { repoInfo . directories . map ( ( dir ) => (
141+ { sortedDirs . map ( ( dir ) => (
120142 < motion . div
121143 key = { dir . path }
122144 variants = { {
123- hidden : { opacity : 0 , y : 8 } ,
124- visible : { opacity : 1 , y : 0 } ,
145+ hidden : { opacity : 0 } ,
146+ visible : { opacity : 1 } ,
125147 } }
126148 >
127- < PackageCard
149+ < PackageRow
128150 dir = { dir }
129151 isSelected = { selected . has ( dir . path ) }
130- maxFiles = { maxFiles }
131152 onToggle = { ( ) => toggleDir ( dir . path ) }
132153 />
133154 </ motion . div >
134155 ) ) }
135156 </ motion . div >
136- </ ScrollArea >
157+ </ div >
137158
138159 { functionLimit && (
139160 < BudgetBar current = { stats . functions } limit = { functionLimit } />
@@ -164,7 +185,7 @@ function PickerHeader({
164185 loading : boolean
165186} ) {
166187 return (
167- < div className = "flex items-center justify-between p-6 border-b border-border" >
188+ < div className = "flex items-center justify-between px-6 py-4 border-b border-border" >
168189 < div className = "flex items-center gap-3" >
169190 < div className = "w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center" >
170191 < FolderGit2 className = "w-5 h-5 text-primary" />
@@ -198,38 +219,80 @@ function PickerHeader({
198219}
199220
200221
201- function PackageCard ( {
222+ function SortButton ( {
223+ label,
224+ sortKey,
225+ current,
226+ asc,
227+ onToggle,
228+ className,
229+ } : {
230+ label : string
231+ sortKey : SortKey
232+ current : SortKey
233+ asc : boolean
234+ onToggle : ( key : SortKey ) => void
235+ className ?: string
236+ } ) {
237+ const active = current === sortKey
238+ return (
239+ < button
240+ onClick = { ( ) => onToggle ( sortKey ) }
241+ className = { cn (
242+ 'flex items-center gap-1 hover:text-foreground transition-colors' ,
243+ active ? 'text-foreground font-medium' : 'text-muted-foreground' ,
244+ className ,
245+ ) }
246+ >
247+ { label }
248+ { active && (
249+ < ArrowUpDown className = "w-3 h-3" />
250+ ) }
251+ </ button >
252+ )
253+ }
254+
255+
256+ function PackageRow ( {
202257 dir,
203258 isSelected,
204- maxFiles,
205259 onToggle,
206260} : {
207261 dir : DirectoryEntry
208262 isSelected : boolean
209- maxFiles : number
210263 onToggle : ( ) => void
211264} ) {
212- // Scale card width: smallest = 120px, largest = 240px
213- const scale = dir . file_count / maxFiles
214- const minWidth = Math . round ( 120 + scale * 120 )
215-
216265 return (
217- < button
266+ < div
267+ role = "checkbox"
268+ aria-checked = { isSelected }
269+ tabIndex = { 0 }
218270 onClick = { onToggle }
219- style = { { minWidth } }
271+ onKeyDown = { ( e ) => { if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; onToggle ( ) } } }
220272 className = { cn (
221- 'flex flex-col gap-1 rounded-lg border p-3 text-left transition-all duration-200 cursor-pointer hover:scale-[1.02] ' ,
273+ 'flex items-center gap-3 w-full px-6 py-2.5 text-left transition-colors cursor-pointer' ,
222274 isSelected
223- ? 'border-primary bg-primary/5 shadow-sm shadow-primary/10 '
224- : 'border-border bg-card/50 opacity-60 hover:opacity-80 hover:border- muted-foreground/30 hover:shadow-sm ' ,
275+ ? 'bg-primary/5'
276+ : 'hover:bg- muted/50 ' ,
225277 ) }
226278 >
227- < span className = "text-sm font-medium truncate" > { dir . name } </ span >
228- < div className = "flex items-center gap-2 text-xs text-muted-foreground" >
229- < span > { dir . file_count } files</ span >
230- < span > ~{ dir . estimated_functions . toLocaleString ( ) } fn</ span >
231- </ div >
232- </ button >
279+ < div className = { cn (
280+ 'h-4 w-4 shrink-0 rounded-sm border' ,
281+ isSelected ? 'bg-primary border-primary' : 'border-muted-foreground/40' ,
282+ ) } />
283+ < span className = { cn (
284+ 'text-sm flex-1 truncate' ,
285+ isSelected ? 'text-foreground font-medium' : 'text-muted-foreground' ,
286+ ) } >
287+ { dir . name }
288+ </ span >
289+ < span className = "text-xs text-muted-foreground tabular-nums w-20 text-right" >
290+ { dir . file_count . toLocaleString ( ) } files
291+ </ span >
292+ < span className = "text-xs text-muted-foreground tabular-nums w-24 text-right" >
293+ ~{ dir . estimated_functions . toLocaleString ( ) } fn
294+ </ span >
295+ </ div >
233296 )
234297}
235298
0 commit comments