mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-12 01:38:16 +02:00
feat(focus): fix drag vs open and unify button design
This commit is contained in:
1 parent
dfa56173b9
commit
22e002f47f
4 files changed
+34
-18
No files matched your search
@@ -73,7 +73,7 @@ export function FocusCard({ article, isTop = false }: FocusCardProps) {
|
||||
{/* Hint (only on top card) */}
|
||||
{isTop && (
|
||||
<div className="mt-auto pt-4 text-center opacity-40 text-[10px] uppercase tracking-widest font-bold text-paper-muted animate-pulse">
|
||||
Cliquer pour lire • Glisser pour trier
|
||||
Double-cliquer pour lire • Glisser pour trier
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion';
|
||||
import type { Article } from '../../api/articles';
|
||||
@@ -28,6 +28,8 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
// Track whether a drag gesture occurred to prevent double-click from firing after swipe
|
||||
const hasDraggedRef = useRef(false);
|
||||
|
||||
// Visible stack size
|
||||
const visibleArticles = articles.slice(currentIndex, currentIndex + 3);
|
||||
@@ -72,6 +74,11 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
const bind = useDrag(({ active, movement: [mx], velocity: [vx] }) => {
|
||||
if (readingArticle || isProcessing) return; // Disable drag when reading or processing
|
||||
|
||||
// Mark as dragged if the user moved more than a few pixels
|
||||
if (active && Math.abs(mx) > 5) {
|
||||
hasDraggedRef.current = true;
|
||||
}
|
||||
|
||||
const trigger = Math.abs(mx) > 200 || (Math.abs(vx) > 0.5 && Math.abs(mx) > 50);
|
||||
|
||||
if (!active && trigger) {
|
||||
@@ -81,9 +88,13 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
|
||||
// Trigger action via handler
|
||||
handleAction(isRight ? 'read' : 'keep');
|
||||
} else if (!active) {
|
||||
// Snap Back — reset drag flag after a short delay so double-click can distinguish
|
||||
x.set(0);
|
||||
setTimeout(() => { hasDraggedRef.current = false; }, 50);
|
||||
} else {
|
||||
// Dragging or Snap Back
|
||||
x.set(active ? mx : 0);
|
||||
// Actively dragging
|
||||
x.set(mx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -190,7 +201,12 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
cursor: isTop ? 'grab' : 'default',
|
||||
}}
|
||||
{...(isTop ? (bind() as any) : {})}
|
||||
onClick={() => isTop && setReadingArticle(article)}
|
||||
onDoubleClick={() => {
|
||||
// Only open on genuine double-click, not after a drag gesture
|
||||
if (isTop && !hasDraggedRef.current) {
|
||||
setReadingArticle(article);
|
||||
}
|
||||
}}
|
||||
animate={{ scale: 1 - i * 0.05, y: i * 20 }}
|
||||
exit={{ x: exitX, opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
|
||||
>
|
||||
@@ -202,10 +218,11 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-10 left-0 right-0 flex justify-center items-center gap-4 px-4 sm:gap-8 z-20">
|
||||
{/* Garder — Outlined style matching Dashboard "Actualiser" button */}
|
||||
<button
|
||||
onClick={() => handleAction('keep')}
|
||||
disabled={isProcessing}
|
||||
className="p-4 rounded-full bg-white shadow-lg text-blue-500 hover:bg-blue-50 hover:scale-110 transition-all disabled:opacity-50 disabled:scale-100"
|
||||
className="p-4 rounded-full bg-white border border-nature/10 text-nature shadow-lg hover:bg-nature hover:text-white hover:border-nature hover:scale-110 transition-all duration-300 disabled:opacity-50 disabled:scale-100"
|
||||
title="Garder (Gauche)"
|
||||
>
|
||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -213,13 +230,13 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Undo Button - Always rendered for layout, hidden if inactive */}
|
||||
{/* Undo — Subtle outlined, invisible when unavailable */}
|
||||
<button
|
||||
onClick={() => handleAction('undo')}
|
||||
disabled={currentIndex === 0 || isProcessing}
|
||||
className={`p-3 rounded-full backdrop-blur-md text-white shadow-lg border border-white/20 transition-all duration-300 ${currentIndex > 0
|
||||
? 'bg-nature/80 hover:bg-nature hover:scale-105 opacity-100 cursor-pointer'
|
||||
: 'bg-white/5 opacity-0 pointer-events-none scale-90'
|
||||
className={`p-3 rounded-full border shadow-lg transition-all duration-300 ${currentIndex > 0
|
||||
? 'bg-white border-nature/10 text-nature hover:bg-nature hover:text-white hover:border-nature hover:scale-105 opacity-100 cursor-pointer'
|
||||
: 'bg-white/5 border-transparent text-transparent opacity-0 pointer-events-none scale-90'
|
||||
}`}
|
||||
title="Revenir en arrière"
|
||||
>
|
||||
@@ -228,10 +245,11 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* J'ai lu — Primary filled, matching "Lancer le Mode Focus" CTA */}
|
||||
<button
|
||||
onClick={() => handleAction('read')}
|
||||
disabled={isProcessing}
|
||||
className="p-4 rounded-full bg-nature shadow-lg text-white hover:bg-nature-light hover:scale-110 transition-all disabled:opacity-50 disabled:scale-100"
|
||||
className="p-4 rounded-full bg-nature text-white border border-nature shadow-lg shadow-nature/20 hover:bg-nature-light hover:scale-110 hover:shadow-xl transition-all duration-300 disabled:opacity-50 disabled:scale-100"
|
||||
title="J'ai lu (Droite)"
|
||||
>
|
||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
||||
@@ -21,7 +21,7 @@ export function FocusEmptyState({ onBack }: FocusEmptyStateProps) {
|
||||
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="px-8 py-3 rounded-full bg-nature text-white font-bold text-sm uppercase tracking-widest hover:bg-nature-light hover:scale-105 transition-all shadow-xl shadow-nature/20"
|
||||
className="px-8 py-3 rounded-full bg-nature text-white font-bold text-sm uppercase tracking-widest border border-nature shadow-lg shadow-nature/20 hover:bg-nature-light hover:scale-105 hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
Retour au Dashboard
|
||||
</button>
|
||||
|
||||
@@ -89,17 +89,15 @@ export function FocusPage({ onExit }: FocusPageProps) {
|
||||
</div>
|
||||
<button
|
||||
onClick={onExit}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 hover:bg-white/20 transition-all text-paper-white group"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-white border border-nature/10 text-nature shadow-lg hover:bg-nature hover:text-white hover:border-nature hover:scale-105 transition-all duration-300 group"
|
||||
title="Quitter le mode Focus"
|
||||
>
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity hidden sm:inline-block">
|
||||
Retour au Dashboard
|
||||
</span>
|
||||
<div className="w-8 h-8 flex items-center justify-center rounded-full bg-white/10 group-hover:bg-white/20">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in new issue
Block a user