1- import { useState , useEffect , useRef } from "react" ;
1+ import { useState , useEffect , useRef , useCallback } from "react" ;
22
33interface TitleBarProps {
44 onArchiveClick : ( ) => void ;
@@ -107,10 +107,10 @@ export function TitleBar({ onArchiveClick, onStatsClick, onRecurringClick, theme
107107 }
108108 } ;
109109
110- const saveOrder = ( order : MenuId [ ] ) => {
110+ const saveOrder = useCallback ( ( order : MenuId [ ] ) => {
111111 setMenuOrder ( order ) ;
112112 localStorage . setItem ( "kanban-menu-order" , JSON . stringify ( order ) ) ;
113- } ;
113+ } , [ ] ) ;
114114
115115 const handleRestoreClick = ( ) => fileInputRef . current ?. click ( ) ;
116116
@@ -126,6 +126,56 @@ export function TitleBar({ onArchiveClick, onStatsClick, onRecurringClick, theme
126126 e . target . value = "" ;
127127 } ;
128128
129+ // Pointer-based menu drag (HTML5 drag conflicts with DndContext)
130+ const menuContainerRef = useRef < HTMLDivElement > ( null ) ;
131+ const dragStartX = useRef ( 0 ) ;
132+ const dragActive = useRef ( false ) ;
133+ const dragIdxRef = useRef < number | null > ( null ) ;
134+ const menuOrderRef = useRef ( menuOrder ) ;
135+ menuOrderRef . current = menuOrder ;
136+
137+ const handlePointerDown = useCallback ( ( idx : number , e : React . PointerEvent ) => {
138+ if ( ! editing ) return ;
139+ e . preventDefault ( ) ;
140+ setDragIdx ( idx ) ;
141+ dragIdxRef . current = idx ;
142+ dragStartX . current = e . clientX ;
143+ dragActive . current = false ;
144+ ( e . currentTarget as HTMLElement ) . setPointerCapture ( e . pointerId ) ;
145+ } , [ editing ] ) ;
146+
147+ const handlePointerMove = useCallback ( ( e : React . PointerEvent ) => {
148+ if ( dragIdxRef . current === null || ! editing ) return ;
149+ e . preventDefault ( ) ;
150+ if ( ! dragActive . current && Math . abs ( e . clientX - dragStartX . current ) > 4 ) {
151+ dragActive . current = true ;
152+ }
153+ if ( ! dragActive . current ) return ;
154+
155+ const container = menuContainerRef . current ;
156+ if ( ! container ) return ;
157+ const children = Array . from ( container . children ) . filter ( c => c . hasAttribute ( 'data-menu-idx' ) ) ;
158+ const currentIdx = dragIdxRef . current ;
159+ for ( let i = 0 ; i < children . length ; i ++ ) {
160+ const rect = children [ i ] . getBoundingClientRect ( ) ;
161+ if ( e . clientX >= rect . left && e . clientX <= rect . right && i !== currentIdx ) {
162+ const newOrder = [ ...menuOrderRef . current ] ;
163+ const [ moved ] = newOrder . splice ( currentIdx , 1 ) ;
164+ newOrder . splice ( i , 0 , moved ) ;
165+ saveOrder ( newOrder ) ;
166+ setDragIdx ( i ) ;
167+ dragIdxRef . current = i ;
168+ break ;
169+ }
170+ }
171+ } , [ editing , saveOrder ] ) ;
172+
173+ const handlePointerUp = useCallback ( ( ) => {
174+ setDragIdx ( null ) ;
175+ dragIdxRef . current = null ;
176+ dragActive . current = false ;
177+ } , [ ] ) ;
178+
129179 const btnClass = "text-xs text-slate-400 hover:text-slate-200 bg-white/[0.06] hover:bg-white/[0.1] border border-white/[0.1] rounded-md px-3 py-1.5 transition-colors" ;
130180
131181 const menuLabels : Record < MenuId , string > = {
@@ -164,30 +214,15 @@ export function TitleBar({ onArchiveClick, onStatsClick, onRecurringClick, theme
164214 </ div >
165215 ) }
166216 </ div >
167- < div className = "flex items-center gap-2" >
217+ < div ref = { menuContainerRef } className = "flex items-center gap-2" >
168218 { menuOrder . map ( ( id , idx ) => (
169219 < div
170220 key = { id }
171- draggable = { editing }
172- onDragStart = { ( e ) => {
173- if ( ! editing ) return ;
174- setDragIdx ( idx ) ;
175- e . dataTransfer . effectAllowed = "move" ;
176- } }
177- onDragOver = { ( e ) => {
178- if ( ! editing || dragIdx === null ) return ;
179- e . preventDefault ( ) ;
180- } }
181- onDrop = { ( ) => {
182- if ( ! editing || dragIdx === null || dragIdx === idx ) { setDragIdx ( null ) ; return ; }
183- const newOrder = [ ...menuOrder ] ;
184- const [ moved ] = newOrder . splice ( dragIdx , 1 ) ;
185- newOrder . splice ( idx , 0 , moved ) ;
186- saveOrder ( newOrder ) ;
187- setDragIdx ( null ) ;
188- } }
189- onDragEnd = { ( ) => setDragIdx ( null ) }
190- className = { `${ editing ? "cursor-grab active:cursor-grabbing" : "" } ${ dragIdx === idx ? "opacity-30" : "" } ` }
221+ data-menu-idx = { idx }
222+ onPointerDown = { ( e ) => handlePointerDown ( idx , e ) }
223+ onPointerMove = { handlePointerMove }
224+ onPointerUp = { handlePointerUp }
225+ className = { `${ editing ? "cursor-grab active:cursor-grabbing touch-none" : "" } ${ dragIdx === idx ? "opacity-30" : "" } ` }
191226 >
192227 < span
193228 onClick = { editing ? undefined : menuActions [ id ] }
0 commit comments