feat: enhance UI with custom sort dropdown, resizable layouts, StackView DnD, and optimize slider/resize performance with layout fixes.

This commit is contained in:
2025-12-18 02:06:57 +01:00
parent ebfdfef5ae
commit db601048d9
13 changed files with 589 additions and 101 deletions

View File

@@ -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. - [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. - [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. - [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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -13,6 +13,7 @@ interface StackViewProps {
onHover?: (card: DraftCard | null) => void; onHover?: (card: DraftCard | null) => void;
disableHoverPreview?: boolean; disableHoverPreview?: boolean;
groupBy?: GroupMode; groupBy?: GroupMode;
renderWrapper?: (card: DraftCard, children: React.ReactNode) => React.ReactNode;
} }
const GROUPS: Record<GroupMode, string[]> = { const GROUPS: Record<GroupMode, string[]> = {
@@ -71,7 +72,7 @@ const getCardGroup = (card: DraftCard, mode: GroupMode): string => {
}; };
export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false, groupBy = 'color' }) => { export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false, groupBy = 'color', renderWrapper }) => {
const categorizedCards = useMemo(() => { const categorizedCards = useMemo(() => {
const categories: Record<string, DraftCard[]> = {}; const categories: Record<string, DraftCard[]> = {};
@@ -139,6 +140,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
onHover={onHover} onHover={onHover}
onCardClick={onCardClick} onCardClick={onCardClick}
disableHoverPreview={disableHoverPreview} disableHoverPreview={disableHoverPreview}
renderWrapper={renderWrapper}
/> />
); );
})} })}
@@ -150,10 +152,10 @@ export const StackView: React.FC<StackViewProps> = ({ 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); const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover || (() => { }), () => onCardClick && onCardClick(card), card);
return ( const content = (
<div <div
className="relative w-full z-0 hover:z-50 transition-all duration-200 group" className="relative w-full z-0 hover:z-50 transition-all duration-200 group"
onMouseEnter={() => onHover && onHover(card)} onMouseEnter={() => onHover && onHover(card)}
@@ -177,4 +179,10 @@ const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHo
</CardHoverWrapper> </CardHoverWrapper>
</div> </div>
); );
if (renderWrapper) {
return renderWrapper(card, content);
}
return content;
}; };

View File

@@ -115,6 +115,14 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
const saved = localStorage.getItem('cube_cardWidth'); const saved = localStorage.getItem('cube_cardWidth');
return saved ? parseInt(saved) : 60; return saved ? parseInt(saved) : 60;
}); });
// Local state for smooth slider
const [localCardWidth, setLocalCardWidth] = useState(cardWidth);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setLocalCardWidth(cardWidth);
if (containerRef.current) containerRef.current.style.setProperty('--card-width', `${cardWidth}px`);
}, [cardWidth]);
// --- Persistence Effects --- // --- Persistence Effects ---
useEffect(() => localStorage.setItem('cube_inputText', inputText), [inputText]); useEffect(() => localStorage.setItem('cube_inputText', inputText), [inputText]);
@@ -453,7 +461,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
}; };
return ( return (
<div className="h-full overflow-y-auto w-full flex flex-col lg:flex-row gap-8 p-4 md:p-6"> <div ref={containerRef} className="h-full overflow-y-auto w-full flex flex-col lg:flex-row gap-8 p-4 md:p-6" style={{ '--card-width': `${localCardWidth}px` } as React.CSSProperties}>
{/* --- LEFT COLUMN: CONTROLS --- */} {/* --- LEFT COLUMN: CONTROLS --- */}
<div className="w-full lg:w-1/3 lg:max-w-[400px] shrink-0 flex flex-col gap-4 lg:sticky lg:top-4 lg:self-start lg:max-h-[calc(100vh-10rem)] lg:overflow-y-auto custom-scrollbar p-1"> <div className="w-full lg:w-1/3 lg:max-w-[400px] shrink-0 flex flex-col gap-4 lg:sticky lg:top-4 lg:self-start lg:max-h-[calc(100vh-10rem)] lg:overflow-y-auto custom-scrollbar p-1">
@@ -841,10 +849,16 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
min="60" min="60"
max="200" max="200"
step="1" step="1"
value={cardWidth} value={localCardWidth}
onChange={(e) => setCardWidth(parseInt(e.target.value))} 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" 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`}
/> />
<div className="w-4 h-6 rounded border border-slate-500 bg-slate-700" title="Large Cards" /> <div className="w-4 h-6 rounded border border-slate-500 bg-slate-700" title="Large Cards" />
</div> </div>
@@ -870,12 +884,12 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
className="grid gap-6 pb-20" className="grid gap-6 pb-20"
style={{ style={{
gridTemplateColumns: cardWidth <= 150 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' : '1fr'
}} }}
> >
{packs.map((pack) => ( {packs.map((pack) => (
<PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={cardWidth} /> <PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={localCardWidth} />
))} ))}
</div> </div>
) )

