- Created RunnersOnBase.vue component with split layout (runners left, catcher right) - Created RunnerCard.vue for individual runner cards with expand-in-place functionality - Integrated component into GamePlay.vue (mobile and desktop layouts) - Added team color computed properties for batting/fielding teams - Component only shows when runners on base (hasRunners computed) - Click runner card to expand in place and show full player card + catcher matchup - Smooth CSS transitions and animations matching pitcher/batter card style - Includes design documentation and HTML mockup for reference
626 lines
30 KiB
HTML
626 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Runner Display Mockups - Options 1, 3, & 4</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
/* Option 1 Styles */
|
|
.runner-card-option1 {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.runner-card-option1:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
.runner-card-option1.occupied {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Option 3 Styles */
|
|
.runner-card-option3 {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.runner-card-option3:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
/* Option 4 Styles */
|
|
.runner-card-option4 {
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
.runner-card-option4.empty {
|
|
cursor: default;
|
|
}
|
|
.runner-card-option4.occupied {
|
|
cursor: pointer;
|
|
}
|
|
.runner-card-option4.occupied:hover:not(.expanded) {
|
|
transform: translateX(4px);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
}
|
|
|
|
/* Expanded state - runner card grows */
|
|
.runner-card-option4.expanded {
|
|
transform: scale(1.02);
|
|
z-index: 10;
|
|
}
|
|
|
|
/* Pulse animation for occupied bases */
|
|
@keyframes pulse-glow {
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
|
|
50% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0); }
|
|
}
|
|
|
|
.pulse-occupied {
|
|
animation: pulse-glow 2s ease-in-out infinite;
|
|
}
|
|
|
|
/* Slide-in animation for Option 4 */
|
|
@keyframes slideInRight {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.slide-in {
|
|
animation: slideInRight 0.3s ease-out;
|
|
}
|
|
|
|
/* Matchup card styles */
|
|
.matchup-card {
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
@keyframes pulseGlowBlue {
|
|
0%, 100% {
|
|
box-shadow: 0 0 15px 2px rgba(59, 130, 246, 0.5), 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 30px 8px rgba(59, 130, 246, 0.7), 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
|
}
|
|
}
|
|
|
|
@keyframes pulseGlowGreen {
|
|
0%, 100% {
|
|
box-shadow: 0 0 15px 2px rgba(16, 185, 129, 0.5), 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 30px 8px rgba(16, 185, 129, 0.7), 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
|
}
|
|
}
|
|
|
|
/* Expand animation */
|
|
.expand-height {
|
|
overflow: hidden;
|
|
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
/* Card image fade in */
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.3s ease-out;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 p-8">
|
|
<div class="max-w-6xl mx-auto">
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-4xl font-bold text-gray-900 mb-2">Runner Display Mockups</h1>
|
|
<p class="text-gray-600">Interactive mockups of Options 1, 3, and 4 for runners on base display</p>
|
|
</div>
|
|
|
|
<!-- Toggle Scenarios -->
|
|
<div class="mb-8 bg-white rounded-lg shadow-md p-6">
|
|
<h2 class="text-xl font-semibold mb-4">Test Scenarios</h2>
|
|
<div class="flex flex-wrap gap-3">
|
|
<button onclick="setScenario('empty')" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium transition">
|
|
All Empty
|
|
</button>
|
|
<button onclick="setScenario('single')" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium transition">
|
|
Runner on 2B
|
|
</button>
|
|
<button onclick="setScenario('double')" class="px-4 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg font-medium transition">
|
|
Runners on 1B & 3B
|
|
</button>
|
|
<button onclick="setScenario('loaded')" class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition">
|
|
Bases Loaded
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OPTION 1: Horizontal Runner Card Bar -->
|
|
<section class="mb-12 bg-white rounded-xl shadow-lg p-8">
|
|
<div class="mb-6">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-2">Option 1: Horizontal Runner Card Bar</h2>
|
|
<p class="text-gray-600">Three equal-width cards showing all bases with equal visual weight</p>
|
|
</div>
|
|
|
|
<div class="bg-gradient-to-b from-gray-50 to-gray-100 rounded-lg p-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-4 uppercase tracking-wide">Runners on Base</h3>
|
|
|
|
<!-- Desktop View -->
|
|
<div class="hidden md:grid md:grid-cols-3 gap-4" id="option1-desktop">
|
|
<!-- Cards will be inserted here -->
|
|
</div>
|
|
|
|
<!-- Mobile View -->
|
|
<div class="md:hidden space-y-3" id="option1-mobile">
|
|
<!-- Cards will be inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pros/Cons -->
|
|
<div class="mt-6 grid md:grid-cols-2 gap-4">
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<h4 class="font-semibold text-green-800 mb-2">✅ Pros</h4>
|
|
<ul class="text-sm text-green-700 space-y-1">
|
|
<li>• Very compact (single row)</li>
|
|
<li>• Clear visual distinction</li>
|
|
<li>• Easy touch targets</li>
|
|
<li>• Balanced layout</li>
|
|
</ul>
|
|
</div>
|
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<h4 class="font-semibold text-red-800 mb-2">❌ Cons</h4>
|
|
<ul class="text-sm text-red-700 space-y-1">
|
|
<li>• Always shows all 3 cards</li>
|
|
<li>• May feel cluttered when bases empty</li>
|
|
<li>• Less visual hierarchy</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- OPTION 3: Stacked Runner Cards -->
|
|
<section class="mb-12 bg-white rounded-xl shadow-lg p-8">
|
|
<div class="mb-6">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-2">Option 3: Stacked Runner Cards</h2>
|
|
<p class="text-gray-600">Only occupied bases get prominent cards, empty bases are minimal chips</p>
|
|
</div>
|
|
|
|
<div class="bg-gradient-to-b from-blue-50 to-blue-100 rounded-lg p-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-4 uppercase tracking-wide">Runners on Base</h3>
|
|
|
|
<div class="flex flex-wrap gap-3" id="option3-container">
|
|
<!-- Cards will be inserted here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pros/Cons -->
|
|
<div class="mt-6 grid md:grid-cols-2 gap-4">
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<h4 class="font-semibold text-green-800 mb-2">✅ Pros</h4>
|
|
<ul class="text-sm text-green-700 space-y-1">
|
|
<li>• Optimizes for common case (0-1 runners)</li>
|
|
<li>• Clear visual hierarchy</li>
|
|
<li>• Easy card access with button</li>
|
|
<li>• Mobile-first design</li>
|
|
<li>• Professional card-based UI</li>
|
|
</ul>
|
|
</div>
|
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<h4 class="font-semibold text-red-808 mb-2">❌ Cons</h4>
|
|
<ul class="text-sm text-red-700 space-y-1">
|
|
<li>• More horizontal space with 3 runners</li>
|
|
<li>• Requires player headshots</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- OPTION 4: Expanding Runner Cards with Matchup (NEW - RECOMMENDED) -->
|
|
<section class="mb-12 bg-gradient-to-br from-purple-50 to-indigo-50 rounded-xl shadow-lg p-8 border-2 border-purple-200">
|
|
<div class="mb-6">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-2">
|
|
Option 4: Expanding Runner Cards with Matchup
|
|
<span class="ml-2 text-sm bg-purple-600 text-white px-3 py-1 rounded-full">NEW RECOMMENDATION</span>
|
|
</h2>
|
|
<p class="text-gray-600">
|
|
Runners list on left, catcher on right. <strong>Click a runner to expand it in place</strong> and show full card + catcher matchup.
|
|
<strong>Only visible when runners on base.</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Container with split layout -->
|
|
<div id="option4-container" class="bg-white rounded-lg shadow-md overflow-hidden">
|
|
<!-- Split Layout: Runners List | Catcher Card -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-0">
|
|
<!-- LEFT: Runners List (with expandable cards) -->
|
|
<div class="border-r border-gray-200 p-4">
|
|
<h3 class="text-xs font-semibold text-gray-500 mb-3 uppercase tracking-wide">Runners on Base</h3>
|
|
|
|
<div id="option4-list" class="space-y-2">
|
|
<!-- Runner cards will be inserted here -->
|
|
</div>
|
|
|
|
<!-- Hidden state message -->
|
|
<div id="option4-hidden" class="hidden text-center py-6 text-gray-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
<p class="text-sm">No runners on base</p>
|
|
<p class="text-xs text-gray-400 mt-1">Component hidden</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT: Catcher Card -->
|
|
<div class="p-4 bg-gray-50" id="option4-catcher-container">
|
|
<h3 class="text-xs font-semibold text-gray-500 mb-3 uppercase tracking-wide">Catcher</h3>
|
|
|
|
<!-- Collapsed state - minimal card -->
|
|
<div id="catcher-collapsed" class="bg-white border-l-4 border-gray-600 rounded-lg p-3 shadow-sm transition-all duration-300">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-12 h-12 rounded-full overflow-hidden border-2 border-gray-600 flex-shrink-0">
|
|
<img src="https://via.placeholder.com/80/6b7280/ffffff?text=C" class="w-full h-full object-cover">
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-sm font-bold text-gray-900">Buster Posey</div>
|
|
<div class="text-xs text-gray-600">#28 • C</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 text-xs text-gray-500 text-center">
|
|
Click a runner to see matchup →
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Expanded state - full card (hidden by default) -->
|
|
<div id="catcher-expanded" class="hidden matchup-card bg-gradient-to-b from-green-900 to-green-950 border-2 border-green-600 rounded-xl overflow-hidden shadow-lg fade-in" style="animation: pulseGlowGreen 2s ease-in-out infinite;">
|
|
<!-- Card Header -->
|
|
<div class="bg-green-800/80 px-3 py-2 flex items-center gap-2 text-white text-sm font-semibold">
|
|
<span class="font-bold text-white/90">CATCHER</span>
|
|
<span class="text-white/70">C</span>
|
|
<span class="truncate flex-1 text-right font-bold">Buster Posey</span>
|
|
</div>
|
|
<!-- Card Image -->
|
|
<div class="p-0">
|
|
<img src="https://via.placeholder.com/400x550/6b7280/ffffff?text=Catcher+Card" class="w-full h-auto">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pros/Cons -->
|
|
<div class="mt-6 grid md:grid-cols-2 gap-4">
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<h4 class="font-semibold text-green-800 mb-2">✅ Pros</h4>
|
|
<ul class="text-sm text-green-700 space-y-1">
|
|
<li>• <strong>Expands in place</strong> - no jarring screen flip</li>
|
|
<li>• <strong>Smooth transition</strong> - runner card grows to show full player card</li>
|
|
<li>• Catcher card appears simultaneously for matchup context</li>
|
|
<li>• Only shows when needed (on_base_code > 0)</li>
|
|
<li>• Consistent with pitcher vs batter styling</li>
|
|
<li>• Clear visual hierarchy</li>
|
|
</ul>
|
|
</div>
|
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<h4 class="font-semibold text-red-800 mb-2">❌ Cons</h4>
|
|
<ul class="text-sm text-red-700 space-y-1">
|
|
<li>• Requires extra click to see full cards</li>
|
|
<li>• Slightly more complex animation</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Implementation Notes -->
|
|
<div class="mt-6 bg-purple-100 border border-purple-300 rounded-lg p-4">
|
|
<h4 class="font-semibold text-purple-900 mb-2">💡 Implementation Notes</h4>
|
|
<ul class="text-sm text-purple-800 space-y-1">
|
|
<li>• Component only renders when <code class="bg-purple-200 px-1 rounded">thisPlay.on_base_code > 0</code></li>
|
|
<li>• Default: Runners list (left) with compact cards + Catcher summary (right)</li>
|
|
<li>• Click runner → Runner card expands in place to show full player card</li>
|
|
<li>• Simultaneously, catcher card expands to show full matchup</li>
|
|
<li>• Click again or click another runner to collapse/switch</li>
|
|
<li>• Smooth height transition using CSS max-height animation</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Comparison -->
|
|
<section class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-xl shadow-lg p-8">
|
|
<h2 class="text-2xl font-bold mb-4">Final Recommendation: Option 4</h2>
|
|
<div class="space-y-2 text-lg">
|
|
<p>🎯 <strong>Smooth in-place expansion</strong> - Runner card grows to reveal full player card</p>
|
|
<p>🔄 <strong>Integrated matchup view</strong> - Catcher card appears alongside for comparison</p>
|
|
<p>👁️ <strong>No screen flipping</strong> - Everything expands/collapses in the same layout</p>
|
|
<p>📱 <strong>Mobile-first design</strong> - Works seamlessly on all screen sizes</p>
|
|
<p>⚾ <strong>Strategic context</strong> - See runner vs catcher for steal decisions</p>
|
|
<p>✨ <strong>Clean UX</strong> - Compact by default, detailed on demand</p>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
// Sample player data
|
|
const players = {
|
|
first: { name: 'Mike Trout', number: '27', team: 'blue', headshot: 'https://via.placeholder.com/80/3b82f6/ffffff?text=MT' },
|
|
second: { name: 'Aaron Judge', number: '99', team: 'red', headshot: 'https://via.placeholder.com/80/ef4444/ffffff?text=AJ' },
|
|
third: { name: 'Shohei Ohtani', number: '17', team: 'green', headshot: 'https://via.placeholder.com/80/10b981/ffffff?text=SO' }
|
|
};
|
|
|
|
const teamColors = {
|
|
blue: { border: '#3b82f6', bg: '#dbeafe' },
|
|
red: { border: '#ef4444', bg: '#fee2e2' },
|
|
green: { border: '#10b981', bg: '#d1fae5' }
|
|
};
|
|
|
|
let currentScenario = {
|
|
first: false,
|
|
second: false,
|
|
third: false
|
|
};
|
|
|
|
let selectedRunner = null;
|
|
|
|
function setScenario(type) {
|
|
switch(type) {
|
|
case 'empty':
|
|
currentScenario = { first: false, second: false, third: false };
|
|
break;
|
|
case 'single':
|
|
currentScenario = { first: false, second: true, third: false };
|
|
break;
|
|
case 'double':
|
|
currentScenario = { first: true, second: false, third: true };
|
|
break;
|
|
case 'loaded':
|
|
currentScenario = { first: true, second: true, third: true };
|
|
break;
|
|
}
|
|
selectedRunner = null;
|
|
updateDisplay();
|
|
}
|
|
|
|
function selectRunner(base) {
|
|
if (!currentScenario[base]) return; // Can't select empty base
|
|
|
|
// Toggle selection
|
|
if (selectedRunner === base) {
|
|
selectedRunner = null;
|
|
} else {
|
|
selectedRunner = base;
|
|
}
|
|
|
|
updateOption4();
|
|
}
|
|
|
|
function updateDisplay() {
|
|
updateOption1();
|
|
updateOption3();
|
|
updateOption4();
|
|
}
|
|
|
|
function updateOption1() {
|
|
const desktopContainer = document.getElementById('option1-desktop');
|
|
const mobileContainer = document.getElementById('option1-mobile');
|
|
|
|
desktopContainer.innerHTML = '';
|
|
mobileContainer.innerHTML = '';
|
|
|
|
['first', 'second', 'third'].forEach(base => {
|
|
const baseLabel = base === 'first' ? '1ST' : base === 'second' ? '2ND' : '3RD';
|
|
const player = players[base];
|
|
const color = teamColors[player.team];
|
|
|
|
// Desktop card
|
|
const desktopCard = document.createElement('div');
|
|
if (currentScenario[base]) {
|
|
desktopCard.className = 'runner-card-option1 occupied bg-white rounded-lg border-2 p-4 text-center pulse-occupied';
|
|
desktopCard.style.borderColor = color.border;
|
|
desktopCard.style.backgroundColor = color.bg;
|
|
desktopCard.innerHTML = `
|
|
<div class="text-xs font-bold mb-2" style="color: ${color.border}">${baseLabel} BASE</div>
|
|
<div class="relative w-16 h-16 mx-auto mb-2">
|
|
<img src="${player.headshot}" class="w-full h-full rounded-full border-2 object-cover" style="border-color: ${color.border}">
|
|
<div class="absolute -top-1 -right-1 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold text-white" style="background: ${color.border}">
|
|
${base === 'first' ? '1' : base === 'second' ? '2' : '3'}
|
|
</div>
|
|
</div>
|
|
<div class="text-sm font-bold text-gray-900">${player.name}</div>
|
|
<div class="text-xs text-gray-600">#${player.number}</div>
|
|
`;
|
|
} else {
|
|
desktopCard.className = 'runner-card-option1 bg-white rounded-lg border-2 border-gray-200 p-4 text-center';
|
|
desktopCard.innerHTML = `
|
|
<div class="text-xs font-bold text-gray-500 mb-2">${baseLabel} BASE</div>
|
|
<div class="w-16 h-16 mx-auto mb-2 rounded-full bg-gray-100 border-2 border-dashed border-gray-300 flex items-center justify-center">
|
|
<span class="text-gray-400 text-2xl">—</span>
|
|
</div>
|
|
<div class="text-sm text-gray-400 font-medium">Empty</div>
|
|
`;
|
|
}
|
|
desktopContainer.appendChild(desktopCard);
|
|
|
|
// Mobile card
|
|
const mobileCard = document.createElement('div');
|
|
if (currentScenario[base]) {
|
|
mobileCard.className = 'runner-card-option1 occupied bg-white rounded-lg border-2 p-3 flex items-center';
|
|
mobileCard.style.borderColor = color.border;
|
|
mobileCard.style.backgroundColor = color.bg;
|
|
mobileCard.innerHTML = `
|
|
<div class="text-xs font-bold w-12" style="color: ${color.border}">${baseLabel}</div>
|
|
<img src="${player.headshot}" class="w-12 h-12 rounded-full border-2 object-cover" style="border-color: ${color.border}">
|
|
<div class="ml-3 flex-1">
|
|
<div class="text-sm font-bold text-gray-900">${player.name}</div>
|
|
<div class="text-xs text-gray-600">#${player.number}</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
mobileCard.className = 'runner-card-option1 bg-white rounded-lg border-2 border-gray-200 p-3 flex items-center';
|
|
mobileCard.innerHTML = `
|
|
<div class="text-xs font-bold text-gray-500 w-12">${baseLabel}</div>
|
|
<div class="w-12 h-12 rounded-full bg-gray-100 border-2 border-dashed border-gray-300 flex items-center justify-center">
|
|
<span class="text-gray-400 text-xl">—</span>
|
|
</div>
|
|
<div class="ml-3 text-sm text-gray-400 font-medium">Empty</div>
|
|
`;
|
|
}
|
|
mobileContainer.appendChild(mobileCard);
|
|
});
|
|
}
|
|
|
|
function updateOption3() {
|
|
const container = document.getElementById('option3-container');
|
|
container.innerHTML = '';
|
|
|
|
['first', 'second', 'third'].forEach(base => {
|
|
const baseLabel = base === 'first' ? '1B' : base === 'second' ? '2B' : '3B';
|
|
|
|
if (currentScenario[base]) {
|
|
const player = players[base];
|
|
const color = teamColors[player.team];
|
|
const card = document.createElement('div');
|
|
card.className = 'runner-card-option3 bg-white rounded-lg shadow-md p-4 cursor-pointer';
|
|
card.style.borderLeft = `4px solid ${color.border}`;
|
|
card.style.minWidth = '160px';
|
|
card.innerHTML = `
|
|
<div class="flex items-start gap-3 mb-3">
|
|
<div class="relative flex-shrink-0">
|
|
<img src="${player.headshot}" class="w-14 h-14 rounded-full border-2 object-cover" style="border-color: ${color.border}">
|
|
<div class="absolute -bottom-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold text-white" style="background: ${color.border}">
|
|
${base === 'first' ? '1' : base === 'second' ? '2' : '3'}
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-sm font-bold text-gray-900 truncate">${player.name}</div>
|
|
<div class="text-xs text-gray-600">#${player.number} • ${baseLabel}</div>
|
|
</div>
|
|
</div>
|
|
<button class="w-full px-3 py-1.5 text-xs font-semibold rounded-md transition text-white hover:opacity-90" style="background: ${color.border}">
|
|
View Card
|
|
</button>
|
|
`;
|
|
container.appendChild(card);
|
|
} else {
|
|
const chip = document.createElement('div');
|
|
chip.className = 'inline-flex items-center px-3 py-1.5 bg-gray-200 text-gray-500 rounded-full text-sm font-medium';
|
|
chip.innerHTML = `
|
|
<span class="font-bold mr-1">${baseLabel}:</span>
|
|
<span>—</span>
|
|
`;
|
|
container.appendChild(chip);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateOption4() {
|
|
const hasRunners = currentScenario.first || currentScenario.second || currentScenario.third;
|
|
const listEl = document.getElementById('option4-list');
|
|
const hiddenEl = document.getElementById('option4-hidden');
|
|
const catcherCollapsed = document.getElementById('catcher-collapsed');
|
|
const catcherExpanded = document.getElementById('catcher-expanded');
|
|
|
|
if (!hasRunners) {
|
|
listEl.classList.add('hidden');
|
|
hiddenEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
listEl.classList.remove('hidden');
|
|
hiddenEl.classList.add('hidden');
|
|
|
|
// Clear and rebuild runner list
|
|
listEl.innerHTML = '';
|
|
|
|
['first', 'second', 'third'].forEach(base => {
|
|
const baseLabel = base === 'first' ? '1B' : base === 'second' ? '2B' : '3B';
|
|
const isExpanded = selectedRunner === base;
|
|
|
|
const card = document.createElement('div');
|
|
card.onclick = () => selectRunner(base);
|
|
|
|
if (currentScenario[base]) {
|
|
const player = players[base];
|
|
const color = teamColors[player.team];
|
|
|
|
card.className = `runner-card-option4 occupied bg-white border-l-4 rounded-lg shadow-sm slide-in ${isExpanded ? 'expanded' : ''}`;
|
|
card.style.borderColor = color.border;
|
|
|
|
// Collapsed state (summary)
|
|
const summaryHTML = `
|
|
<div class="p-2 flex items-center">
|
|
<div class="w-10 h-10 rounded-full flex-shrink-0 overflow-hidden border-2" style="border-color: ${color.border}">
|
|
<img src="${player.headshot}" class="w-full h-full object-cover">
|
|
</div>
|
|
<div class="ml-3 flex-1">
|
|
<div class="text-sm font-bold text-gray-900">${player.name}</div>
|
|
<div class="text-xs text-gray-600">#${player.number} • ${baseLabel}</div>
|
|
</div>
|
|
<div class="text-xs text-gray-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transition-transform ${isExpanded ? 'rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Expanded state (full card)
|
|
const expandedHTML = isExpanded ? `
|
|
<div class="expand-height overflow-hidden fade-in" style="max-height: 800px;">
|
|
<div class="bg-gradient-to-b from-blue-900 to-blue-950 rounded-b-lg overflow-hidden" style="animation: pulseGlowBlue 2s ease-in-out infinite;">
|
|
<div class="bg-blue-800/80 px-3 py-2 flex items-center gap-2 text-white text-xs font-semibold">
|
|
<span class="font-bold text-white/90">RUNNER</span>
|
|
<span class="text-white/70">${baseLabel}</span>
|
|
<span class="truncate flex-1 text-right font-bold">${player.name}</span>
|
|
</div>
|
|
<div class="p-0">
|
|
<img src="https://via.placeholder.com/400x550/${color.border.substring(1)}/ffffff?text=${player.name.replace(' ', '+')}" class="w-full h-auto">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : '';
|
|
|
|
card.innerHTML = summaryHTML + expandedHTML;
|
|
} else {
|
|
card.className = 'runner-card-option4 empty bg-gray-50 border-l-4 border-gray-300 rounded-lg p-2 flex items-center';
|
|
card.innerHTML = `
|
|
<div class="w-10 h-10 rounded-full bg-gray-200 border-2 border-dashed border-gray-400 flex items-center justify-center flex-shrink-0">
|
|
<span class="text-gray-400 text-sm font-bold">${baseLabel}</span>
|
|
</div>
|
|
<div class="ml-3 text-sm text-gray-400 font-medium">Empty</div>
|
|
`;
|
|
}
|
|
|
|
listEl.appendChild(card);
|
|
});
|
|
|
|
// Toggle catcher card state
|
|
if (selectedRunner && currentScenario[selectedRunner]) {
|
|
catcherCollapsed.classList.add('hidden');
|
|
catcherExpanded.classList.remove('hidden');
|
|
} else {
|
|
catcherCollapsed.classList.remove('hidden');
|
|
catcherExpanded.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// Add click handlers for Options 1 & 3
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.closest('.runner-card-option1.occupied') ||
|
|
e.target.closest('.runner-card-option3')) {
|
|
alert('In the real app, this would open the player card modal!');
|
|
}
|
|
});
|
|
|
|
// Initialize
|
|
setScenario('single');
|
|
</script>
|
|
</body>
|
|
</html>
|