From 851e2aa81df3e1017ee7627aec974674353ded99 Mon Sep 17 00:00:00 2001 From: dnviti Date: Thu, 18 Dec 2025 01:30:48 +0100 Subject: [PATCH] feat: refactor StackView for dynamic grouping and add sorting controls to Deck Builder while reducing card size slider ranges. --- docs/development/CENTRAL.md | 1 + ...2025-12-18-020000_stack_sorting_sliders.md | 34 +++++ src/client/src/components/StackView.tsx | 116 +++++++++++++----- src/client/src/modules/cube/CubeManager.tsx | 6 +- .../src/modules/draft/DeckBuilderView.tsx | 51 +++++--- src/client/src/modules/draft/DraftView.tsx | 6 +- 6 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 docs/development/devlog/2025-12-18-020000_stack_sorting_sliders.md diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index b4e7d3e..7fa529c 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -97,3 +97,4 @@ - [Mobile Touch Preview](./devlog/2025-12-18-012500_mobile_touch_preview.md): Completed. Updated card preview logic to disable hover and enable long-press on touch devices, improving usability on tablets and mobile. - [Minimize Slider Defaults](./devlog/2025-12-18-013000_minimize_slider_defaults.md): Completed. Set default card size settings to their minimum values across Cube Manager, Draft View, and Deck Builder. - [Deck Builder Touch Interaction](./devlog/2025-12-18-014500_deck_builder_touch.md): Completed. Renamed "Deck" to "Library" and implemented tap-to-preview logic on touch devices, disabling tap-to-move. +- [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. diff --git a/docs/development/devlog/2025-12-18-020000_stack_sorting_sliders.md b/docs/development/devlog/2025-12-18-020000_stack_sorting_sliders.md new file mode 100644 index 0000000..a95ea3e --- /dev/null +++ b/docs/development/devlog/2025-12-18-020000_stack_sorting_sliders.md @@ -0,0 +1,34 @@ +# Work Plan - Stack View Sorting & Slider Updates + +## Request +1. **Slider Adjustment**: Decrease the scale of sliders globally. + * New Min (0%) should be smaller (~50% of previous min?). + * New Max (100%) should be equivalent to old 50%. +2. **Stack View Default Sort**: "Order for Color and Mana Cost" by default everywhere. +3. **Deck Builder Sorting**: Add UI to change sort order manually in Deck Builder. + +## Changes +- **StackView.tsx**: + - Refactored to support dynamic `groupBy` logic (Type, Color, CMC, Rarity). + - Implemented categorization logic for Color, CMC, and Rarity. + - Set default `groupBy` to `'color'` (sorts by Color groups, then CMC within groups). + - Fixed syntax errors from previous edit. + +- **DeckBuilderView.tsx**: + - Added `groupBy` state (default `'color'`). + - Added "Sort:" dropdown to toolbar when in Stack View. + - Updated `CardsDisplay` to pass sorting preferences. + - Updated Slider range to `min="60" max="200"` (Default `60`). + +- **CubeManager.tsx**: + - Updated Slider range to `min="60" max="200"`. + - Updated default `cardWidth` to `60`. + +- **DraftView.tsx**: + - Updated Slider range to `min="0.35" max="1.0"`. + - Updated default `cardScale` to `0.35`. + +## Verification +- Verified `StackView` defaults to Color grouping in `CubeManager` (implicitly via default prop). +- Verified Deck Builder has sorting controls. +- Verified all sliders allow for much smaller card sizes. diff --git a/src/client/src/components/StackView.tsx b/src/client/src/components/StackView.tsx index 866d9f2..8c2c471 100644 --- a/src/client/src/components/StackView.tsx +++ b/src/client/src/components/StackView.tsx @@ -3,63 +3,111 @@ import { DraftCard } from '../services/PackGeneratorService'; import { FoilOverlay, CardHoverWrapper } from './CardPreview'; import { useCardTouch } from '../utils/interaction'; + +type GroupMode = 'type' | 'color' | 'cmc' | 'rarity'; + interface StackViewProps { cards: DraftCard[]; cardWidth?: number; onCardClick?: (card: DraftCard) => void; onHover?: (card: DraftCard | null) => void; disableHoverPreview?: boolean; + groupBy?: GroupMode; } -const CATEGORY_ORDER = [ - 'Creature', - 'Planeswalker', - 'Instant', - 'Sorcery', - 'Enchantment', - 'Artifact', - 'Land', - 'Battle', - 'Other' -]; +const GROUPS: Record = { + type: ['Creature', 'Planeswalker', 'Instant', 'Sorcery', 'Enchantment', 'Artifact', 'Battle', 'Land', 'Other'], + color: ['White', 'Blue', 'Black', 'Red', 'Green', 'Multicolor', 'Colorless'], + cmc: ['0', '1', '2', '3', '4', '5', '6', '7+'], + rarity: ['Mythic', 'Rare', 'Uncommon', 'Common'] +}; -export const StackView: React.FC = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false }) => { +const getCardGroup = (card: DraftCard, mode: GroupMode): string => { + if (mode === 'type') { + const typeLine = card.typeLine || ''; + if (typeLine.includes('Creature')) return 'Creature'; + if (typeLine.includes('Planeswalker')) return 'Planeswalker'; + if (typeLine.includes('Instant')) return 'Instant'; + if (typeLine.includes('Sorcery')) return 'Sorcery'; + if (typeLine.includes('Enchantment')) return 'Enchantment'; + if (typeLine.includes('Artifact')) return 'Artifact'; + if (typeLine.includes('Battle')) return 'Battle'; + if (typeLine.includes('Land')) return 'Land'; + return 'Other'; + } + + if (mode === 'color') { + const colors = card.colors || []; + if (colors.length > 1) return 'Multicolor'; + if (colors.length === 0) { + // Check if land + if ((card.typeLine || '').includes('Land')) return 'Colorless'; + // Artifacts etc + return 'Colorless'; + } + if (colors[0] === 'W') return 'White'; + if (colors[0] === 'U') return 'Blue'; + if (colors[0] === 'B') return 'Black'; + if (colors[0] === 'R') return 'Red'; + if (colors[0] === 'G') return 'Green'; + return 'Colorless'; + } + + if (mode === 'cmc') { + const cmc = Math.floor(card.cmc || 0); + if (cmc >= 7) return '7+'; + return cmc.toString(); + } + + if (mode === 'rarity') { + const r = (card.rarity || 'common').toLowerCase(); + if (r === 'mythic') return 'Mythic'; + if (r === 'rare') return 'Rare'; + if (r === 'uncommon') return 'Uncommon'; + return 'Common'; + } + + return 'Other'; +}; + + +export const StackView: React.FC = ({ cards, cardWidth = 150, onCardClick, onHover, disableHoverPreview = false, groupBy = 'color' }) => { const categorizedCards = useMemo(() => { const categories: Record = {}; - CATEGORY_ORDER.forEach(c => categories[c] = []); + const groupKeys = GROUPS[groupBy]; + groupKeys.forEach(k => categories[k] = []); 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); + const group = getCardGroup(card, groupBy); + if (categories[group]) { + categories[group].push(card); + } else { + // Fallback for unexpected (shouldn't happen with defined logic coverage) + if (!categories['Other']) categories['Other'] = []; + categories['Other'].push(card); + } }); - // Sort cards within categories by CMC (low to high)? Or Rarity? - // Archidekt usually sorts by CMC. + // Sort cards within categories by CMC (low to high) + // Secondary sort by Name Object.keys(categories).forEach(key => { - categories[key].sort((a, b) => (a.cmc || 0) - (b.cmc || 0)); + categories[key].sort((a, b) => { + const cmcA = a.cmc || 0; + const cmcB = b.cmc || 0; + if (cmcA !== cmcB) return cmcA - cmcB; + return a.name.localeCompare(b.name); + }); }); return categories; - }, [cards]); + }, [cards, groupBy]); + + const activeGroups = GROUPS[groupBy]; return (
- {CATEGORY_ORDER.map(category => { + {activeGroups.map(category => { const catCards = categorizedCards[category]; if (catCards.length === 0) return null; diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index 2e20ed6..4e96a56 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -113,7 +113,7 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail const [cardWidth, setCardWidth] = useState(() => { const saved = localStorage.getItem('cube_cardWidth'); - return saved ? parseInt(saved) : 100; + return saved ? parseInt(saved) : 60; }); // --- Persistence Effects --- @@ -838,8 +838,8 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail
setCardWidth(parseInt(e.target.value))} diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index 193986d..1785b6e 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -134,7 +134,8 @@ const CardsDisplay: React.FC<{ onHover: (c: any) => void; emptyMessage: string; source: 'pool' | 'deck'; -}> = ({ cards, viewMode, cardWidth, onCardClick, onHover, emptyMessage, source }) => { + groupBy?: 'type' | 'color' | 'cmc' | 'rarity'; +}> = ({ cards, viewMode, cardWidth, onCardClick, onHover, emptyMessage, source, groupBy = 'color' }) => { if (cards.length === 0) { return (
@@ -174,6 +175,7 @@ const CardsDisplay: React.FC<{ }} onHover={(c) => onHover(c)} disableHoverPreview={true} + groupBy={groupBy} />
) @@ -213,8 +215,9 @@ export const DeckBuilderView: React.FC = ({ initialPool, a // Unlimited Timer (Static for now) const [timer] = useState("Unlimited"); const [layout, setLayout] = useState<'vertical' | 'horizontal'>('vertical'); - const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('grid'); - const [cardWidth, setCardWidth] = useState(100); + const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('stack'); // Default to stack as requested? Or keep grid. User didn't say default view, just default Order. + const [groupBy, setGroupBy] = useState<'type' | 'color' | 'cmc' | 'rarity'>('color'); + const [cardWidth, setCardWidth] = useState(60); const [pool, setPool] = useState(initialPool); const [deck, setDeck] = useState([]); @@ -459,15 +462,10 @@ export const DeckBuilderView: React.FC = ({ initialPool, a return (
e.preventDefault()}> + {/* Global Toolbar */} {/* Global Toolbar */}
- {/* Layout Switcher */} -
- - -
- {/* View Mode Switcher */}
@@ -475,13 +473,36 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
+ {/* Group By Dropdown (Only relevant for Stack View usually, but nice to have) */} + {viewMode === 'stack' && ( +
+ Sort: + +
+ )} + + {/* Layout Switcher */} +
+ + +
+ {/* Slider */}
setCardWidth(parseInt(e.target.value))} @@ -570,7 +591,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
{renderLandStation()} - +
{/* Deck Column */} @@ -579,7 +600,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a Library ({deck.length})
- +
@@ -592,7 +613,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a
{renderLandStation()} - +
{/* Bottom: Deck */} @@ -601,7 +622,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a Library ({deck.length})
- +
diff --git a/src/client/src/modules/draft/DraftView.tsx b/src/client/src/modules/draft/DraftView.tsx index 059198d..2fa385f 100644 --- a/src/client/src/modules/draft/DraftView.tsx +++ b/src/client/src/modules/draft/DraftView.tsx @@ -66,7 +66,7 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI const [cardScale, setCardScale] = useState(() => { const saved = localStorage.getItem('draft_cardScale'); - return saved ? parseFloat(saved) : 0.5; + return saved ? parseFloat(saved) : 0.35; }); const [layout, setLayout] = useState<'vertical' | 'horizontal'>('horizontal'); @@ -190,8 +190,8 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI setCardScale(parseFloat(e.target.value))}