View File

@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { socketService } from '../../services/SocketService'; 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 { StackView } from '../../components/StackView';
import { FoilOverlay } from '../../components/CardPreview'; import { FoilOverlay } from '../../components/CardPreview';
import { DraftCard } from '../../services/PackGeneratorService'; import { DraftCard } from '../../services/PackGeneratorService';
@@ -145,6 +145,7 @@ const CardsDisplay: React.FC<{
) )
} }
// Use CSS var for grid
if (viewMode === 'list') { if (viewMode === 'list') {
const sorted = [...cards].sort((a, b) => (a.cmc || 0) - (b.cmc || 0)); const sorted = [...cards].sort((a, b) => (a.cmc || 0) - (b.cmc || 0));
return ( return (
@@ -176,6 +177,11 @@ const CardsDisplay: React.FC<{
onHover={(c) => onHover(c)} onHover={(c) => onHover(c)}
disableHoverPreview={true} disableHoverPreview={true}
groupBy={groupBy} groupBy={groupBy}
renderWrapper={(card, children) => (
<DraggableCardWrapper key={card.id} card={card} source={source}>
{children}
</DraggableCardWrapper>
)}
/> />
</div> </div>
) )
@@ -186,7 +192,7 @@ const CardsDisplay: React.FC<{
<div <div
className="grid gap-4 pb-20 content-start" className="grid gap-4 pb-20 content-start"
style={{ style={{
gridTemplateColumns: `repeat(auto-fill, minmax(${cardWidth}px, 1fr))` gridTemplateColumns: `repeat(auto-fill, minmax(var(--card-width, ${cardWidth}px), 1fr))`
}} }}
> >
{cards.map(c => { {cards.map(c => {
@@ -218,6 +224,39 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ 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 [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 [groupBy, setGroupBy] = useState<'type' | 'color' | 'cmc' | 'rarity'>('color');
const [cardWidth, setCardWidth] = useState(60); const [cardWidth, setCardWidth] = useState(60);
// Local state for smooth slider
const [localCardWidth, setLocalCardWidth] = useState(cardWidth);
const containerRef = React.useRef<HTMLDivElement>(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<HTMLDivElement>(null);
const poolRef = React.useRef<HTMLDivElement>(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<any[]>(initialPool); const [pool, setPool] = useState<any[]>(initialPool);
const [deck, setDeck] = useState<any[]>([]); const [deck, setDeck] = useState<any[]>([]);
@@ -395,6 +434,79 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
setDraggedCard(null); 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 --- // --- Render Functions ---
const renderLandStation = () => ( const renderLandStation = () => (
<div className="bg-slate-900/40 rounded border border-slate-700/50 p-2 mb-2 shrink-0 flex flex-col gap-2"> <div className="bg-slate-900/40 rounded border border-slate-700/50 p-2 mb-2 shrink-0 flex flex-col gap-2">
@@ -460,7 +572,12 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
); );
return ( return (
<div className="flex-1 w-full flex h-full bg-slate-950 text-white overflow-hidden flex-col select-none" onContextMenu={(e) => e.preventDefault()}> <div
ref={containerRef}
className="flex-1 w-full flex h-full bg-slate-950 text-white overflow-hidden flex-col select-none"
onContextMenu={(e) => e.preventDefault()}
style={{ '--card-width': `${localCardWidth}px` } as React.CSSProperties}
>
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}> <DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
{/* Global Toolbar */} {/* Global Toolbar */}
{/* Global Toolbar */} {/* Global Toolbar */}
@@ -473,20 +590,43 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
<button onClick={() => setViewMode('stack')} className={`p-1.5 rounded ${viewMode === 'stack' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Stack View"><Layers className="w-4 h-4" /></button> <button onClick={() => setViewMode('stack')} className={`p-1.5 rounded ${viewMode === 'stack' ? 'bg-slate-700 text-white shadow' : 'text-slate-500 hover:text-white'}`} title="Stack View"><Layers className="w-4 h-4" /></button>
</div> </div>
{/* Group By Dropdown (Only relevant for Stack View usually, but nice to have) */} {/* Group By Dropdown (Custom UI) */}
{viewMode === 'stack' && ( {viewMode === 'stack' && (
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700 h-9 items-center px-2 gap-2"> <div className="relative z-50">
<span className="text-[10px] text-slate-500 uppercase font-bold">Sort:</span> <button
<select onClick={() => setSortDropdownOpen(!sortDropdownOpen)}
value={groupBy} className="flex items-center gap-2 bg-slate-900 rounded-lg p-1.5 border border-slate-700 h-9 px-3 text-xs font-bold text-white hover:bg-slate-800 transition-colors"
onChange={(e) => setGroupBy(e.target.value as any)}
className="bg-transparent text-xs font-bold text-white outline-none cursor-pointer"
> >
<option value="color">Color</option> <span className="text-slate-500 uppercase">Sort:</span>
<option value="type">Type</option> <span className="capitalize">{groupBy === 'cmc' ? 'Mana Value' : groupBy}</span>
<option value="cmc">Mana Value</option> <ChevronDown className={`w-3 h-3 text-slate-400 transition-transform ${sortDropdownOpen ? 'rotate-180' : ''}`} />
<option value="rarity">Rarity</option> </button>
</select>
{sortDropdownOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setSortDropdownOpen(false)} />
<div className="absolute top-full left-0 mt-2 w-40 bg-slate-800 border border-slate-700 rounded-xl shadow-xl overflow-hidden z-50 animate-in fade-in zoom-in-95 duration-200">
{[
{ value: 'color', label: 'Color' },
{ value: 'type', label: 'Type' },
{ value: 'cmc', label: 'Mana Value' },
{ value: 'rarity', label: 'Rarity' }
].map((opt) => (
<button
key={opt.value}
onClick={() => {
setGroupBy(opt.value as any);
setSortDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-xs font-bold flex items-center justify-between ${groupBy === opt.value ? 'bg-purple-900/30 text-purple-200' : 'text-slate-300 hover:bg-slate-700 hover:text-white'}`}
>
{opt.label}
{groupBy === opt.value && <Check className="w-3 h-3 text-purple-400" />}
</button>
))}
</div>
</>
)}
</div> </div>
)} )}
@@ -504,8 +644,14 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
min="60" min="60"
max="200" max="200"
step="1" step="1"
value={cardWidth} value={localCardWidth}
onChange={(e) => setCardWidth(parseInt(e.target.value))} 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" className="w-24 accent-purple-500 cursor-pointer h-1.5 bg-slate-800 rounded-lg appearance-none"
/> />
<div className="w-3 h-5 rounded border border-slate-500 bg-slate-700" /> <div className="w-3 h-5 rounded border border-slate-500 bg-slate-700" />
@@ -527,7 +673,12 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
<div className="flex-1 flex overflow-hidden lg:flex-row flex-col"> <div className="flex-1 flex overflow-hidden lg:flex-row flex-col">
{/* Zoom Sidebar */} {/* Zoom Sidebar */}
<div className="hidden xl:flex w-72 shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-10 p-4" style={{ perspective: '1000px' }}> <div
ref={sidebarRef}
className="hidden xl:flex shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-10 p-4 relative"
style={{ perspective: '1000px' }}
>
{/* Front content ... */}
<div className="w-full relative sticky top-4"> <div className="w-full relative sticky top-4">
<div <div
className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out" className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out"
@@ -553,7 +704,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
<h3 className="text-lg font-bold text-slate-200">{(hoveredCard || displayCard).name}</h3> <h3 className="text-lg font-bold text-slate-200">{(hoveredCard || displayCard).name}</h3>
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">{(hoveredCard || displayCard).typeLine || (hoveredCard || displayCard).type_line}</p> <p className="text-xs text-slate-400 uppercase tracking-wider mt-1">{(hoveredCard || displayCard).typeLine || (hoveredCard || displayCard).type_line}</p>
{(hoveredCard || displayCard).oracle_text && ( {(hoveredCard || displayCard).oracle_text && (
<div className="mt-4 text-xs text-slate-400 text-left bg-slate-950 p-3 rounded-lg border border-slate-800 leading-relaxed"> <div className="mt-4 text-xs text-slate-400 text-left bg-slate-950 p-3 rounded-lg border border-slate-800 leading-relaxed max-h-60 overflow-y-auto custom-scrollbar">
{(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) => <p key={i} className="mb-1">{line}</p>)} {(hoveredCard || displayCard).oracle_text.split('\n').map((line: string, i: number) => <p key={i} className="mb-1">{line}</p>)}
</div> </div>
)} )}
@@ -579,11 +730,31 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
</div> </div>
</div> </div>
</div> </div>
{/* Resize Handle */}
<div
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-purple-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors touch-none"
onMouseDown={(e) => handleResizeStart('sidebar', e)}
onTouchStart={(e) => handleResizeStart('sidebar', e)}
>
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-purple-400 transition-colors" />
</div>
</div> </div>
{/* Content Area */} {/* Content Area */}
{layout === 'vertical' ? ( {layout === 'vertical' ? (
<div className="flex-1 flex flex-col lg:flex-row"> <div className="flex-1 flex flex-col lg:flex-row min-w-0">
{/* 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 */} {/* Pool Column */}
<DroppableZone id="pool-zone" className="flex-1 flex flex-col min-w-0 border-r border-slate-800 bg-slate-900/50"> <DroppableZone id="pool-zone" className="flex-1 flex flex-col min-w-0 border-r border-slate-800 bg-slate-900/50">
<div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between"> <div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between">
@@ -594,6 +765,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} /> <CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} />
</div> </div>
</DroppableZone> </DroppableZone>
{/* Deck Column */} {/* Deck Column */}
<DroppableZone id="deck-zone" className="flex-1 flex flex-col min-w-0 bg-slate-900/50"> <DroppableZone id="deck-zone" className="flex-1 flex flex-col min-w-0 bg-slate-900/50">
<div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between"> <div className="p-3 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between">
@@ -605,26 +777,45 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
</DroppableZone> </DroppableZone>
</div> </div>
) : ( ) : (
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col min-h-0 relative">
{/* Top: Pool + Land Station */} {/* Top: Pool + Land Station */}
<DroppableZone id="pool-zone" className="flex-1 flex flex-col min-h-0 border-b border-slate-800 bg-slate-900/50"> {/* Top: Pool + Land Station */}
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0"> <div ref={poolRef} style={{ height: `${poolHeightPercent}%` }} className="flex flex-col border-b border-slate-800 bg-slate-900/50 overflow-hidden">
<span>Card Pool ({pool.length})</span> <DroppableZone
id="pool-zone"
className="flex-1 flex flex-col"
>
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
<span>Card Pool ({pool.length})</span>
</div>
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
{renderLandStation()}
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} />
</div>
</DroppableZone>
{/* Resizer Handle */}
<div
className="h-2 bg-slate-800 hover:bg-purple-500/50 cursor-row-resize flex items-center justify-center shrink-0 z-20 group transition-colors touch-none"
onMouseDown={(e) => handleResizeStart('pool', e)}
onTouchStart={(e) => handleResizeStart('pool', e)}
>
<div className="w-16 h-1 bg-slate-600 rounded-full group-hover:bg-purple-300" />
</div> </div>
<div className="flex-1 overflow-auto p-2 custom-scrollbar flex flex-col">
{renderLandStation()} {/* Bottom: Deck */}
<CardsDisplay cards={pool} viewMode={viewMode} cardWidth={cardWidth} onCardClick={addToDeck} onHover={setHoveredCard} emptyMessage="Pool Empty" source="pool" groupBy={groupBy} /> <DroppableZone
</div> id="deck-zone"
</DroppableZone> className="flex-1 flex flex-col min-h-0 bg-slate-900/50 overflow-hidden"
{/* Bottom: Deck */} >
<DroppableZone id="deck-zone" className="h-[40%] flex flex-col min-h-0 bg-slate-900/50"> <div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0">
<div className="p-2 border-b border-slate-800 font-bold text-slate-400 uppercase text-xs flex justify-between shrink-0"> <span>Library ({deck.length})</span>
<span>Library ({deck.length})</span> </div>
</div> <div className="flex-1 overflow-auto p-2 custom-scrollbar">
<div className="flex-1 overflow-auto p-2 custom-scrollbar"> <CardsDisplay cards={deck} viewMode={viewMode} cardWidth={localCardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" groupBy={groupBy} />
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" groupBy={groupBy} /> </div>
</div> </DroppableZone>
</DroppableZone> </div>
</div> </div>
)} )}
</div> </div>
@@ -632,7 +823,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
<DragOverlay dropAnimation={null}> <DragOverlay dropAnimation={null}>
{draggedCard ? ( {draggedCard ? (
<div <div
style={{ width: `${cardWidth}px` }} style={{ width: `${localCardWidth}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]`} 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} /> <img src={draggedCard.image || draggedCard.image_uris?.normal} alt={draggedCard.name} className="w-full h-full object-cover" draggable={false} />

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { socketService } from '../../services/SocketService'; import { socketService } from '../../services/SocketService';
import { LogOut, Columns, LayoutTemplate } from 'lucide-react'; import { LogOut, Columns, LayoutTemplate } from 'lucide-react';
import { Modal } from '../../components/Modal'; import { Modal } from '../../components/Modal';
@@ -58,19 +58,49 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
return () => clearInterval(interval); return () => clearInterval(interval);
}, [pickExpiresAt]); }, [pickExpiresAt]);
// --- UI State & Persistence --- // --- UI State & Persistence ---
const [sidebarWidth, setSidebarWidth] = useState(320);
const [poolHeight, setPoolHeight] = useState<number>(() => { const [poolHeight, setPoolHeight] = useState<number>(() => {
const saved = localStorage.getItem('draft_poolHeight'); const saved = localStorage.getItem('draft_poolHeight');
return saved ? parseInt(saved, 10) : 220; return saved ? parseInt(saved, 10) : 220;
}); });
const sidebarRef = React.useRef<HTMLDivElement>(null);
const poolRef = React.useRef<HTMLDivElement>(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<number>(() => { const [cardScale, setCardScale] = useState<number>(() => {
const saved = localStorage.getItem('draft_cardScale'); const saved = localStorage.getItem('draft_cardScale');
return saved ? parseFloat(saved) : 0.35; return saved ? parseFloat(saved) : 0.35;
}); });
// Local state for smooth slider
const [localCardScale, setLocalCardScale] = useState(cardScale);
const containerRef = useRef<HTMLDivElement>(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 [layout, setLayout] = useState<'vertical' | 'horizontal'>('horizontal');
const [isResizing, setIsResizing] = useState(false);
// Persist settings // Persist settings
useEffect(() => { useEffect(() => {
@@ -81,34 +111,68 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
localStorage.setItem('draft_cardScale', cardScale.toString()); localStorage.setItem('draft_cardScale', cardScale.toString());
}, [cardScale]); }, [cardScale]);
// Resize Handlers const handleResizeStart = (type: 'sidebar' | 'pool', e: React.MouseEvent | React.TouchEvent) => {
const startResizing = (e: React.MouseEvent) => { // Prevent default to avoid scrolling/selection
setIsResizing(true); if (e.cancelable) e.preventDefault();
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 onResizeMove = React.useCallback((e: MouseEvent | TouchEvent) => {
const stopResizing = () => setIsResizing(false); if (!resizingState.current.active) return;
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);
}
}
};
if (isResizing) { if (e.cancelable) e.preventDefault();
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResizing); 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 () => { if (resizingState.current.active === 'pool' && poolRef.current) {
document.removeEventListener('mousemove', resize); setPoolHeight(parseInt(poolRef.current.style.height));
document.removeEventListener('mouseup', stopResizing); }
};
}, [isResizing]); 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<any>(null); const [hoveredCard, setHoveredCard] = useState<any>(null);
const [displayCard, setDisplayCard] = useState<any>(null); const [displayCard, setDisplayCard] = useState<any>(null);
@@ -152,7 +216,12 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
}; };
return ( return (
<div className="flex-1 w-full flex flex-col h-full bg-slate-950 text-white overflow-hidden relative select-none" onContextMenu={(e) => e.preventDefault()}> <div
ref={containerRef}
className="flex-1 w-full flex flex-col h-full bg-slate-950 text-white overflow-hidden relative select-none"
onContextMenu={(e) => e.preventDefault()}
style={{ '--card-scale': localCardScale } as React.CSSProperties}
>
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}> <DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 via-slate-950 to-black opacity-50 pointer-events-none"></div> <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 via-slate-950 to-black opacity-50 pointer-events-none"></div>
@@ -193,8 +262,17 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
min="0.35" min="0.35"
max="1.0" max="1.0"
step="0.01" step="0.01"
value={cardScale} value={localCardScale}
onChange={(e) => setCardScale(parseFloat(e.target.value))} 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" className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500"
/> />
</div> </div>
@@ -225,7 +303,11 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{/* Dedicated Zoom Zone (Left Sidebar) */} {/* Dedicated Zoom Zone (Left Sidebar) */}
<div className="hidden lg:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800/50 bg-slate-900/20 backdrop-blur-sm z-10 transition-all" style={{ perspective: '1000px' }}> <div
ref={sidebarRef}
className="hidden lg:flex shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800/50 bg-slate-900/20 backdrop-blur-sm z-10 relative"
style={{ perspective: '1000px' }}
>
<div className="w-full relative sticky top-8 px-6"> <div className="w-full relative sticky top-8 px-6">
<div <div
className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out" className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out"
@@ -282,6 +364,14 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
</div> </div>
)} )}
</div> </div>
{/* Resize Handle for Sidebar */}
<div
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-emerald-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors"
onMouseDown={(e) => handleResizeStart('sidebar', e)}
onTouchStart={(e) => handleResizeStart('sidebar', e)}
>
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-emerald-400 transition-colors" />
</div>
</div> </div>
{/* Main Content Area: Handles both Pack and Pool based on layout */} {/* Main Content Area: Handles both Pack and Pool based on layout */}
@@ -372,29 +462,31 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
{/* Resize Handle */} {/* Resize Handle */}
<div <div
className="h-1 bg-slate-800 hover:bg-emerald-500 cursor-row-resize z-30 transition-colors w-full flex items-center justify-center shrink-0" className="h-2 bg-slate-800 hover:bg-emerald-500/50 cursor-row-resize z-30 transition-colors w-full flex items-center justify-center shrink-0 group touch-none"
onMouseDown={startResizing} onMouseDown={(e) => handleResizeStart('pool', e)}
onTouchStart={(e) => handleResizeStart('pool', e)}
> >
<div className="w-16 h-1 bg-slate-600 rounded-full"></div> <div className="w-16 h-1 bg-slate-600 rounded-full group-hover:bg-emerald-300"></div>
</div> </div>
{/* Bottom: Pool (Horizontal Strip) */} {/* Bottom: Pool (Horizontal Strip) */}
<PoolDroppable <div ref={poolRef} style={{ height: `${poolHeight}px` }} className="shrink-0 flex flex-col overflow-hidden">
className="shrink-0 bg-slate-900/90 backdrop-blur-md flex flex-col z-20 shadow-[-10px_-10px_30px_rgba(0,0,0,0.3)] transition-all ease-out duration-75 border-t border-slate-800" <PoolDroppable
style={{ height: `${poolHeight}px` }} className="flex-1 bg-slate-900/90 backdrop-blur-md flex flex-col z-20 shadow-[-10px_-10px_30px_rgba(0,0,0,0.3)] border-t border-slate-800 min-h-0"
> >
<div className="px-6 py-2 flex items-center justify-between shrink-0"> <div className="px-6 py-2 flex items-center justify-between shrink-0">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2"> <h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span> <span className="w-2 h-2 rounded-full bg-emerald-500"></span>
Your Pool ({pickedCards.length}) Your Pool ({pickedCards.length})
</h3> </h3>
</div> </div>
<div className="flex-1 overflow-x-auto flex items-center gap-2 px-6 pb-4 custom-scrollbar"> <div className="flex-1 overflow-x-auto flex gap-2 px-6 pb-2 pt-2 custom-scrollbar min-h-0">
{pickedCards.map((card: any, idx: number) => ( {pickedCards.map((card: any, idx: number) => (
<PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} /> <PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} />
))} ))}
</div> </div>
</PoolDroppable> </PoolDroppable>
</div>
</div> </div>
)} )}
@@ -415,7 +507,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
{draggedCard ? ( {draggedCard ? (
<div <div
className="opacity-90 rotate-3 cursor-grabbing shadow-2xl rounded-xl" className="opacity-90 rotate-3 cursor-grabbing shadow-2xl rounded-xl"
style={{ width: `${14 * cardScale}rem`, aspectRatio: '2.5/3.5' }} style={{ width: `calc(14rem * var(--card-scale, ${localCardScale}))`, aspectRatio: '2.5/3.5' }}
> >
<img src={draggedCard.image} alt={draggedCard.name} className="w-full h-full object-cover rounded-xl" draggable={false} /> <img src={draggedCard.image} alt={draggedCard.name} className="w-full h-full object-cover rounded-xl" draggable={false} />
</div> </div>
@@ -474,7 +566,7 @@ const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any)
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
style={{ ...style, width: `${14 * cardScale}rem` }} style={{ ...style, width: `calc(14rem * var(--card-scale))` }}
{...attributes} {...attributes}
{...mergedListeners} {...mergedListeners}
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer" className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
@@ -506,7 +598,7 @@ const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => {
return ( return (
<div <div
className={`relative group shrink-0 transition-all flex items-center cursor-pointer ${vertical ? 'w-24 h-32' : 'h-full'}`} className={`relative group shrink-0 flex items-center justify-center cursor-pointer ${vertical ? 'w-24 h-32' : 'h-full aspect-[2.5/3.5] p-2'}`}
onMouseEnter={() => setHoveredCard(card)} onMouseEnter={() => setHoveredCard(card)}
onMouseLeave={() => setHoveredCard(null)} onMouseLeave={() => setHoveredCard(null)}
onTouchStart={onTouchStart} onTouchStart={onTouchStart}
@@ -517,7 +609,7 @@ const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => {
<img <img
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal} src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
alt={card.name} alt={card.name}
className={`${vertical ? 'w-full h-full object-cover' : 'h-[90%] w-auto object-contain'} rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all`} className={`${vertical ? 'w-full h-full object-cover' : 'h-full w-auto object-contain'} rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all`}
draggable={false} draggable={false}
/> />
</div> </div>