diff --git a/bun.lock b/bun.lock index fc2f7c2f..0b9d6554 100644 --- a/bun.lock +++ b/bun.lock @@ -79,6 +79,9 @@ "name": "@pascal-app/editor", "version": "0.6.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@iconify/react": "^6.0.2", "@number-flow/react": "^0.5.14", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -278,6 +281,14 @@ "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], diff --git a/packages/editor/package.json b/packages/editor/package.json index dbe2e5d0..24ee4e8b 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -21,6 +21,9 @@ "three": "^0.184" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@iconify/react": "^6.0.2", "@number-flow/react": "^0.5.14", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/packages/editor/src/components/ui/floating-level-selector.tsx b/packages/editor/src/components/ui/floating-level-selector.tsx index a11ff7be..c348a303 100755 --- a/packages/editor/src/components/ui/floating-level-selector.tsx +++ b/packages/editor/src/components/ui/floating-level-selector.tsx @@ -1,5 +1,23 @@ 'use client' +import { + closestCenter, + DndContext, + type DragEndEvent, + type DragStartEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { type AnyNode, type AnyNodeId, @@ -8,8 +26,15 @@ import { useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { Copy, MoreVertical, Plus, Trash2 } from 'lucide-react' -import { useCallback, useEffect, useRef, useState } from 'react' +import { Copy, GripVertical, MoreVertical, Plus, Trash2 } from 'lucide-react' +import { + type ButtonHTMLAttributes, + type CSSProperties, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import { useShallow } from 'zustand/react/shallow' import { buildLevelDuplicateCreateOps, @@ -96,12 +121,18 @@ function LevelInlineRename({ function LevelRow({ level, isSelected, + isDragging, + dragHandleProps, + dragHandleRef, onSelect, onDuplicate, onRequestDelete, }: { level: LevelNode isSelected: boolean + isDragging?: boolean + dragHandleProps?: ButtonHTMLAttributes + dragHandleRef?: (element: HTMLButtonElement | null) => void onSelect: () => void onDuplicate: (preset?: LevelDuplicatePreset) => void onRequestDelete: () => void @@ -121,13 +152,32 @@ function LevelRow({
+ + + {!draggingLevelId && ( + + )} {/* Floating + at bottom edge */} - + {!draggingLevelId && ( + + )} {/* Level list */} -
- {reversedLevels.map((level, i) => { - const isSelected = level.id === levelId - const sortedIndex = levels.indexOf(level) - const showGapBelow = i < reversedLevels.length - 1 + + +
+ {reversedLevels.map((level, i) => { + const isSelected = level.id === levelId + const sortedIndex = levels.indexOf(level) + const showGapBelow = i < reversedLevels.length - 1 - return ( -
- handleDuplicateLevel(level, preset)} - onRequestDelete={() => setDeletingLevel(level)} - onSelect={() => - setSelection( - resolvedBuildingId - ? { buildingId: resolvedBuildingId, levelId: level.id } - : { levelId: level.id }, - ) - } - /> + return ( +
+ handleDuplicateLevel(level, preset)} + onRequestDelete={() => setDeletingLevel(level)} + onSelect={() => + setSelection( + resolvedBuildingId + ? { buildingId: resolvedBuildingId, levelId: level.id } + : { levelId: level.id }, + ) + } + /> - {showGapBelow && ( - - )} -
- ) - })} -
+ {showGapBelow && !draggingLevelId && ( + + )} +
+ ) + })} +
+ +