feat: Introduce custom global context menu for text inputs, refine card touch interactions, and apply global user-select and scrollbar styles.

This commit is contained in:
2025-12-18 00:29:43 +01:00
parent 2bbedfd17f
commit 7d6ce3995c
8 changed files with 396 additions and 88 deletions

View File

@@ -6,6 +6,7 @@ import { LobbyManager } from './modules/lobby/LobbyManager';
import { DeckTester } from './modules/tester/DeckTester';
import { Pack } from './services/PackGeneratorService';
import { ToastProvider } from './components/Toast';
import { GlobalContextMenu } from './components/GlobalContextMenu';
export const App: React.FC = () => {
const [activeTab, setActiveTab] = useState<'draft' | 'bracket' | 'lobby' | 'tester'>(() => {
@@ -68,6 +69,7 @@ export const App: React.FC = () => {
return (
<ToastProvider>
<GlobalContextMenu />
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
<header className="bg-slate-800 border-b border-slate-700 p-4 shrink-0 z-50 shadow-lg">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">

View File

@@ -0,0 +1,183 @@
import React, { useEffect, useState, useRef } from 'react';
import { Copy, Scissors, Clipboard } from 'lucide-react';
interface MenuPosition {
x: number;
y: number;
}
export const GlobalContextMenu: React.FC = () => {
const [visible, setVisible] = useState(false);
const [position, setPosition] = useState<MenuPosition>({ x: 0, y: 0 });
const [targetElement, setTargetElement] = useState<HTMLInputElement | HTMLTextAreaElement | null>(null);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleContextMenu = (e: MouseEvent) => {
const target = e.target as HTMLElement;
// Check if target is an input or textarea
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
const inputTarget = target as HTMLInputElement | HTMLTextAreaElement;
// Only allow text-based inputs (ignore range, checkbox, etc.)
if (target.tagName === 'INPUT') {
const type = (target as HTMLInputElement).type;
if (!['text', 'password', 'email', 'number', 'search', 'tel', 'url'].includes(type)) {
e.preventDefault();
setVisible(false);
return;
}
}
e.preventDefault();
setTargetElement(inputTarget);
// Position menu within viewport
const menuWidth = 150;
const menuHeight = 120; // approx
let x = e.clientX;
let y = e.clientY;
if (x + menuWidth > window.innerWidth) x = window.innerWidth - menuWidth - 10;
if (y + menuHeight > window.innerHeight) y = window.innerHeight - menuHeight - 10;
setPosition({ x, y });
setVisible(true);
} else {
// Disable context menu for everything else
e.preventDefault();
setVisible(false);
}
};
const handleClick = (e: MouseEvent) => {
// Close menu on any click outside
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setVisible(false);
}
};
// Use capture to ensure we intercept early
document.addEventListener('contextmenu', handleContextMenu);
document.addEventListener('click', handleClick);
document.addEventListener('scroll', () => setVisible(false)); // Close on scroll
return () => {
document.removeEventListener('contextmenu', handleContextMenu);
document.removeEventListener('click', handleClick);
};
}, []);
if (!visible) return null;
const handleCopy = async () => {
if (!targetElement) return;
const text = targetElement.value.substring(targetElement.selectionStart || 0, targetElement.selectionEnd || 0);
if (text) {
await navigator.clipboard.writeText(text);
}
setVisible(false);
targetElement.focus();
};
const handleCut = async () => {
if (!targetElement) return;
const start = targetElement.selectionStart || 0;
const end = targetElement.selectionEnd || 0;
const text = targetElement.value.substring(start, end);
if (text) {
await navigator.clipboard.writeText(text);
// Update value
const newVal = targetElement.value.slice(0, start) + targetElement.value.slice(end);
// React state update hack: Trigger native value setter and event
// This ensures React controlled components update their state
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(targetElement, newVal);
} else {
targetElement.value = newVal;
}
const event = new Event('input', { bubbles: true });
targetElement.dispatchEvent(event);
}
setVisible(false);
targetElement.focus();
};
const handlePaste = async () => {
if (!targetElement) return;
try {
const text = await navigator.clipboard.readText();
if (!text) return;
const start = targetElement.selectionStart || 0;
const end = targetElement.selectionEnd || 0;
const currentVal = targetElement.value;
const newVal = currentVal.slice(0, start) + text + currentVal.slice(end);
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(targetElement, newVal);
} else {
targetElement.value = newVal;
}
const event = new Event('input', { bubbles: true });
targetElement.dispatchEvent(event);
// Move cursor
// Timeout needed for React to process input event first
setTimeout(() => {
targetElement.setSelectionRange(start + text.length, start + text.length);
}, 0);
} catch (err) {
console.error('Failed to read clipboard', err);
}
setVisible(false);
targetElement.focus();
};
return (
<div
ref={menuRef}
className="fixed z-[10000] bg-slate-800 border border-slate-600 rounded-lg shadow-2xl py-1 w-36 overflow-hidden animate-in fade-in zoom-in duration-75"
style={{ top: position.y, left: position.x }}
>
<button
onClick={handleCut}
className="w-full text-left px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 hover:text-white flex items-center gap-2 transition-colors disabled:opacity-50"
disabled={!targetElement?.value || targetElement?.selectionStart === targetElement?.selectionEnd}
>
<Scissors className="w-4 h-4" /> Cut
</button>
<button
onClick={handleCopy}
className="w-full text-left px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 hover:text-white flex items-center gap-2 transition-colors disabled:opacity-50"
disabled={!targetElement?.value || targetElement?.selectionStart === targetElement?.selectionEnd}
>
<Copy className="w-4 h-4" /> Copy
</button>
<button
onClick={handlePaste}
className="w-full text-left px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 hover:text-white flex items-center gap-2 transition-colors border-t border-slate-700 mt-1 pt-2"
>
<Clipboard className="w-4 h-4" /> Paste
</button>
</div>
);
};

