ca-grow-ops-manager/frontend/src/components/ui/PullToRefresh.tsx
fullsizemalt 47de301f77
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
style: Dark/Light mode contrast audit
- Boost text contrast in both themes
- Strengthen border visibility (subtle borders now visible)
- Convert 39 files from hardcoded dark:/light: to CSS vars
- Tertiary text now more readable on both backgrounds
2025-12-27 12:12:10 -08:00

100 lines
3.2 KiB
TypeScript

import { useState, useRef, useCallback, ReactNode, TouchEvent } from 'react';
import { RefreshCw } from 'lucide-react';
interface PullToRefreshProps {
onRefresh: () => Promise<void>;
children: ReactNode;
disabled?: boolean;
}
export function PullToRefresh({ onRefresh, children, disabled = false }: PullToRefreshProps) {
const [pulling, setPulling] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [pullDistance, setPullDistance] = useState(0);
const startY = useRef(0);
const containerRef = useRef<HTMLDivElement>(null);
const THRESHOLD = 80; // pixels to pull before triggering refresh
const MAX_PULL = 120;
const handleTouchStart = useCallback((e: TouchEvent) => {
if (disabled || refreshing) return;
const scrollTop = containerRef.current?.scrollTop || 0;
if (scrollTop > 0) return; // Only trigger at top
startY.current = e.touches[0].clientY;
setPulling(true);
}, [disabled, refreshing]);
const handleTouchMove = useCallback((e: TouchEvent) => {
if (!pulling || refreshing) return;
const currentY = e.touches[0].clientY;
const diff = currentY - startY.current;
if (diff > 0) {
// Add resistance to pull
const resistance = Math.min(diff * 0.5, MAX_PULL);
setPullDistance(resistance);
}
}, [pulling, refreshing]);
const handleTouchEnd = useCallback(async () => {
if (!pulling) return;
setPulling(false);
if (pullDistance >= THRESHOLD && !refreshing) {
setRefreshing(true);
setPullDistance(THRESHOLD);
try {
await onRefresh();
} finally {
setRefreshing(false);
setPullDistance(0);
}
} else {
setPullDistance(0);
}
}, [pulling, pullDistance, refreshing, onRefresh]);
const progress = Math.min(pullDistance / THRESHOLD, 1);
const rotation = progress * 180;
return (
<div
ref={containerRef}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
className="relative overflow-auto h-full"
>
{/* Pull indicator */}
<div
className="absolute left-1/2 -translate-x-1/2 z-10 flex items-center justify-center transition-transform"
style={{
top: `${Math.max(-40, pullDistance - 40)}px`,
transform: `translateX(-50%) rotate(${rotation}deg)`,
opacity: progress
}}
>
<div className={`p-2 rounded-full bg-[var(--color-bg-elevated)] shadow-lg ${refreshing ? 'animate-spin' : ''}`}>
<RefreshCw size={20} className="text-[var(--color-primary)]" />
</div>
</div>
{/* Content with pull transform */}
<div
style={{
transform: `translateY(${pullDistance}px)`,
transition: pulling ? 'none' : 'transform 0.2s ease-out'
}}
>
{children}
</div>
</div>
);
}