♻️ refactor(todo): replace edit modal with inline task editing

Redesign TaskItem to expand inline for editing instead of opening
a separate modal. Improves UX by keeping user context and reducing
visual interruption. Removes modal-related code from pages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-27 01:32:14 +01:00
parent 09b8d7b384
commit cb3c1ffb93
7 changed files with 848 additions and 263 deletions

View file

@ -68,7 +68,7 @@
{#if visible}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-[9995] flex items-center justify-center bg-black/50 backdrop-blur-sm"
onclick={handleBackdropClick}
>
<div

View file

@ -385,7 +385,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
z-index: 9995;
padding: 4rem 2rem;
}
@ -409,19 +409,18 @@
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* Mobile: Full screen from bottom, above QuickAdd bar */
/* Mobile: Full screen from bottom, modal covers all UI elements */
@media (max-width: 640px) {
.modal-backdrop {
align-items: flex-end;
padding: 0;
/* QuickAdd is at bottom: 70px + ~60px height = 130px, plus PillNav */
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 140px);
}
.modal-container {
max-width: 100%;
max-height: calc(100vh - 160px); /* Account for QuickAdd + PillNav */
max-height: calc(100vh - 60px - env(safe-area-inset-top, 0px));
border-radius: 1.5rem 1.5rem 0 0;
margin-bottom: env(safe-area-inset-bottom, 0px);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { Task } from '@todo/shared';
import type { Task, UpdateTaskInput } from '@todo/shared';
import TaskItem from './TaskItem.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
@ -22,6 +22,43 @@
onTaskDrop,
}: Props = $props();
// Track which task is expanded for inline editing
let expandedTaskId = $state<string | null>(null);
function handleExpandTask(taskId: string) {
// Toggle - if same task clicked, collapse it
if (expandedTaskId === taskId) {
expandedTaskId = null;
} else {
expandedTaskId = taskId;
}
}
function handleCollapseTask() {
expandedTaskId = null;
}
async function handleSaveTask(taskId: string, data: UpdateTaskInput) {
try {
// Update task
const updateData = {
...data,
metadata: data.metadata as { [key: string]: unknown } | null | undefined,
};
await tasksStore.updateTask(taskId, updateData);
// Update labels if provided
if (data.labelIds !== undefined) {
await tasksStore.updateLabels(taskId, data.labelIds);
}
// Collapse after save
expandedTaskId = null;
} catch (error) {
console.error('Failed to save task:', error);
}
}
// Local mutable state for dnd-zone
let items = $state<Task[]>([]);
@ -111,9 +148,12 @@
{task}
{showCompleted}
animateComplete={animatingTaskId === task.id}
isExpanded={expandedTaskId === task.id}
onToggleComplete={() => handleToggleComplete(task)}
onDelete={() => handleDelete(task.id)}
onEdit={onEditTask ? () => onEditTask(task) : undefined}
onExpand={() => handleExpandTask(task.id)}
onCollapse={handleCollapseTask}
onSave={(data) => handleSaveTask(task.id, data)}
/>
{/each}
{#if items.length === 0}
@ -129,9 +169,12 @@
{task}
{showCompleted}
animateComplete={animatingTaskId === task.id}
isExpanded={expandedTaskId === task.id}
onToggleComplete={() => handleToggleComplete(task)}
onDelete={() => handleDelete(task.id)}
onEdit={onEditTask ? () => onEditTask(task) : undefined}
onExpand={() => handleExpandTask(task.id)}
onCollapse={handleCollapseTask}
onSave={(data) => handleSaveTask(task.id, data)}
/>
{/each}
</div>
@ -140,7 +183,7 @@
<style>
.task-list {
min-height: 60px;
padding: 0.25rem;
padding: 0;
border-radius: 0.5rem;
transition: background-color 0.15s ease;
}

View file

@ -523,7 +523,6 @@
.main-content {
transition: all 300ms ease;
position: relative;
z-index: 0;
/* Space for QuickInputBar at bottom */
padding-bottom: calc(80px + env(safe-area-inset-bottom));
}
@ -554,8 +553,6 @@
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
.content-wrapper.full-width {

View file

@ -2,17 +2,15 @@
import { onMount } from 'svelte';
import { format, addDays, subDays, startOfDay } from 'date-fns';
import { de } from 'date-fns/locale';
import { ListChecks, Sparkle, ArrowDown } from '@manacore/shared-icons';
import { Sparkle, ArrowDown } from '@manacore/shared-icons';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { viewStore } from '$lib/stores/view.svelte';
import TaskList from '$lib/components/TaskList.svelte';
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte';
import TaskEditModal from '$lib/components/TaskEditModal.svelte';
import { TaskListSkeleton } from '$lib/components/skeletons';
import type { Task, UpdateTaskInput } from '@todo/shared';
import type { Task } from '@todo/shared';
let isLoading = $state(true);
let editingTask = $state<Task | null>(null);
onMount(async () => {
viewStore.setToday();
@ -104,46 +102,6 @@
window.dispatchEvent(new CustomEvent('quick-input-set', { detail: { text } }));
}
// Modal handlers
function openEditModal(task: Task) {
editingTask = task;
}
function closeEditModal() {
editingTask = null;
}
async function handleSaveTask(data: UpdateTaskInput) {
if (!editingTask) return;
try {
// Update task - cast metadata to be compatible with store type
const updateData = {
...data,
metadata: data.metadata as { [key: string]: unknown } | null | undefined,
};
await tasksStore.updateTask(editingTask.id, updateData);
// Update labels if provided
if (data.labelIds !== undefined) {
await tasksStore.updateLabels(editingTask.id, data.labelIds);
}
closeEditModal();
} catch (error) {
console.error('Failed to save task:', error);
}
}
async function handleDeleteTask(taskId: string) {
try {
await tasksStore.deleteTask(taskId);
closeEditModal();
} catch (error) {
console.error('Failed to delete task:', error);
}
}
// Drag and drop handler - uses optimistic updates for smooth UX
function handleTaskDrop(taskId: string, targetDate: Date | 'completed' | 'overdue') {
const task = tasksStore.tasks.find((t) => t.id === taskId);
@ -236,7 +194,6 @@
enableDragDrop
dropTargetDate="overdue"
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</CollapsibleSection>
{/if}
@ -255,7 +212,6 @@
enableDragDrop
dropTargetDate={startOfDay(new Date())}
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</CollapsibleSection>
{/if}
@ -274,7 +230,6 @@
enableDragDrop
dropTargetDate={tomorrowDate}
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</CollapsibleSection>
{/if}
@ -299,7 +254,6 @@
enableDragDrop
dropTargetDate={group.date}
onTaskDrop={handleTaskDrop}
onEditTask={openEditModal}
/>
</div>
{/each}
@ -322,7 +276,6 @@
dropTargetDate="completed"
onTaskDrop={handleTaskDrop}
showCompleted
onEditTask={openEditModal}
/>
</CollapsibleSection>
{/if}
@ -340,17 +293,6 @@
{/if}
</div>
<!-- Task Edit Modal -->
{#if editingTask}
<TaskEditModal
task={editingTask}
open={true}
onClose={closeEditModal}
onSave={handleSaveTask}
onDelete={handleDeleteTask}
/>
{/if}
<style>
.unified-view {
padding-bottom: 100px;

View file

@ -7,13 +7,9 @@
import { tasksStore } from '$lib/stores/tasks.svelte';
import { labelsStore } from '$lib/stores/labels.svelte';
import TaskList from '$lib/components/TaskList.svelte';
import TaskEditModal from '$lib/components/TaskEditModal.svelte';
import { TaskListSkeleton } from '$lib/components/skeletons';
import type { Task, Label } from '@todo/shared';
let isLoading = $state(true);
let editingTask = $state<Task | null>(null);
let showEditModal = $state(false);
// Get tag ID from URL
const tagId = $derived($page.params.id ?? '');
@ -46,45 +42,6 @@
isLoading = false;
});
// Modal handlers
function openEditModal(task: Task) {
editingTask = task;
showEditModal = true;
}
function closeEditModal() {
showEditModal = false;
editingTask = null;
}
function handleSaveTask(data: Partial<Task>) {
if (!editingTask) return;
// Extract only the fields that updateTask accepts
const updateData = {
title: data.title,
description: data.description ?? undefined,
projectId: data.projectId,
dueDate: typeof data.dueDate === 'string' ? data.dueDate : data.dueDate?.toISOString(),
priority: data.priority,
status: data.status,
subtasks: data.subtasks ?? undefined,
recurrenceRule: data.recurrenceRule,
};
tasksStore.updateTask(editingTask.id, updateData).catch((error) => {
console.error('Failed to update task:', error);
});
closeEditModal();
}
function handleDeleteTask(taskId: string) {
tasksStore.deleteTask(taskId).catch((error) => {
console.error('Failed to delete task:', error);
});
closeEditModal();
}
</script>
<svelte:head>
@ -141,7 +98,7 @@
<h2 class="section-title">
Offen ({incompleteTasks.length})
</h2>
<TaskList tasks={incompleteTasks} onEditTask={openEditModal} />
<TaskList tasks={incompleteTasks} />
</section>
{/if}
@ -151,7 +108,7 @@
<h2 class="section-title completed">
Erledigt ({completedTasks.length})
</h2>
<TaskList tasks={completedTasks} showCompleted={true} onEditTask={openEditModal} />
<TaskList tasks={completedTasks} showCompleted={true} />
</section>
{/if}
@ -162,17 +119,6 @@
{/if}
</div>
<!-- Task Edit Modal -->
{#if editingTask}
<TaskEditModal
task={editingTask}
open={showEditModal}
onClose={closeEditModal}
onSave={handleSaveTask}
onDelete={handleDeleteTask}
/>
{/if}
<style>
.page-container {
max-width: 640px;