diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 84ed48c..9c2877a 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -34,3 +34,6 @@ - [Game Type Filter](./devlog/2025-12-16-231000_game_type_filter.md): Completed. Added Paper/Digital filter to the expansion selection list. - [Incremental Caching](./devlog/2025-12-16-233000_incremental_caching.md): Completed. Refactored data fetching to cache cards to the server incrementally per set, preventing PayloadTooLarge errors. - [Server Graceful Shutdown Fix](./devlog/2025-12-17-002000_server_shutdown_fix.md): Completed. Implemented signal handling to ensure clean process exit on Ctrl+C. +- [Cube Sticky Sidebar](./devlog/2025-12-17-003000_cube_sticky_sidebar.md): Completed. Made the Cube Manager left sidebar sticky to improve usability with long pack lists. +- [Cube Full Width Layout](./devlog/2025-12-17-003500_cube_full_width.md): Completed. Updated Cube Manager to use the full screen width. +- [Cube Archidekt View](./devlog/2025-12-17-004500_archidekt_view.md): Completed. Implemented column-based stacked view for packs. diff --git a/docs/development/devlog/2025-12-17-003000_cube_sticky_sidebar.md b/docs/development/devlog/2025-12-17-003000_cube_sticky_sidebar.md new file mode 100644 index 0000000..3a78d26 --- /dev/null +++ b/docs/development/devlog/2025-12-17-003000_cube_sticky_sidebar.md @@ -0,0 +1,14 @@ +# Cube Manager Sticky Sidebar + +## Objective +Update the `CubeManager` layout to make the left-side settings/controls panel sticky. This allows the user to access controls (Generate, Reset, etc.) while scrolling through a long list of generated packs on the right. + +## Changes +- Modified `src/client/src/modules/cube/CubeManager.tsx`: + - Added `sticky top-4` to the left column wrapper. + - Added `self-start` to ensure the sticky element doesn't stretch to the full height of the container (which would negate stickiness). + - Added `max-h-[calc(100vh-2rem)]` and `overflow-y-auto` to the left panel to ensure its content remains accessible if it exceeds the viewport height. + - Added `custom-scrollbar` styling for consistent aesthetics. + +## Result +The left panel now follows the user's scroll position, improving usability for large pack generations. diff --git a/docs/development/devlog/2025-12-17-003500_cube_full_width.md b/docs/development/devlog/2025-12-17-003500_cube_full_width.md new file mode 100644 index 0000000..1f38d85 --- /dev/null +++ b/docs/development/devlog/2025-12-17-003500_cube_full_width.md @@ -0,0 +1,12 @@ +# Cube Manager Full Width Layout + +## Objective +Update the `CubeManager` layout to utilize the full width of the screen, removing the maximum width constraint. This allows for better utilization of screen real estate, especially on wider monitors, and provides more space for the pack grid. + +## Changes +- Modified `src/client/src/modules/cube/CubeManager.tsx`: + - Removed `max-w-7xl` and `mx-auto` classes from the main container. + - Added `w-full` to ensure the container spans the entire available width. + +## Result +The Cube Manager interface now stretches to fill the viewport width, providing a more expansive view for managing packs and settings. diff --git a/docs/development/devlog/2025-12-17-004500_archidekt_view.md b/docs/development/devlog/2025-12-17-004500_archidekt_view.md new file mode 100644 index 0000000..8cf29e2 --- /dev/null +++ b/docs/development/devlog/2025-12-17-004500_archidekt_view.md @@ -0,0 +1,16 @@ +# Cube Manager Archidekt View + +## Objective +Implement an "Archidekt-style" stacked view for pack generation. This view organizes cards into columns by type (Creature, Instant, Land, etc.) with vertical overlapping to save space while keeping headers visible. + +## Changes +1. **Refactor PackCard**: Extracted `FloatingPreview` and `CardHoverWrapper` into `src/client/src/components/CardPreview.tsx` to resolve circular dependencies and clean up `PackCard.tsx`. +2. **Update StackView**: + - Rewrite `StackView.tsx` to group cards by `typeLine` (categories: Creature, Planeswalker, Instant, Sorcery, Enchantment, Artifact, Land, Battle, Other). + - Sort cards within categories by CMC. + - Render columns using Flexbox. + - Implement overlapping "card strip" look using negative `margin-bottom` on cards. + - Value tuning: `margin-bottom: -125%` seems appropriate for a standard card aspect ratio to reveal the title bar. + +## Result +The "Stack" view option in Cube Manager now renders packs as organized, sorted columns similar to deck-builder interfaces. diff --git a/src/client/src/components/CardPreview.tsx b/src/client/src/components/CardPreview.tsx new file mode 100644 index 0000000..a02105c --- /dev/null +++ b/src/client/src/components/CardPreview.tsx @@ -0,0 +1,73 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { DraftCard } from '../services/PackGeneratorService'; + +// --- Floating Preview Component --- +export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number }> = ({ card, x, y }) => { + const isFoil = card.finish === 'foil'; + const imgRef = useRef(null); + + // Basic boundary detection + const [adjustedPos, setAdjustedPos] = useState({ top: y, left: x }); + + useEffect(() => { + const OFFSET = 20; + const CARD_WIDTH = 300; + const CARD_HEIGHT = 420; + + let newX = x + OFFSET; + let newY = y + OFFSET; + + if (newX + CARD_WIDTH > window.innerWidth) { + newX = x - CARD_WIDTH - OFFSET; + } + + if (newY + CARD_HEIGHT > window.innerHeight) { + newY = y - CARD_HEIGHT - OFFSET; + } + + setAdjustedPos({ top: newY, left: newX }); + + }, [x, y]); + + return ( +
+
+ {card.name} + {isFoil &&
} +
+
+ ); +}; + +// --- Hover Wrapper to handle mouse events --- +export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.ReactNode; className?: string }> = ({ card, children, className }) => { + const [isHovering, setIsHovering] = useState(false); + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); + + const hasImage = !!card.image; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!hasImage) return; + setMousePos({ x: e.clientX, y: e.clientY }); + }; + + return ( +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + onMouseMove={handleMouseMove} + > + {children} + {isHovering && hasImage && ( + + )} +
+ ); +}; diff --git a/src/client/src/components/PackCard.tsx b/src/client/src/components/PackCard.tsx index e25bc4d..3f168f1 100644 --- a/src/client/src/components/PackCard.tsx +++ b/src/client/src/components/PackCard.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React from 'react'; import { DraftCard, Pack } from '../services/PackGeneratorService'; import { Copy } from 'lucide-react'; import { StackView } from './StackView'; @@ -8,82 +8,7 @@ interface PackCardProps { viewMode: 'list' | 'grid' | 'stack'; } -// --- Floating Preview Component --- -const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number }> = ({ card, x, y }) => { - const isFoil = card.finish === 'foil'; - const imgRef = useRef(null); - - // Basic boundary detection to prevent going off-screen - // We check window dimensions. This might need customization based on the actual viewport, - // but window is a good safe default. - const [adjustedPos, setAdjustedPos] = useState({ top: y, left: x }); - - useEffect(() => { - // Offset from cursor - const OFFSET = 20; - const CARD_WIDTH = 300; // Approx width of preview - const CARD_HEIGHT = 420; // Approx height of preview - - let newX = x + OFFSET; - let newY = y + OFFSET; - - // Flip horizontally if too close to right edge - if (newX + CARD_WIDTH > window.innerWidth) { - newX = x - CARD_WIDTH - OFFSET; - } - - // Flip vertically if too close to bottom edge - if (newY + CARD_HEIGHT > window.innerHeight) { - newY = y - CARD_HEIGHT - OFFSET; - } - - setAdjustedPos({ top: newY, left: newX }); - - }, [x, y]); - - return ( -
-
- {card.name} - {isFoil &&
} -
-
- ); -}; - -// --- Hover Wrapper to handle mouse events --- -const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.ReactNode; className?: string }> = ({ card, children, className }) => { - const [isHovering, setIsHovering] = useState(false); - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); - - // Only show preview if there is an image - const hasImage = !!card.image; - - const handleMouseMove = (e: React.MouseEvent) => { - if (!hasImage) return; - setMousePos({ x: e.clientX, y: e.clientY }); - }; - - return ( -
setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - onMouseMove={handleMouseMove} - > - {children} - {isHovering && hasImage && ( - - )} -
- ); -}; +import { CardHoverWrapper } from './CardPreview'; const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => { diff --git a/src/client/src/components/StackView.tsx b/src/client/src/components/StackView.tsx index 95f22fe..2e780f0 100644 --- a/src/client/src/components/StackView.tsx +++ b/src/client/src/components/StackView.tsx @@ -1,53 +1,100 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { DraftCard } from '../services/PackGeneratorService'; +import { CardHoverWrapper } from './CardPreview'; interface StackViewProps { cards: DraftCard[]; } +const CATEGORY_ORDER = [ + 'Creature', + 'Planeswalker', + 'Instant', + 'Sorcery', + 'Enchantment', + 'Artifact', + 'Land', + 'Battle', + 'Other' +]; + export const StackView: React.FC = ({ cards }) => { - const getRarityColorClass = (rarity: string) => { - switch (rarity) { - case 'common': return 'bg-black text-white border-slate-600'; - case 'uncommon': return 'bg-slate-300 text-slate-900 border-white'; - case 'rare': return 'bg-yellow-500 text-yellow-950 border-yellow-200'; - case 'mythic': return 'bg-orange-600 text-white border-orange-300'; - default: return 'bg-slate-500'; - } - }; + + const categorizedCards = useMemo(() => { + const categories: Record = {}; + CATEGORY_ORDER.forEach(c => categories[c] = []); + + cards.forEach(card => { + let category = 'Other'; + const typeLine = card.typeLine || ''; + + if (typeLine.includes('Creature')) category = 'Creature'; // Includes Artifact Creature, Ench Creature + else if (typeLine.includes('Planeswalker')) category = 'Planeswalker'; + else if (typeLine.includes('Instant')) category = 'Instant'; + else if (typeLine.includes('Sorcery')) category = 'Sorcery'; + else if (typeLine.includes('Enchantment')) category = 'Enchantment'; + else if (typeLine.includes('Artifact')) category = 'Artifact'; + else if (typeLine.includes('Battle')) category = 'Battle'; + else if (typeLine.includes('Land')) category = 'Land'; + + // Special handling: Commander? usually Creature or Planeswalker + // Ensure it lands in one of the predefined bins + + categories[category].push(card); + }); + + // Sort cards within categories by CMC (low to high)? Or Rarity? + // Archidekt usually sorts by CMC. + Object.keys(categories).forEach(key => { + categories[key].sort((a, b) => (a.cmc || 0) - (b.cmc || 0)); + }); + + return categories; + }, [cards]); return ( -
-
- {cards.map((card, index) => { - const colorClass = getRarityColorClass(card.rarity); - // Random slight rotation for "organic" look - const rotation = (index % 2 === 0 ? 1 : -1) * (Math.random() * 2); +
+ {CATEGORY_ORDER.map(category => { + const catCards = categorizedCards[category]; + if (catCards.length === 0) return null; - return ( -
- {card.image ? ( - {card.name} - ) : ( -
- {card.name} -
- )} -
+ return ( +
+ {/* Header */} +
+ {category} + {catCards.length}
- ); - })} -
-
- Hover to expand stack -
+ + {/* Stack */} +
+ {catCards.map((card, index) => { + // Margin calculation: Negative margin to pull up next cards. + // To show a "strip" of say 35px at the top of each card. + const isLast = index === catCards.length - 1; + + return ( + +
+ {card.name} + {/* Optional: Shine effect for foils if visible? */} + {card.finish === 'foil' &&
} +
+ + ) + })} +
+
+ ) + })}
); }; diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index 3d7ff93..55d9ac7 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -324,10 +324,10 @@ export const CubeManager: React.FC = ({ packs, setPacks, onGoT }; return ( -
+
{/* --- LEFT COLUMN: CONTROLS --- */} -
+
{/* Source Toggle */}