View File

@@ -5,6 +5,8 @@ import './styles/main.css';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = createRoot(rootElement);
root.render(

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { Layers, RotateCcw, Box, Check, Loader2, Upload, LayoutGrid, List, Sliders, Settings, Users, Download, Copy, FileDown, Trash2, Search, X, PlayCircle, Plus, Minus } from 'lucide-react';
import { Layers, RotateCcw, Box, Check, Loader2, Upload, LayoutGrid, List, Sliders, Settings, Users, Download, Copy, FileDown, Trash2, Search, X, PlayCircle, Plus, Minus, ChevronDown, MoreHorizontal } from 'lucide-react';
import { ScryfallCard, ScryfallSet } from '../../services/ScryfallService';
import { PackGeneratorService, ProcessedPools, SetsMap, Pack, PackGenerationSettings } from '../../services/PackGeneratorService';
import { PackCard } from '../../components/PackCard';
@@ -766,44 +766,72 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
</h2>
</div>
<div className="flex gap-2 w-full sm:w-auto justify-end overflow-x-auto pb-1 sm:pb-0">
{/* Play Button */}
<div className="flex gap-2 w-full sm:w-auto justify-end">
{/* Actions Menu */}
{packs.length > 0 && (
<>
<div className="relative group z-50">
<button className="px-4 py-2 bg-gradient-to-r from-slate-700 to-slate-800 hover:from-slate-600 hover:to-slate-700 text-white font-bold rounded-lg shadow-lg flex items-center gap-2 transition-all ring-1 ring-white/10">
<MoreHorizontal className="w-4 h-4 text-emerald-400" /> <span className="hidden sm:inline">Actions</span> <ChevronDown className="w-4 h-4 text-slate-400 group-hover:rotate-180 transition-transform" />
</button>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 w-56 bg-slate-800 border border-slate-700 rounded-xl shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right p-2 flex flex-col gap-2 z-[9999]">
{/* Play Online */}
<button
onClick={handlePlayOnline}
className={`px-4 py-2 font-bold rounded-lg shadow-lg flex items-center gap-2 animate-in fade-in zoom-in whitespace-nowrap transition-colors
${packs.length < 12
? 'bg-slate-700 text-slate-400 cursor-not-allowed hover:bg-slate-600'
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white'
className={`w-full text-left px-3 py-3 rounded-lg flex items-center gap-3 transition-all shadow-md ${packs.length < 12
? 'bg-slate-700 text-slate-400 cursor-not-allowed'
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white shadow-purple-900/20'
}`}
>
<Users className="w-4 h-4" /> <span className="hidden sm:inline">Play Online</span>
<Users className="w-5 h-5 shrink-0" />
<div>
<span className="block text-sm font-bold leading-tight">Play Online</span>
<span className={`block text-[10px] leading-tight mt-0.5 ${packs.length < 12 ? 'text-slate-500' : 'text-purple-100'}`}>
Start a multiplayer draft
</span>
</div>
</button>
<div className="h-px bg-slate-700/50 mx-1" />
{/* Test Solo */}
<button
onClick={handleStartSoloTest}
disabled={loading}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white font-bold rounded-lg shadow-lg flex items-center gap-2 animate-in fade-in zoom-in"
title="Test a randomized deck from these packs right now"
className="w-full text-left px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg flex items-center gap-3 transition-colors shadow-sm"
>
<PlayCircle className="w-4 h-4 text-emerald-400" /> <span className="hidden sm:inline">Test Solo</span>
<PlayCircle className="w-4 h-4 text-emerald-400 shrink-0" />
<div>
<span className="block text-sm font-bold">Test Solo</span>
<span className="block text-[10px] text-slate-400 leading-none mt-0.5">Draft against bots</span>
</div>
</button>
{/* Export */}
<button
onClick={handleExportCsv}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white font-bold rounded-lg shadow-lg flex items-center gap-2 animate-in fade-in zoom-in"
title="Export as CSV"
className="w-full text-left px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg flex items-center gap-3 transition-colors shadow-sm"
>
<Download className="w-4 h-4" /> <span className="hidden sm:inline">Export</span>
<Download className="w-4 h-4 text-blue-400 shrink-0" />
<span className="text-sm font-bold">Export CSV</span>
</button>
{/* Copy */}
<button
onClick={handleCopyCsv}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white font-bold rounded-lg shadow-lg flex items-center gap-2 animate-in fade-in zoom-in"
title="Copy CSV to Clipboard"
className="w-full text-left px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg flex items-center gap-3 transition-colors shadow-sm"
>
{copySuccess ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
<span className="hidden sm:inline">{copySuccess ? 'Copied!' : 'Copy'}</span>
{copySuccess ? <Check className="w-4 h-4 text-emerald-400 shrink-0" /> : <Copy className="w-4 h-4 text-slate-400 shrink-0" />}
<span className="text-sm font-bold">{copySuccess ? 'Copied!' : 'Copy List'}</span>
</button>
</div>
</div>
{/* Size Slider */}
<div className="flex items-center gap-2 bg-slate-800 rounded-lg px-2 py-1 border border-slate-700 h-9 mr-2 flex">
<div className="w-3 h-4 rounded border border-slate-500 bg-slate-700" title="Small Cards" />
@@ -830,7 +858,8 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
</div>
</div>
{packs.length === 0 ? (
{
packs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 border-2 border-dashed border-slate-700 rounded-2xl bg-slate-800/30 text-slate-500">
<Box className="w-12 h-12 mb-4 opacity-50" />
<p>No packs generated.</p>
@@ -848,8 +877,9 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
<PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={cardWidth} />
))}
</div>
)}
</div>
)
}
</div >
</div >
);

View File

@@ -39,7 +39,7 @@ const DraggableCardWrapper = ({ children, card, source, disabled }: any) => {
} : undefined;
return (
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className="touch-none">
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className="relative z-0">
{children}
</div>
);
@@ -61,7 +61,7 @@ const DraggableLandWrapper = ({ children, land }: any) => {
} : undefined;
return (
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className="touch-none">
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className="relative z-0">
{children}
</div>
);
@@ -345,7 +345,12 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
// --- DnD Handlers ---
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
useSensor(TouchSensor, { activationConstraint: { distance: 10 } })
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);
const [draggedCard, setDraggedCard] = useState<any>(null);
@@ -591,9 +596,12 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
)}
</div>
<DragOverlay dropAnimation={{ duration: 200, easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)' }}>
<DragOverlay dropAnimation={null}>
{draggedCard ? (
<div className={`w-36 rounded-xl shadow-2xl opacity-90 rotate-3 cursor-grabbing overflow-hidden ring-2 ring-emerald-500 bg-slate-900 aspect-[2.5/3.5]`}>
<div
style={{ width: `${cardWidth}px` }}
className={`rounded-xl shadow-2xl opacity-90 rotate-3 cursor-grabbing overflow-hidden ring-2 ring-emerald-500 bg-slate-900 aspect-[2.5/3.5]`}
>
<img src={draggedCard.image || draggedCard.image_uris?.normal} alt={draggedCard.name} className="w-full h-full object-cover" draggable={false} />
</div>
) : null}
@@ -615,7 +623,7 @@ const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) =
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform touch-none"
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
>
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
{isFoil && <FoilOverlay />}

View File

@@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { socketService } from '../../services/SocketService';
import { LogOut, Columns, LayoutTemplate } from 'lucide-react';
import { Modal } from '../../components/Modal';
import { FoilOverlay } from '../../components/CardPreview';
import { FoilOverlay, FloatingPreview } from '../../components/CardPreview';
import { useCardTouch } from '../../utils/interaction';
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
@@ -22,7 +22,7 @@ const PoolDroppable = ({ children, className, style }: any) => {
});
return (
<div ref={setNodeRef} className={`${className} ${isOver ? 'ring-4 ring-emerald-500/50 bg-emerald-900/20' : ''}`} style={style}>
<div ref={setNodeRef} className={`${className} ${isOver ? 'ring-4 ring-emerald-500/50 bg-emerald-900/20' : ''}`} style={{ ...style, touchAction: 'none' }}>
{children}
</div>
);
@@ -128,7 +128,12 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
useSensor(TouchSensor, { activationConstraint: { distance: 10 } })
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);
const [draggedCard, setDraggedCard] = useState<any>(null);
@@ -408,20 +413,36 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
{/* Drag Overlay */}
<DragOverlay dropAnimation={null}>
{draggedCard ? (
<div className="w-32 h-44 opacity-90 rotate-3 cursor-grabbing shadow-2xl">
<div
className="opacity-90 rotate-3 cursor-grabbing shadow-2xl rounded-xl"
style={{ width: `${14 * cardScale}rem`, aspectRatio: '2.5/3.5' }}
>
<img src={draggedCard.image} alt={draggedCard.name} className="w-full h-full object-cover rounded-xl" draggable={false} />
</div>
) : null}
</DragOverlay>
</DndContext>
{/* Mobile Full Screen Preview (triggered by 2-finger long press) */}
{
hoveredCard && (
<div className="lg:hidden">
<FloatingPreview card={hoveredCard} x={0} y={0} isMobile={true} />
</div>
)
}
</div >
);
};
const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any) => {
const card = normalizeCard(rawCard);
const isFoil = card.finish === 'foil';
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => handlePick(card.id), card);
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => {
// Disable tap-to-pick on touch devices, rely on Drag and Drop
if (window.matchMedia('(pointer: coarse)').matches) return;
handlePick(card.id);
}, card);
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
id: card.id,
@@ -433,19 +454,33 @@ const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any)
opacity: isDragging ? 0 : 1, // Hide original when dragging
} : undefined;
// Merge listeners to avoid overriding dnd-kit's TouchSensor
const mergedListeners = {
...listeners,
onTouchStart: (e: any) => {
listeners?.onTouchStart?.(e);
onTouchStart(e);
},
onTouchEnd: (e: any) => {
listeners?.onTouchEnd?.(e);
onTouchEnd(e);
},
onTouchMove: (e: any) => {
listeners?.onTouchMove?.(e);
onTouchMove();
}
};
return (
<div
ref={setNodeRef}
style={{ ...style, width: `${14 * cardScale}rem` }}
{...listeners}
{...attributes}
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer touch-none"
{...mergedListeners}
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
onClick={onClick}
onMouseEnter={() => setHoveredCard(card)}
onMouseLeave={() => setHoveredCard(null)}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
>
{/* Foil Glow Effect */}
{isFoil && <div className="absolute inset-0 -m-1 rounded-xl bg-purple-500 blur-md opacity-20 group-hover:opacity-60 transition-opacity duration-300 animate-pulse"></div>}
@@ -465,7 +500,9 @@ const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any)
};
const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => {
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => { }, card);
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => {
if (window.matchMedia('(pointer: coarse)').matches) return;
}, card);
return (
<div

View File

@@ -61,3 +61,41 @@
50% { background-position: 100% 50%, 100% 100%; }
100% { background-position: 0% 50%, 0% 0%; }
}
/* Global interaction resets */
body {
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
img {
-webkit-user-drag: none;
user-drag: none;
}
/* Allow selection in inputs and textareas */
input, textarea {
user-select: text;
-webkit-user-select: text;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0f172a; /* slate-900 */
}
::-webkit-scrollbar-thumb {
background: #334155; /* slate-700 */
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #475569; /* slate-600 */
}

View File

@@ -1,10 +1,10 @@
import { useRef, useCallback } from 'react';
/**
* Hook to handle touch interactions for cards (Long Press for Preview).
* - Tap: Click
* - Long Press: Preview (Hover)
* - Drag/Scroll: Cancel
* Hook to handle touch interactions for cards.
* - Tap: Click (can be disabled by caller)
* - 1-Finger Long Press: Drag (handled externally by dnd-kit usually, so we ignore here)
* - 2-Finger Long Press: Preview (onHover)
*/
export function useCardTouch(
onHover: (card: any | null) => void,
@@ -13,40 +13,48 @@ export function useCardTouch(
) {
const timerRef = useRef<any>(null);
const isLongPress = useRef(false);
const touchStartCount = useRef(0);
const handleTouchStart = useCallback(() => {
const handleTouchStart = useCallback((e: React.TouchEvent) => {
touchStartCount.current = e.touches.length;
isLongPress.current = false;
// Only Start "Preview" Timer if 2 fingers
if (e.touches.length === 2) {
timerRef.current = setTimeout(() => {
isLongPress.current = true;
onHover(cardPayload);
}, 400); // 400ms threshold
}
}, [onHover, cardPayload]);
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
if (timerRef.current) clearTimeout(timerRef.current);
// If it was a 2-finger long press, clear hover on release
if (isLongPress.current) {
if (e.cancelable) e.preventDefault();
onHover(null); // Clear preview on release, mimicking "hover out"
onHover(null);
isLongPress.current = false;
return;
}
}, [onHover]);
const handleTouchMove = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
// If we were already previewing?
// If user moves finger while holding, maybe we should effectively cancel the "click" potential too?
// Usually moving means scrolling.
isLongPress.current = false; // ensure we validly cancel any queued longpress action
isLongPress.current = false;
}
}, []);
const handleClick = useCallback((e: React.MouseEvent) => {
// If it was a long press, block click
if (isLongPress.current) {
e.preventDefault();
e.stopPropagation();
return;
}
// Simple click
onClick();
}, [onClick]);