nuxt-hero 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/assets/hero.css +1 -1
- package/dist/runtime/components/navigation/HeroNavigation.d.vue.ts +2 -2
- package/dist/runtime/components/navigation/HeroNavigation.vue.d.ts +2 -2
- package/dist/runtime/components/navigation/HeroPagination.vue +1 -1
- package/dist/runtime/components/slider/HeroSlide.d.vue.ts +14 -7
- package/dist/runtime/components/slider/HeroSlide.vue +11 -3
- package/dist/runtime/components/slider/HeroSlide.vue.d.ts +14 -7
- package/dist/runtime/components/slider/index.vue +5 -23
- package/dist/runtime/components/video/HeroSlideVideo.d.vue.ts +236 -1
- package/dist/runtime/components/video/HeroSlideVideo.vue.d.ts +236 -1
- package/dist/runtime/components/video/HeroVideoControls.d.vue.ts +6 -0
- package/dist/runtime/components/video/HeroVideoControls.vue +146 -26
- package/dist/runtime/components/video/HeroVideoControls.vue.d.ts +6 -0
- package/dist/runtime/components/video/HeroVideoScrubber.d.vue.ts +3 -1
- package/dist/runtime/components/video/HeroVideoScrubber.vue +54 -17
- package/dist/runtime/components/video/HeroVideoScrubber.vue.d.ts +3 -1
- package/dist/runtime/composables/useHeroSlider.js +2 -0
- package/dist/runtime/types.d.ts +1 -0
- package/package.json +8 -4
package/dist/module.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@reference "tailwindcss";:root{--hero-primary:#fff;--hero-bg:hsla(0,0%,100%,.55);--hero-border:hsla(0,0%,100%,.2);--hero-progress-bg:hsla(0,0%,100%,.65);--hero-nav-bg:rgba(0,0,0,.3);--hero-nav-text:#fff}.hero-slider .hero-nav{@apply min-h-0}.hero-slider .nav-slit-btn{@apply pointer-events-auto absolute z-10 block cursor-pointer outline-none}.hero-slider .nav-slit-btn>span{@apply relative block bg-white/40 ring ring-white backdrop-blur-sm text-black transition-transform duration-300;@apply dark:bg-black/40 dark:ring-black dark:text-white;@apply group-hover:opacity-100}.hero-slider .nav-slit-preview{@apply absolute ring ring-white transition-transform duration-300 delay-300;@apply bg-white dark:bg-black dark:ring-black}.hero-slider .nav-slit-preview h3{@apply absolute m-0 truncate px-2 py-0.5 text-sm font-light capitalize leading-5 backface-hidden transition-transform duration-300;@apply bg-white/70 ring ring-white backdrop-blur-sm text-black;@apply dark:bg-black/70 dark:ring-black dark:text-white}.hero-slider .nav-slit-prev .nav-slit-preview{@apply left-0 -translate-x-full}.hero-slider .nav-slit-prev:hover .nav-slit-preview{@apply left-0 translate-x-0}.hero-slider .nav-slit-next .nav-slit-preview{@apply right-0 translate-x-full text-right}.hero-slider .nav-slit-next:hover .nav-slit-preview{@apply right-0 translate-x-0}.hero-video-btn{@apply shadow-none border-none transition-all duration-200 relative;@apply bg-white/35 backdrop-blur-sm opacity-0 hover:scale-110;@apply dark:bg-black/35;@apply group-hover/slider:opacity-100}.media-controls{@apply pointer-events-auto absolute z-999 bottom-
|
|
1
|
+
@reference "tailwindcss";:root{--hero-primary:#fff;--hero-bg:hsla(0,0%,100%,.55);--hero-border:hsla(0,0%,100%,.2);--hero-progress-bg:hsla(0,0%,100%,.65);--hero-nav-bg:rgba(0,0,0,.3);--hero-nav-text:#fff}.hero-slider .hero-nav{@apply min-h-0}.hero-slider .nav-slit-btn{@apply pointer-events-auto absolute z-10 block cursor-pointer outline-none}.hero-slider .nav-slit-btn>span{@apply relative block bg-white/40 ring ring-white backdrop-blur-sm text-black transition-transform duration-300;@apply dark:bg-black/40 dark:ring-black dark:text-white;@apply group-hover:opacity-100}.hero-slider .nav-slit-preview{@apply absolute ring ring-white transition-transform duration-300 delay-300;@apply bg-white dark:bg-black dark:ring-black}.hero-slider .nav-slit-preview h3{@apply absolute m-0 truncate px-2 py-0.5 text-sm font-light capitalize leading-5 backface-hidden transition-transform duration-300;@apply bg-white/70 ring ring-white backdrop-blur-sm text-black;@apply dark:bg-black/70 dark:ring-black dark:text-white}.hero-slider .nav-slit-prev .nav-slit-preview{@apply left-0 -translate-x-full}.hero-slider .nav-slit-prev:hover .nav-slit-preview{@apply left-0 translate-x-0}.hero-slider .nav-slit-next .nav-slit-preview{@apply right-0 translate-x-full text-right}.hero-slider .nav-slit-next:hover .nav-slit-preview{@apply right-0 translate-x-0}.hero-video-btn{@apply shadow-none border-none transition-all duration-200 relative;@apply bg-white/35 backdrop-blur-sm opacity-0 hover:scale-110;@apply dark:bg-black/35;@apply group-hover/slider:opacity-100}.media-controls{@apply pointer-events-auto absolute z-999 bottom-0 left-0 right-0 flex flex-col gap-1 px-3 pb-3}.hero-scrubber-track{background:hsla(0,0%,100%,.2);border-radius:9999px;cursor:pointer;height:4px;overflow:visible;position:relative;transition:height .15s ease;width:100%}.hero-scrubber-track:has(.hero-scrubber-active),.hero-scrubber-track:hover{height:6px}.hero-scrubber-buffered{background:hsla(0,0%,100%,.35)}.hero-scrubber-buffered,.hero-scrubber-progress{border-radius:9999px;height:100%;left:0;pointer-events:none;position:absolute;top:0}.hero-scrubber-progress{background:#fff}.hero-scrubber-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;cursor:pointer;height:calc(100% + 8px);left:0;margin:0;outline:none;position:absolute;top:-4px;width:100%}.hero-scrubber-input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.3);height:0;-webkit-transition:width .15s ease,height .15s ease;transition:width .15s ease,height .15s ease;width:0}.hero-scrubber-input.hero-scrubber-active::-webkit-slider-thumb,.hero-scrubber-track:hover .hero-scrubber-input::-webkit-slider-thumb{height:14px;width:14px}.hero-scrubber-input::-moz-range-track{background:transparent;border:none}.hero-scrubber-input::-moz-range-thumb{background:#fff;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.3);height:0;-moz-transition:width .15s ease,height .15s ease;transition:width .15s ease,height .15s ease;width:0}.hero-scrubber-input.hero-scrubber-active::-moz-range-thumb,.hero-scrubber-track:hover .hero-scrubber-input::-moz-range-thumb{height:14px;width:14px}.hero-ctrl-btn{@apply inline-flex items-center justify-center size-8 rounded-full shadow-none text-white bg-white/25 backdrop-blur-sm hover:bg-white/35 transition-all duration-200 cursor-pointer}.hero-scrub-tooltip{backdrop-filter:blur(8px);border-radius:4px;bottom:calc(100% + 8px);box-shadow:0 2px 8px rgba(0,0,0,.15);color:#000;font-size:.75rem;font-variant-numeric:tabular-nums;line-height:1rem;padding:.25rem .5rem;pointer-events:none;transform:translateX(-50%);white-space:nowrap;z-index:99}.hero-scrub-tooltip,.hero-scrub-tooltip:after{background:hsla(0,0%,100%,.95);position:absolute}.hero-scrub-tooltip:after{bottom:-4px;content:"";height:8px;left:50%;transform:translateX(-50%) rotate(45deg);width:8px}.hero-settings-enter-active,.hero-settings-leave-active{transition:opacity .2s ease,transform .2s ease}.hero-settings-enter-from,.hero-settings-leave-to{opacity:0;transform:translateY(8px) scale(.95)}.hero-vol-icon-enter-active,.hero-vol-icon-leave-active{transition:opacity .15s ease,transform .15s ease}.hero-vol-icon-enter-from,.hero-vol-icon-leave-to{opacity:0;transform:scale(.8)}.swiper-pagination>button>.tooltip-content{@apply pointer-events-none absolute bottom-full mb-1 left-1/2 z-90 w-30 -ml-[60px] cursor-default opacity-0 transition-opacity duration-300 delay-300}.swiper-pagination>button>.tooltip-content:after{@apply pointer-events-none absolute -bottom-2.5 left-1/2 -ml-1.25 size-0 border-5 border-transparent border-t-inherit!;content:""}.swiper-pagination>button>.tooltip-content .tooltip-text{@apply overflow-hidden border-b-2 origin-left scale-x-0 scale-y-100 transition-transform duration-300 delay-300}.swiper-pagination>button>.tooltip-content .tooltip-text .tooltip-inner{@apply max-w-[inherit] rounded-none p-0 translate-y-full transition-transform duration-300}.swiper-pagination>button>.tooltip-content .tooltip-text .tooltip-inner img{@apply opacity-65}.swiper-pagination>button:hover>.tooltip-content{@apply z-99 opacity-100 delay-0}.swiper-pagination>button:hover>.tooltip-content .tooltip-text{@apply scale-x-100 delay-0}.swiper-pagination>button:hover>.tooltip-content .tooltip-text .tooltip-inner{@apply translate-y-0 delay-300}.swiper-pagination .progress-circle-svg{@apply -rotate-90}.swiper-pagination .progress-circle-bar{stroke:var(--hero-primary)}.swiper-pagination .progress-circle-bg{stroke:var(--hero-progress-bg)}.hero-radial-progress{--hero-progress-value:0;--hero-progress-size:1.25rem;--hero-progress-thickness:2px;background:conic-gradient(currentColor calc(var(--hero-progress-value)*1%),transparent 0);border-radius:9999px;display:inline-grid;height:var(--hero-progress-size);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - var(--hero-progress-thickness)),#000 calc(100% - var(--hero-progress-thickness)));mask:radial-gradient(farthest-side,transparent calc(100% - var(--hero-progress-thickness)),#000 calc(100% - var(--hero-progress-thickness)));place-content:center;width:var(--hero-progress-size)}@keyframes heroSpin{to{transform:rotate(1turn)}}.hero-spinner{animation:heroSpin .6s linear infinite;border-color:currentcolor transparent transparent currentcolor;border-radius:9999px;border-style:solid;border-width:2px;display:inline-block}.hero-spinner-sm{height:1rem;width:1rem}.hero-spinner-md{height:1.25rem;width:1.25rem}.hero-range{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:hsla(0,0%,100%,.35);border-radius:9999px;height:4px;outline:none;width:5rem}.hero-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.3);cursor:pointer;height:12px;-webkit-transition:transform .15s ease;transition:transform .15s ease;width:12px}.hero-range::-webkit-slider-thumb:hover{transform:scale(1.2)}.hero-range::-moz-range-track{background:hsla(0,0%,100%,.35);border-radius:9999px;height:4px;width:100%}.hero-range::-moz-range-thumb{background:#fff;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.3);cursor:pointer;height:12px;width:12px}.hero-animated{animation-duration:.6s;animation-fill-mode:both}@keyframes heroFadeIn{0%{opacity:0}to{opacity:1}}@keyframes heroFadeOut{0%{opacity:1}to{opacity:0}}@keyframes heroSlideInUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes heroSlideOutDown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(30px)}}@keyframes heroSlideInRight{0%{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes heroSlideInLeft{0%{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes heroZoomIn{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes heroZoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.1)}}.hero-fadeIn{animation-name:heroFadeIn}.hero-fadeOut{animation-name:heroFadeOut}.hero-slideInUp{animation-name:heroSlideInUp}.hero-slideOutDown{animation-name:heroSlideOutDown}.hero-slideInRight{animation-name:heroSlideInRight}.hero-slideInLeft{animation-name:heroSlideInLeft}.hero-zoomIn{animation-name:heroZoomIn}.hero-zoomOut{animation-name:heroZoomOut}
|
|
@@ -6,11 +6,11 @@ interface NavigationProps {
|
|
|
6
6
|
vertical?: boolean;
|
|
7
7
|
}
|
|
8
8
|
declare const __VLS_export: import("vue").DefineComponent<NavigationProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
-
next: () => any;
|
|
10
9
|
prev: () => any;
|
|
10
|
+
next: () => any;
|
|
11
11
|
}, string, import("vue").PublicProps, Readonly<NavigationProps> & Readonly<{
|
|
12
|
-
onNext?: (() => any) | undefined;
|
|
13
12
|
onPrev?: (() => any) | undefined;
|
|
13
|
+
onNext?: (() => any) | undefined;
|
|
14
14
|
}>, {
|
|
15
15
|
vertical: boolean;
|
|
16
16
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -6,11 +6,11 @@ interface NavigationProps {
|
|
|
6
6
|
vertical?: boolean;
|
|
7
7
|
}
|
|
8
8
|
declare const __VLS_export: import("vue").DefineComponent<NavigationProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
-
next: () => any;
|
|
10
9
|
prev: () => any;
|
|
10
|
+
next: () => any;
|
|
11
11
|
}, string, import("vue").PublicProps, Readonly<NavigationProps> & Readonly<{
|
|
12
|
-
onNext?: (() => any) | undefined;
|
|
13
12
|
onPrev?: (() => any) | undefined;
|
|
13
|
+
onNext?: (() => any) | undefined;
|
|
14
14
|
}>, {
|
|
15
15
|
vertical: boolean;
|
|
16
16
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -17,7 +17,7 @@ const emit = defineEmits(["slideTo"]);
|
|
|
17
17
|
-->
|
|
18
18
|
<nav role="navigation" aria-label="Slide pagination"
|
|
19
19
|
class="hero-pagination swiper-pagination pointer-events-auto absolute z-10 flex items-center gap-1 rounded-full p-1 px-1.5 ring-2 ring-white bg-black/35 backdrop-blur-sm"
|
|
20
|
-
:class="vertical ? 'flex-col top-1/2 -translate-y-1/2 ltr:right-
|
|
20
|
+
:class="vertical ? 'flex-col top-1/2 -translate-y-1/2 ltr:right-4 rtl:left-4' : 'bottom-4 left-1/2 -translate-x-1/2'">
|
|
21
21
|
<button v-for="i in totalSnaps" :key="i - 1" type="button"
|
|
22
22
|
class="group relative flex size-4 shrink-0 items-center justify-center rounded-full"
|
|
23
23
|
:class="{ active: snapIndex === i - 1 }" :aria-label="`Go to slide ${i}`"
|
|
@@ -17,16 +17,20 @@ interface SlideProps {
|
|
|
17
17
|
autoPlay?: boolean;
|
|
18
18
|
containerClass?: string;
|
|
19
19
|
bgClass?: string;
|
|
20
|
+
getContainerEl?: () => HTMLElement | null;
|
|
21
|
+
onSeek?: (time: number) => void;
|
|
22
|
+
onScrubStart?: () => void;
|
|
23
|
+
onScrubEnd?: () => void;
|
|
20
24
|
}
|
|
21
25
|
declare var __VLS_13: {}, __VLS_15: {}, __VLS_17: {
|
|
22
|
-
playing: import("vue").ShallowRef<boolean>;
|
|
26
|
+
playing: import("vue").ShallowRef<boolean, boolean>;
|
|
23
27
|
togglePlay: () => void;
|
|
24
|
-
currentTime: import("vue").ShallowRef<number>;
|
|
25
|
-
duration: import("vue").ShallowRef<number>;
|
|
28
|
+
currentTime: import("vue").ShallowRef<number, number>;
|
|
29
|
+
duration: import("vue").ShallowRef<number, number>;
|
|
26
30
|
buffered: import("vue").Ref<[number, number][], [number, number][]>;
|
|
27
|
-
volume: import("vue").ShallowRef<number>;
|
|
28
|
-
muted: import("vue").ShallowRef<boolean>;
|
|
29
|
-
waiting: import("vue").ShallowRef<boolean>;
|
|
31
|
+
volume: import("vue").ShallowRef<number, number>;
|
|
32
|
+
muted: import("vue").ShallowRef<boolean, boolean>;
|
|
33
|
+
waiting: import("vue").ShallowRef<boolean, boolean>;
|
|
30
34
|
hls: {
|
|
31
35
|
loading: import("vue").Ref<boolean, boolean>;
|
|
32
36
|
error: import("vue").Ref<string | null, string | null>;
|
|
@@ -42,6 +46,7 @@ type __VLS_Slots = {} & {
|
|
|
42
46
|
'video-controls'?: (props: typeof __VLS_17) => any;
|
|
43
47
|
};
|
|
44
48
|
declare const __VLS_base: import("vue").DefineComponent<SlideProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<SlideProps> & Readonly<{}>, {
|
|
49
|
+
eager: boolean;
|
|
45
50
|
showVideoControls: boolean;
|
|
46
51
|
isActive: boolean;
|
|
47
52
|
slideIndex: number;
|
|
@@ -50,8 +55,10 @@ declare const __VLS_base: import("vue").DefineComponent<SlideProps, {}, {}, {},
|
|
|
50
55
|
mediaControlsOptions: MediaControlsOptions;
|
|
51
56
|
videoLoop: boolean;
|
|
52
57
|
autoPlay: boolean;
|
|
58
|
+
onSeek: (time: number) => void;
|
|
59
|
+
onScrubStart: () => void;
|
|
60
|
+
onScrubEnd: () => void;
|
|
53
61
|
imagePreset: string;
|
|
54
|
-
eager: boolean;
|
|
55
62
|
animationClass: string;
|
|
56
63
|
containerClass: string;
|
|
57
64
|
bgClass: string;
|
|
@@ -20,7 +20,11 @@ const props = defineProps({
|
|
|
20
20
|
videoLoop: { type: Boolean, required: false, default: false },
|
|
21
21
|
autoPlay: { type: Boolean, required: false, default: true },
|
|
22
22
|
containerClass: { type: String, required: false, default: "" },
|
|
23
|
-
bgClass: { type: String, required: false, default: "" }
|
|
23
|
+
bgClass: { type: String, required: false, default: "" },
|
|
24
|
+
getContainerEl: { type: Function, required: false },
|
|
25
|
+
onSeek: { type: Function, required: false, default: void 0 },
|
|
26
|
+
onScrubStart: { type: Function, required: false, default: void 0 },
|
|
27
|
+
onScrubEnd: { type: Function, required: false, default: void 0 }
|
|
24
28
|
});
|
|
25
29
|
const colorMode = useColorMode();
|
|
26
30
|
const isDark = computed(() => colorMode.value === "dark");
|
|
@@ -116,8 +120,12 @@ const hlsSlotData = computed(() => {
|
|
|
116
120
|
<HeroVideoControls :playing="videoComponentRef.mediaControls.playing"
|
|
117
121
|
:waiting="videoComponentRef.mediaControls.waiting"
|
|
118
122
|
:current-time="videoComponentRef.mediaControls.currentTime"
|
|
119
|
-
:duration="videoComponentRef.mediaControls.duration"
|
|
120
|
-
:
|
|
123
|
+
:duration="videoComponentRef.mediaControls.duration"
|
|
124
|
+
:buffered="videoComponentRef.mediaControls.buffered"
|
|
125
|
+
:volume="videoComponentRef.mediaControls.volume"
|
|
126
|
+
:muted="videoComponentRef.mediaControls.muted"
|
|
127
|
+
:get-container-el="getContainerEl"
|
|
128
|
+
:on-seek="onSeek" :on-scrub-start="onScrubStart" :on-scrub-end="onScrubEnd" />
|
|
121
129
|
</slot>
|
|
122
130
|
</template>
|
|
123
131
|
</div>
|
|
@@ -17,16 +17,20 @@ interface SlideProps {
|
|
|
17
17
|
autoPlay?: boolean;
|
|
18
18
|
containerClass?: string;
|
|
19
19
|
bgClass?: string;
|
|
20
|
+
getContainerEl?: () => HTMLElement | null;
|
|
21
|
+
onSeek?: (time: number) => void;
|
|
22
|
+
onScrubStart?: () => void;
|
|
23
|
+
onScrubEnd?: () => void;
|
|
20
24
|
}
|
|
21
25
|
declare var __VLS_13: {}, __VLS_15: {}, __VLS_17: {
|
|
22
|
-
playing: import("vue").ShallowRef<boolean>;
|
|
26
|
+
playing: import("vue").ShallowRef<boolean, boolean>;
|
|
23
27
|
togglePlay: () => void;
|
|
24
|
-
currentTime: import("vue").ShallowRef<number>;
|
|
25
|
-
duration: import("vue").ShallowRef<number>;
|
|
28
|
+
currentTime: import("vue").ShallowRef<number, number>;
|
|
29
|
+
duration: import("vue").ShallowRef<number, number>;
|
|
26
30
|
buffered: import("vue").Ref<[number, number][], [number, number][]>;
|
|
27
|
-
volume: import("vue").ShallowRef<number>;
|
|
28
|
-
muted: import("vue").ShallowRef<boolean>;
|
|
29
|
-
waiting: import("vue").ShallowRef<boolean>;
|
|
31
|
+
volume: import("vue").ShallowRef<number, number>;
|
|
32
|
+
muted: import("vue").ShallowRef<boolean, boolean>;
|
|
33
|
+
waiting: import("vue").ShallowRef<boolean, boolean>;
|
|
30
34
|
hls: {
|
|
31
35
|
loading: import("vue").Ref<boolean, boolean>;
|
|
32
36
|
error: import("vue").Ref<string | null, string | null>;
|
|
@@ -42,6 +46,7 @@ type __VLS_Slots = {} & {
|
|
|
42
46
|
'video-controls'?: (props: typeof __VLS_17) => any;
|
|
43
47
|
};
|
|
44
48
|
declare const __VLS_base: import("vue").DefineComponent<SlideProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<SlideProps> & Readonly<{}>, {
|
|
49
|
+
eager: boolean;
|
|
45
50
|
showVideoControls: boolean;
|
|
46
51
|
isActive: boolean;
|
|
47
52
|
slideIndex: number;
|
|
@@ -50,8 +55,10 @@ declare const __VLS_base: import("vue").DefineComponent<SlideProps, {}, {}, {},
|
|
|
50
55
|
mediaControlsOptions: MediaControlsOptions;
|
|
51
56
|
videoLoop: boolean;
|
|
52
57
|
autoPlay: boolean;
|
|
58
|
+
onSeek: (time: number) => void;
|
|
59
|
+
onScrubStart: () => void;
|
|
60
|
+
onScrubEnd: () => void;
|
|
53
61
|
imagePreset: string;
|
|
54
|
-
eager: boolean;
|
|
55
62
|
animationClass: string;
|
|
56
63
|
containerClass: string;
|
|
57
64
|
bgClass: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, toValue, useTemplateRef, watchEffect } from "vue";
|
|
2
|
+
import { computed, ref, toValue, useTemplateRef, watchEffect } from "vue";
|
|
3
3
|
import { Swiper, SwiperSlide } from "swiper/vue";
|
|
4
4
|
import { useElementBounding, useWindowSize } from "@vueuse/core";
|
|
5
5
|
import { useRuntimeConfig } from "#imports";
|
|
6
6
|
import { swiperModules } from "#hero/swiper-modules";
|
|
7
|
-
import { resolveParallaxConfig,
|
|
7
|
+
import { resolveParallaxConfig, isVideoUrl, patternCSS, patternSize, getHeroConfig } from "#hero/utils";
|
|
8
8
|
import { useGSAP } from "#hero/composables/_gsap";
|
|
9
9
|
const props = defineProps({
|
|
10
10
|
slides: { type: Array, required: true },
|
|
@@ -39,7 +39,6 @@ const {
|
|
|
39
39
|
videoPlaying,
|
|
40
40
|
videoCurrentTime,
|
|
41
41
|
videoDuration,
|
|
42
|
-
videoBuffered,
|
|
43
42
|
videoVolume,
|
|
44
43
|
videoMuted,
|
|
45
44
|
videoWaiting,
|
|
@@ -65,11 +64,8 @@ const shouldShowPagination = computed(
|
|
|
65
64
|
const shouldShowNavigation = computed(
|
|
66
65
|
() => features.navigation && props.slides.length > 1 && activeSlideConfig.value.showNavigation
|
|
67
66
|
);
|
|
68
|
-
const shouldShowVideoScrubber = computed(
|
|
69
|
-
() => features.video && activeSlideConfig.value.showProgress && isActiveSlideVideo.value && videoDuration.value > 0 && !isMultiSlide.value
|
|
70
|
-
);
|
|
71
67
|
const shouldShowAutoplayProgress = computed(
|
|
72
|
-
() =>
|
|
68
|
+
() => autoplayEnabled && activeSlideConfig.value.showProgress && !isActiveSlideVideo.value
|
|
73
69
|
);
|
|
74
70
|
const parallaxConfig = computed(() => resolveParallaxConfig(toValue(props.parallax)));
|
|
75
71
|
if (features.parallax) {
|
|
@@ -125,7 +121,8 @@ if (features.parallax) {
|
|
|
125
121
|
:on-video-removed="unregisterSlideVideo" :media-controls-options="slide.config?.mediaControlsOptions"
|
|
126
122
|
:show-video-controls="index === activeIndex || isMultiSlide ? slide.config?.showVideoControls ?? activeSlideConfig.showVideoControls : false"
|
|
127
123
|
:video-loop="slide.config?.videoLoop ?? false" :auto-play="!isMultiSlide" :container-class="ui.container"
|
|
128
|
-
:bg-class="ui.bg"
|
|
124
|
+
:bg-class="ui.bg" :get-container-el="() => containerRef"
|
|
125
|
+
:on-seek="videoSeek" :on-scrub-start="videoScrubStart" :on-scrub-end="videoScrubEnd">
|
|
129
126
|
<slot name="slide" v-bind="{
|
|
130
127
|
slide,
|
|
131
128
|
index,
|
|
@@ -176,21 +173,6 @@ if (features.parallax) {
|
|
|
176
173
|
<HeroNavigation :slides="slides" :active-index="activeIndex" :vertical="isVertical" @prev="prev" @next="next" />
|
|
177
174
|
</slot>
|
|
178
175
|
|
|
179
|
-
<!-- Video scrubber (replaces progress bar when active slide is video) -->
|
|
180
|
-
<HeroVideoScrubber v-if="shouldShowVideoScrubber" :model-value="videoCurrentTime" :max="videoDuration"
|
|
181
|
-
class="pointer-events-auto absolute z-11" :class="isVertical ? 'top-0 ltr:right-0 rtl:left-0 h-full w-1' : 'bottom-0 left-0 h-1 hover:h-2 w-full'" @update:model-value="(v) => videoSeek(v)"
|
|
182
|
-
@scrubber-mousedown="videoScrubStart" @scrubber-mouseup="videoScrubEnd">
|
|
183
|
-
<template #default="{ position, pendingValue }">
|
|
184
|
-
<div
|
|
185
|
-
class="text-white mb-2 pointer-events-none px-2 py-1 rounded-sm bg-black/85 backdrop-blur-sm transform bottom-0 absolute z-99 -translate-x-1/2"
|
|
186
|
-
:style="{ left: position }">
|
|
187
|
-
<div
|
|
188
|
-
class="size-2 rotate-45 left-1/2 top-3.5 absolute overflow-hidden after:bg-white after:size-2 after:content-[''] -translate-x-1/2 after:rotate-45 after:left-1/2 after:top-1/2 after:absolute" />
|
|
189
|
-
{{ formatTime(pendingValue) }}
|
|
190
|
-
</div>
|
|
191
|
-
</template>
|
|
192
|
-
</HeroVideoScrubber>
|
|
193
|
-
|
|
194
176
|
<!-- Autoplay progress bar: horizontal at bottom, vertical on the side -->
|
|
195
177
|
<div v-if="shouldShowAutoplayProgress" class="pointer-events-none absolute z-4 bg-white/50" :class="[isVertical ? 'top-0 ltr:right-0 rtl:left-0 h-full w-1' : 'bottom-0 left-0 w-full h-1', ui.progress]">
|
|
196
178
|
<div class="bg-white transition-all duration-50 rounded-e-sm" :class="isVertical ? 'w-full' : 'h-full'" :style="isVertical ? { height: `${autoplayProgress * 100}%` } : { width: `${autoplayProgress * 100}%` }" />
|
|
@@ -22,7 +22,242 @@ interface SlideVideoProps {
|
|
|
22
22
|
autoPlay?: boolean;
|
|
23
23
|
}
|
|
24
24
|
declare const __VLS_export: import("vue").DefineComponent<SlideVideoProps, {
|
|
25
|
-
mediaControls:
|
|
25
|
+
mediaControls: {
|
|
26
|
+
currentTime: import("vue").ShallowRef<number, number>;
|
|
27
|
+
duration: import("vue").ShallowRef<number, number>;
|
|
28
|
+
waiting: import("vue").ShallowRef<boolean, boolean>;
|
|
29
|
+
seeking: import("vue").ShallowRef<boolean, boolean>;
|
|
30
|
+
ended: import("vue").ShallowRef<boolean, boolean>;
|
|
31
|
+
stalled: import("vue").ShallowRef<boolean, boolean>;
|
|
32
|
+
buffered: import("vue").Ref<[number, number][], [number, number][]>;
|
|
33
|
+
playing: import("vue").ShallowRef<boolean, boolean>;
|
|
34
|
+
rate: import("vue").ShallowRef<number, number>;
|
|
35
|
+
volume: import("vue").ShallowRef<number, number>;
|
|
36
|
+
muted: import("vue").ShallowRef<boolean, boolean>;
|
|
37
|
+
tracks: import("vue").Ref<{
|
|
38
|
+
id: number;
|
|
39
|
+
label: string;
|
|
40
|
+
language: string;
|
|
41
|
+
mode: TextTrackMode;
|
|
42
|
+
kind: TextTrackKind;
|
|
43
|
+
inBandMetadataTrackDispatchType: string;
|
|
44
|
+
cues: {
|
|
45
|
+
[x: number]: {
|
|
46
|
+
endTime: number;
|
|
47
|
+
id: string;
|
|
48
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
49
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
50
|
+
pauseOnExit: boolean;
|
|
51
|
+
startTime: number;
|
|
52
|
+
readonly track: {
|
|
53
|
+
readonly activeCues: any | null;
|
|
54
|
+
readonly cues: any | null;
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
57
|
+
readonly kind: TextTrackKind;
|
|
58
|
+
readonly label: string;
|
|
59
|
+
readonly language: string;
|
|
60
|
+
mode: TextTrackMode;
|
|
61
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
62
|
+
addCue: (cue: TextTrackCue) => void;
|
|
63
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
64
|
+
addEventListener: {
|
|
65
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
66
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
67
|
+
};
|
|
68
|
+
removeEventListener: {
|
|
69
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
70
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
71
|
+
};
|
|
72
|
+
dispatchEvent: {
|
|
73
|
+
(event: Event): boolean;
|
|
74
|
+
(event: Event): boolean;
|
|
75
|
+
};
|
|
76
|
+
} | null;
|
|
77
|
+
addEventListener: {
|
|
78
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
79
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
80
|
+
};
|
|
81
|
+
removeEventListener: {
|
|
82
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
83
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
84
|
+
};
|
|
85
|
+
dispatchEvent: {
|
|
86
|
+
(event: Event): boolean;
|
|
87
|
+
(event: Event): boolean;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
readonly length: number;
|
|
91
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
92
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
93
|
+
} | null;
|
|
94
|
+
activeCues: {
|
|
95
|
+
[x: number]: {
|
|
96
|
+
endTime: number;
|
|
97
|
+
id: string;
|
|
98
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
99
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
100
|
+
pauseOnExit: boolean;
|
|
101
|
+
startTime: number;
|
|
102
|
+
readonly track: {
|
|
103
|
+
readonly activeCues: any | null;
|
|
104
|
+
readonly cues: any | null;
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
107
|
+
readonly kind: TextTrackKind;
|
|
108
|
+
readonly label: string;
|
|
109
|
+
readonly language: string;
|
|
110
|
+
mode: TextTrackMode;
|
|
111
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
112
|
+
addCue: (cue: TextTrackCue) => void;
|
|
113
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
114
|
+
addEventListener: {
|
|
115
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
116
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
117
|
+
};
|
|
118
|
+
removeEventListener: {
|
|
119
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
120
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
121
|
+
};
|
|
122
|
+
dispatchEvent: {
|
|
123
|
+
(event: Event): boolean;
|
|
124
|
+
(event: Event): boolean;
|
|
125
|
+
};
|
|
126
|
+
} | null;
|
|
127
|
+
addEventListener: {
|
|
128
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
129
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
130
|
+
};
|
|
131
|
+
removeEventListener: {
|
|
132
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
133
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
134
|
+
};
|
|
135
|
+
dispatchEvent: {
|
|
136
|
+
(event: Event): boolean;
|
|
137
|
+
(event: Event): boolean;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly length: number;
|
|
141
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
142
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
143
|
+
} | null;
|
|
144
|
+
}[], import("@vueuse/core").UseMediaTextTrack[] | {
|
|
145
|
+
id: number;
|
|
146
|
+
label: string;
|
|
147
|
+
language: string;
|
|
148
|
+
mode: TextTrackMode;
|
|
149
|
+
kind: TextTrackKind;
|
|
150
|
+
inBandMetadataTrackDispatchType: string;
|
|
151
|
+
cues: {
|
|
152
|
+
[x: number]: {
|
|
153
|
+
endTime: number;
|
|
154
|
+
id: string;
|
|
155
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
156
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
157
|
+
pauseOnExit: boolean;
|
|
158
|
+
startTime: number;
|
|
159
|
+
readonly track: {
|
|
160
|
+
readonly activeCues: any | null;
|
|
161
|
+
readonly cues: any | null;
|
|
162
|
+
readonly id: string;
|
|
163
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
164
|
+
readonly kind: TextTrackKind;
|
|
165
|
+
readonly label: string;
|
|
166
|
+
readonly language: string;
|
|
167
|
+
mode: TextTrackMode;
|
|
168
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
169
|
+
addCue: (cue: TextTrackCue) => void;
|
|
170
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
171
|
+
addEventListener: {
|
|
172
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
173
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
174
|
+
};
|
|
175
|
+
removeEventListener: {
|
|
176
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
177
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
178
|
+
};
|
|
179
|
+
dispatchEvent: {
|
|
180
|
+
(event: Event): boolean;
|
|
181
|
+
(event: Event): boolean;
|
|
182
|
+
};
|
|
183
|
+
} | null;
|
|
184
|
+
addEventListener: {
|
|
185
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
186
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
187
|
+
};
|
|
188
|
+
removeEventListener: {
|
|
189
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
190
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
191
|
+
};
|
|
192
|
+
dispatchEvent: {
|
|
193
|
+
(event: Event): boolean;
|
|
194
|
+
(event: Event): boolean;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
readonly length: number;
|
|
198
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
199
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
200
|
+
} | null;
|
|
201
|
+
activeCues: {
|
|
202
|
+
[x: number]: {
|
|
203
|
+
endTime: number;
|
|
204
|
+
id: string;
|
|
205
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
206
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
207
|
+
pauseOnExit: boolean;
|
|
208
|
+
startTime: number;
|
|
209
|
+
readonly track: {
|
|
210
|
+
readonly activeCues: any | null;
|
|
211
|
+
readonly cues: any | null;
|
|
212
|
+
readonly id: string;
|
|
213
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
214
|
+
readonly kind: TextTrackKind;
|
|
215
|
+
readonly label: string;
|
|
216
|
+
readonly language: string;
|
|
217
|
+
mode: TextTrackMode;
|
|
218
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
219
|
+
addCue: (cue: TextTrackCue) => void;
|
|
220
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
221
|
+
addEventListener: {
|
|
222
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
223
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
224
|
+
};
|
|
225
|
+
removeEventListener: {
|
|
226
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
227
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
228
|
+
};
|
|
229
|
+
dispatchEvent: {
|
|
230
|
+
(event: Event): boolean;
|
|
231
|
+
(event: Event): boolean;
|
|
232
|
+
};
|
|
233
|
+
} | null;
|
|
234
|
+
addEventListener: {
|
|
235
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
236
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
237
|
+
};
|
|
238
|
+
removeEventListener: {
|
|
239
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
240
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
241
|
+
};
|
|
242
|
+
dispatchEvent: {
|
|
243
|
+
(event: Event): boolean;
|
|
244
|
+
(event: Event): boolean;
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
readonly length: number;
|
|
248
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
249
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
250
|
+
} | null;
|
|
251
|
+
}[]>;
|
|
252
|
+
selectedTrack: import("vue").ShallowRef<number, number>;
|
|
253
|
+
enableTrack: (track: number | import("@vueuse/core").UseMediaTextTrack, disableTracks?: boolean) => void;
|
|
254
|
+
disableTrack: (track?: number | import("@vueuse/core").UseMediaTextTrack) => void;
|
|
255
|
+
supportsPictureInPicture: boolean | undefined;
|
|
256
|
+
togglePictureInPicture: () => Promise<unknown>;
|
|
257
|
+
isPictureInPicture: import("vue").ShallowRef<boolean, boolean>;
|
|
258
|
+
onSourceError: import("@vueuse/core").EventHookOn<Event>;
|
|
259
|
+
onPlaybackError: import("@vueuse/core").EventHookOn<Event>;
|
|
260
|
+
};
|
|
26
261
|
hlsState: import("#hero/composables/_hls").UseHlsReturn | null;
|
|
27
262
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<SlideVideoProps> & Readonly<{}>, {
|
|
28
263
|
poster: string;
|
|
@@ -22,7 +22,242 @@ interface SlideVideoProps {
|
|
|
22
22
|
autoPlay?: boolean;
|
|
23
23
|
}
|
|
24
24
|
declare const __VLS_export: import("vue").DefineComponent<SlideVideoProps, {
|
|
25
|
-
mediaControls:
|
|
25
|
+
mediaControls: {
|
|
26
|
+
currentTime: import("vue").ShallowRef<number, number>;
|
|
27
|
+
duration: import("vue").ShallowRef<number, number>;
|
|
28
|
+
waiting: import("vue").ShallowRef<boolean, boolean>;
|
|
29
|
+
seeking: import("vue").ShallowRef<boolean, boolean>;
|
|
30
|
+
ended: import("vue").ShallowRef<boolean, boolean>;
|
|
31
|
+
stalled: import("vue").ShallowRef<boolean, boolean>;
|
|
32
|
+
buffered: import("vue").Ref<[number, number][], [number, number][]>;
|
|
33
|
+
playing: import("vue").ShallowRef<boolean, boolean>;
|
|
34
|
+
rate: import("vue").ShallowRef<number, number>;
|
|
35
|
+
volume: import("vue").ShallowRef<number, number>;
|
|
36
|
+
muted: import("vue").ShallowRef<boolean, boolean>;
|
|
37
|
+
tracks: import("vue").Ref<{
|
|
38
|
+
id: number;
|
|
39
|
+
label: string;
|
|
40
|
+
language: string;
|
|
41
|
+
mode: TextTrackMode;
|
|
42
|
+
kind: TextTrackKind;
|
|
43
|
+
inBandMetadataTrackDispatchType: string;
|
|
44
|
+
cues: {
|
|
45
|
+
[x: number]: {
|
|
46
|
+
endTime: number;
|
|
47
|
+
id: string;
|
|
48
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
49
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
50
|
+
pauseOnExit: boolean;
|
|
51
|
+
startTime: number;
|
|
52
|
+
readonly track: {
|
|
53
|
+
readonly activeCues: any | null;
|
|
54
|
+
readonly cues: any | null;
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
57
|
+
readonly kind: TextTrackKind;
|
|
58
|
+
readonly label: string;
|
|
59
|
+
readonly language: string;
|
|
60
|
+
mode: TextTrackMode;
|
|
61
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
62
|
+
addCue: (cue: TextTrackCue) => void;
|
|
63
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
64
|
+
addEventListener: {
|
|
65
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
66
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
67
|
+
};
|
|
68
|
+
removeEventListener: {
|
|
69
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
70
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
71
|
+
};
|
|
72
|
+
dispatchEvent: {
|
|
73
|
+
(event: Event): boolean;
|
|
74
|
+
(event: Event): boolean;
|
|
75
|
+
};
|
|
76
|
+
} | null;
|
|
77
|
+
addEventListener: {
|
|
78
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
79
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
80
|
+
};
|
|
81
|
+
removeEventListener: {
|
|
82
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
83
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
84
|
+
};
|
|
85
|
+
dispatchEvent: {
|
|
86
|
+
(event: Event): boolean;
|
|
87
|
+
(event: Event): boolean;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
readonly length: number;
|
|
91
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
92
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
93
|
+
} | null;
|
|
94
|
+
activeCues: {
|
|
95
|
+
[x: number]: {
|
|
96
|
+
endTime: number;
|
|
97
|
+
id: string;
|
|
98
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
99
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
100
|
+
pauseOnExit: boolean;
|
|
101
|
+
startTime: number;
|
|
102
|
+
readonly track: {
|
|
103
|
+
readonly activeCues: any | null;
|
|
104
|
+
readonly cues: any | null;
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
107
|
+
readonly kind: TextTrackKind;
|
|
108
|
+
readonly label: string;
|
|
109
|
+
readonly language: string;
|
|
110
|
+
mode: TextTrackMode;
|
|
111
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
112
|
+
addCue: (cue: TextTrackCue) => void;
|
|
113
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
114
|
+
addEventListener: {
|
|
115
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
116
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
117
|
+
};
|
|
118
|
+
removeEventListener: {
|
|
119
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
120
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
121
|
+
};
|
|
122
|
+
dispatchEvent: {
|
|
123
|
+
(event: Event): boolean;
|
|
124
|
+
(event: Event): boolean;
|
|
125
|
+
};
|
|
126
|
+
} | null;
|
|
127
|
+
addEventListener: {
|
|
128
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
129
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
130
|
+
};
|
|
131
|
+
removeEventListener: {
|
|
132
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
133
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
134
|
+
};
|
|
135
|
+
dispatchEvent: {
|
|
136
|
+
(event: Event): boolean;
|
|
137
|
+
(event: Event): boolean;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly length: number;
|
|
141
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
142
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
143
|
+
} | null;
|
|
144
|
+
}[], import("@vueuse/core").UseMediaTextTrack[] | {
|
|
145
|
+
id: number;
|
|
146
|
+
label: string;
|
|
147
|
+
language: string;
|
|
148
|
+
mode: TextTrackMode;
|
|
149
|
+
kind: TextTrackKind;
|
|
150
|
+
inBandMetadataTrackDispatchType: string;
|
|
151
|
+
cues: {
|
|
152
|
+
[x: number]: {
|
|
153
|
+
endTime: number;
|
|
154
|
+
id: string;
|
|
155
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
156
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
157
|
+
pauseOnExit: boolean;
|
|
158
|
+
startTime: number;
|
|
159
|
+
readonly track: {
|
|
160
|
+
readonly activeCues: any | null;
|
|
161
|
+
readonly cues: any | null;
|
|
162
|
+
readonly id: string;
|
|
163
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
164
|
+
readonly kind: TextTrackKind;
|
|
165
|
+
readonly label: string;
|
|
166
|
+
readonly language: string;
|
|
167
|
+
mode: TextTrackMode;
|
|
168
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
169
|
+
addCue: (cue: TextTrackCue) => void;
|
|
170
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
171
|
+
addEventListener: {
|
|
172
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
173
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
174
|
+
};
|
|
175
|
+
removeEventListener: {
|
|
176
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
177
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
178
|
+
};
|
|
179
|
+
dispatchEvent: {
|
|
180
|
+
(event: Event): boolean;
|
|
181
|
+
(event: Event): boolean;
|
|
182
|
+
};
|
|
183
|
+
} | null;
|
|
184
|
+
addEventListener: {
|
|
185
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
186
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
187
|
+
};
|
|
188
|
+
removeEventListener: {
|
|
189
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
190
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
191
|
+
};
|
|
192
|
+
dispatchEvent: {
|
|
193
|
+
(event: Event): boolean;
|
|
194
|
+
(event: Event): boolean;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
readonly length: number;
|
|
198
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
199
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
200
|
+
} | null;
|
|
201
|
+
activeCues: {
|
|
202
|
+
[x: number]: {
|
|
203
|
+
endTime: number;
|
|
204
|
+
id: string;
|
|
205
|
+
onenter: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
206
|
+
onexit: ((this: TextTrackCue, ev: Event) => any) | null;
|
|
207
|
+
pauseOnExit: boolean;
|
|
208
|
+
startTime: number;
|
|
209
|
+
readonly track: {
|
|
210
|
+
readonly activeCues: any | null;
|
|
211
|
+
readonly cues: any | null;
|
|
212
|
+
readonly id: string;
|
|
213
|
+
readonly inBandMetadataTrackDispatchType: string;
|
|
214
|
+
readonly kind: TextTrackKind;
|
|
215
|
+
readonly label: string;
|
|
216
|
+
readonly language: string;
|
|
217
|
+
mode: TextTrackMode;
|
|
218
|
+
oncuechange: ((this: TextTrack, ev: Event) => any) | null;
|
|
219
|
+
addCue: (cue: TextTrackCue) => void;
|
|
220
|
+
removeCue: (cue: TextTrackCue) => void;
|
|
221
|
+
addEventListener: {
|
|
222
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
223
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
224
|
+
};
|
|
225
|
+
removeEventListener: {
|
|
226
|
+
<K extends keyof TextTrackEventMap>(type: K, listener: (this: TextTrack, ev: TextTrackEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
227
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
228
|
+
};
|
|
229
|
+
dispatchEvent: {
|
|
230
|
+
(event: Event): boolean;
|
|
231
|
+
(event: Event): boolean;
|
|
232
|
+
};
|
|
233
|
+
} | null;
|
|
234
|
+
addEventListener: {
|
|
235
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
236
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
237
|
+
};
|
|
238
|
+
removeEventListener: {
|
|
239
|
+
<K extends keyof TextTrackCueEventMap>(type: K, listener: (this: TextTrackCue, ev: TextTrackCueEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
240
|
+
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
241
|
+
};
|
|
242
|
+
dispatchEvent: {
|
|
243
|
+
(event: Event): boolean;
|
|
244
|
+
(event: Event): boolean;
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
readonly length: number;
|
|
248
|
+
getCueById: (id: string) => TextTrackCue | null;
|
|
249
|
+
[Symbol.iterator]: () => ArrayIterator<TextTrackCue>;
|
|
250
|
+
} | null;
|
|
251
|
+
}[]>;
|
|
252
|
+
selectedTrack: import("vue").ShallowRef<number, number>;
|
|
253
|
+
enableTrack: (track: number | import("@vueuse/core").UseMediaTextTrack, disableTracks?: boolean) => void;
|
|
254
|
+
disableTrack: (track?: number | import("@vueuse/core").UseMediaTextTrack) => void;
|
|
255
|
+
supportsPictureInPicture: boolean | undefined;
|
|
256
|
+
togglePictureInPicture: () => Promise<unknown>;
|
|
257
|
+
isPictureInPicture: import("vue").ShallowRef<boolean, boolean>;
|
|
258
|
+
onSourceError: import("@vueuse/core").EventHookOn<Event>;
|
|
259
|
+
onPlaybackError: import("@vueuse/core").EventHookOn<Event>;
|
|
260
|
+
};
|
|
26
261
|
hlsState: import("#hero/composables/_hls").UseHlsReturn | null;
|
|
27
262
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<SlideVideoProps> & Readonly<{}>, {
|
|
28
263
|
poster: string;
|
|
@@ -6,6 +6,12 @@ interface VideoControlsProps {
|
|
|
6
6
|
volume: Ref<number>;
|
|
7
7
|
muted: Ref<boolean>;
|
|
8
8
|
duration: Ref<number>;
|
|
9
|
+
/** Raw buffered time ranges from useMediaControls: [start, end][] */
|
|
10
|
+
buffered?: Ref<[number, number][]>;
|
|
11
|
+
getContainerEl?: () => HTMLElement | null;
|
|
12
|
+
onSeek?: (time: number) => void;
|
|
13
|
+
onScrubStart?: () => void;
|
|
14
|
+
onScrubEnd?: () => void;
|
|
9
15
|
}
|
|
10
16
|
declare const __VLS_export: import("vue").DefineComponent<VideoControlsProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<VideoControlsProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
17
|
declare const _default: typeof __VLS_export;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref, useTemplateRef } from "vue";
|
|
3
|
+
import { useFullscreen, useMouseInElement, useElementHover } from "@vueuse/core";
|
|
3
4
|
import { formatTime } from "#hero/utils";
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
playing: { type: Object, required: true },
|
|
@@ -7,8 +8,25 @@ const props = defineProps({
|
|
|
7
8
|
currentTime: { type: Object, required: true },
|
|
8
9
|
volume: { type: Object, required: true },
|
|
9
10
|
muted: { type: Object, required: true },
|
|
10
|
-
duration: { type: Object, required: true }
|
|
11
|
+
duration: { type: Object, required: true },
|
|
12
|
+
buffered: { type: Object, required: false },
|
|
13
|
+
getContainerEl: { type: Function, required: false },
|
|
14
|
+
onSeek: { type: Function, required: false },
|
|
15
|
+
onScrubStart: { type: Function, required: false },
|
|
16
|
+
onScrubEnd: { type: Function, required: false }
|
|
11
17
|
});
|
|
18
|
+
const { isFullscreen } = useFullscreen();
|
|
19
|
+
function toggleFullscreen() {
|
|
20
|
+
const el = props.getContainerEl?.();
|
|
21
|
+
if (!el) return;
|
|
22
|
+
if (document.fullscreenElement) {
|
|
23
|
+
document.exitFullscreen();
|
|
24
|
+
} else {
|
|
25
|
+
el.requestFullscreen();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const settingsOpen = ref(false);
|
|
29
|
+
const scrubbing = ref(false);
|
|
12
30
|
const formattedTime = computed(
|
|
13
31
|
() => `${formatTime(Number(props.currentTime.value))} / ${formatTime(Number(props.duration.value))}`
|
|
14
32
|
);
|
|
@@ -19,6 +37,13 @@ const progress = computed(() => {
|
|
|
19
37
|
if (props.duration.value === 0) return 0;
|
|
20
38
|
return props.currentTime.value / props.duration.value * 100;
|
|
21
39
|
});
|
|
40
|
+
const bufferedPercent = computed(() => {
|
|
41
|
+
if (!props.buffered || props.duration.value === 0) return 0;
|
|
42
|
+
const ranges = props.buffered.value;
|
|
43
|
+
if (!ranges.length) return 0;
|
|
44
|
+
const bufferedEnd = ranges[ranges.length - 1]?.[1] ?? 0;
|
|
45
|
+
return bufferedEnd / props.duration.value * 100;
|
|
46
|
+
});
|
|
22
47
|
const volumePercent = computed(() => Math.round((props.volume.value ?? 0) * 100));
|
|
23
48
|
function onVolumeInput(e) {
|
|
24
49
|
const val = Number(e.target.value);
|
|
@@ -35,9 +60,41 @@ const volumeIcon = computed(() => {
|
|
|
35
60
|
if (props.volume.value < 0.5) return "low";
|
|
36
61
|
return "high";
|
|
37
62
|
});
|
|
63
|
+
const scrubberTrackRef = useTemplateRef("scrubberTrackRef");
|
|
64
|
+
const scrubberHovered = useElementHover(scrubberTrackRef);
|
|
65
|
+
const { elementX, elementWidth } = useMouseInElement(scrubberTrackRef);
|
|
66
|
+
const showTooltip = computed(() => scrubberHovered.value || scrubbing.value);
|
|
67
|
+
const hoverProgress = computed(() => Math.max(0, Math.min(1, elementX.value / elementWidth.value)));
|
|
68
|
+
const hoverTime = computed(() => hoverProgress.value * props.duration.value);
|
|
69
|
+
const tooltipLeft = computed(() => `${Math.max(0, Math.min(elementX.value, elementWidth.value))}px`);
|
|
70
|
+
function onScrubInput(e) {
|
|
71
|
+
const val = Number(e.target.value);
|
|
72
|
+
const time = val / 100 * props.duration.value;
|
|
73
|
+
props.onSeek?.(time);
|
|
74
|
+
}
|
|
75
|
+
function onScrubDown() {
|
|
76
|
+
scrubbing.value = true;
|
|
77
|
+
props.onScrubStart?.();
|
|
78
|
+
}
|
|
79
|
+
function onScrubUp() {
|
|
80
|
+
scrubbing.value = false;
|
|
81
|
+
props.onScrubEnd?.();
|
|
82
|
+
}
|
|
83
|
+
const playbackRate = ref(1);
|
|
84
|
+
const playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
85
|
+
function setPlaybackRate(rate) {
|
|
86
|
+
playbackRate.value = rate;
|
|
87
|
+
const el = props.getContainerEl?.();
|
|
88
|
+
if (el) {
|
|
89
|
+
const video = el.querySelector(".swiper-slide-active video");
|
|
90
|
+
if (video) video.playbackRate = rate;
|
|
91
|
+
}
|
|
92
|
+
settingsOpen.value = false;
|
|
93
|
+
}
|
|
38
94
|
</script>
|
|
39
95
|
|
|
40
96
|
<template>
|
|
97
|
+
<!-- Center play button -->
|
|
41
98
|
<div aria-live="polite"
|
|
42
99
|
class="pointer-events-auto absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex items-center justify-center">
|
|
43
100
|
<button type="button"
|
|
@@ -55,33 +112,96 @@ const volumeIcon = computed(() => {
|
|
|
55
112
|
</div>
|
|
56
113
|
</button>
|
|
57
114
|
</div>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
115
|
+
|
|
116
|
+
<!-- Bottom bar -->
|
|
117
|
+
<div class="media-controls">
|
|
118
|
+
<!-- Scrubber bar: full width above buttons -->
|
|
119
|
+
<div ref="scrubberTrackRef" class="hero-scrubber-track bottom-2">
|
|
120
|
+
<!-- Buffered -->
|
|
121
|
+
<div class="hero-scrubber-buffered" :style="{ width: `${bufferedPercent}%` }" />
|
|
122
|
+
<!-- Progress -->
|
|
123
|
+
<div class="hero-scrubber-progress" :style="{ width: `${progress}%` }" />
|
|
124
|
+
<!-- Native range input on top -->
|
|
125
|
+
<input type="range" min="0" max="100" step="0.1" :value="progress" class="hero-scrubber-input"
|
|
126
|
+
:class="{ 'hero-scrubber-active': scrubbing }" aria-label="Seek video"
|
|
127
|
+
:aria-valuetext="`${formatTime(currentTime.value)} of ${formatTime(duration.value)}`" @input="onScrubInput"
|
|
128
|
+
@mousedown="onScrubDown" @touchstart="onScrubDown" @mouseup="onScrubUp" @touchend="onScrubUp"
|
|
129
|
+
@touchcancel="onScrubUp" />
|
|
130
|
+
<!-- Hover tooltip -->
|
|
131
|
+
<div v-show="showTooltip" class="hero-scrub-tooltip" :style="{ left: tooltipLeft }">
|
|
132
|
+
{{ formatTime(hoverTime) }}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Controls row -->
|
|
137
|
+
<div class="flex items-center justify-between w-full">
|
|
138
|
+
<!-- Left: play, volume, time -->
|
|
139
|
+
<div class="flex items-center gap-1">
|
|
140
|
+
<button type="button" class="hero-ctrl-btn" :aria-label="playing.value ? 'Pause video' : 'Play video'"
|
|
141
|
+
@click="toggle">
|
|
142
|
+
<span v-if="waiting.value" class="hero-spinner hero-spinner-sm text-white" />
|
|
143
|
+
<Transition v-else name="hero-vol-icon" mode="out-in">
|
|
144
|
+
<Icon v-if="playing.value" key="pause" name="lucide:pause" class="size-4" />
|
|
145
|
+
<Icon v-else key="play" name="lucide:play" class="size-4" />
|
|
146
|
+
</Transition>
|
|
74
147
|
</button>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
148
|
+
|
|
149
|
+
<div
|
|
150
|
+
class="group/volume flex items-center bg-white/25 rounded-full backdrop-blur-sm overflow-hidden transition-all duration-300 ease-out hover:bg-white/35">
|
|
151
|
+
<button type="button"
|
|
152
|
+
class="inline-flex flex-none items-center justify-center size-8 rounded-full shadow-none text-white hover:opacity-100 transition-all duration-200 cursor-pointer"
|
|
153
|
+
aria-label="Toggle mute" @click="toggleMute">
|
|
154
|
+
<Transition name="hero-vol-icon" mode="out-in">
|
|
155
|
+
<Icon v-if="volumeIcon === 'high'" key="high" name="lucide:volume-2" class="size-4" />
|
|
156
|
+
<Icon v-else-if="volumeIcon === 'low'" key="low" name="lucide:volume-1" class="size-4" />
|
|
157
|
+
<Icon v-else key="muted" name="lucide:volume-x" class="size-4" />
|
|
158
|
+
</Transition>
|
|
159
|
+
</button>
|
|
160
|
+
<div
|
|
161
|
+
class="flex items-center max-w-0 group-hover/volume:max-w-28 transition-all duration-300 ease-out overflow-x-clip">
|
|
162
|
+
<input type="range" min="0" max="100" :value="muted.value ? 0 : volumePercent"
|
|
163
|
+
class="hero-range mx-2 cursor-pointer transition-opacity duration-200 opacity-0 group-hover/volume:opacity-100"
|
|
164
|
+
aria-label="Volume" :aria-valuetext="`${muted.value ? 0 : volumePercent}%`" @input="onVolumeInput" />
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<span class="text-xs text-white/80 px-2 tabular-nums whitespace-nowrap">
|
|
169
|
+
{{ formattedTime }}
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- Right: settings, fullscreen -->
|
|
174
|
+
<div class="flex items-center gap-1">
|
|
175
|
+
<!-- Settings -->
|
|
176
|
+
<div class="relative">
|
|
177
|
+
<button type="button" class="hero-ctrl-btn" aria-label="Settings" @click="settingsOpen = !settingsOpen">
|
|
178
|
+
<Icon name="lucide:settings" class="size-4 transition-transform duration-300"
|
|
179
|
+
:class="{ 'rotate-90': settingsOpen }" />
|
|
180
|
+
</button>
|
|
181
|
+
<Transition name="hero-settings">
|
|
182
|
+
<div v-if="settingsOpen"
|
|
183
|
+
class="absolute bottom-full right-0 mb-2 rounded-lg bg-black/80 backdrop-blur-md text-white text-xs min-w-36 overflow-hidden shadow-lg">
|
|
184
|
+
<div class="px-3 py-2 flex text-white/75 font-medium border-b border-white/10">Playback
|
|
185
|
+
speed</div>
|
|
186
|
+
<button v-for="rate in playbackRates" :key="rate" type="button"
|
|
187
|
+
class="flex w-full items-center justify-between px-3 py-1.5 hover:bg-white/10 transition-colors cursor-pointer"
|
|
188
|
+
@click="setPlaybackRate(rate)">
|
|
189
|
+
<span>{{ rate === 1 ? "Normal" : `${rate}x` }}</span>
|
|
190
|
+
<Icon v-if="playbackRate === rate" name="lucide:check" class="size-3 text-white/70" />
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</Transition>
|
|
78
194
|
</div>
|
|
195
|
+
|
|
196
|
+
<!-- Fullscreen -->
|
|
197
|
+
<button type="button" class="hero-ctrl-btn" :aria-label="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
|
|
198
|
+
@click="toggleFullscreen">
|
|
199
|
+
<Transition name="hero-vol-icon" mode="out-in">
|
|
200
|
+
<Icon v-if="isFullscreen" key="minimize" name="lucide:minimize" class="size-4" />
|
|
201
|
+
<Icon v-else key="maximize" name="lucide:maximize" class="size-4" />
|
|
202
|
+
</Transition>
|
|
203
|
+
</button>
|
|
79
204
|
</div>
|
|
80
205
|
</div>
|
|
81
|
-
<!-- Time Display -->
|
|
82
|
-
<span
|
|
83
|
-
class="text-xs p-2 mb-4 mr-2 text-center rounded backdrop-blur-sm bg-white/85 min-w-25 self-end bottom-1 right-0 absolute z-2 dark:bg-black/85">
|
|
84
|
-
{{ formattedTime }}
|
|
85
|
-
</span>
|
|
86
206
|
</div>
|
|
87
207
|
</template>
|
|
@@ -6,6 +6,12 @@ interface VideoControlsProps {
|
|
|
6
6
|
volume: Ref<number>;
|
|
7
7
|
muted: Ref<boolean>;
|
|
8
8
|
duration: Ref<number>;
|
|
9
|
+
/** Raw buffered time ranges from useMediaControls: [start, end][] */
|
|
10
|
+
buffered?: Ref<[number, number][]>;
|
|
11
|
+
getContainerEl?: () => HTMLElement | null;
|
|
12
|
+
onSeek?: (time: number) => void;
|
|
13
|
+
onScrubStart?: () => void;
|
|
14
|
+
onScrubEnd?: () => void;
|
|
9
15
|
}
|
|
10
16
|
declare const __VLS_export: import("vue").DefineComponent<VideoControlsProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<VideoControlsProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
17
|
declare const _default: typeof __VLS_export;
|
|
@@ -21,7 +21,9 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
21
21
|
modelValue: {
|
|
22
22
|
type: import("vue").PropType<number>;
|
|
23
23
|
};
|
|
24
|
-
}>, {
|
|
24
|
+
}>, {
|
|
25
|
+
active: import("vue").ComputedRef<boolean>;
|
|
26
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
27
|
click: (...args: any[]) => void;
|
|
26
28
|
scrubbing: (...args: any[]) => void;
|
|
27
29
|
scrubberMousedown: (...args: any[]) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, watch, useTemplateRef } from "vue";
|
|
3
|
-
import { useEventListener, useMouseInElement, onClickOutside } from "@vueuse/core";
|
|
2
|
+
import { computed, ref, watch, useTemplateRef } from "vue";
|
|
3
|
+
import { useEventListener, useMouseInElement, useElementHover, onClickOutside } from "@vueuse/core";
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
min: { type: Number, default: 0 },
|
|
6
6
|
max: { type: Number, default: 100 },
|
|
@@ -10,20 +10,30 @@ const emit = defineEmits(["scrubbing", "scrubberMousedown", "scrubberMouseup", "
|
|
|
10
10
|
const scrubber = useTemplateRef("scrubber");
|
|
11
11
|
const scrubbing = ref(false);
|
|
12
12
|
const pendingValue = ref(0);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const hovered = useElementHover(scrubber);
|
|
14
|
+
const active = computed(() => hovered.value || scrubbing.value);
|
|
15
|
+
defineExpose({ active });
|
|
16
|
+
useEventListener("mouseup", endScrub);
|
|
17
|
+
useEventListener("touchend", endScrub);
|
|
18
|
+
useEventListener("touchcancel", endScrub);
|
|
19
19
|
onClickOutside(scrubber, () => {
|
|
20
|
+
if (scrubbing.value) endScrub();
|
|
21
|
+
});
|
|
22
|
+
function startScrub() {
|
|
23
|
+
scrubbing.value = true;
|
|
24
|
+
emit("scrubberMousedown", true);
|
|
25
|
+
}
|
|
26
|
+
function endScrub() {
|
|
20
27
|
if (!scrubbing.value) return;
|
|
21
28
|
scrubbing.value = false;
|
|
22
29
|
emit("scrubbing", false);
|
|
23
30
|
emit("scrubberMouseup", true);
|
|
24
|
-
}
|
|
31
|
+
}
|
|
25
32
|
const value = defineModel({ type: Number, ...{ required: false, default: 0 } });
|
|
26
33
|
const { elementX, elementWidth } = useMouseInElement(scrubber);
|
|
34
|
+
const progressPercent = computed(() => props.max > 0 ? value.value / props.max * 100 : 0);
|
|
35
|
+
const bufferedPercent = computed(() => props.max > 0 ? props.secondary / props.max * 100 : 0);
|
|
36
|
+
const pendingPercent = computed(() => props.max > 0 ? pendingValue.value / props.max * 100 : 0);
|
|
27
37
|
watch([scrubbing, elementX], () => {
|
|
28
38
|
const progress = Math.max(0, Math.min(1, elementX.value / elementWidth.value));
|
|
29
39
|
pendingValue.value = progress * props.max;
|
|
@@ -35,15 +45,42 @@ watch([scrubbing, elementX], () => {
|
|
|
35
45
|
</script>
|
|
36
46
|
|
|
37
47
|
<template>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
<!-- Outer: full-width hit area, never changes size -->
|
|
49
|
+
<div ref="scrubber" role="slider" :aria-valuemin="min" :aria-valuemax="max" :aria-valuenow="Math.round(value)"
|
|
50
|
+
:aria-label="`Seek: ${Math.round(progressPercent)}%`" tabindex="0"
|
|
51
|
+
class="group/scrubber cursor-pointer select-none overflow-visible relative" @mousedown.stop.prevent="startScrub"
|
|
52
|
+
@touchstart.stop.prevent="startScrub" @keydown.left.prevent="value = Math.max(min, value - max * 0.05)"
|
|
53
|
+
@keydown.right.prevent="value = Math.min(max, value + max * 0.05)">
|
|
54
|
+
|
|
55
|
+
<!-- Hit area extends above/below for easier targeting -->
|
|
56
|
+
<div class="absolute inset-x-0 -top-3 -bottom-1 z-1" />
|
|
57
|
+
|
|
58
|
+
<!-- Inner wrapper: handles visual inset + rounded on active -->
|
|
59
|
+
<div class="relative w-full transition-[margin,border-radius,height] duration-200 ease-out"
|
|
60
|
+
:class="active ? 'mx-3 rounded-full h-2.5' : 'mx-0 h-full'">
|
|
61
|
+
|
|
62
|
+
<!-- Track: clips buffered/progress bars -->
|
|
63
|
+
<div class="h-full w-full relative overflow-hidden bg-white/15" :class="active ? 'rounded-full' : ''">
|
|
64
|
+
<!-- Buffered -->
|
|
65
|
+
<div class="bg-white/30 h-full left-0 top-0 absolute" :style="{ width: `${bufferedPercent}%` }" />
|
|
66
|
+
<!-- Progress -->
|
|
67
|
+
<div class="bg-white h-full left-0 top-0 absolute" :style="{ width: `${progressPercent}%` }" />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Scrub head: outside track so not clipped by overflow-hidden -->
|
|
71
|
+
<div
|
|
72
|
+
class="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 rounded-full bg-white transition-[width,height,box-shadow] duration-150 z-2"
|
|
73
|
+
:class="active ? 'size-3.5 shadow-md' : 'size-0'" :style="{ left: `${progressPercent}%` }" />
|
|
45
74
|
</div>
|
|
46
|
-
|
|
75
|
+
|
|
76
|
+
<!-- Hover preview line -->
|
|
77
|
+
<div v-if="active && !scrubbing"
|
|
78
|
+
class="absolute top-0 h-full w-px bg-white/50 pointer-events-none z-1 -translate-x-1/2"
|
|
79
|
+
:style="{ left: `${pendingPercent}%` }" />
|
|
80
|
+
|
|
81
|
+
<!-- Tooltip slot -->
|
|
82
|
+
<div class="absolute left-0 right-0 pointer-events-none z-3 transition-opacity duration-100"
|
|
83
|
+
style="bottom: calc(100% + 0.5rem);" :class="active ? 'opacity-100' : 'opacity-0'">
|
|
47
84
|
<slot :pending-value="pendingValue" :position="`${Math.max(0, Math.min(elementX, elementWidth))}px`" />
|
|
48
85
|
</div>
|
|
49
86
|
</div>
|
|
@@ -21,7 +21,9 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
21
21
|
modelValue: {
|
|
22
22
|
type: import("vue").PropType<number>;
|
|
23
23
|
};
|
|
24
|
-
}>, {
|
|
24
|
+
}>, {
|
|
25
|
+
active: import("vue").ComputedRef<boolean>;
|
|
26
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
27
|
click: (...args: any[]) => void;
|
|
26
28
|
scrubbing: (...args: any[]) => void;
|
|
27
29
|
scrubberMousedown: (...args: any[]) => void;
|
|
@@ -122,6 +122,8 @@ export function useHeroSlider(containerRef, slides, options = {}) {
|
|
|
122
122
|
isHovered,
|
|
123
123
|
// Swiper options (merged)
|
|
124
124
|
mergedSwiperOptions,
|
|
125
|
+
// Container
|
|
126
|
+
containerEl: resolvedContainer,
|
|
125
127
|
// Internal bindings
|
|
126
128
|
onSwiper: swiper.onSwiper,
|
|
127
129
|
onSlideChange,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -203,6 +203,7 @@ export interface UseHeroSliderReturn {
|
|
|
203
203
|
videoSetVolume: (v: number) => void;
|
|
204
204
|
videoToggleMute: () => void;
|
|
205
205
|
isHovered: Ref<boolean>;
|
|
206
|
+
containerEl: ComputedRef<HTMLElement | null>;
|
|
206
207
|
mergedSwiperOptions: ComputedRef<Record<string, unknown>>;
|
|
207
208
|
onSwiper: (swiper: any) => void;
|
|
208
209
|
onSlideChange: () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-hero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A full-featured hero slider Nuxt module with parallax, video backgrounds, overlay patterns, and customizable animations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "weskhaled",
|
|
@@ -38,14 +38,17 @@
|
|
|
38
38
|
"dev": "nuxi dev playground",
|
|
39
39
|
"dev:build": "nuxi build playground",
|
|
40
40
|
"build": "nuxt-module-build build",
|
|
41
|
-
"prepare": "nuxt-module-build build --stub",
|
|
41
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare",
|
|
42
42
|
"prepack": "nuxt-module-build build",
|
|
43
43
|
"typecheck": "nuxi typecheck playground",
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"test:watch": "vitest",
|
|
46
|
-
"lint": "oxlint .",
|
|
46
|
+
"lint": "oxlint . && pnpm build && pnpm test",
|
|
47
47
|
"lint:fix": "oxlint --fix .",
|
|
48
|
-
"lint:all": "oxlint . && pnpm typecheck"
|
|
48
|
+
"lint:all": "oxlint . && pnpm build && pnpm test && pnpm typecheck",
|
|
49
|
+
"docs:dev": "vitepress dev docs",
|
|
50
|
+
"docs:build": "vitepress build docs",
|
|
51
|
+
"docs:preview": "vitepress preview docs"
|
|
49
52
|
},
|
|
50
53
|
"dependencies": {
|
|
51
54
|
"@nuxt/icon": "^2.2.1",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"swiper": "^12.1.0",
|
|
89
92
|
"tailwindcss": "^4.2.2",
|
|
90
93
|
"typescript": "^6.0.2",
|
|
94
|
+
"vitepress": "^1.6.4",
|
|
91
95
|
"vitest": "^4.1.4",
|
|
92
96
|
"vue-tsc": "^3.2.6"
|
|
93
97
|
}
|