feat: enhance UI with custom sort dropdown, resizable layouts, StackView DnD, and optimize slider/resize performance with layout fixes.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
28
docs/development/devlog/2025-12-18-030000_ui_enhancements.md
Normal file
28
docs/development/devlog/2025-12-18-030000_ui_enhancements.md
Normal 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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
20
docs/development/devlog/2025-12-18-041500_touch_resize.md
Normal file
20
docs/development/devlog/2025-12-18-041500_touch_resize.md
Normal 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`).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,9 +777,14 @@ 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 ref={poolRef} style={{ height: `${poolHeightPercent}%` }} className="flex flex-col border-b border-slate-800 bg-slate-900/50 overflow-hidden">
|
||||||
|
<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">
|
<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>
|
<span>Card Pool ({pool.length})</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -616,23 +793,37 @@ 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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
{/* Bottom: Deck */}
|
{/* Bottom: Deck */}
|
||||||
<DroppableZone id="deck-zone" className="h-[40%] flex flex-col min-h-0 bg-slate-900/50">
|
<DroppableZone
|
||||||
|
id="deck-zone"
|
||||||
|
className="flex-1 flex flex-col min-h-0 bg-slate-900/50 overflow-hidden"
|
||||||
|
>
|
||||||
<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={cardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" groupBy={groupBy} />
|
<CardsDisplay cards={deck} viewMode={viewMode} cardWidth={localCardWidth} onCardClick={removeFromDeck} onHover={setHoveredCard} emptyMessage="Your Library is Empty" source="deck" groupBy={groupBy} />
|
||||||
</div>
|
</div>
|
||||||
</DroppableZone>
|
</DroppableZone>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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} />
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
document.addEventListener('mousemove', onResizeMove);
|
||||||
const stopResizing = () => setIsResizing(false);
|
document.addEventListener('touchmove', onResizeMove, { passive: false });
|
||||||
const resize = (e: MouseEvent) => {
|
document.addEventListener('mouseup', onResizeEnd);
|
||||||
if (isResizing) {
|
document.addEventListener('touchend', onResizeEnd);
|
||||||
const newHeight = window.innerHeight - e.clientY;
|
document.body.style.cursor = type === 'sidebar' ? 'col-resize' : 'row-resize';
|
||||||
// Limits: Min 100px, Max 60% of screen
|
|
||||||
const maxHeight = window.innerHeight * 0.6;
|
|
||||||
if (newHeight >= 100 && newHeight <= maxHeight) {
|
|
||||||
setPoolHeight(newHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isResizing) {
|
const onResizeMove = React.useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
document.addEventListener('mousemove', resize);
|
if (!resizingState.current.active) return;
|
||||||
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`;
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousemove', resize);
|
if (resizingState.current.active === 'pool' && poolRef.current) {
|
||||||
document.removeEventListener('mouseup', stopResizing);
|
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));
|
||||||
}, [isResizing]);
|
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));
|
||||||
|
}
|
||||||
|
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<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,16 +462,17 @@ 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) */}
|
||||||
|
<div ref={poolRef} style={{ height: `${poolHeight}px` }} className="shrink-0 flex flex-col overflow-hidden">
|
||||||
<PoolDroppable
|
<PoolDroppable
|
||||||
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"
|
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"
|
||||||
style={{ height: `${poolHeight}px` }}
|
|
||||||
>
|
>
|
||||||
<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">
|
||||||
@@ -389,13 +480,14 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user