1- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
1+ import { lazy , Suspense , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
22import { useTranslation } from 'react-i18next' ;
3- import {
4- DndContext ,
5- closestCenter ,
6- KeyboardSensor ,
7- PointerSensor ,
8- TouchSensor ,
9- useSensor ,
10- useSensors ,
11- DragEndEvent ,
12- } from "@dnd-kit/core" ;
13- import {
14- SortableContext ,
15- sortableKeyboardCoordinates ,
16- verticalListSortingStrategy ,
17- } from "@dnd-kit/sortable" ;
183import { GroupMeta } from "../../types" ;
194import { classNames } from "../../utils/classNames" ;
205import { CloseIcon , FolderIcon , ChevronDownIcon , ChevronLeftIcon , ChevronRightIcon , PlusIcon } from "../Icons" ;
21- import { SortableGroupItem } from "./SortableGroupItem " ;
6+ import { GroupSidebarItem } from "./GroupSidebarItem " ;
227import { SIDEBAR_MAX_WIDTH , SIDEBAR_MIN_WIDTH } from "../../stores/useUIStore" ;
238import { useBrandingStore } from "../../stores" ;
249
10+ const GroupSidebarSortableList = lazy ( ( ) =>
11+ import ( "./GroupSidebarSortableList" ) . then ( ( module ) => ( { default : module . GroupSidebarSortableList } ) )
12+ ) ;
13+
2514export interface GroupSidebarProps {
2615 orderedGroups : GroupMeta [ ] ;
2716 archivedGroupIds : string [ ] ;
@@ -65,6 +54,7 @@ export function GroupSidebar({
6554 const branding = useBrandingStore ( ( s ) => s . branding ) ;
6655 const dragStateRef = useRef < { startX : number ; startWidth : number } | null > ( null ) ;
6756 const [ isResizing , setIsResizing ] = useState ( false ) ;
57+ const [ sortableReady , setSortableReady ] = useState ( false ) ;
6858 const archivedSet = useMemo ( ( ) => new Set ( archivedGroupIds ) , [ archivedGroupIds ] ) ;
6959 const workingGroups = useMemo (
7060 ( ) => orderedGroups . filter ( ( g ) => ! archivedSet . has ( String ( g . group_id || "" ) . trim ( ) ) ) ,
@@ -91,37 +81,6 @@ export function GroupSidebar({
9181 const autoArchivedOpen = selectedArchived || ( orderedGroups . length > 0 && workingGroups . length === 0 && archivedGroups . length > 0 ) ;
9282 const archivedPanelOpen = archivedOpen || autoArchivedOpen ;
9383
94- const sensors = useSensors (
95- useSensor ( PointerSensor , {
96- activationConstraint : {
97- distance : 8 ,
98- } ,
99- } ) ,
100- useSensor ( TouchSensor , {
101- activationConstraint : {
102- delay : 200 ,
103- tolerance : 5 ,
104- } ,
105- } ) ,
106- useSensor ( KeyboardSensor , {
107- coordinateGetter : sortableKeyboardCoordinates ,
108- } )
109- ) ;
110-
111- const handleDragEnd = useCallback (
112- ( section : "working" | "archived" , groups : GroupMeta [ ] ) => ( event : DragEndEvent ) => {
113- const { active, over } = event ;
114- if ( ! over || active . id === over . id ) return ;
115- const ids = groups . map ( ( g ) => String ( g . group_id || "" ) ) ;
116- const oldIndex = ids . indexOf ( String ( active . id ) ) ;
117- const newIndex = ids . indexOf ( String ( over . id ) ) ;
118- if ( oldIndex !== - 1 && newIndex !== - 1 ) {
119- onReorderSection ( section , oldIndex , newIndex ) ;
120- }
121- } ,
122- [ onReorderSection ]
123- ) ;
124-
12584 useEffect ( ( ) => {
12685 if ( ! isResizing ) return undefined ;
12786
@@ -149,6 +108,37 @@ export function GroupSidebar({
149108 } ;
150109 } , [ isResizing , onResizeWidth ] ) ;
151110
111+ useEffect ( ( ) => {
112+ if ( sortableReady ) return ;
113+ if ( typeof window === "undefined" ) return ;
114+ if ( ! window . matchMedia ( "(min-width: 768px)" ) . matches ) return ;
115+
116+ let cancelled = false ;
117+ let timeoutId : ReturnType < typeof globalThis . setTimeout > | null = null ;
118+ let idleId : number | null = null ;
119+ const loadSortableList = ( ) => {
120+ void import ( "./GroupSidebarSortableList" ) . then ( ( ) => {
121+ if ( ! cancelled ) setSortableReady ( true ) ;
122+ } ) ;
123+ } ;
124+
125+ if ( "requestIdleCallback" in window ) {
126+ idleId = window . requestIdleCallback ( ( ) => loadSortableList ( ) , { timeout : 1200 } ) ;
127+ } else {
128+ timeoutId = globalThis . setTimeout ( ( ) => loadSortableList ( ) , 400 ) ;
129+ }
130+
131+ return ( ) => {
132+ cancelled = true ;
133+ if ( idleId !== null && "cancelIdleCallback" in window ) {
134+ window . cancelIdleCallback ( idleId ) ;
135+ }
136+ if ( timeoutId !== null ) {
137+ globalThis . clearTimeout ( timeoutId ) ;
138+ }
139+ } ;
140+ } , [ sortableReady ] ) ;
141+
152142 const handleResizeStart = useCallback ( ( event : React . PointerEvent < HTMLDivElement > ) => {
153143 if ( isCollapsed ) return ;
154144 event . preventDefault ( ) ;
@@ -164,67 +154,96 @@ export function GroupSidebar({
164154
165155 const renderGroupList = useCallback (
166156 ( groups : GroupMeta [ ] , section : "working" | "archived" ) => {
167- const sortableIds = groups . map ( ( g ) => String ( g . group_id || "" ) ) ;
168157 const isArchivedSection = section === "archived" ;
169- return (
170- < DndContext
171- sensors = { sensors }
172- collisionDetection = { closestCenter }
173- onDragEnd = { handleDragEnd ( section , groups ) }
174- >
175- < SortableContext
176- items = { sortableIds }
177- strategy = { verticalListSortingStrategy }
178- >
179- < div className = { classNames (
180- isCollapsed ? "flex flex-col items-center gap-2" : "space-y-1"
181- ) } >
158+ const menuActionLabel = isArchivedSection ? t ( "restoreGroup" ) : t ( "archiveGroup" ) ;
159+ const handleMenuAction = ( gid : string ) => {
160+ if ( isArchivedSection ) {
161+ onRestoreGroup ( gid ) ;
162+ return ;
163+ }
164+ setArchivedOpen ( true ) ;
165+ onArchiveGroup ( gid ) ;
166+ } ;
167+
168+ if ( ! isCollapsed && ! readOnly && sortableReady ) {
169+ return (
170+ < Suspense fallback = {
171+ < div className = "space-y-1" >
182172 { groups . map ( ( g ) => {
183173 const gid = String ( g . group_id || "" ) ;
184- const active = gid === selectedGroupId ;
185174 return (
186- < SortableGroupItem
175+ < GroupSidebarItem
187176 key = { gid }
188177 group = { g }
189- isActive = { active }
190- isDark = { isDark }
191- isCollapsed = { isCollapsed }
178+ isActive = { gid === selectedGroupId }
179+ isCollapsed = { false }
192180 isArchived = { isArchivedSection }
193- dragDisabled = { ! ! readOnly }
194- menuActionLabel = { isArchivedSection ? t ( "restoreGroup" ) : t ( "archiveGroup" ) }
181+ menuActionLabel = { menuActionLabel }
195182 menuAriaLabel = { `${ t ( "groupActions" ) } · ${ g . title || gid } ` }
196- onMenuAction = {
197- isCollapsed
198- ? undefined
199- : isArchivedSection
200- ? ( ) => onRestoreGroup ( gid )
201- : ( ) => {
202- setArchivedOpen ( true ) ;
203- onArchiveGroup ( gid ) ;
204- }
205- }
183+ onMenuAction = { ( ) => handleMenuAction ( gid ) }
206184 onSelect = { ( ) => {
207185 onSelectGroup ( gid ) ;
208186 if ( window . matchMedia ( "(max-width: 767px)" ) . matches ) onClose ( ) ;
209187 } }
210- onWarm = { active ? undefined : ( ) => onWarmGroup ?.( gid ) }
188+ onWarm = { gid === selectedGroupId ? undefined : ( ) => onWarmGroup ?.( gid ) }
211189 />
212190 ) ;
213191 } ) }
214192 </ div >
215- </ SortableContext >
216- </ DndContext >
193+ } >
194+ < GroupSidebarSortableList
195+ groups = { groups }
196+ section = { section }
197+ selectedGroupId = { selectedGroupId }
198+ isDark = { isDark }
199+ isCollapsed = { false }
200+ readOnly = { readOnly }
201+ menuActionLabel = { menuActionLabel }
202+ menuAriaLabel = { t ( "groupActions" ) }
203+ onMenuAction = { handleMenuAction }
204+ onReorderSection = { onReorderSection }
205+ onSelectGroup = { onSelectGroup }
206+ onWarmGroup = { onWarmGroup }
207+ onClose = { onClose }
208+ />
209+ </ Suspense >
210+ ) ;
211+ }
212+
213+ return (
214+ < div className = { classNames ( isCollapsed ? "flex flex-col items-center gap-2" : "space-y-1" ) } >
215+ { groups . map ( ( g ) => {
216+ const gid = String ( g . group_id || "" ) ;
217+ return (
218+ < GroupSidebarItem
219+ key = { gid }
220+ group = { g }
221+ isActive = { gid === selectedGroupId }
222+ isCollapsed = { isCollapsed }
223+ isArchived = { isArchivedSection }
224+ menuActionLabel = { isCollapsed ? undefined : menuActionLabel }
225+ menuAriaLabel = { isCollapsed ? undefined : `${ t ( "groupActions" ) } · ${ g . title || gid } ` }
226+ onMenuAction = { isCollapsed ? undefined : ( ) => handleMenuAction ( gid ) }
227+ onSelect = { ( ) => {
228+ onSelectGroup ( gid ) ;
229+ if ( window . matchMedia ( "(max-width: 767px)" ) . matches ) onClose ( ) ;
230+ } }
231+ onWarm = { gid === selectedGroupId ? undefined : ( ) => onWarmGroup ?.( gid ) }
232+ />
233+ ) ;
234+ } ) }
235+ </ div >
217236 ) ;
218237 } ,
219- [ handleDragEnd , isCollapsed , isDark , onArchiveGroup , onClose , onRestoreGroup , onSelectGroup , onWarmGroup , readOnly , selectedGroupId , sensors , t ]
238+ [ isCollapsed , isDark , onArchiveGroup , onClose , onReorderSection , onRestoreGroup , onSelectGroup , onWarmGroup , readOnly , selectedGroupId , sortableReady , t ]
220239 ) ;
221240
222241 return (
223242 < >
224243 < aside
225244 className = { classNames (
226- "h-full flex flex-col glass-sidebar" ,
227- "fixed md:relative z-40" ,
245+ "h-full min-h-0 flex flex-col glass-sidebar" ,
246+ "fixed inset-y-0 left-0 md:relative md:inset-auto z-40" ,
228247 isResizing ? "transition-none" : "transition-[width,transform] duration-300 ease-out" ,
229248 isCollapsed ? "w-[60px]" : "w-[280px] md:w-[var(--sidebar-width)]" ,
230249 isOpen ? "translate-x-0" : "-translate-x-full" ,
@@ -335,7 +354,7 @@ export function GroupSidebar({
335354
336355 { /* Group list */ }
337356 < div className = { classNames (
338- "flex-1 overflow-auto" ,
357+ "min-h-0 flex-1 overflow-auto scrollbar-hide " ,
339358 isCollapsed ? "p-2" : "p-3"
340359 ) } >
341360 { ! isCollapsed && (
0 commit comments