Dev #4

Merged
sandrodach merged 12 commits from dev into main 2026-10-25 21:09:45 +01:00
3 changed files with 128 additions and 124 deletions
Showing only changes of commit 09725611b5 - Show all commits

No files matched your search

+13 -19
View File
@@ -9,7 +9,6 @@ import {
Share, Share,
X, X,
Hash, Hash,
GripVertical,
} from 'lucide-react'; } from 'lucide-react';
import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types'; import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types';
import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api'; import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api';
@@ -968,11 +967,18 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
return ( return (
<div <div
key={item.id} key={item.id}
draggable
onDragStart={(e) => handleItemDragStart(e, item)}
onDragEnd={() => {
setDraggingItemId(null);
setDragOverItemTarget(null);
setDragOverCategoryTarget(null);
}}
onDragOver={(e) => handleItemDragOver(e, item.id)} onDragOver={(e) => handleItemDragOver(e, item.id)}
onDragLeave={(e) => handleItemDragLeave(e, item.id)} onDragLeave={(e) => handleItemDragLeave(e, item.id)}
onDrop={(e) => handleDropOnItem(e, item, group.category)} onDrop={(e) => handleDropOnItem(e, item, group.category)}
onClick={() => handleToggleItem(item)} 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' : '' isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''
}`} }`}
> >
@@ -984,26 +990,12 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" /> <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" />
)} )}
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2.5 min-w-0 flex-1">
{/* Grip handle for dragging */}
<div
draggable
onDragStart={(e) => 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"
>
<GripVertical className="w-3.5 h-3.5" />
</div>
{/* Eckiges Abhaken */} {/* Eckiges Abhaken */}
<button <button
type="button" type="button"
draggable={false}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleToggleItem(item); handleToggleItem(item);
@@ -1037,6 +1029,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<button <button
type="button" type="button"
draggable={false}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteItem(item.id); handleDeleteItem(item.id);
+44 -90
View File
@@ -25,7 +25,6 @@ import {
Settings, Settings,
ChevronDown, ChevronDown,
Folder, Folder,
GripVertical,
} from 'lucide-react'; } from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types'; import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api'; import { api, triggerHaptic } from '../../lib/api';
@@ -69,7 +68,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
// Drag and Drop state // Drag and Drop state
const [draggingTaskId, setDraggingTaskId] = useState<string | null>(null); const [draggingTaskId, setDraggingTaskId] = useState<string | null>(null);
const [dragOverTaskTarget, setDragOverTaskTarget] = useState<{ taskId: string; position: 'before' | 'after' } | null>(null); const [dragOverTaskTarget, setDragOverTaskTarget] = useState<{ taskId: string; position: 'before' | 'after' } | null>(null);
const [isDragOverTopZone, setIsDragOverTopZone] = useState<boolean>(false);
const [dragOverKanbanCol, setDragOverKanbanCol] = useState<string | null>(null); const [dragOverKanbanCol, setDragOverKanbanCol] = useState<string | null>(null);
// Quick Task Add state // Quick Task Add state
@@ -110,7 +108,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
// Hashtag filtering, grouping and sorting state // Hashtag filtering, grouping and sorting state
const [selectedTag, setSelectedTag] = useState<string | null>(null); const [selectedTag, setSelectedTag] = useState<string | null>(null);
const [groupBy, setGroupBy] = useState<'none' | 'tag' | 'status' | 'priority' | 'due'>('none'); const [groupBy, setGroupBy] = useState<'none' | 'tag' | 'status' | 'priority' | 'due'>('none');
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'alpha'>('created'); const [sortBy, setSortBy] = useState<'custom' | 'created' | 'due' | 'priority' | 'alpha'>('custom');
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
@@ -269,6 +267,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
if (selectedTag) { if (selectedTag) {
result = result.filter((t) => Array.isArray(t.tags) && t.tags.includes(selectedTag)); result = result.filter((t) => Array.isArray(t.tags) && t.tags.includes(selectedTag));
} }
if (sortBy === 'custom') {
return [...result].sort((a, b) => {
const orderA = a.sortOrder ?? 0;
const orderB = b.sortOrder ?? 0;
if (orderA !== orderB) return orderA - orderB;
return b.createdAt.localeCompare(a.createdAt);
});
}
return [...result].sort((a, b) => { return [...result].sort((a, b) => {
if (sortBy === 'due') { if (sortBy === 'due') {
if (!a.dueDate && !b.dueDate) return 0; if (!a.dueDate && !b.dueDate) return 0;
@@ -589,7 +595,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
const position = dragOverTaskTarget?.position || 'after'; const position = dragOverTaskTarget?.position || 'after';
setDraggingTaskId(null); setDraggingTaskId(null);
setDragOverTaskTarget(null); setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null); setDragOverKanbanCol(null);
if (!sourceId || sourceId === targetTask.id) return; if (!sourceId || sourceId === targetTask.id) return;
@@ -612,55 +617,20 @@ export const TasksView: React.FC<TasksViewProps> = ({
const newTasks = [...remainingTasks]; const newTasks = [...remainingTasks];
newTasks.splice(insertIdx, 0, updatedSourceTask); newTasks.splice(insertIdx, 0, updatedSourceTask);
const reorderPayload = newTasks.map((t, idx) => ({ const updatedWithSort = newTasks.map((t, idx) => ({
id: t.id, ...t,
sortOrder: idx, sortOrder: idx,
kanbanStatus: t.kanbanStatus,
})); }));
setTasks(newTasks); setTasks(updatedWithSort);
setSortBy('custom');
try { const reorderPayload = updatedWithSort.map((t) => ({
await api.reorderTasks(
reorderPayload,
selectedListId !== 'all' ? selectedListId : undefined
);
} catch {
loadData();
}
};
const handleDropAtTop = async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
const sourceId =
e.dataTransfer.getData('application/mindsift-task-id') ||
e.dataTransfer.getData('text/plain') ||
draggingTaskId;
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null);
if (!sourceId) return;
triggerHaptic(15);
const sourceTask = tasks.find((t) => t.id === sourceId);
if (!sourceTask) return;
const remainingTasks = tasks.filter((t) => t.id !== sourceId);
const newTasks = [sourceTask, ...remainingTasks];
const reorderPayload = newTasks.map((t, idx) => ({
id: t.id, id: t.id,
sortOrder: idx, sortOrder: t.sortOrder ?? 0,
kanbanStatus: t.kanbanStatus, kanbanStatus: t.kanbanStatus,
})); }));
setTasks(newTasks);
try { try {
await api.reorderTasks( await api.reorderTasks(
reorderPayload, reorderPayload,
@@ -682,7 +652,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
setDraggingTaskId(null); setDraggingTaskId(null);
setDragOverTaskTarget(null); setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null); setDragOverKanbanCol(null);
if (!sourceId) return; if (!sourceId) return;
@@ -701,13 +670,19 @@ export const TasksView: React.FC<TasksViewProps> = ({
const remainingTasks = tasks.filter((t) => t.id !== sourceId); const remainingTasks = tasks.filter((t) => t.id !== sourceId);
const newTasks = [updatedSourceTask, ...remainingTasks]; const newTasks = [updatedSourceTask, ...remainingTasks];
const reorderPayload = newTasks.map((t, idx) => ({ const updatedWithSort = newTasks.map((t, idx) => ({
id: t.id, ...t,
sortOrder: idx, sortOrder: idx,
kanbanStatus: t.kanbanStatus,
})); }));
setTasks(newTasks); setTasks(updatedWithSort);
setSortBy('custom');
const reorderPayload = updatedWithSort.map((t) => ({
id: t.id,
sortOrder: t.sortOrder ?? 0,
kanbanStatus: t.kanbanStatus,
}));
try { try {
await api.reorderTasks( await api.reorderTasks(
@@ -893,10 +868,21 @@ export const TasksView: React.FC<TasksViewProps> = ({
return ( return (
<div <div
key={task.id} key={task.id}
draggable
onDragStart={(e) => handleTaskDragStart(e, task)}
onDragEnd={() => {
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setDragOverKanbanCol(null);
}}
onDragOver={(e) => handleTaskDragOver(e, task.id)} onDragOver={(e) => handleTaskDragOver(e, task.id)}
onDragLeave={(e) => handleTaskDragLeave(e, task.id)} onDragLeave={(e) => handleTaskDragLeave(e, task.id)}
onDrop={(e) => handleDropOnTask(e, task)} onDrop={(e) => handleDropOnTask(e, task)}
className={`px-3 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 relative ${ onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
}}
className={`px-3 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 relative cursor-pointer cursor-grab active:cursor-grabbing select-none ${
isCompleted ? 'opacity-65' : '' isCompleted ? 'opacity-65' : ''
} ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`} } ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`}
> >
@@ -909,26 +895,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
)} )}
<div className="flex items-center gap-2.5 min-w-0 flex-1"> <div className="flex items-center gap-2.5 min-w-0 flex-1">
{/* Grip handle for dragging */}
<div
draggable
onDragStart={(e) => handleTaskDragStart(e, task)}
onDragEnd={() => {
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(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="Aufgabe ziehen zum Sortieren"
>
<GripVertical className="w-3.5 h-3.5" />
</div>
{/* Checkbox */} {/* Checkbox */}
<button <button
type="button" type="button"
draggable={false}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleToggleDone(task); handleToggleDone(task);
@@ -1050,9 +1021,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
{/* Action Controls */} {/* Action Controls */}
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
<button <button
type="button" type="button"
draggable={false}
onClick={() => { onClick={() => {
setDetailTaskId(task.id); setDetailTaskId(task.id);
setShowDetailModal(true); setShowDetailModal(true);
@@ -1064,6 +1036,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button> </button>
<button <button
type="button" type="button"
draggable={false}
onClick={() => handleConvertToDoc(task)} onClick={() => handleConvertToDoc(task)}
title="Als Notiz vertiefen" title="Als Notiz vertiefen"
className="text-faint hover:text-accent p-1.5 transition-colors hidden sm:block" className="text-faint hover:text-accent p-1.5 transition-colors hidden sm:block"
@@ -1072,6 +1045,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button> </button>
<button <button
type="button" type="button"
draggable={false}
onClick={() => handleDeletePermanent(task.id)} onClick={() => handleDeletePermanent(task.id)}
title="Aufgabe löschen" title="Aufgabe löschen"
className="text-faint hover:text-status-err p-1.5 transition-colors" className="text-faint hover:text-status-err p-1.5 transition-colors"
@@ -1347,6 +1321,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
onChange={(e) => setSortBy(e.target.value as any)} onChange={(e) => setSortBy(e.target.value as any)}
className="h-7 px-2 input-field text-xs bg-panel" className="h-7 px-2 input-field text-xs bg-panel"
> >
<option value="custom">Manuell (Drag & Drop)</option>
<option value="created">Erstelldatum</option> <option value="created">Erstelldatum</option>
<option value="due">Fälligkeitsdatum</option> <option value="due">Fälligkeitsdatum</option>
<option value="priority">Priorität</option> <option value="priority">Priorität</option>
@@ -1601,7 +1576,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
onDragEnd={() => { onDragEnd={() => {
setDraggingTaskId(null); setDraggingTaskId(null);
setDragOverTaskTarget(null); setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null); setDragOverKanbanCol(null);
}} }}
onDragOver={(e) => handleTaskDragOver(e, task.id)} onDragOver={(e) => handleTaskDragOver(e, task.id)}
@@ -1835,26 +1809,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{/* Dedicated Top Drop Zone when dragging a task */}
{draggingTaskId && (
<div
onDragOver={(e) => {
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'
}`}
>
<ArrowRight className="w-4 h-4 -rotate-90 text-accent animate-bounce" />
<span>Ganz oben in der Liste ablegen</span>
</div>
)}
{groupedTasks ? ( {groupedTasks ? (
/* GROUPED LIST */ /* GROUPED LIST */
groupedTasks.map((grp) => ( groupedTasks.map((grp) => (
@@ -171,7 +171,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
// Drag and Drop reordering state for blocks // Drag and Drop reordering state for blocks
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null); const [draggingBlockId, setDraggingBlockId] = useState<string | null>(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 --- // --- GLOBAL TO-DO ROLLUP STATE ---
const [todoFilter, setTodoFilter] = useState<'all' | 'open' | 'done'>('open'); const [todoFilter, setTodoFilter] = useState<'all' | 'open' | 'done'>('open');
@@ -1952,7 +1952,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (!draggingBlockId || draggingBlockId === targetBlockId) return; if (!draggingBlockId || draggingBlockId === targetBlockId) return;
const rect = e.currentTarget.getBoundingClientRect(); 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 ( if (
!dragOverBlockTarget || !dragOverBlockTarget ||
@@ -1970,7 +1980,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} }
}; };
const handleBlockDrop = (e: React.DragEvent, targetBlockId: string) => { const handleBlockDrop = (e: React.DragEvent, targetBlockId: string, explicitPosition?: 'before' | 'after' | 'inside') => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -1979,7 +1989,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
e.dataTransfer.getData('text/plain') || e.dataTransfer.getData('text/plain') ||
draggingBlockId; draggingBlockId;
const position = dragOverBlockTarget?.position || 'after'; const position = explicitPosition || dragOverBlockTarget?.position || 'after';
setDraggingBlockId(null); setDraggingBlockId(null);
setDragOverBlockTarget(null); setDragOverBlockTarget(null);
@@ -2005,6 +2015,25 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (newTargetIdx === -1) return; if (newTargetIdx === -1) return;
const targetBlock = blocks[newTargetIdx]; const targetBlock = blocks[newTargetIdx];
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 targetLevel = targetBlock.level || 0;
const oldRootLevel = subtree[0].level || 0; const oldRootLevel = subtree[0].level || 0;
const levelDiff = targetLevel - oldRootLevel; const levelDiff = targetLevel - oldRootLevel;
@@ -2022,6 +2051,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} }
blocks.splice(insertIdx, 0, ...subtree); blocks.splice(insertIdx, 0, ...subtree);
}
persistDocument({ ...selectedDoc, blocks }, true); persistDocument({ ...selectedDoc, blocks }, true);
}; };
@@ -3505,9 +3536,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onDragOver={(e) => !isReadOnly && handleBlockDragOver(e, block.id)} onDragOver={(e) => !isReadOnly && handleBlockDragOver(e, block.id)}
onDragLeave={(e) => !isReadOnly && handleBlockDragLeave(e, block.id)} onDragLeave={(e) => !isReadOnly && handleBlockDragLeave(e, block.id)}
onDrop={(e) => !isReadOnly && handleBlockDrop(e, block.id)} onDrop={(e) => !isReadOnly && handleBlockDrop(e, block.id)}
style={{ paddingLeft: `${blockLevel * 22}px` }} style={{ paddingLeft: `${blockLevel * 20}px` }}
className={`group flex items-start gap-1 py-0.5 relative transition-all ${ className={`group flex items-start gap-0.5 py-0.5 relative transition-all rounded ${
isDraggingThis ? 'opacity-30 border border-dashed border-accent/40 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 */} {/* Drop indicator lines */}
@@ -3517,8 +3552,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{isTargetThis && dragOverBlockTarget.position === 'after' && ( {isTargetThis && dragOverBlockTarget.position === 'after' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" /> <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent z-20 pointer-events-none rounded-full shadow-[0_0_8px_rgba(var(--accent-rgb),0.6)]" />
)} )}
{isTargetThis && dragOverBlockTarget.position === 'inside' && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-accent rounded-l z-20 pointer-events-none" />
)}
{/* Drag Handle on hover */} {/* Drag Handle on hover - tightened spacing to the bullet */}
<div <div
draggable={!isReadOnly} draggable={!isReadOnly}
onDragStart={(e) => !isReadOnly && handleBlockDragStart(e, block.id)} onDragStart={(e) => !isReadOnly && handleBlockDragStart(e, block.id)}
@@ -3526,7 +3564,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setDraggingBlockId(null); setDraggingBlockId(null);
setDragOverBlockTarget(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" title="Ziehen zum Verschieben"
> >
<GripVertical className="w-3 h-3" /> <GripVertical className="w-3 h-3" />
@@ -3537,20 +3575,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button <button
type="button" type="button"
onClick={() => toggleCollapse(block.id)} onClick={() => toggleCollapse(block.id)}
className="w-4 h-4 mt-0.5 flex items-center justify-center text-muted hover:text-main flex-shrink-0" className="w-3.5 h-3.5 mt-0.5 flex items-center justify-center text-muted hover:text-main flex-shrink-0"
title={block.collapsed ? 'Unterpunkte aufklappen' : 'Unterpunkte einklappen'} title={block.collapsed ? 'Unterpunkte aufklappen' : 'Unterpunkte einklappen'}
> >
{block.collapsed ? ( {block.collapsed ? (
<ChevronRight className="w-3 h-3 text-accent" /> <ChevronRight className="w-2.5 h-2.5 text-accent" />
) : ( ) : (
<ChevronDown className="w-3 h-3" /> <ChevronDown className="w-2.5 h-2.5" />
)} )}
</button> </button>
) : ( ) : (
<div className="w-4 flex-shrink-0" /> <div className="w-1.5 flex-shrink-0" />
)} )}
{/* Interactive Bullet Dot (Click: Zoom-in! Drag: Reorder!) */} {/* Interactive Bullet Dot (Click: Zoom-in! Drag: Reorder! Drop on it: Nest as child!) */}
<div <div
draggable={!isReadOnly} draggable={!isReadOnly}
onDragStart={(e) => !isReadOnly && handleBlockDragStart(e, block.id)} onDragStart={(e) => !isReadOnly && handleBlockDragStart(e, block.id)}
@@ -3558,13 +3596,31 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setDraggingBlockId(null); setDraggingBlockId(null);
setDragOverBlockTarget(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)} onClick={() => handleZoomTo(block.id)}
className={`w-4 h-4 mt-1 rounded-pill flex items-center justify-center transition-colors flex-shrink-0 ${ className={`w-4 h-4 mt-1 rounded-pill flex items-center justify-center transition-colors flex-shrink-0 ${
isReadOnly isReadOnly
? 'cursor-pointer text-muted' ? 'cursor-pointer text-muted'
: 'hover:bg-accent/20 cursor-grab active:cursor-grabbing text-muted hover:text-accent' : '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"
> >
<span <span
className={`w-1.5 h-1.5 rounded-pill transition-colors ${ className={`w-1.5 h-1.5 rounded-pill transition-colors ${