CLAUDE: Enhance baserunner panel with lead runner auto-select and clickable catcher
- Swap base order to 3B, 2B, 1B (left to right, closer to baseball diamond) - Auto-select lead runner on mount (priority: 3B > 2B > 1B) - Make catcher pill clickable to show catcher card only - Add 'catcher' as a selection option alongside runner bases - Update expanded view to handle catcher-only display (centered, single card) - Add toggleCatcher() function - Update tests for new base order and auto-selection behavior All 15 RunnersOnBase tests passing All 16 RunnerCard tests passing
This commit is contained in:
parent
5118335020
commit
d6ea5104d6
2317
.claude/X_CHECK_INTERACTIVE_WORKFLOW.md
Normal file
2317
.claude/X_CHECK_INTERACTIVE_WORKFLOW.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,9 +17,15 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Catcher summary pill (fixed width, full row height) -->
|
||||
<!-- Catcher summary pill (fixed width, full row height, clickable) -->
|
||||
<div class="w-28 md:w-36 flex-shrink-0">
|
||||
<div class="catcher-pill h-full flex flex-col items-center justify-center p-2 rounded-lg bg-white border border-gray-200 shadow-sm text-center">
|
||||
<div
|
||||
:class="[
|
||||
'catcher-pill h-full flex flex-col items-center justify-center p-2 rounded-lg border shadow-sm text-center cursor-pointer transition-all duration-200',
|
||||
selectedRunner === 'catcher' ? 'bg-green-50 border-green-500 ring-2 ring-green-500' : 'bg-white border-gray-200 hover:bg-green-50'
|
||||
]"
|
||||
@click="toggleCatcher"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-full overflow-hidden border-2 border-gray-600 flex-shrink-0">
|
||||
<img
|
||||
v-if="catcherPlayer?.headshot || catcherPlayer?.image"
|
||||
@ -37,49 +43,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EXPANDED ROW: Runner card + Catcher card side by side (when a runner is selected) -->
|
||||
<!-- EXPANDED ROW: Runner card + Catcher card side by side (when a runner or catcher is selected) -->
|
||||
<Transition name="expand">
|
||||
<div v-if="hasSelection && selectedRunnerPlayer" class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<!-- Selected Runner full card -->
|
||||
<div class="matchup-card-blue bg-gradient-to-b from-blue-900 to-blue-950 border-2 border-blue-600 rounded-xl overflow-hidden shadow-lg">
|
||||
<div class="bg-blue-800/80 px-3 py-2 flex items-center gap-2 text-white text-sm font-semibold">
|
||||
<span class="font-bold text-white/90" :style="{ color: battingTeamColor }">
|
||||
{{ battingTeamAbbrev }}
|
||||
</span>
|
||||
<span class="text-white/70">{{ selectedBase }}</span>
|
||||
<span class="truncate flex-1 text-right font-bold">{{ selectedRunnerName }}</span>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<img
|
||||
v-if="selectedRunnerPlayer?.image"
|
||||
:src="selectedRunnerPlayer.image"
|
||||
:alt="`${selectedRunnerName} card`"
|
||||
class="w-full h-auto"
|
||||
>
|
||||
<div v-else class="w-full aspect-[4/5.5] bg-gradient-to-br from-blue-700 to-blue-900 flex items-center justify-center">
|
||||
<span class="text-5xl font-bold text-white/60">{{ selectedRunnerInitials }}</span>
|
||||
<div v-if="hasSelection" class="mt-3">
|
||||
<!-- Catcher only view (when catcher selected) -->
|
||||
<div v-if="selectedRunner === 'catcher'" class="flex justify-center">
|
||||
<div class="matchup-card bg-gradient-to-b from-green-900 to-green-950 border-2 border-green-600 rounded-xl overflow-hidden shadow-lg max-w-md w-full">
|
||||
<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" :style="{ color: fieldingTeamColor }">
|
||||
{{ fieldingTeamAbbrev }}
|
||||
</span>
|
||||
<span class="text-white/70">C</span>
|
||||
<span class="truncate flex-1 text-right font-bold">{{ catcherName }}</span>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<img
|
||||
v-if="catcherPlayer?.image"
|
||||
:src="catcherPlayer.image"
|
||||
:alt="`${catcherName} card`"
|
||||
class="w-full h-auto"
|
||||
>
|
||||
<div v-else class="w-full aspect-[4/5.5] bg-gradient-to-br from-green-700 to-green-900 flex items-center justify-center">
|
||||
<span class="text-5xl font-bold text-white/60">{{ getCatcherInitials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Catcher full card -->
|
||||
<div class="matchup-card bg-gradient-to-b from-green-900 to-green-950 border-2 border-green-600 rounded-xl overflow-hidden shadow-lg">
|
||||
<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" :style="{ color: fieldingTeamColor }">
|
||||
{{ fieldingTeamAbbrev }}
|
||||
</span>
|
||||
<span class="text-white/70">C</span>
|
||||
<span class="truncate flex-1 text-right font-bold">{{ catcherName }}</span>
|
||||
<!-- Runner + Catcher view (when runner selected) -->
|
||||
<div v-else-if="selectedRunnerPlayer" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<!-- Selected Runner full card -->
|
||||
<div class="matchup-card-blue bg-gradient-to-b from-blue-900 to-blue-950 border-2 border-blue-600 rounded-xl overflow-hidden shadow-lg">
|
||||
<div class="bg-blue-800/80 px-3 py-2 flex items-center gap-2 text-white text-sm font-semibold">
|
||||
<span class="font-bold text-white/90" :style="{ color: battingTeamColor }">
|
||||
{{ battingTeamAbbrev }}
|
||||
</span>
|
||||
<span class="text-white/70">{{ selectedBase }}</span>
|
||||
<span class="truncate flex-1 text-right font-bold">{{ selectedRunnerName }}</span>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<img
|
||||
v-if="selectedRunnerPlayer?.image"
|
||||
:src="selectedRunnerPlayer.image"
|
||||
:alt="`${selectedRunnerName} card`"
|
||||
class="w-full h-auto"
|
||||
>
|
||||
<div v-else class="w-full aspect-[4/5.5] bg-gradient-to-br from-blue-700 to-blue-900 flex items-center justify-center">
|
||||
<span class="text-5xl font-bold text-white/60">{{ selectedRunnerInitials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<img
|
||||
v-if="catcherPlayer?.image"
|
||||
:src="catcherPlayer.image"
|
||||
:alt="`${catcherName} card`"
|
||||
class="w-full h-auto"
|
||||
>
|
||||
<div v-else class="w-full aspect-[4/5.5] bg-gradient-to-br from-green-700 to-green-900 flex items-center justify-center">
|
||||
<span class="text-5xl font-bold text-white/60">{{ getCatcherInitials }}</span>
|
||||
|
||||
<!-- Catcher full card -->
|
||||
<div class="matchup-card bg-gradient-to-b from-green-900 to-green-950 border-2 border-green-600 rounded-xl overflow-hidden shadow-lg">
|
||||
<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" :style="{ color: fieldingTeamColor }">
|
||||
{{ fieldingTeamAbbrev }}
|
||||
</span>
|
||||
<span class="text-white/70">C</span>
|
||||
<span class="truncate flex-1 text-right font-bold">{{ catcherName }}</span>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<img
|
||||
v-if="catcherPlayer?.image"
|
||||
:src="catcherPlayer.image"
|
||||
:alt="`${catcherName} card`"
|
||||
class="w-full h-auto"
|
||||
>
|
||||
<div v-else class="w-full aspect-[4/5.5] bg-gradient-to-br from-green-700 to-green-900 flex items-center justify-center">
|
||||
<span class="text-5xl font-bold text-white/60">{{ getCatcherInitials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -89,7 +122,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import type { LineupPlayerState } from '~/types/game'
|
||||
import type { Lineup } from '~/types/player'
|
||||
import { useGameStore } from '~/store/game'
|
||||
@ -117,13 +150,27 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const selectedRunner = ref<'first' | 'second' | 'third' | null>(null)
|
||||
const selectedRunner = ref<'first' | 'second' | 'third' | 'catcher' | null>(null)
|
||||
|
||||
// Helper constants for iteration
|
||||
const baseKeys = ['first', 'second', 'third'] as const
|
||||
const baseLabels: ('1B' | '2B' | '3B')[] = ['1B', '2B', '3B']
|
||||
// Helper constants for iteration (3B, 2B, 1B order - left to right)
|
||||
const baseKeys = ['third', 'second', 'first'] as const
|
||||
const baseLabels: ('1B' | '2B' | '3B')[] = ['3B', '2B', '1B']
|
||||
const baseNameToLabel: Record<string, string> = { first: '1B', second: '2B', third: '3B' }
|
||||
|
||||
// Auto-select lead runner on mount
|
||||
onMounted(() => {
|
||||
if (!selectedRunner.value) {
|
||||
// Select lead runner (third → second → first priority)
|
||||
if (props.runners.third) {
|
||||
selectedRunner.value = 'third'
|
||||
} else if (props.runners.second) {
|
||||
selectedRunner.value = 'second'
|
||||
} else if (props.runners.first) {
|
||||
selectedRunner.value = 'first'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Check if any runners on base
|
||||
const hasRunners = computed(() => {
|
||||
return !!(props.runners.first || props.runners.second || props.runners.third)
|
||||
@ -196,9 +243,18 @@ function toggleRunner(base: 'first' | 'second' | 'third') {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCatcher() {
|
||||
// Toggle catcher selection
|
||||
if (selectedRunner.value === 'catcher') {
|
||||
selectedRunner.value = null
|
||||
} else {
|
||||
selectedRunner.value = 'catcher'
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-deselect when the selected runner's base becomes empty
|
||||
watch(() => props.runners, (newRunners) => {
|
||||
if (selectedRunner.value && !newRunners[selectedRunner.value]) {
|
||||
if (selectedRunner.value && selectedRunner.value !== 'catcher' && !newRunners[selectedRunner.value]) {
|
||||
selectedRunner.value = null
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
@ -159,9 +159,10 @@ describe("RunnersOnBase", () => {
|
||||
|
||||
const runnerCards = wrapper.findAllComponents(RunnerCard);
|
||||
expect(runnerCards.length).toBeGreaterThanOrEqual(3);
|
||||
expect(runnerCards[0].props("base")).toBe("1B");
|
||||
// Order is now 3B, 2B, 1B (left to right)
|
||||
expect(runnerCards[0].props("base")).toBe("3B");
|
||||
expect(runnerCards[1].props("base")).toBe("2B");
|
||||
expect(runnerCards[2].props("base")).toBe("3B");
|
||||
expect(runnerCards[2].props("base")).toBe("1B");
|
||||
});
|
||||
|
||||
it("passes runner data to RunnerCard components", () => {
|
||||
@ -182,9 +183,10 @@ describe("RunnersOnBase", () => {
|
||||
});
|
||||
|
||||
const runnerCards = wrapper.findAllComponents(RunnerCard);
|
||||
expect(runnerCards[0].props("runner")).toEqual(mockRunnerFirst);
|
||||
expect(runnerCards[1].props("runner")).toEqual(mockRunnerSecond);
|
||||
expect(runnerCards[2].props("runner")).toBeNull();
|
||||
// Order is now 3B, 2B, 1B (left to right)
|
||||
expect(runnerCards[0].props("runner")).toBeNull(); // 3B
|
||||
expect(runnerCards[1].props("runner")).toEqual(mockRunnerSecond); // 2B
|
||||
expect(runnerCards[2].props("runner")).toEqual(mockRunnerFirst); // 1B
|
||||
});
|
||||
|
||||
it("passes team color to RunnerCard components", () => {
|
||||
@ -483,15 +485,19 @@ describe("RunnersOnBase", () => {
|
||||
|
||||
const runnerCards = wrapper.findAllComponents(RunnerCard);
|
||||
|
||||
// Select first runner
|
||||
await runnerCards[0].trigger("click");
|
||||
expect(runnerCards[0].props("isSelected")).toBe(true);
|
||||
expect(runnerCards[1].props("isSelected")).toBe(false);
|
||||
// Lead runner (2nd base, which is index 1 in 3B-2B-1B order) is auto-selected on mount
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(runnerCards[1].props("isSelected")).toBe(true); // 2B auto-selected
|
||||
|
||||
// Select second runner
|
||||
// Click 1B runner (index 2)
|
||||
await runnerCards[2].trigger("click");
|
||||
expect(runnerCards[1].props("isSelected")).toBe(false); // 2B deselected
|
||||
expect(runnerCards[2].props("isSelected")).toBe(true); // 1B selected
|
||||
|
||||
// Click 2B runner again (index 1)
|
||||
await runnerCards[1].trigger("click");
|
||||
expect(runnerCards[0].props("isSelected")).toBe(false);
|
||||
expect(runnerCards[1].props("isSelected")).toBe(true);
|
||||
expect(runnerCards[2].props("isSelected")).toBe(false); // 1B deselected
|
||||
expect(runnerCards[1].props("isSelected")).toBe(true); // 2B selected
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user