feat(todo): redesign task detail modal + add inline subtasks

- Redesign TaskEditModal as a multi-column page-like layout:
  - Title (large, borderless textarea with autoGrow)
  - Content grid: description (left) | subtasks + links (right)
  - Props strip: all metadata as horizontal flex-wrap cells (status,
    priority, due date, recurrence, tags, assignee, story points, etc.)
  - No sidebar, no scrollable property list
- Remove redundant `notes` field from TaskMetadata (keep only `description`)
- Remove all legacy migration code from useTaskForm composable
- Add inline subtasks display to TaskItem on the homepage:
  - Shown indented under parent task when task has subtasks and is incomplete
  - Each subtask is directly checkable (toggleSubtask via onSave callback)
  - Vertical connecting line via CSS pseudo-element

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 17:13:59 +02:00
parent d8a2b37126
commit b37df6facf
5 changed files with 637 additions and 433 deletions

File diff suppressed because it is too large Load diff

View file

@ -97,7 +97,6 @@
form.selectedLabelIds,
form.subtasks,
form.recurrenceRule,
form.notes,
form.storyPoints,
form.effectiveDuration,
form.funRating,
@ -208,6 +207,20 @@
form.subtasks = newSubtasks;
}
function toggleSubtask(subtaskId: string) {
if (!onSave) return;
const updated = (task.subtasks ?? []).map((s) =>
s.id === subtaskId
? {
...s,
isCompleted: !s.isCompleted,
completedAt: !s.isCompleted ? new Date().toISOString() : null,
}
: s
);
onSave({ subtasks: updated });
}
const priorityColors = PRIORITY_COLORS;
// Format due date
@ -224,7 +237,7 @@
});
// Subtasks progress
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks));
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks ?? undefined));
// Long press to expand (mobile)
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
@ -315,25 +328,16 @@
{task.title}
</span>
<!-- Labels and subtasks below title -->
{#if subtaskProgress() || (task.labels && task.labels.length > 0)}
<!-- Labels below title -->
{#if task.labels && task.labels.length > 0}
<div class="task-meta">
{#if subtaskProgress()}
<span class="meta-item">
<CheckSquare size={20} class="meta-icon" />
{subtaskProgress()}
{#each task.labels.slice(0, 2) as label}
<span class="label-tag" style="--label-color: {label.color}">
{label.name}
</span>
{/if}
{#if task.labels && task.labels.length > 0}
{#each task.labels.slice(0, 2) as label}
<span class="label-tag" style="--label-color: {label.color}">
{label.name}
</span>
{/each}
{#if task.labels.length > 2}
<span class="meta-item">+{task.labels.length - 2}</span>
{/if}
{/each}
{#if task.labels.length > 2}
<span class="meta-item">+{task.labels.length - 2}</span>
{/if}
</div>
{/if}
@ -399,6 +403,24 @@
{/if}
</div>
<!-- Inline subtasks -->
{#if task.subtasks && task.subtasks.length > 0 && !task.isCompleted}
<div class="subtasks-inline">
{#each task.subtasks as subtask (subtask.id)}
<button
class="subtask-row"
class:done={subtask.isCompleted}
onclick={() => toggleSubtask(subtask.id)}
>
<span class="subtask-check" class:checked={subtask.isCompleted}>
{#if subtask.isCompleted}<Check size={10} />{/if}
</span>
<span class="subtask-title">{subtask.title}</span>
</button>
{/each}
</div>
{/if}
<!-- Expanded inline edit form -->
{#if isExpanded}
<div class="expanded-form">
@ -533,18 +555,6 @@
/>
</div>
<!-- Notes -->
<div class="form-section">
<label class="form-label" for="task-notes-{task.id}">Notizen</label>
<textarea
id="task-notes-{task.id}"
class="form-textarea"
bind:value={form.notes}
placeholder="Zusätzliche Notizen..."
rows="2"
></textarea>
</div>
<!-- Story Points & Duration & Fun Rating row -->
<div class="form-row-3">
<div class="form-section">
@ -1191,4 +1201,86 @@
background: #ef4444;
color: white;
}
/* ── Inline subtasks ────────────────────────────── */
.subtasks-inline {
display: flex;
flex-direction: column;
/* align with task title: padding-left + gap + checkbox + gap */
padding: 0.125rem 1.5rem 0.375rem calc(1.5rem + 1.25rem + 1.25rem);
position: relative;
}
.subtasks-inline::before {
content: '';
position: absolute;
left: calc(1.5rem + 0.625rem + 0.625rem);
top: 0;
bottom: 0.375rem;
width: 1px;
background: rgba(0, 0, 0, 0.1);
}
:global(.dark) .subtasks-inline::before {
background: rgba(255, 255, 255, 0.1);
}
.subtask-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.1875rem 0;
background: none;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
border-radius: 0.25rem;
transition: background 0.1s;
}
.subtask-row:hover {
background: rgba(0, 0, 0, 0.03);
}
:global(.dark) .subtask-row:hover {
background: rgba(255, 255, 255, 0.04);
}
.subtask-check {
width: 0.875rem;
height: 0.875rem;
border-radius: 50%;
border: 1.5px solid rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s;
color: white;
}
:global(.dark) .subtask-check {
border-color: rgba(255, 255, 255, 0.25);
}
.subtask-check.checked {
background: #8b5cf6;
border-color: #8b5cf6;
}
.subtask-title {
font-size: 0.8125rem;
color: #374151;
line-height: 1.4;
}
:global(.dark) .subtask-title {
color: #d1d5db;
}
.subtask-row.done .subtask-title {
text-decoration: line-through;
color: #9ca3af;
}
</style>

View file

@ -54,7 +54,7 @@
});
// Subtasks progress
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks));
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks ?? undefined));
// Click to open modal
function handleCardClick(e: MouseEvent) {

View file

@ -26,7 +26,6 @@ export function useTaskForm() {
let selectedLabelIds = $state<string[]>([]);
let subtasks = $state<Subtask[]>([]);
let recurrenceRule = $state('');
let notes = $state('');
let storyPoints = $state<number | null>(null);
let effectiveDuration = $state<EffectiveDuration | null>(null);
let funRating = $state<number | null>(null);
@ -53,7 +52,6 @@ export function useTaskForm() {
selectedLabelIds = task.labels?.map((l) => l.id) || [];
subtasks = task.subtasks ? [...task.subtasks] : [];
recurrenceRule = task.recurrenceRule || '';
notes = task.metadata?.notes || '';
storyPoints = task.metadata?.storyPoints ?? null;
effectiveDuration = task.metadata?.effectiveDuration ?? null;
funRating = task.metadata?.funRating ?? null;
@ -99,7 +97,6 @@ export function useTaskForm() {
recurrenceRule: recurrenceRule || null,
metadata: {
...task.metadata,
notes: notes.trim() || undefined,
storyPoints: storyPoints ?? undefined,
effectiveDuration: effectiveDuration ?? undefined,
funRating: funRating ?? undefined,
@ -172,12 +169,6 @@ export function useTaskForm() {
set recurrenceRule(v: string) {
recurrenceRule = v;
},
get notes() {
return notes;
},
set notes(v: string) {
notes = v;
},
get storyPoints() {
return storyPoints;
},

View file

@ -20,7 +20,6 @@ export interface EffectiveDuration {
}
export interface TaskMetadata {
notes?: string;
attachments?: string[];
linkedCalendarEventId?: string | null;
// Agile/Productivity metadata