Files
takerofnotes-website/components/Slider.vue
2026-05-29 11:22:56 -04:00

174 lines
3.7 KiB
Vue

<template>
<div class="slider">
<div class="slider-container" ref="emblaNode">
<div class="slides">
<slot />
</div>
</div>
<div v-if="controls" class="controls">
<button @click="onPrev">
<arrow />
</button>
<button @click="onNext">
<arrow />
</button>
</div>
<div v-if="nav && slideCount > 1" class="nav">
<btn
v-for="i in slideCount"
@click="onNavClick(i)"
:secondary="activeIndex !== i"
number
>{{ i }}</btn
>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import Btn from '@/components/Btn.vue'
import Arrow from '@/components/svg/util/Arrow.vue'
import embla from 'embla-carousel-vue'
import ClassNames from 'embla-carousel-class-names'
import Autoplay from 'embla-carousel-autoplay'
const props = defineProps({
emblaOptions: {
type: Object,
default: () => {},
},
autoplay: {
type: Boolean,
default: () => false,
},
autoplayOptions: {
type: Object,
default: () => {},
},
controls: {
type: Boolean,
default: () => false,
},
nav: {
type: Boolean,
default: () => false,
},
slideCount: {
type: Number,
default: () => 0,
},
})
const plugins = computed(() => {
const base = [ClassNames()]
if (props.autoplay) {
base.push(Autoplay(props.autoplayOptions))
}
return base
})
const [emblaNode, emblaApi] = embla(props.emblaOptions, plugins.value)
defineExpose({
api: emblaApi,
})
const activeIndex = ref(1)
const onNext = () => {
emblaApi.value.scrollNext()
}
const onPrev = () => {
emblaApi.value.scrollPrev()
}
const onNavClick = (i) => {
activeIndex.value = i
emblaApi.value.scrollTo(i - 1)
}
watch(emblaApi, () => {
if (!emblaApi.value) return
emblaApi.value.on('select', () => {
activeIndex.value = emblaApi.value.selectedScrollSnap() + 1
})
})
</script>
<style lang="scss">
.slider {
position: relative;
.slider-container {
max-width: 100vw;
overflow: hidden;
}
.slides {
display: flex;
> * {
flex-shrink: 0;
}
}
.controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--layout-margin);
position: absolute;
inset: 0;
pointer-events: none;
button {
pointer-events: all;
border-radius: 50%;
background: var(--black);
color: var(--white);
width: desktop-vw(40px);
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
svg {
width: desktop-vw(21px);
height: auto;
}
&:first-child {
transform: scaleX(-1);
}
&:disabled {
opacity: 0.5;
cursor: default;
}
}
@include mobile {
button {
width: mobile-vw(32px);
svg {
width: mobile-vw(16px);
}
}
}
}
.nav {
position: absolute;
left: 0;
bottom: desktop-vw(25px);
right: 0;
display: flex;
justify-content: center;
@include mobile {
bottom: mobile-vw(25px);
}
}
}
</style>