diff --git a/apps/todo/apps/web/src/lib/components/TaskItem.svelte b/apps/todo/apps/web/src/lib/components/TaskItem.svelte
index 18c692db7..8a6f44cd1 100644
--- a/apps/todo/apps/web/src/lib/components/TaskItem.svelte
+++ b/apps/todo/apps/web/src/lib/components/TaskItem.svelte
@@ -515,11 +515,6 @@
{dueDateText()}
{/if}
-
-
- {#if projectColor()}
-
(null);
- // Create a stable key from task IDs and updatedAt to detect real changes
- let lastTaskKey = '';
+ // After a drop, ignore external syncs until the timeout clears
+ let dropInProgress = false;
- // Sync items with tasks when IDs change OR when tasks are updated
+ // Sync items with tasks prop — but preserve local order during/after DnD
$effect(() => {
- // Include updatedAt in the key to detect task updates
- const currentKey = tasks
- .map((t) => `${t.id}:${t.updatedAt || ''}`)
- .sort()
- .join(',');
- if (currentKey !== lastTaskKey) {
- items = [...tasks];
- lastTaskKey = currentKey;
- }
+ // Subscribe to tasks (the reactive dependency)
+ const currentTasks = tasks;
+
+ // Read items without subscribing to avoid infinite loop
+ untrack(() => {
+ const taskIds = new Set(currentTasks.map((t) => t.id));
+ const itemIds = new Set(items.map((t) => t.id));
+
+ // Check if the actual set of IDs changed (task added or removed)
+ const idsChanged =
+ taskIds.size !== itemIds.size ||
+ currentTasks.some((t) => !itemIds.has(t.id)) ||
+ items.some((t) => !taskIds.has(t.id));
+
+ if (idsChanged) {
+ // Real structural change — full resync
+ items = [...currentTasks];
+ dropInProgress = false;
+ } else if (!dropInProgress) {
+ // Same IDs — update task data in current order (no reorder flicker)
+ const taskMap = new Map(currentTasks.map((t) => [t.id, t]));
+ items = items.map((item) => taskMap.get(item.id) || item);
+ }
+ });
});
const flipDurationMs = 200;
@@ -207,12 +222,12 @@
tasksStore.reorderTasks(taskIds);
}
- // Update local state and sync lastTaskKey to prevent $effect from reverting
+ // Update local state and block sync from reverting order
items = newItems;
- lastTaskKey = newItems
- .map((t) => `${t.id}:${t.updatedAt || ''}`)
- .sort()
- .join(',');
+ dropInProgress = true;
+ setTimeout(() => {
+ dropInProgress = false;
+ }, 1000);
}
async function handleToggleComplete(task: Task) {
@@ -242,21 +257,25 @@
onconsider={handleDndConsider}
onfinalize={handleDndFinalize}
>
- {#each items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID) as task (task.id)}
-
- handleContextMenu(e, task)}>
- handleToggleComplete(task)}
- onDelete={() => handleDelete(task.id)}
- onExpand={() => handleExpandTask(task.id)}
- onCollapse={handleCollapseTask}
- onSave={(data) => handleSaveTask(task.id, data)}
- />
-
+ {#each items as task (task.id)}
+ {#if task.id === SHADOW_PLACEHOLDER_ITEM_ID}
+
+ {:else}
+
+ handleContextMenu(e, task)}>
+ handleToggleComplete(task)}
+ onDelete={() => handleDelete(task.id)}
+ onExpand={() => handleExpandTask(task.id)}
+ onCollapse={handleCollapseTask}
+ onSave={(data) => handleSaveTask(task.id, data)}
+ />
+
+ {/if}
{/each}
{#if items.length === 0}
@@ -342,6 +361,10 @@
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
+ .dnd-shadow-placeholder {
+ min-height: 3rem;
+ }
+
/* Shadow placeholder (where dragged item will land) */
:global(.task-list [data-is-dnd-shadow-item-hint]) {
background: rgba(139, 92, 246, 0.06);
diff --git a/apps/todo/apps/web/src/routes/(app)/+layout.svelte b/apps/todo/apps/web/src/routes/(app)/+layout.svelte
index 64498d6fc..ab2327713 100644
--- a/apps/todo/apps/web/src/routes/(app)/+layout.svelte
+++ b/apps/todo/apps/web/src/routes/(app)/+layout.svelte
@@ -605,7 +605,7 @@
max-width: 900px;
margin-left: auto;
margin-right: auto;
- padding: 1rem;
+ padding: 0.5rem;
}
.content-wrapper.full-width {
@@ -616,7 +616,7 @@
@media (min-width: 640px) {
.content-wrapper {
- padding: 1.5rem;
+ padding: 1rem;
}
.content-wrapper.full-width {
padding-left: 0;