diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 46f4f23..ab63bc0 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -38,3 +38,6 @@ - [Cube Full Width Layout](./devlog/2025-12-17-003500_cube_full_width.md): Completed. Updated Cube Manager to use the full screen width. - [Cube Archidekt View](./devlog/2025-12-17-004500_archidekt_view.md): Completed. Implemented column-based stacked view for packs. - [Cube Mobile UI Fixes](./devlog/2025-12-17-005000_mobile_ui_fixes.md): Completed. Fixed overlapping elements and disabled hover previews on mobile. +- [Mobile Long-Press Preview](./devlog/2025-12-17-005500_mobile_long_press.md): Completed. Implemented long-press trigger for card magnification on small screens. +- [Mobile Fullscreen Preview](./devlog/2025-12-17-010000_mobile_fullscreen_preview.md): Completed. Updated mobile preview to be a centered fullscreen overlay. +- [Mobile Preview Animations](./devlog/2025-12-17-010500_mobile_preview_animations.md): Completed. Implemented phase-in layout and phase-out animations for mobile preview. diff --git a/docs/development/devlog/2025-12-17-005500_mobile_long_press.md b/docs/development/devlog/2025-12-17-005500_mobile_long_press.md new file mode 100644 index 0000000..b7c343d --- /dev/null +++ b/docs/development/devlog/2025-12-17-005500_mobile_long_press.md @@ -0,0 +1,15 @@ +# Mobile Long-Press Card Preview + +## Objective +Enhance mobile usability by allowing users to view a magnified card preview upon long-pressing (500ms) a card, instead of hover (which is disabled on mobile). + +## Changes +- Modified `src/client/src/components/CardPreview.tsx`: + - Updated `CardHoverWrapper` to include `touchstart`, `touchend`, and `touchmove` handlers. + - Implemented a 500ms timer on touch start. + - Added logic to cancel the long-press if the user drags/scrolls more than 10 pixels. + - Added `onContextMenu` handler to prevent the default browser menu when a long-press triggers the preview. + - Updated render condition to show preview if `isHovering` (desktop) OR `isLongPressing` (mobile). + +## Result +On mobile devices, users can now press and hold on a card to see the full-size preview. Lifting the finger or scrolling hides the preview. diff --git a/docs/development/devlog/2025-12-17-010000_mobile_fullscreen_preview.md b/docs/development/devlog/2025-12-17-010000_mobile_fullscreen_preview.md new file mode 100644 index 0000000..9acae7c --- /dev/null +++ b/docs/development/devlog/2025-12-17-010000_mobile_fullscreen_preview.md @@ -0,0 +1,16 @@ +# Mobile Fullscreen Preview + +## Objective +Update the mobile card preview mechanism to display a centered, fullscreen overlay upon long-press, rather than a floating element following the touch point. This provides a clearer view of the card on small screens. + +## Changes +- Modified `src/client/src/components/CardPreview.tsx`: + - Updated `FloatingPreview` interface to accept `isMobile: boolean`. + - Added conditional rendering in `FloatingPreview`: + - If `isMobile` is true, it renders a `fixed inset-0` overlay with a centered image, `backdrop-blur`, and entrance animations (`zoom-in` + `fade-in`). + - If false (desktop), it retains the original cursor-following behavior. + - Updated `CardHoverWrapper` to pass the `isMobile` state down to the preview component. + - The preview automatically disappears (unmounts) when the long-press is released, effectively creating a "fade out/close" interaction (visually, the instant close is standard; entrance is animated). + +## Result +Long-pressing a card on mobile now brings up a high-quality, centered view of the card that dims the background, improving readability and usability. diff --git a/docs/development/devlog/2025-12-17-010500_mobile_preview_animations.md b/docs/development/devlog/2025-12-17-010500_mobile_preview_animations.md new file mode 100644 index 0000000..0817309 --- /dev/null +++ b/docs/development/devlog/2025-12-17-010500_mobile_preview_animations.md @@ -0,0 +1,18 @@ +# Mobile Preview Animations + +## Objective +Implement smooth "Phase In" and "Phase Out" animations for the mobile fullscreen card preview to replace the instant appear/disappear behavior. + +## Changes +- Modified `src/client/src/components/CardPreview.tsx`: + - Updated `CardHoverWrapper` to handle component unmounting with a delay (300ms) when the preview should be hidden on mobile. + - Passed a new `isClosing` prop to `FloatingPreview` during this delay period. + - In `FloatingPreview` (Mobile View): + - Added `transition-all duration-300` base classes. + - Used conditional classes: + - Entrance: `animate-in fade-in zoom-in-95` + - Exit: `animate-out fade-out zoom-out-95` (triggered when `isClosing` is true). + - Fixed syntax errors introduced in previous steps (removed spaces in class names). + +## Result +On mobile, the card preview now fades and zooms in smoothly when long-pressed, and fades/zooms out smoothly when released. diff --git a/src/client/src/components/CardPreview.tsx b/src/client/src/components/CardPreview.tsx index ba383fd..ddabc93 100644 --- a/src/client/src/components/CardPreview.tsx +++ b/src/client/src/components/CardPreview.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { DraftCard } from '../services/PackGeneratorService'; // --- Floating Preview Component --- -export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number }> = ({ card, x, y }) => { +export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number; isMobile?: boolean; isClosing?: boolean }> = ({ card, x, y, isMobile, isClosing }) => { const isFoil = card.finish === 'foil'; const imgRef = useRef(null); @@ -10,6 +10,8 @@ export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number } const [adjustedPos, setAdjustedPos] = useState({ top: y, left: x }); useEffect(() => { + if (isMobile) return; + const OFFSET = 20; const CARD_WIDTH = 300; const CARD_HEIGHT = 420; @@ -27,7 +29,18 @@ export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number } setAdjustedPos({ top: newY, left: newX }); - }, [x, y]); + }, [x, y, isMobile]); + + if (isMobile) { + return ( +
+
+ {card.name} + {isFoil &&
} +
+
+ ); + } return (
= ({ card, children, className }) => { const [isHovering, setIsHovering] = useState(false); - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); + const [isLongPressing, setIsLongPressing] = useState(false); + const [renderPreview, setRenderPreview] = useState(false); + const [coords, setCoords] = useState({ x: 0, y: 0 }); + + const timerRef = useRef(null); + const initialTouchRef = useRef<{ x: number, y: number } | null>(null); + const closeTimerRef = useRef(null); const hasImage = !!card.image; + // Use a stable value for isMobile to avoid hydration mismatches if using SSR, + // but since this is client-side mostly, window check is okay. + const isMobile = typeof window !== 'undefined' && window.innerWidth < 1024; + + const shouldShow = (isHovering && !isMobile) || isLongPressing; + + // Handle mounting/unmounting animation + useEffect(() => { + if (shouldShow) { + if (closeTimerRef.current) clearTimeout(closeTimerRef.current); + setRenderPreview(true); + } else { + // Delay unmount for mobile animation + if (isMobile && renderPreview) { + closeTimerRef.current = setTimeout(() => { + setRenderPreview(false); + }, 300); // 300ms matches duration-300 + } else { + setRenderPreview(false); + } + } + return () => { + if (closeTimerRef.current) clearTimeout(closeTimerRef.current); + }; + }, [shouldShow, isMobile, renderPreview]); const handleMouseMove = (e: React.MouseEvent) => { - if (!hasImage) return; - setMousePos({ x: e.clientX, y: e.clientY }); + if (!hasImage || isMobile) return; + setCoords({ x: e.clientX, y: e.clientY }); }; - const isMobile = typeof window !== 'undefined' && window.innerWidth < 1024; // Disable on tablet/mobile + const handleMouseEnter = () => { + if (!isMobile) setIsHovering(true); + }; + + const handleMouseLeave = () => { + setIsHovering(false); + }; + + const handleTouchStart = (e: React.TouchEvent) => { + if (!hasImage || !isMobile) return; + const touch = e.touches[0]; + const { clientX, clientY } = touch; + + initialTouchRef.current = { x: clientX, y: clientY }; + setCoords({ x: clientX, y: clientY }); + + timerRef.current = setTimeout(() => { + setIsLongPressing(true); + }, 500); + }; + + const handleTouchEnd = () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + setIsLongPressing(false); + initialTouchRef.current = null; + }; + + const handleTouchMove = (e: React.TouchEvent) => { + if (!initialTouchRef.current) return; + + const touch = e.touches[0]; + const moveX = Math.abs(touch.clientX - initialTouchRef.current.x); + const moveY = Math.abs(touch.clientY - initialTouchRef.current.y); + + // Cancel if moved more than 10px + if (moveX > 10 || moveY > 10) { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + setIsLongPressing(false); + } + }; return (
setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} onMouseMove={handleMouseMove} + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} + onTouchMove={handleTouchMove} + onContextMenu={(e) => { + // Prevent context menu if we are long pressing to view card + if (isLongPressing) { + e.preventDefault(); + e.stopPropagation(); + } + }} > {children} - {isHovering && hasImage && !isMobile && ( - + {hasImage && renderPreview && ( + )}
);