From 22e002f47f21bf062653127889efce552af5f827 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Fri, 13 Feb 2026 08:17:53 +0100 Subject: [PATCH] feat(focus): fix drag vs open and unify button design --- web/src/components/focus/FocusCard.tsx | 2 +- web/src/components/focus/FocusCardStack.tsx | 38 ++++++++++++++------ web/src/components/focus/FocusEmptyState.tsx | 2 +- web/src/pages/FocusPage.tsx | 10 +++--- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/web/src/components/focus/FocusCard.tsx b/web/src/components/focus/FocusCard.tsx index a5cdfcd..0b1f49d 100644 --- a/web/src/components/focus/FocusCard.tsx +++ b/web/src/components/focus/FocusCard.tsx @@ -73,7 +73,7 @@ export function FocusCard({ article, isTop = false }: FocusCardProps) { {/* Hint (only on top card) */} {isTop && (
- Cliquer pour lire • Glisser pour trier + Double-cliquer pour lire • Glisser pour trier
)} diff --git a/web/src/components/focus/FocusCardStack.tsx b/web/src/components/focus/FocusCardStack.tsx index ed54185..89ff0fe 100644 --- a/web/src/components/focus/FocusCardStack.tsx +++ b/web/src/components/focus/FocusCardStack.tsx @@ -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,
+ {/* Garder — Outlined style matching Dashboard "Actualiser" button */} - {/* Undo Button - Always rendered for layout, hidden if inactive */} + {/* Undo — Subtle outlined, invisible when unavailable */} + {/* J'ai lu — Primary filled, matching "Lancer le Mode Focus" CTA */}