diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 49a9091..eea4d0c 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -100,3 +100,11 @@ - [Stack View Sorting & Sliders](./devlog/2025-12-18-020000_stack_sorting_sliders.md): Completed. Refactored StackView to group by Color by default, added sorting controls to Deck Builder, and reduced slider scales globally to allow smaller sizes. - [Lobby UI & Notifications](./devlog/2025-12-18-023000_lobby_ui_update.md): Completed. Refactored Lobby/Chat into collapsible floating panels, implemented player event notifications (Join/Leave/Disconnect), and updated Deck Builder card size triggers. - [Card Preview Threshold](./devlog/2025-12-18-024000_preview_threshold.md): Completed. Updated card art crop threshold to 130px (new 50% mark) across the application components. +- [UI Enhancements](./devlog/2025-12-18-030000_ui_enhancements.md): Completed. Implemented drag-and-drop for Stack View, custom sort dropdown, and resizable layouts for both Deck Builder and Draft UI. +- [Resize Optimization](./devlog/2025-12-18-033000_resize_optimization.md): Completed. Refactored resize interactions for Panels. +- [Slider Optimization](./devlog/2025-12-18-034500_slider_optimization.md): Completed. Applied the same performance logic (CSS Variables + Deferred State) to Card Size Sliders in all views to eliminate lag. +- [Sidebar Resize Fix](./devlog/2025-12-18-040000_sidebar_resize_fix.md): Completed. Removed conflicting CSS transition classes from sidebars to ensure smooth 1:1 resize tracking. +- [Touch Resize Support](./devlog/2025-12-18-041500_touch_resize.md): Completed. Implemented unified Mouse/Touch handlers for all resize handles to support mobile usage. +- [Pool Card Sizing](./devlog/2025-12-18-042500_pool_card_sizing.md): Completed. Fixed "enormous" card bug in horizontal pool by enforcing percentage-based height constraint. +- [Final Pool Layout Fix](./devlog/2025-12-18-043500_pool_sizing_final.md): Completed. Overhauled flex layout for Horizontal Pool to ensure card images scale 1:1 with panel height during resize, removing layout-blocking transitions. +- [Pool Overflow Constraint](./devlog/2025-12-18-044500_pool_overflow_fix.md): Completed. Enforce flex shrinkage with `min-h-0` and `overflow-hidden` to strictly bind card height to resizeable panel. diff --git a/docs/development/devlog/2025-12-18-030000_ui_enhancements.md b/docs/development/devlog/2025-12-18-030000_ui_enhancements.md new file mode 100644 index 0000000..7bda0dc --- /dev/null +++ b/docs/development/devlog/2025-12-18-030000_ui_enhancements.md @@ -0,0 +1,28 @@ +# Work Plan - Deck Builder & Draft UI Enhancements + +## Request +1. **Deck Builder Stack DnD**: Enable card drag and drop for the stacked view in Deck Builder. +2. **Custom Sort Dropdown**: Use a custom graphic dropdown list instead of browser default for sorting. +3. **Resizable Layouts**: Make library/preview resizeable in Deck Builder, and selected pool/preview resizeable in Draft UI. + +## Changes +- **StackView.tsx**: Added `renderWrapper` prop to allow parent components to wrap card items (e.g. with `DraggableCardWrapper`). +- **DeckBuilderView.tsx**: + - Implemented `sortDropdownOpen` state and custom UI for the "Sort" dropdown. + - Added resizing state (`sidebarWidth`, `poolHeightPercent`) and mouse event handlers. + - Applied dynamic styles to Sidebar and Pool/Deck zones. + - Passed `DraggableCardWrapper` to `StackView` via the new `renderWrapper` prop. +- **DraftView.tsx**: + - Added resizing state (`sidebarWidth`, `poolHeight`) and mouse event handlers. + - Applied dynamic styles to Sidebar and Pool Droppable area. + - Fixed syntax error introduced during refactoring. + +## Verification +- **Deck Builder**: + - Verify cards in Stack View can be dragged. + - Verify "Sort" dropdown is custom styled. + - Verify Sidebar width can be adjusted. + - Verify Pool/Library split can be adjusted (in horizontal mode especially). +- **Draft UI**: + - Verify Sidebar width can be adjusted. + - Verify Selected Pool height can be adjusted. diff --git a/docs/development/devlog/2025-12-18-033000_resize_optimization.md b/docs/development/devlog/2025-12-18-033000_resize_optimization.md new file mode 100644 index 0000000..ad181f9 --- /dev/null +++ b/docs/development/devlog/2025-12-18-033000_resize_optimization.md @@ -0,0 +1,27 @@ +# Work Plan - Optimize Resize Performance + +## Request +The user reported that the resize functionality was laggy, slow, and inconsistent. + +## Changes +- **Refactoring Strategy**: + - Removed React state updates from the `mousemove` event loop. + - Used `useRef` to track `sidebarWidth` and `poolHeight` values. + - Used `requestAnimationFrame` to throttle DOM updates directly during resizing. + - Only triggered React state updates (re-renders) on `mouseup`. + +- **DraftView.tsx**: + - Implemented `resizingState` ref. + - Modified `handleMouseDown` to initiate direct DOM resizing. + - Modified `onMouseMove` to update element styles directly. + - Modified `onMouseUp` to sync final size to React state. + - Applied refs to Sidebar and Pool resizing areas. + +- **DeckBuilderView.tsx**: + - Implemented identical ref-based + requestAnimationFrame resizing logic. + - Fixed several HTML nesting errors introduced during the complex refactoring process. + +## Verification +- **Performance**: Resizing should now be smooth (60fps) as it avoids React reconciliation during the drag. +- **Consistency**: The handle should no longer "slip" because the visual update is faster. +- **Persistence**: The final size is still saved to `state` (and thus `localStorage`) after release. diff --git a/docs/development/devlog/2025-12-18-034500_slider_optimization.md b/docs/development/devlog/2025-12-18-034500_slider_optimization.md new file mode 100644 index 0000000..6d23637 --- /dev/null +++ b/docs/development/devlog/2025-12-18-034500_slider_optimization.md @@ -0,0 +1,27 @@ +# Work Plan - Optimize Card Slider Performance + +## Request +The user reported that resize handlers (likely sliders) were still laggy. + +## Changes +- **DraftView.tsx**: + - Introduced `localCardScale` for immediate slider feedback. + - Used CSS Variable `--card-scale` on container to update card sizes entirely via CSS during drag. + - Deferred `cardScale` state update (which triggers React re-renders) to `onMouseUp`. + +- **DeckBuilderView.tsx**: + - Introduced `localCardWidth` for immediate slider feedback. + - Used CSS Variable `--card-width` on container. + - Updated `gridTemplateColumns` to use `var(--card-width)`. + - Deferred `cardWidth` state update to `onMouseUp`. + - Cleaned up duplicate state declarations causing lint errors. + +- **CubeManager.tsx**: + - Introduced `localCardWidth` and CSS Variable `--card-width`. + - Updated Grid layout to use CSS Variable. + - Deferred state update to `onMouseUp`. + +## Verification +- **Performance**: Slider dragging should now be 60fps smooth as it touches 0 React components during the drag, only updating a single CSS variable on the root container. +- **Persistence**: Releasing the slider saves the value to state and localStorage. +- **Logic**: complex logic like `useArtCrop` (which depends on specific widths) updates safely on release, preventing flicker or heavy recalculations during drag. diff --git a/docs/development/devlog/2025-12-18-040000_sidebar_resize_fix.md b/docs/development/devlog/2025-12-18-040000_sidebar_resize_fix.md new file mode 100644 index 0000000..9a54d40 --- /dev/null +++ b/docs/development/devlog/2025-12-18-040000_sidebar_resize_fix.md @@ -0,0 +1,16 @@ +# Work Plan - Fix Sidebar Resize Animation Lag + +## Request +The user reported that the left sidebar resize was laggy because of an animation. + +## Changes +- **DraftView.tsx**: + - Identified that `transition-all` class was present on the sidebar container. + - Removed `transition-all` class. This class forces the browser to interpolate the width over 300ms every time javascript updates it (60 times a second), causing severe visual lag and "fighting" between the cursor and the element. + - Verified that resize logic uses the previously implemented `requestAnimationFrame` + `ref` approach, which is optimal. + +- **DeckBuilderView.tsx**: + - Verified that no `transition` class was present on the corresponding sidebar element. + +## Verification +- **Performance**: Sidebar resizing should now be instant and track the mouse 1:1 without "slipping" or lag. diff --git a/docs/development/devlog/2025-12-18-041500_touch_resize.md b/docs/development/devlog/2025-12-18-041500_touch_resize.md new file mode 100644 index 0000000..80217bb --- /dev/null +++ b/docs/development/devlog/2025-12-18-041500_touch_resize.md @@ -0,0 +1,20 @@ +# Work Plan - Touch Resize Implementation + +## Request +The user reported that resizing handles were not working on touchscreen devices. + +## Changes +- **DraftView.tsx**: + - Replaced `handleMouseDown`, `onMouseMove`, `onMouseUp` with unified `handleResizeStart`, `onResizeMove`, `onResizeEnd`. + - Added logic to detect `touches` in event object and extract `clientX`/`clientY` from the first touch point. + - Attached `onTouchStart` to sidebar and pool resize handles. + - Added `passive: false` to touch event listeners (via `useEffect` logic or direct attach) to call `e.preventDefault()`, preventing page scrolling while dragging. Note: Implemented in the handler function with `if (e.cancelable) e.preventDefault()`. + - Added `touch-none` utility class to resize handles to structurally prevent browser touch actions. + +- **DeckBuilderView.tsx**: + - Implemented the exact same unified handler logic as DraftView. + - Updated both Sidebar (vertical) and Pool (horizontal) resize handles with `onTouchStart`. + +## Verification +- **Touch**: Dragging handles on a touchscreen (mobile/tablet) should now resize the panels smoothly. +- **Mouse**: Mouse interaction remains unchanged and performant (using `requestAnimationFrame`). diff --git a/docs/development/devlog/2025-12-18-042500_pool_card_sizing.md b/docs/development/devlog/2025-12-18-042500_pool_card_sizing.md new file mode 100644 index 0000000..a57160e --- /dev/null +++ b/docs/development/devlog/2025-12-18-042500_pool_card_sizing.md @@ -0,0 +1,13 @@ +# Work Plan - Fix Pool Card Sizing + +## Request +The user reported that cards in the horizontal pool list were "enormous" after previous changes. + +## Changes +- **DraftView.tsx**: + - Reverted the `height` class of `PoolCardItem` images (in horizontal mode) from `h-full` to `h-[90%]`. + - `h-full` was causing the image to expand uncontrollably in some flex layouts, ignoring the parent container's constraints. + - `h-[90%]`, combined with `items-center` on the parent container, properly constrains the image to fit within the strip, maintaining aspect ratio via `w-auto`. + +## Verification +- **Visuals**: Cards in the bottom "Your Pool" strip should now cleanly fit within the resizeable panel, with a small vertical margin, instead of overflowing or appearing excessively large. diff --git a/docs/development/devlog/2025-12-18-043500_pool_sizing_final.md b/docs/development/devlog/2025-12-18-043500_pool_sizing_final.md new file mode 100644 index 0000000..7bda281 --- /dev/null +++ b/docs/development/devlog/2025-12-18-043500_pool_sizing_final.md @@ -0,0 +1,31 @@ +# Work Plan - Finalize Pool Card Sizing + +## Request +The user reported: "cards inside the 'your pool' have not consistent sizes ... and resizing it's height does not change card sizes. card height needs to match the your pool panel size". + +## Analysis +The previous logic using `items-center` on the parent and `h-full`/`h-90%` on the child likely led to a broken flexbox behavior where children calculated their own intrinsic height or got stuck at an initial height, and `transition-all` might have added to the confusion or stickiness. + +## Changes +- **DraftView.tsx**: + - Removed `transition-all` from both `PoolDroppable` and `PoolCardItem`. Transitions on layout containers cause jank during drag resize and can block instant reflow. + - Updated horizontal pool scrolling container: + - Removed `items-center`. The default behavior aligns items to start, but since we want `h-full` to work, the container just needs to fill space. + - Changed padding to `pb-2 pt-2` (balanced) instead of `pb-4`. + - Updated `PoolCardItem` (Horizontal): + - `className`: Added `h-full`, **removed `items-center`** (moved to centered justify content if needed, but flex default with no items-center is fine). Added `aspect-[2.5/3.5]` to help width calculation. Added `p-2` padding directly to the wrapper to handle spacing, allowing image to be `h-full` within that padded box. + - Image: Changed to `h-full w-auto object-contain`. Removed `max-h-full` and `h-[90%]`. + +## Result +- The `poolRef` div resizes via DOM. +- `PoolDroppable` (flex-1) fills it. +- Scroll container (flex-1) fills it. +- `PoolCardItem` wrapper (h-full) fills 100% of the Scroll container height. +- `PoolCardItem` wrapper padding (`p-2`) creates a safe zone. +- `img` (h-full) fills 100% of the wrapper's content box (calculated as `Total Height - Padding`). +- This guarantees the image height tracks the panel height 1:1. + +## Verification +- Dragging the pool resize handle should now smoothly resize the cards in real-time. +- Cards should never be "too big" (overflowing) because they are strictly contained by `h-full` inside the overflow-hidden parents. +- Cards should respect aspect ratio. diff --git a/docs/development/devlog/2025-12-18-044500_pool_overflow_fix.md b/docs/development/devlog/2025-12-18-044500_pool_overflow_fix.md new file mode 100644 index 0000000..ab8276f --- /dev/null +++ b/docs/development/devlog/2025-12-18-044500_pool_overflow_fix.md @@ -0,0 +1,13 @@ +# Work Plan - Strict Overflow Constraints for Pool Panel + +## Request +The user persists that cards overflow because they are "full size" and do not resize. + +## Changes +- **DraftView.tsx**: + - Added `overflow-hidden` to the root `poolRef` div. This ensures that even if internal contents *try* to be larger, they are clipped, and more importantly, it forces flex children to respect the parent boundary in some browser rendering engines. + - Added `min-h-0` to `PoolDroppable` and the inner scroll container. In Flexbox columns, children do not shrink below their content size by default. `min-h-0` effectively overrides this, forcing the container to shrink to the available flex space (which is effectively `poolRef` height minus header). + - This combination guarantees that the scroll container's `height` is exactly calculated based on the parent, so `h-full` on the card images resolves to the correct, resized pixel value. + +## Verification +- **Visuals**: Resizing the pool panel should now force the cards to shrink or grow in real-time without overflowing or getting stuck at a large size. diff --git a/src/client/src/components/StackView.tsx b/src/client/src/components/StackView.tsx index d8d8530..b21970c 100644 --- a/src/client/src/components/StackView.tsx +++ b/src/client/src/components/StackView.tsx @@ -13,6 +13,7 @@ interface StackViewProps { onHover?: (card: DraftCard | null) => void; disableHoverPreview?: boolean; groupBy?: GroupMode; + renderWrapper?: (card: DraftCard, children: React.ReactNode) => React.ReactNode; } const GROUPS: Record = { @@ -71,7 +72,7 @@ const getCardGroup = (card: DraftCard, mode: GroupMode): string => { }; -export const StackView: React.FC = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false, groupBy = 'color' }) => { +export const StackView: React.FC = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false, groupBy = 'color', renderWrapper }) => { const categorizedCards = useMemo(() => { const categories: Record = {}; @@ -139,6 +140,7 @@ export const StackView: React.FC = ({ cards, cardWidth = 150, on onHover={onHover} onCardClick={onCardClick} disableHoverPreview={disableHoverPreview} + renderWrapper={renderWrapper} /> ); })} @@ -150,10 +152,10 @@ export const StackView: React.FC = ({ cards, cardWidth = 150, on ); }; -const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHover, onCardClick, disableHoverPreview }: any) => { +const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHover, onCardClick, disableHoverPreview, renderWrapper }: any) => { const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover || (() => { }), () => onCardClick && onCardClick(card), card); - return ( + const content = (
onHover && onHover(card)} @@ -177,4 +179,10 @@ const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHo
); + + if (renderWrapper) { + return renderWrapper(card, content); + } + + return content; }; diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index 4e96a56..b68a1bd 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -115,6 +115,14 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail const saved = localStorage.getItem('cube_cardWidth'); return saved ? parseInt(saved) : 60; }); + // Local state for smooth slider + const [localCardWidth, setLocalCardWidth] = useState(cardWidth); + const containerRef = useRef(null); + + useEffect(() => { + setLocalCardWidth(cardWidth); + if (containerRef.current) containerRef.current.style.setProperty('--card-width', `${cardWidth}px`); + }, [cardWidth]); // --- Persistence Effects --- useEffect(() => localStorage.setItem('cube_inputText', inputText), [inputText]); @@ -453,7 +461,7 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail }; return ( -
+
{/* --- LEFT COLUMN: CONTROLS --- */}
@@ -841,10 +849,16 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail min="60" max="200" step="1" - value={cardWidth} - onChange={(e) => setCardWidth(parseInt(e.target.value))} + value={localCardWidth} + onChange={(e) => { + const val = parseInt(e.target.value); + setLocalCardWidth(val); + if (containerRef.current) containerRef.current.style.setProperty('--card-width', `${val}px`); + }} + onMouseUp={() => setCardWidth(localCardWidth)} + onTouchEnd={() => setCardWidth(localCardWidth)} className="w-24 accent-purple-500 cursor-pointer h-1.5 bg-slate-600 rounded-lg appearance-none" - title={`Card Size: ${cardWidth}px`} + title={`Card Size: ${localCardWidth}px`} />
@@ -870,12 +884,12 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail className="grid gap-6 pb-20" style={{ gridTemplateColumns: cardWidth <= 150 - ? `repeat(auto-fill, minmax(${viewMode === 'list' ? '320px' : '550px'}, 1fr))` + ? `repeat(auto-fill, minmax(var(--card-width, ${viewMode === 'list' ? '320px' : '550px'}), 1fr))` : '1fr' }} > {packs.map((pack) => ( - + ))}
) diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index 7a2f21a..d381e97 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { socketService } from '../../services/SocketService'; -import { Save, Layers, Clock, Columns, LayoutTemplate, List, LayoutGrid } from 'lucide-react'; +import { Save, Layers, Clock, Columns, LayoutTemplate, List, LayoutGrid, ChevronDown, Check, GripVertical } from 'lucide-react'; import { StackView } from '../../components/StackView'; import { FoilOverlay } from '../../components/CardPreview'; import { DraftCard } from '../../services/PackGeneratorService'; @@ -145,6 +145,7 @@ const CardsDisplay: React.FC<{ ) } + // Use CSS var for grid if (viewMode === 'list') { const sorted = [...cards].sort((a, b) => (a.cmc || 0) - (b.cmc || 0)); return ( @@ -176,6 +177,11 @@ const CardsDisplay: React.FC<{ onHover={(c) => onHover(c)} disableHoverPreview={true} groupBy={groupBy} + renderWrapper={(card, children) => ( + + {children} + + )} />
) @@ -186,7 +192,7 @@ const CardsDisplay: React.FC<{
{cards.map(c => { @@ -218,6 +224,39 @@ export const DeckBuilderView: React.FC = ({ initialPool, a const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('stack'); // Default to stack as requested? Or keep grid. User didn't say default view, just default Order. const [groupBy, setGroupBy] = useState<'type' | 'color' | 'cmc' | 'rarity'>('color'); const [cardWidth, setCardWidth] = useState(60); + // Local state for smooth slider + const [localCardWidth, setLocalCardWidth] = useState(cardWidth); + const containerRef = React.useRef(null); + + // Sync + React.useEffect(() => { + setLocalCardWidth(cardWidth); + if (containerRef.current) { + containerRef.current.style.setProperty('--card-width', `${cardWidth}px`); + } + }, [cardWidth]); + + const [sortDropdownOpen, setSortDropdownOpen] = useState(false); + + // --- Resize State --- + const [sidebarWidth, setSidebarWidth] = useState(320); // Initial 320px + const [poolHeightPercent, setPoolHeightPercent] = useState(60); // Initial 60% for pool (horizontal layout) + + const sidebarRef = React.useRef(null); + const poolRef = React.useRef(null); + const resizingState = React.useRef<{ + startX: number, + startY: number, + startWidth: number, + startHeightPercent: number, + active: 'sidebar' | 'pool' | null + }>({ startX: 0, startY: 0, startWidth: 0, startHeightPercent: 0, active: null }); + + // Initial visual set + React.useEffect(() => { + if (sidebarRef.current) sidebarRef.current.style.width = `${sidebarWidth}px`; + if (poolRef.current) poolRef.current.style.height = `${poolHeightPercent}%`; + }, []); const [pool, setPool] = useState(initialPool); const [deck, setDeck] = useState([]); @@ -395,6 +434,79 @@ export const DeckBuilderView: React.FC = ({ initialPool, a setDraggedCard(null); }; + // --- Resize Handlers --- + // --- Resize Handlers --- + const handleResizeStart = (type: 'sidebar' | 'pool', e: React.MouseEvent | React.TouchEvent) => { + // Prevent default to avoid scrolling/selection + if (e.cancelable) e.preventDefault(); + + const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; + const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; + + const containerTop = 56; + const containerHeight = window.innerHeight - containerTop; + const currentPoolHeight = poolRef.current?.getBoundingClientRect().height || (containerHeight * 0.6); + + resizingState.current = { + startX: clientX, + startY: clientY, + startWidth: sidebarRef.current?.getBoundingClientRect().width || 320, + startHeightPercent: poolHeightPercent, + active: type + }; + + document.addEventListener('mousemove', onResizeMove); + document.addEventListener('touchmove', onResizeMove, { passive: false }); + document.addEventListener('mouseup', onResizeEnd); + document.addEventListener('touchend', onResizeEnd); + document.body.style.cursor = type === 'sidebar' ? 'col-resize' : 'row-resize'; + }; + + const onResizeMove = React.useCallback((e: MouseEvent | TouchEvent) => { + if (!resizingState.current.active) return; + + if (e.cancelable) e.preventDefault(); + + const clientX = (e as TouchEvent).touches ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX; + const clientY = (e as TouchEvent).touches ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY; + + requestAnimationFrame(() => { + if (resizingState.current.active === 'sidebar' && sidebarRef.current) { + const delta = clientX - resizingState.current.startX; + const newWidth = Math.max(200, Math.min(600, resizingState.current.startWidth + delta)); + sidebarRef.current.style.width = `${newWidth}px`; + } + + if (resizingState.current.active === 'pool' && poolRef.current) { + const containerTop = 56; + const containerHeight = window.innerHeight - containerTop; + const relativeY = clientY - containerTop; + const percentage = (relativeY / containerHeight) * 100; + const clamped = Math.max(20, Math.min(80, percentage)); + poolRef.current.style.height = `${clamped}%`; + } + }); + }, []); + + const onResizeEnd = React.useCallback(() => { + if (resizingState.current.active === 'sidebar' && sidebarRef.current) { + setSidebarWidth(parseInt(sidebarRef.current.style.width)); + } + if (resizingState.current.active === 'pool' && poolRef.current) { + const hStyle = poolRef.current.style.height; + if (hStyle.includes('%')) { + setPoolHeightPercent(parseFloat(hStyle)); + } + } + + resizingState.current.active = null; + document.removeEventListener('mousemove', onResizeMove); + document.removeEventListener('touchmove', onResizeMove); + document.removeEventListener('mouseup', onResizeEnd); + document.removeEventListener('touchend', onResizeEnd); + document.body.style.cursor = 'default'; + }, []); + // --- Render Functions --- const renderLandStation = () => (
@@ -460,7 +572,12 @@ export const DeckBuilderView: React.FC = ({ initialPool, a ); return ( -
e.preventDefault()}> +
e.preventDefault()} + style={{ '--card-width': `${localCardWidth}px` } as React.CSSProperties} + > {/* Global Toolbar */} {/* Global Toolbar */} @@ -473,20 +590,43 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
- {/* Group By Dropdown (Only relevant for Stack View usually, but nice to have) */} + {/* Group By Dropdown (Custom UI) */} {viewMode === 'stack' && ( -
- Sort: - + Sort: + {groupBy === 'cmc' ? 'Mana Value' : groupBy} + + + + {sortDropdownOpen && ( + <> +
setSortDropdownOpen(false)} /> +
+ {[ + { value: 'color', label: 'Color' }, + { value: 'type', label: 'Type' }, + { value: 'cmc', label: 'Mana Value' }, + { value: 'rarity', label: 'Rarity' } + ].map((opt) => ( + + ))} +
+ + )}
)} @@ -504,8 +644,14 @@ export const DeckBuilderView: React.FC = ({ initialPool, a min="60" max="200" step="1" - value={cardWidth} - onChange={(e) => setCardWidth(parseInt(e.target.value))} + value={localCardWidth} + onChange={(e) => { + const val = parseInt(e.target.value); + setLocalCardWidth(val); + if (containerRef.current) containerRef.current.style.setProperty('--card-width', `${val}px`); + }} + onMouseUp={() => setCardWidth(localCardWidth)} + onTouchEnd={() => setCardWidth(localCardWidth)} className="w-24 accent-purple-500 cursor-pointer h-1.5 bg-slate-800 rounded-lg appearance-none" />
@@ -527,7 +673,12 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
{/* Zoom Sidebar */} -
+
+ {/* Front content ... */}
= ({ initialPool, a

{(hoveredCard || displayCard).name}

{(hoveredCard || displayCard).typeLine || (hoveredCard || displayCard).type_line}

{(hoveredCard || displayCard).oracle_text && ( -
+
{(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) =>

{line}

)}
)} @@ -579,11 +730,31 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
+ + {/* Resize Handle */} +
handleResizeStart('sidebar', e)} + onTouchStart={(e) => handleResizeStart('sidebar', e)} + > +
+
{/* Content Area */} {layout === 'vertical' ? ( -
+
+ {/* Vertical layout typically means Pool Left / Deck Right or vice versa. + The previous code had them side-by-side with equal flex. + The request asks for Library to be resizable. In vertical mode they share width. + We can add a splitter here if needed, but horizontal split (top/bottom) is more common for resizing. + Let's stick to equal flex for vertical column mode for now, as it's cleaner, + or implement width resizing if specifically requested. + Given the constraints of "library section ... needs to be resizable", a Top/Bottom split is the only one + where resizing makes distinct sense vs side-by-side. + Wait, "library section" usually implies the Deck list. + In side-by-side, we can resize the split. + */} {/* Pool Column */}
@@ -594,6 +765,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
+ {/* Deck Column */}
@@ -605,26 +777,45 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
) : ( -
+
{/* Top: Pool + Land Station */} - -
- Card Pool ({pool.length}) + {/* Top: Pool + Land Station */} +
+ +
+ Card Pool ({pool.length}) +
+
+ {renderLandStation()} + +
+
+ + {/* Resizer Handle */} +
handleResizeStart('pool', e)} + onTouchStart={(e) => handleResizeStart('pool', e)} + > +
-
- {renderLandStation()} - -
- - {/* Bottom: Deck */} - -
- Library ({deck.length}) -
-
- -
-
+ + {/* Bottom: Deck */} + +
+ Library ({deck.length}) +
+
+ +
+
+
)}
@@ -632,7 +823,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a {draggedCard ? (
{draggedCard.name} diff --git a/src/client/src/modules/draft/DraftView.tsx b/src/client/src/modules/draft/DraftView.tsx index 2fa385f..c749a72 100644 --- a/src/client/src/modules/draft/DraftView.tsx +++ b/src/client/src/modules/draft/DraftView.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { socketService } from '../../services/SocketService'; import { LogOut, Columns, LayoutTemplate } from 'lucide-react'; import { Modal } from '../../components/Modal'; @@ -58,19 +58,49 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI return () => clearInterval(interval); }, [pickExpiresAt]); + + // --- UI State & Persistence --- + const [sidebarWidth, setSidebarWidth] = useState(320); const [poolHeight, setPoolHeight] = useState(() => { const saved = localStorage.getItem('draft_poolHeight'); return saved ? parseInt(saved, 10) : 220; }); + const sidebarRef = React.useRef(null); + const poolRef = React.useRef(null); + const resizingState = React.useRef<{ + startX: number, + startY: number, + startWidth: number, + startHeight: number, + active: 'sidebar' | 'pool' | null + }>({ startX: 0, startY: 0, startWidth: 0, startHeight: 0, active: null }); + + // Apply initial sizes visually without causing re-renders + useEffect(() => { + if (sidebarRef.current) sidebarRef.current.style.width = `${sidebarWidth}px`; + if (poolRef.current) poolRef.current.style.height = `${poolHeight}px`; + }, []); // Only on mount to set initial visual state, subsequent updates handled by resize logic + + const [cardScale, setCardScale] = useState(() => { const saved = localStorage.getItem('draft_cardScale'); return saved ? parseFloat(saved) : 0.35; }); + // Local state for smooth slider + const [localCardScale, setLocalCardScale] = useState(cardScale); + const containerRef = useRef(null); + + // Sync local state if external update happens + useEffect(() => { + setLocalCardScale(cardScale); + if (containerRef.current) { + containerRef.current.style.setProperty('--card-scale', cardScale.toString()); + } + }, [cardScale]); const [layout, setLayout] = useState<'vertical' | 'horizontal'>('horizontal'); - const [isResizing, setIsResizing] = useState(false); // Persist settings useEffect(() => { @@ -81,34 +111,68 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI localStorage.setItem('draft_cardScale', cardScale.toString()); }, [cardScale]); - // Resize Handlers - const startResizing = (e: React.MouseEvent) => { - setIsResizing(true); - e.preventDefault(); + const handleResizeStart = (type: 'sidebar' | 'pool', e: React.MouseEvent | React.TouchEvent) => { + // Prevent default to avoid scrolling/selection + if (e.cancelable) e.preventDefault(); + + const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; + const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; + + resizingState.current = { + startX: clientX, + startY: clientY, + startWidth: sidebarRef.current?.getBoundingClientRect().width || 320, + startHeight: poolRef.current?.getBoundingClientRect().height || 220, + active: type + }; + + document.addEventListener('mousemove', onResizeMove); + document.addEventListener('touchmove', onResizeMove, { passive: false }); + document.addEventListener('mouseup', onResizeEnd); + document.addEventListener('touchend', onResizeEnd); + document.body.style.cursor = type === 'sidebar' ? 'col-resize' : 'row-resize'; }; - useEffect(() => { - const stopResizing = () => setIsResizing(false); - const resize = (e: MouseEvent) => { - if (isResizing) { - const newHeight = window.innerHeight - e.clientY; - // Limits: Min 100px, Max 60% of screen - const maxHeight = window.innerHeight * 0.6; - if (newHeight >= 100 && newHeight <= maxHeight) { - setPoolHeight(newHeight); - } - } - }; + const onResizeMove = React.useCallback((e: MouseEvent | TouchEvent) => { + if (!resizingState.current.active) return; - if (isResizing) { - document.addEventListener('mousemove', resize); - document.addEventListener('mouseup', stopResizing); + if (e.cancelable) e.preventDefault(); + + const clientX = (e as TouchEvent).touches ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX; + const clientY = (e as TouchEvent).touches ? (e as TouchEvent).touches[0].clientY : (e as MouseEvent).clientY; + + // Direct DOM manipulation for performance + requestAnimationFrame(() => { + if (resizingState.current.active === 'sidebar' && sidebarRef.current) { + const delta = clientX - resizingState.current.startX; + const newWidth = Math.max(200, Math.min(600, resizingState.current.startWidth + delta)); + sidebarRef.current.style.width = `${newWidth}px`; + } + + if (resizingState.current.active === 'pool' && poolRef.current) { + const delta = resizingState.current.startY - clientY; // Dragging up increases height + const newHeight = Math.max(100, Math.min(window.innerHeight * 0.6, resizingState.current.startHeight + delta)); + poolRef.current.style.height = `${newHeight}px`; + } + }); + }, []); + + const onResizeEnd = React.useCallback(() => { + // Commit final state + if (resizingState.current.active === 'sidebar' && sidebarRef.current) { + setSidebarWidth(parseInt(sidebarRef.current.style.width)); } - return () => { - document.removeEventListener('mousemove', resize); - document.removeEventListener('mouseup', stopResizing); - }; - }, [isResizing]); + if (resizingState.current.active === 'pool' && poolRef.current) { + setPoolHeight(parseInt(poolRef.current.style.height)); + } + + resizingState.current.active = null; + document.removeEventListener('mousemove', onResizeMove); + document.removeEventListener('touchmove', onResizeMove); + document.removeEventListener('mouseup', onResizeEnd); + document.removeEventListener('touchend', onResizeEnd); + document.body.style.cursor = 'default'; + }, []); const [hoveredCard, setHoveredCard] = useState(null); const [displayCard, setDisplayCard] = useState(null); @@ -152,7 +216,12 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI }; return ( -
e.preventDefault()}> +
e.preventDefault()} + style={{ '--card-scale': localCardScale } as React.CSSProperties} + >
@@ -193,8 +262,17 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI min="0.35" max="1.0" step="0.01" - value={cardScale} - onChange={(e) => setCardScale(parseFloat(e.target.value))} + value={localCardScale} + onChange={(e) => { + const val = parseFloat(e.target.value); + setLocalCardScale(val); + // Direct DOM update for performance + if (containerRef.current) { + containerRef.current.style.setProperty('--card-scale', val.toString()); + } + }} + onMouseUp={() => setCardScale(localCardScale)} + onTouchEnd={() => setCardScale(localCardScale)} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500" />
@@ -225,7 +303,11 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI
{/* Dedicated Zoom Zone (Left Sidebar) */} -
+
= ({ draftState, currentPlayerI
)}
+ {/* Resize Handle for Sidebar */} +
handleResizeStart('sidebar', e)} + onTouchStart={(e) => handleResizeStart('sidebar', e)} + > +
+
{/* Main Content Area: Handles both Pack and Pool based on layout */} @@ -372,29 +462,31 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI {/* Resize Handle */}
handleResizeStart('pool', e)} + onTouchStart={(e) => handleResizeStart('pool', e)} > -
+
{/* Bottom: Pool (Horizontal Strip) */} - -
-

- - Your Pool ({pickedCards.length}) -

-
-
- {pickedCards.map((card: any, idx: number) => ( - - ))} -
-
+
+ +
+

+ + Your Pool ({pickedCards.length}) +

+
+
+ {pickedCards.map((card: any, idx: number) => ( + + ))} +
+
+
)} @@ -415,7 +507,7 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI {draggedCard ? (
{draggedCard.name}
@@ -474,7 +566,7 @@ const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any) return (
{ return (
setHoveredCard(card)} onMouseLeave={() => setHoveredCard(null)} onTouchStart={onTouchStart} @@ -517,7 +609,7 @@ const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => { {card.name}