feat: add cross-parent drag-and-drop with validation feedback

Enable moving nodes between different parents in the tree editor.
Drop targets show blue indicator for valid drops and red pulsing
glow for invalid drops (e.g., dropping onto solution nodes or
onto descendants of the dragged node).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #48.
This commit is contained in:
chihlasm
2026-02-09 00:13:48 -05:00
parent b265269024
commit 1381aaae99
2 changed files with 94 additions and 26 deletions

View File

@@ -17,8 +17,8 @@ const DRAFT_STORAGE_KEY = 'tree-editor-draft'
// Helper to generate unique IDs
const generateId = () => crypto.randomUUID()
// Helper to find a node in the tree structure
const findNodeInTree = (
// Helper to find a node in the tree structure (exported for drag validation)
export const findNodeInTree = (
nodeId: string,
structure: TreeStructure | null
): TreeStructure | null => {
@@ -144,6 +144,7 @@ interface TreeEditorState {
// Actions - Node ordering
reorderNodes: (parentId: string, fromIndex: number, toIndex: number) => void
moveNode: (nodeId: string, targetParentId: string, targetIndex: number) => void
reorderOptions: (nodeId: string, fromIndex: number, toIndex: number) => void
// Actions - Selection
@@ -496,6 +497,37 @@ export const useTreeEditorStore = create<TreeEditorState>()(
get().autoSaveDraft()
},
moveNode: (nodeId: string, targetParentId: string, targetIndex: number) => {
set((state) => {
// Find and remove from current parent
const currentParent = findParentNode(nodeId, state.treeStructure)
if (!currentParent?.children) return
const sourceIndex = currentParent.children.findIndex(c => c.id === nodeId)
if (sourceIndex === -1) return
const [movedNode] = currentParent.children.splice(sourceIndex, 1)
// Find target parent and insert
const targetParent = findNodeInTree(targetParentId, state.treeStructure)
if (!targetParent) return
if (!targetParent.children) {
targetParent.children = []
}
// Adjust index if moving within same parent and source was before target
let adjustedIndex = targetIndex
if (currentParent.id === targetParent.id && sourceIndex < targetIndex) {
adjustedIndex = targetIndex - 1
}
targetParent.children.splice(adjustedIndex, 0, movedNode)
state.isDirty = true
})
get().autoSaveDraft()
},
reorderOptions: (nodeId: string, fromIndex: number, toIndex: number) => {
set((state) => {
const node = findNodeInTree(nodeId, state.treeStructure)