feat(focus): fix drag vs open and unify button design

This commit is contained in:
Michael committed 2026-02-13 08:17:53 +01:00
1 parent dfa56173b9
commit 22e002f47f
4 files changed
+34 -18

No files matched your search

+1 -1
View File
@@ -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>
+28 -10
View File
@@ -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">
+1 -1
View File
@@ -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>
+4 -6
View File
@@ -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>