Skip to content

Commit de799c2

Browse files
committed
feat(groups): align desktop/mobile layout with sessions
1 parent 9ada7ed commit de799c2

4 files changed

Lines changed: 186 additions & 19 deletions

File tree

web/src/lib/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default {
4848
'sessions.display.toggleToCompact': 'Switch to compact view',
4949
'sessions.display.toggleToComfortable': 'Switch to comfortable view',
5050
'sessions.search.placeholder': 'Search sessions…',
51+
'groups.search.placeholder': 'Search groups…',
5152
'sessions.sidebar.resize': 'Resize sidebar',
5253
'sessions.sidebar.open': 'Open sessions sidebar',
5354
'sessions.sidebar.close': 'Close sessions sidebar',

web/src/lib/locales/zh-CN.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default {
4848
'sessions.display.toggleToCompact': '切换到紧凑视图',
4949
'sessions.display.toggleToComfortable': '切换到舒适视图',
5050
'sessions.search.placeholder': '搜索会话…',
51+
'groups.search.placeholder': '搜索群组…',
5152
'sessions.sidebar.resize': '调整侧边栏宽度',
5253
'sessions.sidebar.open': '打开会话侧边栏',
5354
'sessions.sidebar.close': '关闭会话侧边栏',

web/src/router.tsx

Lines changed: 181 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
769784
function 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">

web/src/routes/groups/detail.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ function TimelineBubble(props: {
602602
<div
603603
className={`rounded-2xl px-3 py-2 text-sm ${
604604
isUser
605-
? 'bg-[var(--app-link)] text-white rounded-br-sm'
605+
? 'bg-[var(--app-button)] text-[var(--app-button-text)] rounded-br-sm'
606606
: 'bg-[var(--app-secondary-bg)] text-[var(--app-fg)] rounded-bl-sm'
607607
} ${isCommand
608608
? 'font-mono text-xs whitespace-pre-wrap break-words'
@@ -1033,7 +1033,7 @@ export default function GroupDetailPage() {
10331033
type="button"
10341034
onClick={() => { void handleSaveNote() }}
10351035
disabled={isPending}
1036-
className="rounded-md bg-[var(--app-link)] px-3 py-1.5 text-xs text-white disabled:opacity-60"
1036+
className="rounded-md bg-[var(--app-button)] px-3 py-1.5 text-xs text-[var(--app-button-text)] disabled:opacity-60"
10371037
>
10381038
Save Note
10391039
</button>
@@ -1099,7 +1099,7 @@ export default function GroupDetailPage() {
10991099
<button
11001100
type="submit"
11011101
disabled={isPending || composer.trim().length === 0}
1102-
className="shrink-0 rounded-xl bg-[var(--app-link)] px-4 py-2 text-sm text-white disabled:opacity-60"
1102+
className="shrink-0 rounded-xl bg-[var(--app-button)] px-4 py-2 text-sm text-[var(--app-button-text)] disabled:opacity-60"
11031103
>
11041104
Send
11051105
</button>

0 commit comments

Comments
 (0)