mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-21 14:18:06 +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) */}
|
{/* Hint (only on top card) */}
|
||||||
{isTop && (
|
{isTop && (
|
||||||
<div className="mt-auto pt-4 text-center opacity-40 text-[10px] uppercase tracking-widest font-bold text-paper-muted animate-pulse">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useDrag } from '@use-gesture/react';
|
import { useDrag } from '@use-gesture/react';
|
||||||
import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion';
|
import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion';
|
||||||
import type { Article } from '../../api/articles';
|
import type { Article } from '../../api/articles';
|
||||||
@@ -28,6 +28,8 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
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
|
// Visible stack size
|
||||||
const visibleArticles = articles.slice(currentIndex, currentIndex + 3);
|
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] }) => {
|
const bind = useDrag(({ active, movement: [mx], velocity: [vx] }) => {
|
||||||
if (readingArticle || isProcessing) return; // Disable drag when reading or processing
|
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);
|
const trigger = Math.abs(mx) > 200 || (Math.abs(vx) > 0.5 && Math.abs(mx) > 50);
|
||||||
|
|
||||||
if (!active && trigger) {
|
if (!active && trigger) {
|
||||||
@@ -81,9 +88,13 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
|||||||
|
|
||||||
// Trigger action via handler
|
// Trigger action via handler
|
||||||
handleAction(isRight ? 'read' : 'keep');
|
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 {
|
} else {
|
||||||
// Dragging or Snap Back
|
// Actively dragging
|
||||||
x.set(active ? mx : 0);
|
x.set(mx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,7 +201,12 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
|||||||
cursor: isTop ? 'grab' : 'default',
|
cursor: isTop ? 'grab' : 'default',
|
||||||
}}
|
}}
|
||||||
{...(isTop ? (bind() as any) : {})}
|
{...(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 }}
|
animate={{ scale: 1 - i * 0.05, y: i * 20 }}
|
||||||
exit={{ x: exitX, opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
|
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>
|
||||||
|
|
||||||
<div className="absolute bottom-10 left-0 right-0 flex justify-center items-center gap-4 px-4 sm:gap-8 z-20">
|
<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
|
<button
|
||||||
onClick={() => handleAction('keep')}
|
onClick={() => handleAction('keep')}
|
||||||
disabled={isProcessing}
|
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)"
|
title="Garder (Gauche)"
|
||||||
>
|
>
|
||||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Undo Button - Always rendered for layout, hidden if inactive */}
|
{/* Undo — Subtle outlined, invisible when unavailable */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction('undo')}
|
onClick={() => handleAction('undo')}
|
||||||
disabled={currentIndex === 0 || isProcessing}
|
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
|
className={`p-3 rounded-full border shadow-lg transition-all duration-300 ${currentIndex > 0
|
||||||
? 'bg-nature/80 hover:bg-nature hover:scale-105 opacity-100 cursor-pointer'
|
? '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 opacity-0 pointer-events-none scale-90'
|
: 'bg-white/5 border-transparent text-transparent opacity-0 pointer-events-none scale-90'
|
||||||
}`}
|
}`}
|
||||||
title="Revenir en arrière"
|
title="Revenir en arrière"
|
||||||
>
|
>
|
||||||
@@ -228,10 +245,11 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite,
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* J'ai lu — Primary filled, matching "Lancer le Mode Focus" CTA */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction('read')}
|
onClick={() => handleAction('read')}
|
||||||
disabled={isProcessing}
|
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)"
|
title="J'ai lu (Droite)"
|
||||||
>
|
>
|
||||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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
|
<button
|
||||||
onClick={onBack}
|
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
|
Retour au Dashboard
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -89,17 +89,15 @@ export function FocusPage({ onExit }: FocusPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onExit}
|
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"
|
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">
|
<span className="text-[10px] font-bold uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity hidden sm:inline-block">
|
||||||
Retour au Dashboard
|
Retour au Dashboard
|
||||||
</span>
|
</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">
|
||||||
<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" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user