fix(02-04): improve swipe-to-delete with direct touch handlers

- Replace svelte-gestures with native touch event handlers
- Add invalidateAll() after save/delete for seamless list updates
- Add $effect to sync edit state when entry prop changes
- Improve swipe animation with isSwiping state tracking
This commit is contained in:
Thomas Richter
2026-01-29 14:27:05 +01:00
parent 104c437ea6
commit 1533759c47

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { Entry } from '$lib/server/db/schema'; import type { Entry } from '$lib/server/db/schema';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
interface Props { interface Props {
entry: Entry; entry: Entry;
@@ -13,11 +13,20 @@
// Expand/collapse state // Expand/collapse state
let expanded = $state(false); let expanded = $state(false);
// Edit state - initialize from entry // Edit state - use $derived to stay in sync with entry prop
let editTitle = $state(entry.title || ''); let editTitle = $state(entry.title || '');
let editContent = $state(entry.content); let editContent = $state(entry.content);
let editType = $state(entry.type); let editType = $state(entry.type);
// Sync edit state when entry changes (after invalidateAll)
$effect(() => {
if (!expanded) {
editTitle = entry.title || '';
editContent = entry.content;
editType = entry.type;
}
});
// Debounced auto-save // Debounced auto-save
let saveTimeout: ReturnType<typeof setTimeout>; let saveTimeout: ReturnType<typeof setTimeout>;
let isSaving = $state(false); let isSaving = $state(false);
@@ -25,21 +34,35 @@
// Swipe state // Swipe state
let swipeOffset = $state(0); let swipeOffset = $state(0);
let isConfirmingDelete = $state(false); let isConfirmingDelete = $state(false);
let touchStartX = 0;
let isSwiping = $state(false);
function handleSwipe(event: SwipeCustomEvent) { function handleTouchStart(e: TouchEvent) {
const { direction } = event.detail; touchStartX = e.touches[0].clientX;
if (direction === 'left') { isSwiping = true;
}
function handleTouchMove(e: TouchEvent) {
if (!isSwiping) return;
const currentX = e.touches[0].clientX;
const diff = currentX - touchStartX;
// Only allow left swipe (negative diff), cap at -100px
if (diff < 0) {
swipeOffset = Math.max(diff, -100);
}
}
function handleTouchEnd() {
isSwiping = false;
// If swiped far enough, show confirmation
if (swipeOffset < -60) {
isConfirmingDelete = true; isConfirmingDelete = true;
swipeOffset = -100; // Snap to reveal delete area
} else {
swipeOffset = 0; // Snap back
} }
} }
// Create swipe gesture using useSwipe hook
const swipeGesture = useSwipe(handleSwipe, () => ({
timeframe: 300,
minSwipeDistance: 60,
touchAction: 'pan-y'
}));
function cancelDelete() { function cancelDelete() {
isConfirmingDelete = false; isConfirmingDelete = false;
swipeOffset = 0; swipeOffset = 0;
@@ -54,7 +77,7 @@
body: formData body: formData
}); });
window.location.reload(); await invalidateAll();
} }
async function debouncedSave() { async function debouncedSave() {
@@ -72,6 +95,8 @@
method: 'POST', method: 'POST',
body: formData body: formData
}); });
// Refresh data so list shows updated values
await invalidateAll();
} finally { } finally {
isSaving = false; isSaving = false;
} }
@@ -118,10 +143,12 @@
<!-- Swipeable entry card --> <!-- Swipeable entry card -->
<article <article
{...swipeGesture} ontouchstart={handleTouchStart}
style="transform: translateX({swipeOffset}px); transition: {swipeOffset === 0 ontouchmove={handleTouchMove}
? 'transform 0.2s' ontouchend={handleTouchEnd}
: 'none'}" style="transform: translateX({swipeOffset}px); transition: {isSwiping
? 'none'
: 'transform 0.2s'}"
class="p-4 border-b border-gray-100 md:border md:rounded-lg md:shadow-sm md:mb-3 bg-white relative" class="p-4 border-b border-gray-100 md:border md:rounded-lg md:shadow-sm md:mb-3 bg-white relative"
> >
<!-- Collapsed view - clickable header --> <!-- Collapsed view - clickable header -->