Skip to content

Commit d3ce751

Browse files
committed
feat: implement drag-and-drop layer reordering
- Replaces up/down buttons with drag-and-drop for reordering layers - Enables visual rearrangement of device layers by dragging list items - Swaps `zIndex` values of devices to reflect the new display order - Adds visual styling for dragging and drop target states - Ensures the selected device remains selected after reordering
1 parent c4af8f4 commit d3ce751

1 file changed

Lines changed: 78 additions & 52 deletions

File tree

components/LayersPanel.tsx

Lines changed: 78 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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';
33
import { DeviceConfig, DeviceType } from '../types';
44
import { 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

Comments
 (0)