@@ -15,6 +15,7 @@ import { relativeTime } from "../../lib/format";
1515import LoadingSpinner from "../shared/LoadingSpinner" ;
1616import FilterInput from "../shared/FilterInput" ;
1717import { Tooltip , InfoTooltip } from "../shared/Tooltip" ;
18+ import ChevronIcon from "../shared/ChevronIcon" ;
1819
1920// Validates owner/repo format (both segments must be non-empty, no spaces)
2021const VALID_REPO_NAME = / ^ [ a - z A - Z 0 - 9 . _ - ] { 1 , 100 } \/ [ a - z A - Z 0 - 9 . _ - ] { 1 , 100 } $ / ;
@@ -407,6 +408,18 @@ export default function RepoSelector(props: RepoSelectorProps) {
407408 ) ;
408409 } ) ;
409410
411+ // ── Accordion state ───────────────────────────────────────────────────────
412+
413+ const [ userExpandedOrg , setUserExpandedOrg ] = createSignal < string | null > ( null ) ;
414+ const isAccordion = createMemo ( ( ) => sortedOrgStates ( ) . length >= 6 ) ;
415+ const expandedOrg = createMemo ( ( ) => {
416+ if ( ! isAccordion ( ) ) return null ;
417+ const states = sortedOrgStates ( ) ;
418+ const userChoice = userExpandedOrg ( ) ;
419+ if ( userChoice !== null && states . some ( s => s . org === userChoice ) ) return userChoice ;
420+ return states . length > 0 ? states [ 0 ] . org : null ;
421+ } ) ;
422+
410423 // ── Status ────────────────────────────────────────────────────────────────
411424
412425 const totalOrgs = ( ) => props . selectedOrgs . length ;
@@ -453,42 +466,40 @@ export default function RepoSelector(props: RepoSelectorProps) {
453466 < Index each = { sortedOrgStates ( ) } >
454467 { ( state ) => {
455468 const visible = createMemo ( ( ) => filteredReposForOrg ( state ( ) ) ) ;
469+ const selectedCount = createMemo ( ( ) =>
470+ visible ( ) . filter ( ( r ) => isSelected ( r . fullName ) ) . length
471+ ) ;
456472
457- return (
458- < div class = "overflow-hidden rounded-lg border border-base-300" >
459- { /* Org header */ }
460- < div class = "flex items-center justify-between border-b border-base-300 bg-base-200 px-4 py-2" >
461- < span class = "text-sm font-semibold text-base-content" >
462- { state ( ) . org }
463- </ span >
464- < Show when = { ! state ( ) . loading && ! state ( ) . error } >
465- < div class = "flex gap-2" >
466- < button
467- type = "button"
468- onClick = { ( ) => selectAllInOrg ( state ( ) ) }
469- disabled = {
470- visible ( ) . length === 0 ||
471- visible ( ) . every ( ( r ) => isSelected ( r . fullName ) )
472- }
473- class = "btn btn-ghost btn-xs"
474- >
475- Select All
476- </ button >
477- < span class = "text-base-content/30" > ·</ span >
478- < button
479- type = "button"
480- onClick = { ( ) => deselectAllInOrg ( state ( ) ) }
481- disabled = {
482- visible ( ) . length === 0 ||
483- visible ( ) . every ( ( r ) => ! isSelected ( r . fullName ) )
484- }
485- class = "btn btn-ghost btn-xs"
486- >
487- Deselect All
488- </ button >
489- </ div >
490- </ Show >
491- </ div >
473+ const orgContent = ( ) => (
474+ < >
475+ { /* Per-org bulk selection bar (accordion mode only — in non-accordion, these are in the header) */ }
476+ < Show when = { isAccordion ( ) && ! state ( ) . loading && ! state ( ) . error } >
477+ < div class = "flex justify-end gap-2 border-b border-base-300 px-4 py-1" >
478+ < button
479+ type = "button"
480+ onClick = { ( ) => selectAllInOrg ( state ( ) ) }
481+ disabled = {
482+ visible ( ) . length === 0 ||
483+ visible ( ) . every ( ( r ) => isSelected ( r . fullName ) )
484+ }
485+ class = "btn btn-ghost btn-xs"
486+ >
487+ Select All
488+ </ button >
489+ < span class = "text-base-content/30" > ·</ span >
490+ < button
491+ type = "button"
492+ onClick = { ( ) => deselectAllInOrg ( state ( ) ) }
493+ disabled = {
494+ visible ( ) . length === 0 ||
495+ visible ( ) . every ( ( r ) => ! isSelected ( r . fullName ) )
496+ }
497+ class = "btn btn-ghost btn-xs"
498+ >
499+ Deselect All
500+ </ button >
501+ </ div >
502+ </ Show >
492503
493504 { /* Loading state for this org */ }
494505 < Show when = { state ( ) . loading } >
@@ -532,8 +543,7 @@ export default function RepoSelector(props: RepoSelectorProps) {
532543 >
533544 < ul class = "divide-y divide-base-300" >
534545 < Index each = { visible ( ) } >
535- { ( repo ) => {
536- return (
546+ { ( repo ) => (
537547 < li >
538548 < div class = "flex items-center" >
539549 < label class = "flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-base-200 flex-1" >
@@ -582,13 +592,94 @@ export default function RepoSelector(props: RepoSelectorProps) {
582592 </ Show >
583593 </ div >
584594 </ li >
585- ) ;
586- } }
595+ ) }
587596 </ Index >
588597 </ ul >
589598 </ div >
590599 </ Show >
591600 </ Show >
601+ </ >
602+ ) ;
603+
604+ return (
605+ < div class = "overflow-hidden rounded-lg border border-base-300" >
606+ { /* Org header — accordion button when >= 6 orgs, plain header otherwise */ }
607+ < Show
608+ when = { isAccordion ( ) }
609+ fallback = {
610+ < div class = "flex items-center justify-between border-b border-base-300 bg-base-200 px-4 py-2" >
611+ < span class = "text-sm font-semibold text-base-content" >
612+ { state ( ) . org }
613+ </ span >
614+ < Show when = { ! state ( ) . loading && ! state ( ) . error } >
615+ < div class = "flex gap-2" >
616+ < button
617+ type = "button"
618+ onClick = { ( ) => selectAllInOrg ( state ( ) ) }
619+ disabled = {
620+ visible ( ) . length === 0 ||
621+ visible ( ) . every ( ( r ) => isSelected ( r . fullName ) )
622+ }
623+ class = "btn btn-ghost btn-xs"
624+ >
625+ Select All
626+ </ button >
627+ < span class = "text-base-content/30" > ·</ span >
628+ < button
629+ type = "button"
630+ onClick = { ( ) => deselectAllInOrg ( state ( ) ) }
631+ disabled = {
632+ visible ( ) . length === 0 ||
633+ visible ( ) . every ( ( r ) => ! isSelected ( r . fullName ) )
634+ }
635+ class = "btn btn-ghost btn-xs"
636+ >
637+ Deselect All
638+ </ button >
639+ </ div >
640+ </ Show >
641+ </ div >
642+ }
643+ >
644+ < button
645+ type = "button"
646+ id = { `accordion-header-${ state ( ) . org } ` }
647+ class = "flex w-full items-center gap-2 border-b border-base-300 bg-base-200 px-4 py-2 text-left"
648+ aria-expanded = { expandedOrg ( ) === state ( ) . org }
649+ aria-controls = { `accordion-panel-${ state ( ) . org } ` }
650+ onClick = { ( ) => setUserExpandedOrg ( state ( ) . org ) }
651+ >
652+ < ChevronIcon size = "md" rotated = { expandedOrg ( ) !== state ( ) . org } />
653+ < span class = "text-sm font-semibold text-base-content flex-1" >
654+ { state ( ) . org }
655+ </ span >
656+ < span class = "badge badge-sm badge-ghost" > { visible ( ) . length } { visible ( ) . length === 1 ? "repo" : "repos" } </ span >
657+ < Show when = { selectedCount ( ) > 0 } >
658+ < span class = "badge badge-sm badge-ghost" > { selectedCount ( ) } selected</ span >
659+ </ Show >
660+ </ button >
661+ </ Show >
662+
663+ { /* Content: wrapped in grid animation for accordion, direct otherwise */ }
664+ < Show when = { ! isAccordion ( ) } >
665+ { orgContent ( ) }
666+ </ Show >
667+ < Show when = { isAccordion ( ) } >
668+ < div
669+ class = "accordion-panel grid transition-[grid-template-rows] duration-200"
670+ style = { { "grid-template-rows" : expandedOrg ( ) === state ( ) . org ? "1fr" : "0fr" } }
671+ >
672+ < div
673+ id = { `accordion-panel-${ state ( ) . org } ` }
674+ role = "region"
675+ aria-labelledby = { `accordion-header-${ state ( ) . org } ` }
676+ class = "overflow-hidden"
677+ inert = { expandedOrg ( ) !== state ( ) . org }
678+ >
679+ { orgContent ( ) }
680+ </ div >
681+ </ div >
682+ </ Show >
592683 </ div >
593684 ) ;
594685 } }
0 commit comments