strat-gameplay-webapp/frontend-sba/components/UI/ToggleSwitch.vue
Cal Corum 52706bed40 CLAUDE: Mobile drag-drop lineup builder and touch-friendly UI improvements
- Add vuedraggable for mobile-friendly lineup building
- Add touch delay and threshold settings for better mobile UX
- Add drag ghost/chosen/dragging visual states
- Add replacement mode visual feedback when dragging over occupied slots
- Add getBench action to useGameActions for substitution panel
- Add BN (bench) to valid positions in LineupPlayerState
- Update lineup service to load full lineup (active + bench)
- Add touch-manipulation CSS to UI components (ActionButton, ButtonGroup, ToggleSwitch)
- Add select-none to prevent text selection during touch interactions
- Add mobile touch patterns documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 22:17:16 -06:00

128 lines
3.0 KiB
Vue

<template>
<div class="flex items-center gap-3 select-none">
<!-- Toggle Switch -->
<button
type="button"
:disabled="disabled"
:class="switchClasses"
role="switch"
:aria-checked="modelValue"
@click="handleToggle"
>
<!-- Track -->
<span
aria-hidden="true"
:class="trackClasses"
/>
<!-- Thumb -->
<span
aria-hidden="true"
:class="thumbClasses"
/>
</button>
<!-- Label (optional) -->
<label
v-if="label"
:class="labelClasses"
@click="handleToggle"
>
{{ label }}
</label>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
modelValue: boolean
label?: string
disabled?: boolean
size?: 'sm' | 'md' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
size: 'md',
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const switchClasses = computed(() => {
const base = 'relative inline-flex items-center flex-shrink-0 cursor-pointer rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed'
// Size classes
const sizeClasses = {
sm: 'h-5 w-9',
md: 'h-6 w-11',
lg: 'h-7 w-14',
}
return `${base} ${sizeClasses[props.size]}`
})
const trackClasses = computed(() => {
const base = 'pointer-events-none absolute h-full w-full rounded-full transition-colors duration-200'
const color = props.modelValue
? 'bg-gradient-to-r from-green-500 to-green-600'
: 'bg-gray-300 dark:bg-gray-600'
return `${base} ${color}`
})
const thumbClasses = computed(() => {
const base = 'pointer-events-none absolute bg-white rounded-full shadow-lg transform transition-transform duration-200 ease-in-out'
// Size-specific dimensions and positions
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-6 w-6',
}
// Position based on state
const translateClasses = {
sm: props.modelValue ? 'translate-x-4' : 'translate-x-0.5',
md: props.modelValue ? 'translate-x-5' : 'translate-x-0.5',
lg: props.modelValue ? 'translate-x-7' : 'translate-x-0.5',
}
return `${base} ${sizeClasses[props.size]} ${translateClasses[props.size]}`
})
const labelClasses = computed(() => {
const base = 'text-sm font-medium cursor-pointer select-none'
const color = props.disabled
? 'text-gray-400 cursor-not-allowed'
: 'text-gray-700 dark:text-gray-200'
return `${base} ${color}`
})
const handleToggle = () => {
if (!props.disabled) {
emit('update:modelValue', !props.modelValue)
}
}
</script>
<style scoped>
/* Prevent text selection on toggle elements - critical for mobile UX */
:deep(button),
:deep(button *) {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
-webkit-touch-callout: none !important;
}
button {
touch-action: manipulation;
}
</style>