mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
refactor: ♻️ More work on rewriting notes
This commit is contained in:
parent
d29f181000
commit
8cc4ff1348
20 changed files with 514 additions and 63 deletions
65
components/ui/carousel/Carousel.vue
Normal file
65
components/ui/carousel/Carousel.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
CarouselEmits,
|
||||
CarouselProps,
|
||||
WithClassAsProps,
|
||||
} from "./interface";
|
||||
import { useProvideCarousel } from "./useCarousel";
|
||||
|
||||
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
|
||||
orientation: "horizontal",
|
||||
});
|
||||
|
||||
const emits = defineEmits<CarouselEmits>();
|
||||
|
||||
const {
|
||||
canScrollNext,
|
||||
canScrollPrev,
|
||||
carouselApi,
|
||||
carouselRef,
|
||||
orientation,
|
||||
scrollNext,
|
||||
scrollPrev,
|
||||
} = useProvideCarousel(props, emits);
|
||||
|
||||
defineExpose({
|
||||
canScrollNext,
|
||||
canScrollPrev,
|
||||
carouselApi,
|
||||
carouselRef,
|
||||
orientation,
|
||||
scrollNext,
|
||||
scrollPrev,
|
||||
});
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
const prevKey = props.orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
|
||||
const nextKey =
|
||||
props.orientation === "vertical" ? "ArrowDown" : "ArrowRight";
|
||||
|
||||
if (event.key === prevKey) {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === nextKey) {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('relative', props.class)"
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
tabindex="0"
|
||||
@keydown="onKeyDown"
|
||||
>
|
||||
<slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev />
|
||||
</div>
|
||||
</template>
|
||||
29
components/ui/carousel/CarouselContent.vue
Normal file
29
components/ui/carousel/CarouselContent.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { WithClassAsProps } from "./interface";
|
||||
import { useCarousel } from "./useCarousel";
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<WithClassAsProps>();
|
||||
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="carouselRef" class="overflow-hidden">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
23
components/ui/carousel/CarouselItem.vue
Normal file
23
components/ui/carousel/CarouselItem.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { WithClassAsProps } from "./interface";
|
||||
import { useCarousel } from "./useCarousel";
|
||||
|
||||
const props = defineProps<WithClassAsProps>();
|
||||
|
||||
const { orientation } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
:class="cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
31
components/ui/carousel/CarouselNext.vue
Normal file
31
components/ui/carousel/CarouselNext.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowRight } from "lucide-vue-next";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { WithClassAsProps } from "./interface";
|
||||
import { useCarousel } from "./useCarousel";
|
||||
|
||||
const props = defineProps<WithClassAsProps>();
|
||||
|
||||
const { orientation, canScrollNext, scrollNext } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:disabled="!canScrollNext"
|
||||
:class="cn(
|
||||
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
|
||||
orientation === 'horizontal'
|
||||
? '-right-12 top-1/2 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
props.class,
|
||||
)"
|
||||
variant="outline"
|
||||
@click="scrollNext"
|
||||
>
|
||||
<slot>
|
||||
<ArrowRight class="h-4 w-4 text-current" />
|
||||
<span class="sr-only">Next Slide</span>
|
||||
</slot>
|
||||
</Button>
|
||||
</template>
|
||||
31
components/ui/carousel/CarouselPrevious.vue
Normal file
31
components/ui/carousel/CarouselPrevious.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowLeft } from "lucide-vue-next";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { WithClassAsProps } from "./interface";
|
||||
import { useCarousel } from "./useCarousel";
|
||||
|
||||
const props = defineProps<WithClassAsProps>();
|
||||
|
||||
const { orientation, canScrollPrev, scrollPrev } = useCarousel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:disabled="!canScrollPrev"
|
||||
:class="cn(
|
||||
'touch-manipulation absolute h-8 w-8 rounded-full p-0',
|
||||
orientation === 'horizontal'
|
||||
? '-left-12 top-1/2 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
props.class,
|
||||
)"
|
||||
variant="outline"
|
||||
@click="scrollPrev"
|
||||
>
|
||||
<slot>
|
||||
<ArrowLeft class="h-4 w-4 text-current" />
|
||||
<span class="sr-only">Previous Slide</span>
|
||||
</slot>
|
||||
</Button>
|
||||
</template>
|
||||
8
components/ui/carousel/index.ts
Normal file
8
components/ui/carousel/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Carousel } from "./Carousel.vue";
|
||||
export { default as CarouselContent } from "./CarouselContent.vue";
|
||||
export { default as CarouselItem } from "./CarouselItem.vue";
|
||||
export { default as CarouselNext } from "./CarouselNext.vue";
|
||||
export { default as CarouselPrevious } from "./CarouselPrevious.vue";
|
||||
export type { UnwrapRefCarouselApi as CarouselApi } from "./interface";
|
||||
|
||||
export { useCarousel } from "./useCarousel";
|
||||
25
components/ui/carousel/interface.ts
Normal file
25
components/ui/carousel/interface.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type useEmblaCarousel from "embla-carousel-vue";
|
||||
import type { EmblaCarouselVueType } from "embla-carousel-vue";
|
||||
import type { HTMLAttributes, UnwrapRef } from "vue";
|
||||
|
||||
type CarouselApi = EmblaCarouselVueType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
export type UnwrapRefCarouselApi = UnwrapRef<CarouselApi>;
|
||||
|
||||
export interface CarouselProps {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export type CarouselEmits = (
|
||||
e: "init-api",
|
||||
payload: UnwrapRefCarouselApi,
|
||||
) => void;
|
||||
|
||||
export interface WithClassAsProps {
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
69
components/ui/carousel/useCarousel.ts
Normal file
69
components/ui/carousel/useCarousel.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { createInjectionState } from "@vueuse/core";
|
||||
import emblaCarouselVue from "embla-carousel-vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import type {
|
||||
UnwrapRefCarouselApi as CarouselApi,
|
||||
CarouselEmits,
|
||||
CarouselProps,
|
||||
} from "./interface";
|
||||
|
||||
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
|
||||
({ opts, orientation, plugins }: CarouselProps, emits: CarouselEmits) => {
|
||||
const [emblaNode, emblaApi] = emblaCarouselVue(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
|
||||
function scrollPrev() {
|
||||
emblaApi.value?.scrollPrev();
|
||||
}
|
||||
function scrollNext() {
|
||||
emblaApi.value?.scrollNext();
|
||||
}
|
||||
|
||||
const canScrollNext = ref(false);
|
||||
const canScrollPrev = ref(false);
|
||||
|
||||
function onSelect(api: CarouselApi) {
|
||||
canScrollNext.value = !!api?.canScrollNext();
|
||||
canScrollPrev.value = !!api?.canScrollPrev();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!emblaApi.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emblaApi.value?.on("init", onSelect);
|
||||
emblaApi.value?.on("reInit", onSelect);
|
||||
emblaApi.value?.on("select", onSelect);
|
||||
|
||||
emits("init-api", emblaApi.value);
|
||||
});
|
||||
|
||||
return {
|
||||
carouselRef: emblaNode,
|
||||
carouselApi: emblaApi,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
orientation,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
function useCarousel() {
|
||||
const carouselState = useInjectCarousel();
|
||||
|
||||
if (!carouselState) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return carouselState;
|
||||
}
|
||||
|
||||
export { useCarousel, useProvideCarousel };
|
||||
Loading…
Add table
Add a link
Reference in a new issue