piclets / src /lib /components /UI /PullToRefresh.svelte
Fraser's picture
build warnings
b3d44ce
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
onRefresh: () => Promise<void>;
threshold?: number;
children?: any;
}
let { onRefresh, threshold = 80, children }: Props = $props();
let container: HTMLDivElement;
let startY = 0;
let currentY = 0;
let pulling = $state(false);
let releasing = $state(false);
let refreshing = $state(false);
let pullDistance = $state(0);
let pullProgress = $derived(Math.min(pullDistance / threshold, 1));
let canRefresh = $derived(pullProgress >= 1 && !refreshing);
function handleTouchStart(e: TouchEvent) {
if (container.scrollTop !== 0 || refreshing) return;
startY = e.touches[0].clientY;
pulling = true;
}
function handleTouchMove(e: TouchEvent) {
if (!pulling || refreshing) return;
currentY = e.touches[0].clientY;
const diff = currentY - startY;
if (diff > 0 && container.scrollTop === 0) {
e.preventDefault();
pullDistance = diff * 0.5; // Add resistance
}
}
async function handleTouchEnd() {
if (!pulling) return;
pulling = false;
if (canRefresh) {
releasing = true;
refreshing = true;
try {
await onRefresh();
} finally {
refreshing = false;
setTimeout(() => {
releasing = false;
pullDistance = 0;
}, 300);
}
} else {
releasing = true;
setTimeout(() => {
releasing = false;
pullDistance = 0;
}, 300);
}
}
onMount(() => {
// Also handle mouse events for desktop testing
function handleMouseDown(e: MouseEvent) {
if (container.scrollTop !== 0 || refreshing) return;
startY = e.clientY;
pulling = true;
}
function handleMouseMove(e: MouseEvent) {
if (!pulling || refreshing) return;
currentY = e.clientY;
const diff = currentY - startY;
if (diff > 0 && container.scrollTop === 0) {
e.preventDefault();
pullDistance = diff * 0.5;
}
}
function handleMouseUp() {
handleTouchEnd();
}
container.addEventListener('mousedown', handleMouseDown);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
container?.removeEventListener('mousedown', handleMouseDown);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
});
</script>
<div class="pull-to-refresh-container">
<div
class="pull-indicator"
class:pulling
class:releasing
class:refreshing
style="transform: translateY({refreshing ? threshold : pullDistance}px)"
>
<div class="spinner-container" style="transform: rotate({pullProgress * 180}deg)">
{#if refreshing}
<div class="spinner"></div>
{:else}
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"
opacity={pullProgress} />
</svg>
{/if}
</div>
<div class="pull-text">
{#if refreshing}
Refreshing...
{:else if canRefresh}
Release to refresh
{:else}
Pull to refresh
{/if}
</div>
</div>
<div
bind:this={container}
class="content-container"
class:pulling
ontouchstart={handleTouchStart}
ontouchmove={handleTouchMove}
ontouchend={handleTouchEnd}
style="transform: translateY({refreshing ? threshold : pullDistance}px)"
>
{@render children?.()}
</div>
</div>
<style>
.pull-to-refresh-container {
position: relative;
height: 100%;
overflow: hidden;
}
.content-container {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
transition: transform 0.3s ease;
}
.content-container.pulling {
transition: none;
}
.pull-indicator {
position: absolute;
top: -60px;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
transition: transform 0.3s ease;
z-index: 10;
}
.pull-indicator.pulling {
transition: none;
}
.spinner-container {
transition: transform 0.2s ease;
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid #e0e0e0;
border-top-color: #007bff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.pull-text {
font-size: 14px;
color: #666;
white-space: nowrap;
}
</style>