feat: Introduce card size slider for unified scaling across grid and stack views, and add smart preview suppression.
This commit is contained in:
@@ -55,3 +55,6 @@
|
||||
- [Entrance Animation Fix](./devlog/2025-12-17-021500_entrance_animation_fix.md): Completed. Implemented 'isMounted' state to ensuring scale-in animation triggers correctly on first render.
|
||||
- [Foil Bug Fix](./devlog/2025-12-17-022000_foil_bug_fix.md): Completed. Fixed regression where foil animations applied to non-foil cards on desktop.
|
||||
- [Universal Foil Application](./devlog/2025-12-17-022500_universal_foil_application.md): Completed. Applied foil animation to Grid View and Stack View card thumbnails.
|
||||
- [Smart Preview Suppression](./devlog/2025-12-17-023000_smart_preview_suppression.md): Completed. Disabled hover preview for card elements that are already rendered large enough on screen.
|
||||
- [Compact Card Layout](./devlog/2025-12-17-023500_compact_card_layout.md): Completed. Decreased card sizes in Grid and Stack views for a denser UI.
|
||||
- [View Scale Slider](./devlog/2025-12-17-024000_view_scale_slider.md): Completed. Added a slider to dynamically adjust card dimensions, synced across Grid and Stack views.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Intelligent Preview Suppression
|
||||
|
||||
## Objective
|
||||
Prevent the card preview popup from appearing when the user hovers over a card that is already displayed at a significantly large size on the screen (e.g., in a large grid view), reducing UI clutter.
|
||||
|
||||
## Changes
|
||||
- Modified `CardHoverWrapper` in `src/client/src/components/CardPreview.tsx`:
|
||||
- Updated `handleMouseEnter` to inspect the dimensions of the hovered element using `getBoundingClientRect`.
|
||||
- Implemented a threshold check: `Width > 240px` AND `Height > 300px`.
|
||||
- **Logic**:
|
||||
- **Large Grid Items**: If a card in the grid is rendered wider than 240px and taller than 300px, the hover preview is suppressed.
|
||||
- **List Items**: Even if a list row is wide (e.g., 800px), its height is small (e.g., 40px), so the preview **will still appear**.
|
||||
- **Small Thumbnails**: Small grid items or stack views usually fall below this threshold, ensuring the preview appears when needed.
|
||||
|
||||
## Result
|
||||
The system now intelligently hides the preview when it is redundant, creating a cleaner experience on large desktop screens while maintaining necessary functionality for smaller thumbnails and list views.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Compact Card Layout
|
||||
|
||||
## Objective
|
||||
Slightly resize the card visualizations in both Grid and Stack views to allow more cards to fit on the screen, creating a denser and more "compact" interface as requested.
|
||||
|
||||
## Changes
|
||||
- **Pack Grid View** (`src/client/src/components/PackCard.tsx`):
|
||||
- Increased the column density across all breakpoints:
|
||||
- Base: `grid-cols-2` -> `grid-cols-3`
|
||||
- Small: `grid-cols-3` -> `grid-cols-4`
|
||||
- Medium: `grid-cols-4` -> `grid-cols-5`
|
||||
- Large: `grid-cols-5` -> `grid-cols-6`
|
||||
- This reduces the individual card width, making them visually smaller.
|
||||
- **Stack / Deck View** (`src/client/src/components/StackView.tsx`):
|
||||
- Reduced the fixed width of each stack column from `w-44` (176px) to `w-36` (144px).
|
||||
|
||||
## Result
|
||||
Cards appear slightly smaller ("a little more smaller"), providing a broader overview of the pool and deck without requiring as much scrolling. This works in tandem with the "Smart Preview Suppression" (which will likely now re-enable previews for these smaller cards, aiding readability).
|
||||
@@ -0,0 +1,19 @@
|
||||
# View Scale Slider
|
||||
|
||||
## Objective
|
||||
Provide the user with granular control over card thumbnail sizes across the application, ensuring consistency between Grid and Stack views.
|
||||
|
||||
## Changes
|
||||
- **CubeManager**:
|
||||
- Added a new `cardWidth` state variable, persisted to `localStorage` (default `140px`).
|
||||
- Introduced a **Range Slider** in the top-right control toolbar (visible on desktop) allowing adjustment from 100px to 300px.
|
||||
- Passed `cardWidth` down to `PackCard`.
|
||||
- **PackCard (Grid View)**:
|
||||
- Replaced the responsive `grid-cols-*` logic with a `flex flex-wrap` layout.
|
||||
- Each card container now receives an explicit `style={{ width: cardWidth }}`.
|
||||
- **StackView (Stack View)**:
|
||||
- Accepted `cardWidth` prop.
|
||||
- Applied `style={{ width: cardWidth }}` to the column containers, dynamically ensuring that stacks resize in sync with the grid view setting.
|
||||
|
||||
## Result
|
||||
Users can now drag a slider to instantly resize all card thumbnails on the screen. This allows for customized density—make cards huge to admire the art, or tiny to see the entire cube/pool at a glance—with perfect size synchronization between the different view modes.
|
||||
@@ -125,8 +125,17 @@ export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.React
|
||||
setCoords({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isMobile) setIsHovering(true);
|
||||
const handleMouseEnter = (e: React.MouseEvent) => {
|
||||
if (isMobile) return;
|
||||
|
||||
// Check if the card is already "big enough" on screen
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
// Width > 240 && Height > 300 targets large grid items but excludes thin list rows
|
||||
if (rect.width > 240 && rect.height > 300) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsHovering(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { StackView } from './StackView';
|
||||
interface PackCardProps {
|
||||
pack: Pack;
|
||||
viewMode: 'list' | 'grid' | 'stack';
|
||||
cardWidth?: number;
|
||||
}
|
||||
|
||||
import { CardHoverWrapper, FoilOverlay } from './CardPreview';
|
||||
@@ -41,7 +42,7 @@ const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode }) => {
|
||||
export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth = 150 }) => {
|
||||
const mythics = pack.cards.filter(c => c.rarity === 'mythic');
|
||||
const rares = pack.cards.filter(c => c.rarity === 'rare');
|
||||
const uncommons = pack.cards.filter(c => c.rarity === 'uncommon');
|
||||
@@ -97,10 +98,10 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode }) => {
|
||||
)}
|
||||
|
||||
{viewMode === 'grid' && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{pack.cards.map((card) => (
|
||||
<CardHoverWrapper key={card.id} card={card}>
|
||||
<div className="relative group bg-slate-900 rounded-lg">
|
||||
<div style={{ width: cardWidth }} className="relative group bg-slate-900 rounded-lg shrink-0">
|
||||
{/* Visual Card */}
|
||||
<div className={`relative aspect-[2.5/3.5] overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 cursor-pointer ${isFoil(card) ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
||||
{isFoil(card) && <FoilOverlay />}
|
||||
@@ -126,7 +127,7 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'stack' && <StackView cards={pack.cards} />}
|
||||
{viewMode === 'stack' && <StackView cards={pack.cards} cardWidth={cardWidth} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CardHoverWrapper, FoilOverlay } from './CardPreview';
|
||||
|
||||
interface StackViewProps {
|
||||
cards: DraftCard[];
|
||||
cardWidth?: number;
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER = [
|
||||
@@ -18,7 +19,7 @@ const CATEGORY_ORDER = [
|
||||
'Other'
|
||||
];
|
||||
|
||||
export const StackView: React.FC<StackViewProps> = ({ cards }) => {
|
||||
export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150 }) => {
|
||||
|
||||
const categorizedCards = useMemo(() => {
|
||||
const categories: Record<string, DraftCard[]> = {};
|
||||
@@ -59,7 +60,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards }) => {
|
||||
if (catCards.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={category} className="flex-shrink-0 w-44 snap-start">
|
||||
<div key={category} className="flex-shrink-0 snap-start" style={{ width: cardWidth }}>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-2 px-1 border-b border-slate-700 pb-1">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">{category}</span>
|
||||
|
||||
@@ -101,6 +101,11 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
return saved ? parseInt(saved) : 3;
|
||||
});
|
||||
|
||||
const [cardWidth, setCardWidth] = useState(() => {
|
||||
const saved = localStorage.getItem('cube_cardWidth');
|
||||
return saved ? parseInt(saved) : 140;
|
||||
});
|
||||
|
||||
// --- Persistence Effects ---
|
||||
useEffect(() => localStorage.setItem('cube_inputText', inputText), [inputText]);
|
||||
useEffect(() => localStorage.setItem('cube_filters', JSON.stringify(filters)), [filters]);
|
||||
@@ -108,6 +113,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
useEffect(() => localStorage.setItem('cube_sourceMode', sourceMode), [sourceMode]);
|
||||
useEffect(() => localStorage.setItem('cube_selectedSets', JSON.stringify(selectedSets)), [selectedSets]);
|
||||
useEffect(() => localStorage.setItem('cube_numBoxes', numBoxes.toString()), [numBoxes]);
|
||||
useEffect(() => localStorage.setItem('cube_cardWidth', cardWidth.toString()), [cardWidth]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -660,6 +666,22 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
{copySuccess ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
|
||||
<span className="hidden sm:inline">{copySuccess ? 'Copied!' : 'Copy'}</span>
|
||||
</button>
|
||||
|
||||
{/* Size Slider */}
|
||||
<div className="flex items-center gap-2 bg-slate-800 rounded-lg px-2 py-1 border border-slate-700 h-9 mr-2 hidden sm:flex">
|
||||
<div className="w-3 h-4 rounded border border-slate-500 bg-slate-700" title="Small Cards" />
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="300"
|
||||
step="10"
|
||||
value={cardWidth}
|
||||
onChange={(e) => setCardWidth(parseInt(e.target.value))}
|
||||
className="w-24 accent-purple-500 cursor-pointer h-1.5 bg-slate-600 rounded-lg appearance-none"
|
||||
title={`Card Size: ${cardWidth}px`}
|
||||
/>
|
||||
<div className="w-4 h-6 rounded border border-slate-500 bg-slate-700" title="Large Cards" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -679,7 +701,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 pb-20">
|
||||
{packs.map((pack) => (
|
||||
<PackCard key={pack.id} pack={pack} viewMode={viewMode} />
|
||||
<PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={cardWidth} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user