mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 06:19:41 +02:00
feat(todo): complete animation + "Heute erledigt" section on focus pages
KanbanTaskCard: - Checkbox click animates first (checkPop 300ms, fade to 50% opacity) - After 500ms the actual onToggleComplete fires and task moves - Modal cannot open during animation (pointer-events: none) FokusLayout: - Derives completedToday from tasks context (isCompleted + completedAt today) - Shows "Heute erledigt" section at bottom of every sheet - New items slide in from above (slideDown 350ms animation) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
402baf7c7f
commit
a22f1de6d0
2 changed files with 111 additions and 5 deletions
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID, type DndEvent } from 'svelte-dnd-action';
|
||||
import { getContext } from 'svelte';
|
||||
import { isToday } from 'date-fns';
|
||||
import type { Task } from '@todo/shared';
|
||||
import type { GroupedColumn } from '$lib/data/view-grouping';
|
||||
import KanbanTaskCard from '../kanban/KanbanTaskCard.svelte';
|
||||
|
|
@ -34,6 +36,12 @@
|
|||
onAddColumn,
|
||||
}: Props = $props();
|
||||
|
||||
// Today's completed tasks — shown at the bottom of every sheet
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
let completedToday = $derived(
|
||||
tasksCtx.value.filter((t) => t.isCompleted && t.completedAt && isToday(new Date(t.completedAt)))
|
||||
);
|
||||
|
||||
const PAGE_WIDTH_MAP: Record<string, string> = {
|
||||
narrow: 'min(360px, 85vw)',
|
||||
medium: 'min(480px, 85vw)',
|
||||
|
|
@ -161,6 +169,22 @@
|
|||
<div class="sheet-footer">
|
||||
<QuickAddTaskInline onAdd={(title) => handleAddTask(column, title)} />
|
||||
</div>
|
||||
|
||||
{#if completedToday.length > 0}
|
||||
<div class="completed-today">
|
||||
<div class="completed-today-label">Heute erledigt</div>
|
||||
{#each completedToday as task (task.id)}
|
||||
<div class="completed-today-item">
|
||||
<KanbanTaskCard
|
||||
{task}
|
||||
onToggleComplete={() => onTaskToggle(task)}
|
||||
onSave={(data) => onTaskUpdate(task.id, data)}
|
||||
onDelete={() => onTaskDelete(task.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
|
@ -309,6 +333,42 @@
|
|||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Heute erledigt section */
|
||||
.completed-today {
|
||||
padding: 0.75rem 1rem 1rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
:global(.dark) .completed-today {
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.completed-today-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
:global(.dark) .completed-today-label {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.completed-today-item {
|
||||
animation: slideDown 0.35s ease-out both;
|
||||
}
|
||||
|
||||
/* Page dots */
|
||||
.page-dots {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@
|
|||
let showModal = $state(false);
|
||||
let showDeleteConfirm = $state(false);
|
||||
|
||||
// Completion animation
|
||||
let isAnimatingComplete = $state(false);
|
||||
|
||||
// Inline edit state
|
||||
let isEditingTitle = $state(false);
|
||||
let editTitle = $state('');
|
||||
|
|
@ -58,13 +61,27 @@
|
|||
|
||||
// Click to open modal
|
||||
function handleCardClick(e: MouseEvent) {
|
||||
// Don't open modal if clicking on checkbox or during inline edit
|
||||
if (isEditingTitle) return;
|
||||
// Don't open modal if clicking on checkbox or during inline edit or animation
|
||||
if (isEditingTitle || isAnimatingComplete) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('.task-checkbox')) return;
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
function handleCheckboxClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (task.isCompleted) {
|
||||
onToggleComplete?.();
|
||||
return;
|
||||
}
|
||||
if (isAnimatingComplete) return;
|
||||
isAnimatingComplete = true;
|
||||
setTimeout(() => {
|
||||
isAnimatingComplete = false;
|
||||
onToggleComplete?.();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Double-click to edit title inline
|
||||
function handleTitleDoubleClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
|
|
@ -172,6 +189,7 @@
|
|||
<div
|
||||
class="kanban-card group"
|
||||
class:completed={task.isCompleted}
|
||||
class:completing={isAnimatingComplete}
|
||||
onclick={handleCardClick}
|
||||
oncontextmenu={handleContextMenu}
|
||||
role="button"
|
||||
|
|
@ -182,8 +200,12 @@
|
|||
|
||||
<!-- Checkbox -->
|
||||
{#if onToggleComplete}
|
||||
<button class="task-checkbox" class:checked={task.isCompleted} onclick={onToggleComplete}>
|
||||
{#if task.isCompleted}
|
||||
<button
|
||||
class="task-checkbox"
|
||||
class:checked={task.isCompleted || isAnimatingComplete}
|
||||
onclick={handleCheckboxClick}
|
||||
>
|
||||
{#if task.isCompleted || isAnimatingComplete}
|
||||
<Check size={20} class="check-icon" />
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -203,7 +225,7 @@
|
|||
{:else}
|
||||
<span
|
||||
class="task-title"
|
||||
class:line-through={task.isCompleted}
|
||||
class:line-through={task.isCompleted || isAnimatingComplete}
|
||||
ondblclick={handleTitleDoubleClick}
|
||||
>
|
||||
{task.title}
|
||||
|
|
@ -363,6 +385,30 @@
|
|||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.kanban-card.completing {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes checkPop {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.task-checkbox.checked {
|
||||
animation: checkPop 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Priority dot — slim left accent, aligned to first line */
|
||||
.priority-dot {
|
||||
width: 3px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue