152 lines
3.0 KiB
Vue
152 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
import type { PropType } from 'vue'
|
|
|
|
const emit = defineEmits(['input', 'change', 'update:modelValue'])
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
required: false,
|
|
},
|
|
size: {
|
|
type: String as PropType<'sm' | 'md' | 'lg'>,
|
|
required: false,
|
|
default: 'md',
|
|
},
|
|
value: {
|
|
type: Boolean,
|
|
required: false,
|
|
},
|
|
onIcon: {
|
|
type: String,
|
|
required: false,
|
|
},
|
|
offIcon: {
|
|
type: String,
|
|
required: false,
|
|
},
|
|
})
|
|
|
|
const checked = ref(false)
|
|
|
|
const buttonSizeClass = computed(() => {
|
|
switch (props.size) {
|
|
case 'sm':
|
|
return 'h-6 w-10'
|
|
case 'md':
|
|
return 'h-8 w-14'
|
|
case 'lg':
|
|
return 'h-10 w-[calc(2.5rem/0.54)]'
|
|
}
|
|
})
|
|
|
|
const buttonPaddingClass = computed(() => {
|
|
switch (props.size) {
|
|
case 'sm':
|
|
return 'p-1'
|
|
case 'md':
|
|
return 'p-1'
|
|
case 'lg':
|
|
return 'p-1.5'
|
|
}
|
|
})
|
|
|
|
const bulletSizeClass = computed(() => {
|
|
switch (props.size) {
|
|
case 'sm':
|
|
return 'h-4'
|
|
case 'md':
|
|
return 'h-6'
|
|
case 'lg':
|
|
return 'h-7'
|
|
}
|
|
})
|
|
|
|
const bulletTranslateClass = computed(() => {
|
|
switch (props.size) {
|
|
case 'sm':
|
|
return 'translate-x-4'
|
|
case 'md':
|
|
return 'translate-x-6'
|
|
case 'lg':
|
|
return 'translate-x-8'
|
|
}
|
|
})
|
|
|
|
const handleCheck = () => {
|
|
checked.value = !checked.value
|
|
emit('update:modelValue', checked.value)
|
|
emit('change', checked.value)
|
|
emit('input', checked.value)
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (props.modelValue) {
|
|
checked.value = props.modelValue
|
|
} else if (props.value) {
|
|
checked.value = props.value
|
|
}
|
|
})
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(value) => {
|
|
checked.value = value
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<button
|
|
class="relative flex items-center rounded-lg bg-neutral-100 dark:bg-neutral-800 shadow-inner transition ease-in-out group outline-hidden"
|
|
:class="{
|
|
'bg-green-400! dark:bg-green-400/50!': checked,
|
|
[buttonSizeClass]: buttonSizeClass,
|
|
[buttonPaddingClass]: buttonPaddingClass,
|
|
}"
|
|
@click="handleCheck"
|
|
>
|
|
<span
|
|
class="aspect-1/1 translate-x-0 transition ease-in-out bg-white dark:bg-black rounded-md shadow-sm duration-300 group-active:scale-90"
|
|
:class="{
|
|
'shadow-lg!': checked,
|
|
'group-active:translate-x-3 group-active:duration-500': !checked,
|
|
[bulletSizeClass]: bulletSizeClass,
|
|
[bulletTranslateClass]: checked,
|
|
}"
|
|
>
|
|
<span
|
|
v-if="onIcon || offIcon"
|
|
class="absolute inset-0 flex items-center justify-center text-neutral-400"
|
|
>
|
|
<Transition
|
|
name="icon"
|
|
mode="out-in"
|
|
>
|
|
<slot
|
|
v-if="checked"
|
|
name="on-icon"
|
|
/>
|
|
<slot
|
|
v-else
|
|
name="off-icon"
|
|
/>
|
|
</Transition>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.icon-enter-active,
|
|
.icon-leave-active {
|
|
transition: all 0.1s ease;
|
|
}
|
|
|
|
.icon-enter-from,
|
|
.icon-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.5);
|
|
}
|
|
</style>
|