diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index c852ff6..adb12ea 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -9,7 +9,6 @@ import { Share, X, Hash, - GripVertical, } from 'lucide-react'; import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types'; import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api'; @@ -968,11 +967,18 @@ export const SharedListsView: React.FC = ({ onRefreshGloba return (
handleItemDragStart(e, item)} + onDragEnd={() => { + setDraggingItemId(null); + setDragOverItemTarget(null); + setDragOverCategoryTarget(null); + }} onDragOver={(e) => handleItemDragOver(e, item.id)} onDragLeave={(e) => handleItemDragLeave(e, item.id)} onDrop={(e) => handleDropOnItem(e, item, group.category)} onClick={() => handleToggleItem(item)} - className={`px-3 py-2.5 flex items-center justify-between gap-3 cursor-pointer transition-colors hover:bg-panel-hover group relative ${ + className={`px-3 py-2.5 flex items-center justify-between gap-3 cursor-pointer cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-panel-hover group relative ${ isDragging ? 'opacity-30 border border-dashed border-accent/40' : '' }`} > @@ -984,26 +990,12 @@ export const SharedListsView: React.FC = ({ onRefreshGloba
)} -
- {/* Grip handle for dragging */} -
handleItemDragStart(e, item)} - onDragEnd={() => { - setDraggingItemId(null); - setDragOverItemTarget(null); - setDragOverCategoryTarget(null); - }} - onClick={(e) => e.stopPropagation()} - className="opacity-0 group-hover:opacity-70 hover:opacity-100 text-faint hover:text-accent cursor-grab active:cursor-grabbing flex-shrink-0 -ml-1 transition-opacity" - title="Ziehen zum Sortieren oder in andere Hashtag-Kategorie schieben" - > - -
- +
{/* Eckiges Abhaken */}
{/* Action Controls */} -
+
e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
) : (
- {/* Dedicated Top Drop Zone when dragging a task */} - {draggingTaskId && ( -
{ - e.preventDefault(); - setIsDragOverTopZone(true); - }} - onDragLeave={() => setIsDragOverTopZone(false)} - onDrop={(e) => handleDropAtTop(e)} - className={`p-3 text-center text-xs font-semibold rounded-card border-2 border-dashed transition-all flex items-center justify-center gap-2 cursor-pointer ${ - isDragOverTopZone - ? 'border-accent bg-accent/20 text-accent scale-[1.01] shadow-lg' - : 'border-accent/40 text-accent bg-accent/5 hover:border-accent' - }`} - > - - Ganz oben in der Liste ablegen -
- )} - {groupedTasks ? ( /* GROUPED LIST */ groupedTasks.map((grp) => ( diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 9fcaa48..f124ec3 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -171,7 +171,7 @@ export const WorkspaceView: React.FC = ({ // Drag and Drop reordering state for blocks const [draggingBlockId, setDraggingBlockId] = useState(null); - const [dragOverBlockTarget, setDragOverBlockTarget] = useState<{ blockId: string; position: 'before' | 'after' } | null>(null); + const [dragOverBlockTarget, setDragOverBlockTarget] = useState<{ blockId: string; position: 'before' | 'after' | 'inside' } | null>(null); // --- GLOBAL TO-DO ROLLUP STATE --- const [todoFilter, setTodoFilter] = useState<'all' | 'open' | 'done'>('open'); @@ -1952,7 +1952,17 @@ export const WorkspaceView: React.FC = ({ if (!draggingBlockId || draggingBlockId === targetBlockId) return; const rect = e.currentTarget.getBoundingClientRect(); - const position = e.clientY - rect.top < rect.height / 2 ? 'before' : 'after'; + const relY = e.clientY - rect.top; + const height = rect.height; + + let position: 'before' | 'after' | 'inside'; + if (relY < height * 0.25) { + position = 'before'; + } else if (relY > height * 0.75) { + position = 'after'; + } else { + position = 'inside'; + } if ( !dragOverBlockTarget || @@ -1970,7 +1980,7 @@ export const WorkspaceView: React.FC = ({ } }; - const handleBlockDrop = (e: React.DragEvent, targetBlockId: string) => { + const handleBlockDrop = (e: React.DragEvent, targetBlockId: string, explicitPosition?: 'before' | 'after' | 'inside') => { e.preventDefault(); e.stopPropagation(); @@ -1979,7 +1989,7 @@ export const WorkspaceView: React.FC = ({ e.dataTransfer.getData('text/plain') || draggingBlockId; - const position = dragOverBlockTarget?.position || 'after'; + const position = explicitPosition || dragOverBlockTarget?.position || 'after'; setDraggingBlockId(null); setDragOverBlockTarget(null); @@ -2005,23 +2015,44 @@ export const WorkspaceView: React.FC = ({ if (newTargetIdx === -1) return; const targetBlock = blocks[newTargetIdx]; - const targetLevel = targetBlock.level || 0; - const oldRootLevel = subtree[0].level || 0; - const levelDiff = targetLevel - oldRootLevel; - // Adjust levels of all blocks in subtree to match target hierarchy - for (const b of subtree) { - b.level = Math.max(0, (b.level || 0) + levelDiff); + if (position === 'inside') { + // Nest directly under targetBlock as its sub-bullet + const targetLevel = (targetBlock.level || 0) + 1; + const oldRootLevel = subtree[0].level || 0; + const levelDiff = targetLevel - oldRootLevel; + + for (const b of subtree) { + b.level = Math.max(0, (b.level || 0) + levelDiff); + } + + // Automatically uncollapse targetBlock so the newly added sub-bullet is visible + if (targetBlock.collapsed) { + blocks[newTargetIdx] = { ...targetBlock, collapsed: false }; + } + + // Insert immediately below targetBlock + blocks.splice(newTargetIdx + 1, 0, ...subtree); + } else { + const targetLevel = targetBlock.level || 0; + const oldRootLevel = subtree[0].level || 0; + const levelDiff = targetLevel - oldRootLevel; + + // Adjust levels of all blocks in subtree to match target hierarchy + for (const b of subtree) { + b.level = Math.max(0, (b.level || 0) + levelDiff); + } + + // Calculate insertion index + let insertIdx = newTargetIdx; + if (position === 'after') { + const targetSubtreeCount = getBlockSubtreeCount(blocks, newTargetIdx); + insertIdx = newTargetIdx + targetSubtreeCount; + } + + blocks.splice(insertIdx, 0, ...subtree); } - // Calculate insertion index - let insertIdx = newTargetIdx; - if (position === 'after') { - const targetSubtreeCount = getBlockSubtreeCount(blocks, newTargetIdx); - insertIdx = newTargetIdx + targetSubtreeCount; - } - - blocks.splice(insertIdx, 0, ...subtree); persistDocument({ ...selectedDoc, blocks }, true); }; @@ -3505,9 +3536,13 @@ export const WorkspaceView: React.FC = ({ onDragOver={(e) => !isReadOnly && handleBlockDragOver(e, block.id)} onDragLeave={(e) => !isReadOnly && handleBlockDragLeave(e, block.id)} onDrop={(e) => !isReadOnly && handleBlockDrop(e, block.id)} - style={{ paddingLeft: `${blockLevel * 22}px` }} - className={`group flex items-start gap-1 py-0.5 relative transition-all ${ - isDraggingThis ? 'opacity-30 border border-dashed border-accent/40 rounded' : '' + style={{ paddingLeft: `${blockLevel * 20}px` }} + className={`group flex items-start gap-0.5 py-0.5 relative transition-all rounded ${ + isDraggingThis ? 'opacity-30 border border-dashed border-accent/40' : '' + } ${ + isTargetThis && dragOverBlockTarget.position === 'inside' + ? 'ring-2 ring-accent/60 bg-accent/10 shadow-sm' + : '' }`} > {/* Drop indicator lines */} @@ -3517,8 +3552,11 @@ export const WorkspaceView: React.FC = ({ {isTargetThis && dragOverBlockTarget.position === 'after' && (
)} + {isTargetThis && dragOverBlockTarget.position === 'inside' && ( +
+ )} - {/* Drag Handle on hover */} + {/* Drag Handle on hover - tightened spacing to the bullet */}
!isReadOnly && handleBlockDragStart(e, block.id)} @@ -3526,7 +3564,7 @@ export const WorkspaceView: React.FC = ({ setDraggingBlockId(null); setDragOverBlockTarget(null); }} - className="opacity-0 group-hover:opacity-100 flex items-center pt-1 text-faint hover:text-accent cursor-grab active:cursor-grabbing flex-shrink-0" + className="opacity-0 group-hover:opacity-100 flex items-center pt-1 text-faint hover:text-accent cursor-grab active:cursor-grabbing flex-shrink-0 -ml-1 mr-0.5" title="Ziehen zum Verschieben" > @@ -3537,20 +3575,20 @@ export const WorkspaceView: React.FC = ({ ) : ( -
+
)} - {/* Interactive Bullet Dot (Click: Zoom-in! Drag: Reorder!) */} + {/* Interactive Bullet Dot (Click: Zoom-in! Drag: Reorder! Drop on it: Nest as child!) */}
!isReadOnly && handleBlockDragStart(e, block.id)} @@ -3558,13 +3596,31 @@ export const WorkspaceView: React.FC = ({ setDraggingBlockId(null); setDragOverBlockTarget(null); }} + onDragOver={(e) => { + if (!isReadOnly && draggingBlockId && draggingBlockId !== block.id) { + e.preventDefault(); + e.stopPropagation(); + setDragOverBlockTarget({ blockId: block.id, position: 'inside' }); + } + }} + onDrop={(e) => { + if (!isReadOnly) { + e.preventDefault(); + e.stopPropagation(); + handleBlockDrop(e, block.id, 'inside'); + } + }} onClick={() => handleZoomTo(block.id)} className={`w-4 h-4 mt-1 rounded-pill flex items-center justify-center transition-colors flex-shrink-0 ${ isReadOnly ? 'cursor-pointer text-muted' : 'hover:bg-accent/20 cursor-grab active:cursor-grabbing text-muted hover:text-accent' + } ${ + isTargetThis && dragOverBlockTarget.position === 'inside' + ? 'scale-125 bg-accent/30 ring-2 ring-accent' + : '' }`} - title="Klick: In diesen Ast zoomen · Ziehen: Verschieben" + title="Klick: Zoomen · Ziehen: Verschieben · Hierher ziehen: Als Unterpunkt einordnen" >