@@ -766,14 +766,32 @@ function GroupListItem(props: {
766766 )
767767}
768768
769+ function filterGroupsBySearch ( groups : GroupDetail [ ] , query : string ) : GroupDetail [ ] {
770+ const normalizedQuery = query . trim ( ) . toLowerCase ( )
771+ if ( ! normalizedQuery ) {
772+ return groups
773+ }
774+ return groups . filter ( ( item ) => {
775+ const name = item . group . name . toLowerCase ( )
776+ const description = ( item . group . description ?? '' ) . toLowerCase ( )
777+ const groupId = item . group . id . toLowerCase ( )
778+ return name . includes ( normalizedQuery )
779+ || description . includes ( normalizedQuery )
780+ || groupId . includes ( normalizedQuery )
781+ } )
782+ }
783+
769784function GroupsLayout ( ) {
770785 const { api } = useAppContext ( )
771786 const navigate = useNavigate ( )
787+ const pathname = useLocation ( { select : location => location . pathname } )
772788 const matchRoute = useMatchRoute ( )
773789 const { t } = useTranslation ( )
774790 const { groups, isLoading } = useGroups ( api )
775- const { density } = useSessionListDensity ( )
791+ const { density, toggleDensity } = useSessionListDensity ( )
792+ const { desktopSidebarHidden, setDesktopSidebarHidden, toggleDesktopSidebar } = useSessionSidebarVisibility ( )
776793 const [ showCreateModal , setShowCreateModal ] = useState ( false )
794+ const [ groupSearchQuery , setGroupSearchQuery ] = useState ( '' )
777795 const [ newGroupName , setNewGroupName ] = useState ( '' )
778796 const [ newGroupDesc , setNewGroupDesc ] = useState ( '' )
779797 const [ createError , setCreateError ] = useState < string | null > ( null )
@@ -784,19 +802,32 @@ function GroupsLayout() {
784802 const [ renameDraft , setRenameDraft ] = useState ( '' )
785803 const [ renameError , setRenameError ] = useState < string | null > ( null )
786804 const [ deleteModalOpen , setDeleteModalOpen ] = useState ( false )
805+ const [ mobileSidebarOpen , setMobileSidebarOpen ] = useState ( false )
787806 const { createGroup, isPending : isCreatingGroup } = useGroupActions ( api , null )
788807 const { updateGroup, deleteGroup, isPending : isActionPending } = useGroupActions ( api , actionGroupId )
789808 const { sidebarWidth, isResizing, startSidebarResize } = useSessionSidebarWidth ( )
790809
791810 const groupMatch = matchRoute ( { to : '/groups/$groupId' , fuzzy : true } )
792811 const selectedGroupId = groupMatch ? groupMatch . groupId : null
812+ const isGroupsIndex = pathname === '/groups' || pathname === '/groups/'
813+ const showDesktopSidebar = isGroupsIndex || ! desktopSidebarHidden
814+ const toggleDensityLabel = density === 'comfortable'
815+ ? t ( 'sessions.display.toggleToCompact' )
816+ : t ( 'sessions.display.toggleToComfortable' )
817+ const desktopSidebarToggleLabel = showDesktopSidebar
818+ ? t ( 'sessions.sidebar.hideDesktop' )
819+ : t ( 'sessions.sidebar.showDesktop' )
793820 const sidebarStyle = { '--sessions-sidebar-width' : `${ sidebarWidth } px` } as CSSProperties
821+ const visibleGroups = useMemo (
822+ ( ) => filterGroupsBySearch ( groups , groupSearchQuery ) ,
823+ [ groups , groupSearchQuery ]
824+ )
794825 const actionTarget = actionGroupId
795826 ? groups . find ( ( item ) => item . group . id === actionGroupId ) ?? null
796827 : null
797828
798829 // Calculate total member count across all groups
799- const totalMemberCount = groups . reduce ( ( acc , item ) => acc + item . members . length , 0 )
830+ const totalMemberCount = visibleGroups . reduce ( ( acc , item ) => acc + item . members . length , 0 )
800831
801832 useEffect ( ( ) => {
802833 if ( ! actionGroupId ) {
@@ -810,6 +841,36 @@ function GroupsLayout() {
810841 }
811842 } , [ actionGroupId , groups ] )
812843
844+ useEffect ( ( ) => {
845+ if ( isGroupsIndex ) {
846+ setMobileSidebarOpen ( false )
847+ }
848+ } , [ isGroupsIndex ] )
849+
850+ useEffect ( ( ) => {
851+ if ( isGroupsIndex && desktopSidebarHidden ) {
852+ setDesktopSidebarHidden ( false )
853+ }
854+ } , [ isGroupsIndex , desktopSidebarHidden , setDesktopSidebarHidden ] )
855+
856+ useEffect ( ( ) => {
857+ if ( ! mobileSidebarOpen ) return
858+ const previousOverflow = document . body . style . overflow
859+ document . body . style . overflow = 'hidden'
860+
861+ const handleResize = ( ) => {
862+ if ( window . innerWidth >= 1024 ) {
863+ setMobileSidebarOpen ( false )
864+ }
865+ }
866+
867+ window . addEventListener ( 'resize' , handleResize )
868+ return ( ) => {
869+ window . removeEventListener ( 'resize' , handleResize )
870+ document . body . style . overflow = previousOverflow
871+ }
872+ } , [ mobileSidebarOpen ] )
873+
813874 const handleCreate = async ( ) => {
814875 const trimmedName = newGroupName . trim ( )
815876 if ( ! trimmedName ) {
@@ -881,20 +942,34 @@ function GroupsLayout() {
881942 }
882943 }
883944
884- return (
885- < div className = "flex h-full min-h-0" >
886- { /* Sidebar - matching SessionsPage width system */ }
887- < div
888- className = "flex w-full lg:w-[var(--sessions-sidebar-width)] shrink-0 flex-col border-r border-[var(--app-divider)] bg-[var(--app-bg)]"
889- style = { sidebarStyle }
890- >
945+ const closeSidebarOnMobile = useCallback ( ( ) => {
946+ setMobileSidebarOpen ( false )
947+ } , [ ] )
948+
949+ const toggleSidebarFromBar = useCallback ( ( ) => {
950+ if ( typeof window !== 'undefined' && window . innerWidth >= 1024 ) {
951+ toggleDesktopSidebar ( )
952+ return
953+ }
954+ setMobileSidebarOpen ( true )
955+ } , [ toggleDesktopSidebar ] )
956+
957+ const renderSidebarContent = ( options ?: { inDrawer ?: boolean ; onClose ?: ( ) => void } ) => {
958+ const inDrawer = options ?. inDrawer === true
959+ const onClose = options ?. onClose
960+
961+ return (
962+ < >
891963 < div className = "bg-[var(--app-bg)] pt-[env(safe-area-inset-top)]" >
892964 { /* Tab switcher row - exactly matching SessionsPage */ }
893965 < div className = "mx-auto w-full max-w-content flex items-center justify-between border-b border-[var(--app-divider)] px-3 py-2" >
894966 < div className = "flex items-center gap-1" >
895967 < button
896968 type = "button"
897- onClick = { ( ) => navigate ( { to : '/sessions' } ) }
969+ onClick = { ( ) => {
970+ onClose ?.( )
971+ navigate ( { to : '/sessions' } )
972+ } }
898973 className = "rounded-md px-2.5 py-1.5 text-xs text-[var(--app-hint)] hover:text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)] transition-colors"
899974 >
900975 Sessions
@@ -907,6 +982,26 @@ function GroupsLayout() {
907982 </ button >
908983 </ div >
909984 < div className = "flex items-center gap-1.5" >
985+ { ! isGroupsIndex ? (
986+ < button
987+ type = "button"
988+ onClick = { toggleDesktopSidebar }
989+ className = "hidden lg:flex p-1.5 rounded-full text-[var(--app-hint)] hover:text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)] transition-colors"
990+ title = { desktopSidebarToggleLabel }
991+ aria-label = { desktopSidebarToggleLabel }
992+ >
993+ < SidebarIcon className = "h-4 w-4" />
994+ </ button >
995+ ) : null }
996+ < button
997+ type = "button"
998+ onClick = { toggleDensity }
999+ className = "p-1.5 rounded-full text-[var(--app-hint)] hover:text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)] transition-colors"
1000+ title = { toggleDensityLabel }
1001+ aria-label = { toggleDensityLabel }
1002+ >
1003+ < DensityIcon className = "h-5 w-5" />
1004+ </ button >
9101005 < button
9111006 type = "button"
9121007 onClick = { ( ) => navigate ( { to : '/settings' } ) }
@@ -924,31 +1019,56 @@ function GroupsLayout() {
9241019 >
9251020 < PlusIcon className = "h-5 w-5" />
9261021 </ button >
1022+ { inDrawer && onClose ? (
1023+ < >
1024+ < span className = "mx-0.5 h-5 w-px bg-[var(--app-divider)]" aria-hidden = "true" />
1025+ < button
1026+ type = "button"
1027+ onClick = { onClose }
1028+ className = "p-1.5 rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
1029+ title = { t ( 'sessions.sidebar.close' ) }
1030+ aria-label = { t ( 'sessions.sidebar.close' ) }
1031+ >
1032+ < CloseIcon className = "h-4 w-4" />
1033+ </ button >
1034+ </ >
1035+ ) : null }
9271036 </ div >
9281037 </ div >
9291038 { /* Count info row - matching SessionsPage */ }
9301039 < div className = "mx-auto w-full max-w-content flex items-center justify-between px-3 py-1.5" >
9311040 < div className = "text-xs text-[var(--app-hint)]" >
932- { groups . length } { groups . length === 1 ? 'group' : 'groups' } • { totalMemberCount } { totalMemberCount === 1 ? 'member' : 'members' }
1041+ { visibleGroups . length } { visibleGroups . length === 1 ? 'group' : 'groups' } • { totalMemberCount } { totalMemberCount === 1 ? 'member' : 'members' }
9331042 </ div >
9341043 </ div >
1044+ < div className = "mx-auto w-full max-w-content px-3 pb-2" >
1045+ < input
1046+ value = { groupSearchQuery }
1047+ onChange = { ( e ) => setGroupSearchQuery ( e . target . value ) }
1048+ placeholder = { t ( 'groups.search.placeholder' ) }
1049+ className = "w-full rounded-md border border-[var(--app-divider)] bg-[var(--app-secondary-bg)] px-3 py-1.5 text-sm outline-none focus:border-[var(--app-link)]"
1050+ />
1051+ </ div >
9351052 </ div >
9361053
9371054 < div className = "flex-1 min-h-0 overflow-y-auto" >
9381055 { isLoading ? (
9391056 < div className = "px-3 py-4 text-sm text-[var(--app-hint)]" > Loading...</ div >
940- ) : groups . length === 0 ? (
941- < div className = "px-3 py-4 text-sm text-[var(--app-hint)]" > No groups yet.</ div >
1057+ ) : visibleGroups . length === 0 ? (
1058+ < div className = "px-3 py-4 text-sm text-[var(--app-hint)]" >
1059+ { groupSearchQuery . trim ( ) ? 'No groups match.' : 'No groups yet.' }
1060+ </ div >
9421061 ) : (
9431062 < div className = "py-1" >
944- { groups . map ( ( item ) => (
1063+ { visibleGroups . map ( ( item ) => (
9451064 < GroupListItem
9461065 key = { item . group . id }
9471066 item = { item }
9481067 selected = { selectedGroupId === item . group . id }
9491068 density = { density }
9501069 onSelect = { ( groupId ) => {
9511070 setActionMenuOpen ( false )
1071+ setMobileSidebarOpen ( false )
9521072 navigate ( { to : '/groups/$groupId' , params : { groupId } } )
9531073 } }
9541074 onOpenActions = { handleOpenGroupActions }
@@ -957,11 +1077,23 @@ function GroupsLayout() {
9571077 </ div >
9581078 ) }
9591079 </ div >
1080+ </ >
1081+ )
1082+ }
1083+
1084+ return (
1085+ < div className = "flex h-full min-h-0" >
1086+ { /* Sidebar - matching SessionsPage width system */ }
1087+ < div
1088+ className = { `${ isGroupsIndex ? 'flex' : showDesktopSidebar ? 'hidden lg:flex' : 'hidden' } w-full lg:w-[var(--sessions-sidebar-width)] shrink-0 flex-col border-r border-[var(--app-divider)] bg-[var(--app-bg)]` }
1089+ style = { sidebarStyle }
1090+ >
1091+ { renderSidebarContent ( ) }
9601092 </ div >
9611093
9621094 { /* Sidebar resize handle - matching SessionsPage */ }
9631095 < div
964- className = " group relative w-2 shrink-0 cursor-col-resize"
1096+ className = { ` ${ showDesktopSidebar ? 'hidden lg:block' : 'hidden' } group relative w-2 shrink-0 cursor-col-resize` }
9651097 role = "separator"
9661098 aria-orientation = "vertical"
9671099 aria-label = { t ( 'sessions.sidebar.resize' ) }
@@ -974,12 +1106,45 @@ function GroupsLayout() {
9741106 </ div >
9751107
9761108 { /* Main area */ }
977- < div className = "flex min-w-0 flex-1 flex-col bg-[var(--app-bg)]" >
1109+ < div className = { `${ isGroupsIndex ? 'hidden lg:flex' : 'flex' } min-w-0 flex-1 flex-col bg-[var(--app-bg)]` } >
1110+ { ! isGroupsIndex ? (
1111+ < div className = "flex items-center gap-2 border-b border-[var(--app-divider)] bg-[var(--app-bg)] px-3 py-2 pt-[calc(0.5rem+env(safe-area-inset-top))] lg:pt-2" >
1112+ < button
1113+ type = "button"
1114+ onClick = { toggleSidebarFromBar }
1115+ className = "flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
1116+ title = { t ( 'sessions.sidebar.open' ) }
1117+ aria-label = { t ( 'sessions.sidebar.open' ) }
1118+ >
1119+ < SidebarIcon className = "h-5 w-5" />
1120+ </ button >
1121+ < div className = "text-sm font-medium text-[var(--app-hint)]" > Groups</ div >
1122+ </ div >
1123+ ) : null }
9781124 < div className = "flex-1 min-h-0" >
9791125 < Outlet />
9801126 </ div >
9811127 </ div >
9821128
1129+ { mobileSidebarOpen ? (
1130+ < div
1131+ className = "fixed inset-0 z-40 flex lg:hidden"
1132+ role = "dialog"
1133+ aria-modal = "true"
1134+ aria-label = "Groups sidebar"
1135+ >
1136+ < button
1137+ type = "button"
1138+ className = "absolute inset-0 bg-black/35"
1139+ onClick = { closeSidebarOnMobile }
1140+ aria-label = { t ( 'sessions.sidebar.close' ) }
1141+ />
1142+ < div className = "relative flex h-full w-[min(88vw,420px)] max-w-full flex-col border-r border-[var(--app-divider)] bg-[var(--app-bg)] shadow-xl" >
1143+ { renderSidebarContent ( { inDrawer : true , onClose : closeSidebarOnMobile } ) }
1144+ </ div >
1145+ </ div >
1146+ ) : null }
1147+
9831148 { /* Create group modal */ }
9841149 { showCreateModal ? (
9851150 < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" >
0 commit comments