1- import React from 'react' ;
2- import { GripVertical , Trash2 , ChevronUp , ChevronDown } from 'lucide-react' ;
1+ import React , { useState , useRef } from 'react' ;
2+ import { GripVertical , Trash2 } from 'lucide-react' ;
33import { DeviceConfig , DeviceType } from '../types' ;
44import { cn } from '@/lib/utils' ;
55
@@ -42,25 +42,10 @@ export const LayersPanel: React.FC<LayersPanelProps> = ({
4242 onUpdateDevices,
4343 onDeleteDevice,
4444} ) => {
45- const moveLayer = ( index : number , direction : 'up' | 'down' ) => {
46- const targetIndex = direction === 'up' ? index - 1 : index + 1 ;
47- if ( targetIndex < 0 || targetIndex >= devices . length ) return ;
48-
49- const newDevices = [ ...devices ] ;
50- // Swap zIndex values between the two devices
51- const tempZ = newDevices [ index ] . zIndex ;
52- newDevices [ index ] = { ...newDevices [ index ] , zIndex : newDevices [ targetIndex ] . zIndex } ;
53- newDevices [ targetIndex ] = { ...newDevices [ targetIndex ] , zIndex : tempZ } ;
54-
55- onUpdateDevices ( newDevices ) ;
56-
57- // Keep selection following the moved device
58- if ( selectedIndex === index ) {
59- onSelectDevice ( targetIndex ) ;
60- } else if ( selectedIndex === targetIndex ) {
61- onSelectDevice ( index ) ;
62- }
63- } ;
45+ // Drag state tracks the display-order indices (sorted by zIndex desc)
46+ const [ dragFromDisplay , setDragFromDisplay ] = useState < number | null > ( null ) ;
47+ const [ dragOverDisplay , setDragOverDisplay ] = useState < number | null > ( null ) ;
48+ const dragCounterRef = useRef ( 0 ) ;
6449
6550 if ( devices . length <= 1 ) return null ;
6651
@@ -69,24 +54,91 @@ export const LayersPanel: React.FC<LayersPanelProps> = ({
6954 . map ( ( device , index ) => ( { device, index } ) )
7055 . sort ( ( a , b ) => b . device . zIndex - a . device . zIndex ) ;
7156
57+ const handleDragStart = ( displayIndex : number , e : React . DragEvent ) => {
58+ setDragFromDisplay ( displayIndex ) ;
59+ e . dataTransfer . effectAllowed = 'move' ;
60+ // Make the drag image semi-transparent
61+ if ( e . currentTarget instanceof HTMLElement ) {
62+ e . dataTransfer . setDragImage ( e . currentTarget , 0 , 0 ) ;
63+ }
64+ } ;
65+
66+ const handleDragEnd = ( ) => {
67+ setDragFromDisplay ( null ) ;
68+ setDragOverDisplay ( null ) ;
69+ dragCounterRef . current = 0 ;
70+ } ;
71+
72+ const handleDragEnter = ( displayIndex : number ) => {
73+ dragCounterRef . current ++ ;
74+ setDragOverDisplay ( displayIndex ) ;
75+ } ;
76+
77+ const handleDragLeave = ( ) => {
78+ dragCounterRef . current -- ;
79+ if ( dragCounterRef . current <= 0 ) {
80+ setDragOverDisplay ( null ) ;
81+ dragCounterRef . current = 0 ;
82+ }
83+ } ;
84+
85+ const handleDrop = ( targetDisplayIndex : number ) => {
86+ if ( dragFromDisplay === null || dragFromDisplay === targetDisplayIndex ) {
87+ handleDragEnd ( ) ;
88+ return ;
89+ }
90+
91+ const fromDeviceIndex = layerOrder [ dragFromDisplay ] . index ;
92+ const toDeviceIndex = layerOrder [ targetDisplayIndex ] . index ;
93+
94+ // Swap zIndex values
95+ const newDevices = [ ...devices ] ;
96+ const tempZ = newDevices [ fromDeviceIndex ] . zIndex ;
97+ newDevices [ fromDeviceIndex ] = { ...newDevices [ fromDeviceIndex ] , zIndex : newDevices [ toDeviceIndex ] . zIndex } ;
98+ newDevices [ toDeviceIndex ] = { ...newDevices [ toDeviceIndex ] , zIndex : tempZ } ;
99+
100+ onUpdateDevices ( newDevices ) ;
101+
102+ // Keep selection following the dragged device
103+ if ( selectedIndex === fromDeviceIndex ) {
104+ onSelectDevice ( fromDeviceIndex ) ; // index doesn't change, only zIndex
105+ }
106+
107+ handleDragEnd ( ) ;
108+ } ;
109+
72110 return (
73- < div className = "space-y-1 " >
74- { layerOrder . map ( ( { device, index } ) => {
111+ < div className = "space-y-0.5 " >
112+ { layerOrder . map ( ( { device, index } , displayIndex ) => {
75113 const isSelected = index === selectedIndex ;
114+ const isDragging = dragFromDisplay === displayIndex ;
115+ const isDragOver = dragOverDisplay === displayIndex && dragFromDisplay !== displayIndex ;
76116
77117 return (
78118 < div
79119 key = { device . id }
120+ draggable
121+ onDragStart = { ( e ) => handleDragStart ( displayIndex , e ) }
122+ onDragEnd = { handleDragEnd }
123+ onDragOver = { ( e ) => e . preventDefault ( ) }
124+ onDragEnter = { ( ) => handleDragEnter ( displayIndex ) }
125+ onDragLeave = { handleDragLeave }
126+ onDrop = { ( e ) => {
127+ e . preventDefault ( ) ;
128+ handleDrop ( displayIndex ) ;
129+ } }
80130 onClick = { ( ) => onSelectDevice ( index ) }
81131 className = { cn (
82132 'group flex items-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] transition-colors cursor-pointer' ,
83133 isSelected
84134 ? 'bg-indigo-600/20 border border-indigo-500/30'
85- : 'hover:bg-zinc-800/50 border border-transparent'
135+ : 'hover:bg-zinc-800/50 border border-transparent' ,
136+ isDragging && 'opacity-40' ,
137+ isDragOver && 'border-indigo-400/60 bg-indigo-600/10'
86138 ) }
87139 >
88- { /* Drag hint */ }
89- < GripVertical className = "w-3 h-3 text-zinc-600 shrink-0" />
140+ { /* Drag handle */ }
141+ < GripVertical className = "w-3 h-3 text-zinc-600 shrink-0 cursor-grab active:cursor-grabbing " />
90142
91143 { /* Device info */ }
92144 < div className = "flex-1 min-w-0 flex items-center gap-1.5" >
@@ -108,32 +160,6 @@ export const LayersPanel: React.FC<LayersPanelProps> = ({
108160 z{ device . zIndex }
109161 </ span >
110162
111- { /* Reorder buttons */ }
112- < div className = "flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity" >
113- < button
114- onClick = { ( e ) => {
115- e . stopPropagation ( ) ;
116- moveLayer ( index , 'up' ) ;
117- } }
118- disabled = { index <= 0 }
119- className = "p-0.5 text-zinc-600 hover:text-zinc-300 disabled:opacity-20 disabled:cursor-not-allowed"
120- title = "Move up (higher z-index)"
121- >
122- < ChevronUp className = "w-3 h-3" />
123- </ button >
124- < button
125- onClick = { ( e ) => {
126- e . stopPropagation ( ) ;
127- moveLayer ( index , 'down' ) ;
128- } }
129- disabled = { index >= devices . length - 1 }
130- className = "p-0.5 text-zinc-600 hover:text-zinc-300 disabled:opacity-20 disabled:cursor-not-allowed"
131- title = "Move down (lower z-index)"
132- >
133- < ChevronDown className = "w-3 h-3" />
134- </ button >
135- </ div >
136-
137163 { /* Delete */ }
138164 < button
139165 onClick = { ( e ) => {
0 commit comments