feat(skin): show scrubber preview timestamps (#1652)

This commit is contained in:
Sam Potts
2026-06-03 19:27:54 +10:00
committed by GitHub
parent d96a8dd474
commit 7453d538a8
35 changed files with 382 additions and 166 deletions
@@ -85,6 +85,9 @@ function getTemplateHTML() {
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
<media-slider-preview class="${slider.preview}">
<media-slider-value type="pointer" class="${slider.value}"></media-slider-value>
</media-slider-preview>
</media-time-slider>
</div>
@@ -68,6 +68,9 @@ function getTemplateHTML() {
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
<media-slider-preview class="media-slider__preview">
<media-slider-value type="pointer" class="media-slider__value media-time"></media-slider-value>
</media-slider-preview>
</media-time-slider>
</div>
@@ -80,6 +80,9 @@ function getTemplateHTML() {
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
<media-slider-preview class="${slider.preview}">
<media-slider-value type="pointer" class="${slider.value}"></media-slider-value>
</media-slider-preview>
</media-time-slider>
<media-time type="duration" class="${time.duration}"></media-time>
</div>
+3
View File
@@ -63,6 +63,9 @@ function getTemplateHTML() {
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
<media-slider-preview class="media-slider__preview">
<media-slider-value type="pointer" class="media-slider__value media-time"></media-slider-value>
</media-slider-preview>
</media-time-slider>
<media-time type="duration" class="media-time"></media-time>
</div>
@@ -16,10 +16,10 @@ import {
playbackRate,
popup,
poster,
preview,
root,
seek,
slider,
thumbnail,
time,
} from '@videojs/skins/minimal/tailwind/video.tailwind';
import { createTemplate } from '@videojs/utils/dom';
@@ -100,13 +100,16 @@ function getTemplateHTML() {
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
<div class="${preview.root}">
<div class="${preview.thumbnailWrapper}">
<media-slider-thumbnail class="${preview.thumbnail}"></media-slider-thumbnail>
<div class="${thumbnail.root}">
<div class="${thumbnail.imageWrapper}">
<media-slider-thumbnail class="${thumbnail.image}"></media-slider-thumbnail>
</div>
<media-slider-value type="pointer" class="${preview.time}"></media-slider-value>
${renderIcon('spinner', { class: cn(icon, preview.spinner) })}
<media-slider-value type="pointer" class="${cn(time.current, thumbnail.time)}"></media-slider-value>
${renderIcon('spinner', { class: cn(icon, thumbnail.spinner) })}
</div>
<media-slider-preview class="${slider.preview}">
<media-slider-value type="pointer" class="${cn(slider.value, time.current)}"></media-slider-value>
</media-slider-preview>
</media-time-slider>
</div>
@@ -77,13 +77,17 @@ function getTemplateHTML() {
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
<div class="media-preview media-slider__preview">
<div class="media-preview__thumbnail-wrapper">
<media-slider-thumbnail class="media-preview__thumbnail"></media-slider-thumbnail>
<div class="media-thumbnail media-slider__thumbnail">
<div class="media-thumbnail__image-wrapper">
<media-slider-thumbnail class="media-thumbnail__image"></media-slider-thumbnail>
</div>
<media-slider-value type="pointer" class="media-time media-preview__time"></media-slider-value>
${renderIcon('spinner', { class: 'media-preview__spinner media-icon' })}
<media-slider-value type="pointer" class="media-time media-thumbnail__time"></media-slider-value>
${renderIcon('spinner', { class: 'media-thumbnail__spinner media-icon' })}
</div>
<media-slider-preview class="media-slider__preview">
<media-slider-value type="pointer" class="media-slider__value media-time"></media-slider-value>
</media-slider-preview>
</media-time-slider>
</div>
@@ -16,10 +16,10 @@ import {
playbackRate,
popup,
poster,
preview,
root,
seek,
slider,
thumbnail,
time,
} from '@videojs/skins/default/tailwind/video.tailwind';
import { createTemplate } from '@videojs/utils/dom';
@@ -97,11 +97,14 @@ function getTemplateHTML() {
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
<div class="${preview.root}">
<media-slider-thumbnail class="${preview.thumbnail}"></media-slider-thumbnail>
<media-slider-value type="pointer" class="${preview.time}"></media-slider-value>
${renderIcon('spinner', { class: cn(icon, preview.spinner) })}
<div class="${thumbnail.root}">
<media-slider-thumbnail class="${thumbnail.image}"></media-slider-thumbnail>
<media-slider-value type="pointer" class="${cn(time.current, thumbnail.time)}"></media-slider-value>
${renderIcon('spinner', { class: cn(icon, thumbnail.spinner) })}
</div>
<media-slider-preview class="${slider.preview}">
<media-slider-value type="pointer" class="${cn(slider.value, time.current)}"></media-slider-value>
</media-slider-preview>
</media-time-slider>
<media-time type="duration" class="${time.duration}"></media-time>
</div>
+8 -4
View File
@@ -74,11 +74,15 @@ function getTemplateHTML() {
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
<div class="media-surface media-preview media-slider__preview">
<media-slider-thumbnail class="media-preview__thumbnail"></media-slider-thumbnail>
<media-slider-value type="pointer" class="media-time media-preview__time"></media-slider-value>
${renderIcon('spinner', { class: 'media-preview__spinner media-icon' })}
<div class="media-surface media-thumbnail media-slider__thumbnail">
<media-slider-thumbnail class="media-thumbnail__image"></media-slider-thumbnail>
<media-slider-value type="pointer" class="media-time media-thumbnail__time"></media-slider-value>
${renderIcon('spinner', { class: 'media-thumbnail__spinner media-icon' })}
</div>
<media-slider-preview class="media-slider__preview">
<media-slider-value type="pointer" class="media-slider__value media-time"></media-slider-value>
</media-slider-preview>
</media-time-slider>
<media-time type="duration" class="media-time"></media-time>
</div>
@@ -187,7 +187,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top" boundary="viewport">
@@ -201,7 +201,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
</div>
@@ -218,6 +218,9 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={<SliderThumb />} />
<TimeSlider.Preview className={slider.preview}>
<TimeSlider.Value type="pointer" className={slider.value} />
</TimeSlider.Preview>
</TimeSlider.Root>
</div>
@@ -134,7 +134,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
</SeekButton>
}
/>
<Tooltip.Popup className="media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className="media-tooltip" />
</Tooltip.Root>
<Tooltip.Root side="top" boundary="viewport">
@@ -148,7 +148,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
</SeekButton>
}
/>
<Tooltip.Popup className="media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className="media-tooltip" />
</Tooltip.Root>
</div>
@@ -165,6 +165,9 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<TimeSlider.Buffer className="media-slider__buffer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
<TimeSlider.Preview className="media-slider__preview">
<TimeSlider.Value type="pointer" className="media-slider__value media-time" />
</TimeSlider.Preview>
</TimeSlider.Root>
</div>
@@ -189,7 +189,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top" boundary="viewport">
@@ -203,7 +203,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
</div>
@@ -215,6 +215,9 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={<SliderThumb />} />
<TimeSlider.Preview className={slider.preview}>
<TimeSlider.Value type="pointer" className={slider.value} />
</TimeSlider.Preview>
</TimeSlider.Root>
<Time.Value type="duration" className={time.duration} />
</div>
+5 -2
View File
@@ -134,7 +134,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
</SeekButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className="media-surface media-tooltip" />
</Tooltip.Root>
<Tooltip.Root side="top" boundary="viewport">
@@ -148,7 +148,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
</SeekButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
<Tooltip.Popup className="media-surface media-tooltip" />
</Tooltip.Root>
</div>
@@ -160,6 +160,9 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<TimeSlider.Buffer className="media-slider__buffer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
<TimeSlider.Preview className="media-slider__preview">
<TimeSlider.Value type="pointer" className="media-slider__value media-time" />
</TimeSlider.Preview>
</TimeSlider.Root>
<Time.Value type="duration" className="media-time" />
</div>
@@ -129,7 +129,7 @@ export function MinimalLiveAudioSkinTailwind(props: MinimalLiveAudioSkinProps):
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<LiveButton className={cn(button.base, button.subtle, button.live)} />
@@ -129,7 +129,7 @@ export function LiveAudioSkinTailwind(props: LiveAudioSkinProps): ReactNode {
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<LiveButton className={cn(button.base, button.subtle, button.live)} />
@@ -187,7 +187,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps):
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<LiveButton className={cn(button.base, button.subtle, button.live)} />
@@ -207,7 +207,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps):
</CaptionsButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top">
@@ -219,7 +219,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps):
</CastButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top">
@@ -243,7 +243,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps):
</PiPButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top">
@@ -255,7 +255,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps):
</FullscreenButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
</div>
</Tooltip.Provider>
@@ -189,7 +189,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<LiveButton className={cn(button.base, button.subtle, button.live)} />
@@ -209,7 +209,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
</CaptionsButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top">
@@ -221,7 +221,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
</CastButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top">
@@ -245,7 +245,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
</PiPButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
<Tooltip.Root side="top">
@@ -257,7 +257,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
</FullscreenButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}></Tooltip.Popup>
<Tooltip.Popup className={cn(popup.tooltip)} />
</Tooltip.Root>
</div>
</Tooltip.Provider>
@@ -15,10 +15,10 @@ import {
playbackRate,
popup,
poster,
preview,
root,
seek,
slider,
thumbnail,
time,
} from '@videojs/skins/minimal/tailwind/video.tailwind';
import { isString } from '@videojs/utils/predicate';
@@ -274,13 +274,16 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={<SliderThumb />} />
<div className={preview.root}>
<div className={preview.thumbnailWrapper}>
<Slider.Thumbnail className={preview.thumbnail} />
<div className={thumbnail.root}>
<div className={thumbnail.imageWrapper}>
<Slider.Thumbnail className={thumbnail.image} />
</div>
<TimeSlider.Value type="pointer" className={preview.time} />
<SpinnerIcon className={cn(icon, preview.spinner)} />
<TimeSlider.Value type="pointer" className={cn(time.current, thumbnail.time)} />
<SpinnerIcon className={cn(icon, thumbnail.spinner)} />
</div>
<TimeSlider.Preview className={slider.preview}>
<TimeSlider.Value type="pointer" className={cn(slider.value, time.current)} />
</TimeSlider.Preview>
</TimeSlider.Root>
</div>
@@ -207,13 +207,17 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
<div className="media-preview media-slider__preview">
<div className="media-preview__thumbnail-wrapper">
<Slider.Thumbnail className="media-preview__thumbnail" />
<div className="media-thumbnail media-slider__thumbnail">
<div className="media-thumbnail__image-wrapper">
<Slider.Thumbnail className="media-thumbnail__image" />
</div>
<TimeSlider.Value type="pointer" className="media-time media-preview__time" />
<SpinnerIcon className="media-preview__spinner media-icon" />
<TimeSlider.Value type="pointer" className="media-time media-thumbnail__time" />
<SpinnerIcon className="media-thumbnail__spinner media-icon" />
</div>
<TimeSlider.Preview className="media-slider__preview">
<TimeSlider.Value type="pointer" className="media-slider__value media-time" />
</TimeSlider.Preview>
</TimeSlider.Root>
</div>
@@ -15,10 +15,10 @@ import {
playbackRate,
popup,
poster,
preview,
root,
seek,
slider,
thumbnail,
time,
} from '@videojs/skins/default/tailwind/video.tailwind';
import { isString } from '@videojs/utils/predicate';
@@ -271,11 +271,14 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={<SliderThumb />} />
<div className={preview.root}>
<Slider.Thumbnail className={preview.thumbnail} />
<TimeSlider.Value type="pointer" className={preview.time} />
<SpinnerIcon className={cn(icon, preview.spinner)} />
<div className={thumbnail.root}>
<Slider.Thumbnail className={thumbnail.image} />
<TimeSlider.Value type="pointer" className={cn(time.current, thumbnail.time)} />
<SpinnerIcon className={cn(icon, thumbnail.spinner)} />
</div>
<TimeSlider.Preview className={slider.preview}>
<TimeSlider.Value type="pointer" className={cn(slider.value, time.current)} />
</TimeSlider.Preview>
</TimeSlider.Root>
<Time.Value type="duration" className={time.duration} />
</div>
+8 -4
View File
@@ -204,11 +204,15 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
<div className="media-surface media-preview media-slider__preview">
<Slider.Thumbnail className="media-preview__thumbnail" />
<TimeSlider.Value type="pointer" className="media-time media-preview__time" />
<SpinnerIcon className="media-preview__spinner media-icon" />
<div className="media-surface media-thumbnail media-slider__thumbnail">
<Slider.Thumbnail className="media-thumbnail__image" />
<TimeSlider.Value type="pointer" className="media-time media-thumbnail__time" />
<SpinnerIcon className="media-thumbnail__spinner media-icon" />
</div>
<TimeSlider.Preview className="media-slider__preview">
<TimeSlider.Value type="pointer" className="media-slider__value media-time" />
</TimeSlider.Preview>
</TimeSlider.Root>
<Time.Value type="duration" className="media-time" />
</div>
@@ -55,9 +55,9 @@
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
0 1px 3px 0 oklch(0 0 0 / 0.15),
0 1px 2px -1px oklch(0 0 0 / 0.15);
0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.1)),
0 1px 3px 0 oklch(0 0 0 / 0.35),
0 1px 2px -1px oklch(0 0 0 / 0.35);
opacity: 0;
translate: -50% -50%;
transition-timing-function: ease-out;
@@ -76,7 +76,7 @@
&:hover,
&:focus {
outline-color: oklch(from currentColor l c h / 0.25);
outline-color: oklch(from currentColor l c h / 0.15);
outline-offset: 0;
}
@@ -109,6 +109,49 @@
opacity: 1;
}
/* Preview */
.media-default-skin .media-slider__preview {
& .media-slider__value,
&::before {
opacity: 0;
scale: 0.5;
transition-timing-function: ease-out;
transition-duration: 200ms;
transition-property: opacity, scale;
}
& .media-slider__value {
position: absolute;
bottom: 2.25rem;
text-shadow: 0 1px 0 var(--media-current-shadow-color);
filter: blur(8px);
translate: -50% 0.5rem;
transition-property: filter, opacity, scale, translate;
}
&::before {
display: block;
min-width: 0.25rem;
height: 0.25rem;
content: "";
background-color: currentColor;
border-radius: 100%;
box-shadow:
0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
0 1px 2px 0 oklch(0 0 0 / 0.35);
}
&[data-pointing] .media-slider__value,
&[data-pointing]:not([data-dragging])::before {
opacity: 1;
scale: 1;
}
&[data-pointing] .media-slider__value {
filter: blur(0);
translate: -50% 0;
}
}
/* Shared track fills */
.media-default-skin .media-slider__buffer,
.media-default-skin .media-slider__fill {
@@ -1,12 +1,13 @@
/* ==========================================================================
Media preview
Media thumbnail
========================================================================== */
.media-default-skin .media-preview {
.media-default-skin .media-thumbnail {
pointer-events: none;
background-color: oklch(0 0 0 / 0.9);
border-radius: 0.75rem;
& .media-preview__thumbnail {
& .media-thumbnail__image {
position: relative;
display: block;
overflow: clip;
@@ -21,18 +22,14 @@
}
}
& .media-preview__time {
& .media-thumbnail__time {
position: absolute;
inset-inline: 0;
bottom: 0.5rem;
text-align: center;
}
& .media-overlay {
opacity: 1;
}
& .media-preview__spinner {
& .media-thumbnail__spinner {
position: absolute;
top: 50%;
left: 50%;
@@ -40,22 +37,22 @@
translate: -50% -50%;
}
& .media-preview__thumbnail,
& .media-preview__spinner {
& .media-thumbnail__image,
& .media-thumbnail__spinner {
transition: opacity 150ms ease-out;
}
&:not(:has(.media-preview__thumbnail[data-loading])) {
& .media-preview__spinner {
&:not(:has(.media-thumbnail__image[data-loading])) {
& .media-thumbnail__spinner {
--media-spinner-animation: none;
}
}
&:has(.media-preview__thumbnail[data-loading]) {
& .media-preview__thumbnail {
&:has(.media-thumbnail__image[data-loading]) {
& .media-thumbnail__image {
opacity: 0;
}
& .media-preview__spinner {
& .media-thumbnail__spinner {
opacity: 1;
}
}
+28 -11
View File
@@ -12,7 +12,7 @@
@import "./components/icons.css";
@import "./components/menus.css";
@import "./components/poster.css";
@import "./components/preview.css";
@import "./components/thumbnail.css";
@import "./components/slider.css";
@import "./components/popup.css";
@import "./components/captions.css";
@@ -245,21 +245,32 @@
box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05);
}
.media-default-skin--video .media-slider__preview {
--media-preview-max-width: 11rem;
--media-preview-padding: -1.125rem;
.media-default-skin--video .media-slider__thumbnail {
--media-slider-thumbnail-max-width: 11rem;
--media-slider-thumbnail-padding: -1.125rem;
/**
Inset is the difference between the container width and the slider (100%) width.
Divided by 2 as we render the time on both sides.
*/
--media-preview-inset: calc((100cqi - 100%) / 2);
--media-slider-thumbnail-inset: calc((100cqi - 100%) / 2);
position: absolute;
bottom: calc(100% + 1.2rem);
left: clamp(
calc(var(--media-preview-max-width) / 2 + var(--media-preview-padding) - var(--media-preview-inset)),
calc(
var(--media-slider-thumbnail-max-width) /
2 +
var(--media-slider-thumbnail-padding) -
var(--media-slider-thumbnail-inset)
),
var(--media-slider-pointer),
calc(100% - var(--media-preview-max-width) / 2 - var(--media-preview-padding) + var(--media-preview-inset))
calc(
100% -
var(--media-slider-thumbnail-max-width) /
2 -
var(--media-slider-thumbnail-padding) +
var(--media-slider-thumbnail-inset)
)
);
pointer-events: none;
opacity: 0;
@@ -271,16 +282,22 @@
transition-duration: 150ms;
transition-property: scale, opacity, filter;
& .media-preview__thumbnail {
max-width: var(--media-preview-max-width);
& .media-thumbnail__image {
max-width: var(--media-slider-thumbnail-max-width);
}
&:has(.media-preview__thumbnail[data-loading]) {
&:has(.media-thumbnail__image[data-loading]) {
max-height: 6rem;
}
}
.media-default-skin--video .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
.media-default-skin--video .media-slider[data-pointing] .media-slider__thumbnail:has([role="img"]:not([data-hidden])) {
opacity: 1;
filter: blur(0);
scale: 1;
}
.media-default-skin--video
.media-slider__thumbnail:has([role="img"]:not([data-hidden]))
+ .media-slider__preview
.media-slider__value {
display: none;
}
@@ -40,11 +40,11 @@ export const slider = {
base: cn(
'z-10 absolute -translate-x-1/2 -translate-y-1/2',
'bg-current rounded-full',
'shadow-[0_0_0_1px_var(--media-current-shadow-color-subtle,oklch(0_0_0/0.1)),0_1px_3px_0_oklch(0_0_0/0.15),0_1px_2px_-1px_oklch(0_0_0/0.15)]',
'shadow-[0_0_0_1px_var(--media-current-shadow-color,oklch(0_0_0/0.1)),0_1px_3px_0_oklch(0_0_0/0.35),0_1px_2px_-1px_oklch(0_0_0/0.35)]',
'transition-[opacity,height,width,outline-offset] duration-150 ease-out select-none',
'outline-4 outline-transparent -outline-offset-4',
'hover:outline-current/25 hover:outline-offset-0',
'focus-visible:outline-current/25 focus-visible:outline-offset-0',
'hover:outline-current/15 hover:outline-offset-0',
'focus-visible:outline-current/15 focus-visible:outline-offset-0',
// Horizontal
'data-[orientation=horizontal]:top-1/2 data-[orientation=horizontal]:left-(--media-slider-fill)',
// Vertical
@@ -57,4 +57,20 @@ export const slider = {
'group-active/slider:size-3'
),
},
preview: cn(
'group/preview',
'before:block before:min-w-1 before:h-1 before:bg-current before:rounded-full before:opacity-0 before:scale-50',
'before:shadow-[0_0_0_1px_var(--media-current-shadow-color,oklch(0_0_0/0.15)),0_1px_2px_0_oklch(0_0_0/0.35)]',
'before:transition-[opacity,scale] before:duration-200 before:ease-out',
'data-pointing:not-data-dragging:before:opacity-100 data-pointing:not-data-dragging:before:scale-100',
'peer-has-[[role=img]:not([data-hidden])]/thumbnail:*:hidden'
),
value: cn(
'absolute bottom-9 tabular-nums',
'-translate-x-1/2 translate-y-2 scale-50 opacity-0 blur-lg',
'text-shadow-2xs text-shadow-(color:--media-current-shadow-color)',
'transition-[filter,opacity,scale,translate] duration-200 ease-out',
'group-data-pointing/preview:translate-y-0 group-data-pointing/preview:scale-100',
'group-data-pointing/preview:opacity-100 group-data-pointing/preview:blur-none'
),
};
@@ -1,8 +1,8 @@
import { cn } from '@videojs/utils/style';
export const preview = {
root: cn('group/preview pointer-events-none bg-black/90 rounded-xl'),
thumbnail: cn(
export const thumbnail = {
root: cn('group/thumbnail peer/thumbnail pointer-events-none bg-black/90 rounded-xl'),
image: cn(
'block relative overflow-clip rounded-[inherit]',
'transition-opacity duration-150 ease-out',
'after:absolute after:inset-0 after:rounded-[inherit]',
@@ -13,6 +13,6 @@ export const preview = {
spinner: cn(
'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0',
'transition-opacity duration-150 ease-out',
'group-not-has-[[role=img][data-loading]]/preview:[--media-spinner-animation:none] group-has-[[role=img][data-loading]]/preview:opacity-100'
'group-not-has-[[role=img][data-loading]]/thumbnail:[--media-spinner-animation:none] group-has-[[role=img][data-loading]]/thumbnail:opacity-100'
),
};
@@ -5,10 +5,10 @@ import { controls as baseControls } from './components/controls';
import { error as baseError } from './components/error';
import { inputFeedback as baseInputFeedback } from './components/input-feedback';
import { popup as basePopup } from './components/popup';
import { preview as basePreview } from './components/preview';
import { root as baseRoot } from './components/root';
import { slider as baseSlider } from './components/slider';
import { surface } from './components/surface';
import { thumbnail as baseThumbnail } from './components/thumbnail';
import { time as baseTime } from './components/time';
/* ==========================================================================
@@ -133,23 +133,24 @@ export const time = {
};
/* ==========================================================================
Preview (with video surface)
Thumbnail (with video surface)
========================================================================== */
export const preview = {
...basePreview,
export const thumbnail = {
...baseThumbnail,
root: cn(
'[--media-preview-max-width:11rem] [--media-preview-padding:-1.125rem] [--media-preview-inset:calc((100cqi-100%)/2)]',
'absolute [left:clamp(calc(var(--media-preview-max-width)/2+var(--media-preview-padding)-var(--media-preview-inset)),var(--media-slider-pointer),calc(100%-var(--media-preview-max-width)/2-var(--media-preview-padding)+var(--media-preview-inset)))] bottom-[calc(100%+1.2rem)] -translate-x-1/2',
baseThumbnail.root,
surface,
'[--media-slider-thumbnail-max-width:11rem] [--media-slider-thumbnail-padding:-1.125rem] [--media-slider-thumbnail-inset:calc((100cqi-100%)/2)]',
'absolute [left:clamp(calc(var(--media-slider-thumbnail-max-width)/2+var(--media-slider-thumbnail-padding)-var(--media-slider-thumbnail-inset)),var(--media-slider-pointer),calc(100%-var(--media-slider-thumbnail-max-width)/2-var(--media-slider-thumbnail-padding)+var(--media-slider-thumbnail-inset)))] bottom-[calc(100%+1.2rem)] -translate-x-1/2',
'opacity-0 scale-80 blur-sm origin-bottom',
'transition-[scale,opacity,filter] duration-150',
'group-data-pointing/slider:opacity-100 group-data-pointing/slider:scale-100 group-data-pointing/slider:blur-none',
'has-[[role=img][data-hidden]]:opacity-0 has-[[role=img][data-hidden]]:scale-80 has-[[role=img][data-hidden]]:blur-sm',
'has-[[role=img][data-loading]]:max-h-24',
surface,
basePreview.root
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:opacity-100',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:scale-100',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:blur-none',
'has-[[role=img][data-loading]]:max-h-24'
),
thumbnail: cn(basePreview.thumbnail, 'max-w-(--media-preview-max-width)'),
image: cn(baseThumbnail.image, 'max-w-(--media-slider-thumbnail-max-width)'),
};
/* ==========================================================================
+4
View File
@@ -112,3 +112,7 @@
padding: 0 0 0 4rem;
background: linear-gradient(to left, var(--media-controls-background-color) 80%, transparent 100%);
}
.media-minimal-skin--audio .media-slider__preview .media-slider__value {
bottom: 2.5rem;
}
@@ -56,7 +56,7 @@
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
0 0 0 1px var(--media-current-shadow-color, oklch(0 0 0 / 0.15)),
0 1px 3px 0 oklch(0 0 0 / 0.15),
0 1px 2px -1px oklch(0 0 0 / 0.15);
opacity: 0;
@@ -90,6 +90,53 @@
scale: 1;
}
/* Preview */
.media-minimal-skin .media-slider__preview {
& .media-slider__value,
&::before {
opacity: 0;
scale: 0.5;
transition-timing-function: ease-out;
transition-duration: 200ms;
transition-property: opacity, scale;
}
& .media-slider__value {
position: absolute;
bottom: 1.5rem;
text-shadow: 0 1px 0 var(--media-current-shadow-color);
filter: blur(8px);
translate: -50% 0.5rem;
transition-property: filter, opacity, scale, translate;
}
&::before {
display: block;
content: "";
background-color: oklch(from currentColor l c h / 0.35);
}
&[data-pointing] .media-slider__value,
&[data-pointing]:not([data-dragging])::before {
opacity: 1;
scale: 1;
}
&[data-pointing] .media-slider__value {
filter: blur(0);
translate: -50% 0;
}
&[data-orientation="horizontal"]::before {
min-width: 1px;
height: 1.25rem;
}
&[data-orientation="vertical"]::before {
width: 1.25rem;
min-height: 1px;
}
}
/* Shared track fills */
.media-minimal-skin .media-slider__buffer,
.media-minimal-skin .media-slider__fill {
@@ -1,20 +1,21 @@
/* ==========================================================================
Media preview
Media thumbnail
========================================================================== */
.media-minimal-skin .media-preview {
.media-minimal-skin .media-thumbnail {
pointer-events: none;
& .media-preview__thumbnail-wrapper {
& .media-thumbnail__image-wrapper {
position: relative;
background-color: oklch(0 0 0 / 0.9);
border-radius: 0.5rem;
}
& .media-preview__thumbnail {
& .media-thumbnail__image {
display: block;
border-radius: inherit;
}
& .media-preview__time {
& .media-thumbnail__time {
display: block;
margin-top: 0.5rem;
text-align: center;
@@ -24,7 +25,7 @@
opacity: 1;
}
& .media-preview__spinner {
& .media-thumbnail__spinner {
position: absolute;
top: 50%;
left: 50%;
@@ -32,22 +33,22 @@
translate: -50% -50%;
}
& .media-preview__thumbnail,
& .media-preview__spinner {
& .media-thumbnail__image,
& .media-thumbnail__spinner {
transition: opacity 150ms ease-out;
}
&:not(:has(.media-preview__thumbnail[data-loading])) {
& .media-preview__spinner {
&:not(:has(.media-thumbnail__image[data-loading])) {
& .media-thumbnail__spinner {
--media-spinner-animation: none;
}
}
&:has(.media-preview__thumbnail[data-loading]) {
& .media-preview__thumbnail {
&:has(.media-thumbnail__image[data-loading]) {
& .media-thumbnail__image {
opacity: 0;
}
& .media-preview__spinner {
& .media-thumbnail__spinner {
opacity: 1;
}
}
+24 -13
View File
@@ -10,7 +10,7 @@
@import "./components/button-group.css";
@import "./components/icons.css";
@import "./components/poster.css";
@import "./components/preview.css";
@import "./components/thumbnail.css";
@import "./components/slider.css";
@import "./components/popup.css";
@import "./components/menus.css";
@@ -261,21 +261,27 @@
Slider preview
========================================================================== */
.media-minimal-skin--video .media-slider__preview {
--media-preview-max-width: 11rem;
--media-preview-padding: -0.5rem;
.media-minimal-skin--video .media-slider__thumbnail {
--media-slider-thumbnail-max-width: 11rem;
--media-slider-thumbnail-padding: -0.5rem;
/**
Inset is the difference between the container width and the slider (100%) width.
We only add to the end as we render the time there.
*/
--media-preview-inset: calc(100cqi - 100%);
--media-slider-thumbnail-inset: calc(100cqi - 100%);
position: absolute;
bottom: 100%;
left: clamp(
calc(var(--media-preview-max-width) / 2 + var(--media-preview-padding)),
calc(var(--media-slider-thumbnail-max-width) / 2 + var(--media-slider-thumbnail-padding)),
var(--media-slider-pointer),
calc(100% - var(--media-preview-max-width) / 2 - var(--media-preview-padding) + var(--media-preview-inset))
calc(
100% -
var(--media-slider-thumbnail-max-width) /
2 -
var(--media-slider-thumbnail-padding) +
var(--media-slider-thumbnail-inset)
)
);
opacity: 0;
filter: blur(8px);
@@ -287,11 +293,10 @@
transition-property: scale, opacity, filter;
@container media-root (width > 42rem) {
bottom: calc(100% + 0.25rem);
left: var(--media-slider-pointer);
}
& .media-preview__thumbnail-wrapper {
& .media-thumbnail__image-wrapper {
position: relative;
&::after {
@@ -306,16 +311,22 @@
}
}
& .media-preview__thumbnail {
max-width: var(--media-preview-max-width);
& .media-thumbnail__image {
max-width: var(--media-slider-thumbnail-max-width);
}
&:has(.media-preview__thumbnail[data-loading]) {
&:has(.media-thumbnail__image[data-loading]) {
max-height: 6rem;
}
}
.media-minimal-skin--video .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
.media-minimal-skin--video .media-slider[data-pointing] .media-slider__thumbnail:has([role="img"]:not([data-hidden])) {
opacity: 1;
filter: blur(0);
scale: 1;
}
.media-minimal-skin--video
.media-slider__thumbnail:has([role="img"]:not([data-hidden]))
+ .media-slider__preview
.media-slider__value {
display: none;
}
@@ -3,6 +3,7 @@ import { controls as baseControls } from './components/controls';
import { error as baseError } from './components/error';
import { popup as basePopup } from './components/popup';
import { root as baseRoot } from './components/root';
import { slider as baseSlider } from './components/slider';
/* ==========================================================================
Root
@@ -67,6 +68,15 @@ export const popup = {
),
};
/* ==========================================================================
Sliders
========================================================================== */
export const slider = {
...baseSlider,
value: cn(baseSlider.value, 'bottom-10'),
};
/* ==========================================================================
Error
========================================================================== */
@@ -98,5 +108,4 @@ export { icon, iconContainer, iconFlipped, iconHidden } from './components/icon'
export { menu } from './components/menu';
export { playbackRate } from './components/playback-rate';
export { seek } from './components/seek';
export { slider } from './components/slider';
export { time } from './components/time';
@@ -1,13 +0,0 @@
import { cn } from '@videojs/utils/style';
export const preview = {
root: 'group/preview pointer-events-none',
thumbnailWrapper: 'relative rounded-lg bg-black/90',
thumbnail: cn('block rounded-[inherit] transition-opacity duration-150 ease-out', 'data-loading:opacity-0'),
time: 'mt-2 block text-center tabular-nums',
spinner: cn(
'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0',
'transition-opacity duration-150 ease-out',
'group-not-has-[[role=img][data-loading]]/preview:[--media-spinner-animation:none] group-has-[[role=img][data-loading]]/preview:opacity-100'
),
};
@@ -40,7 +40,7 @@ export const slider = {
base: cn(
'z-10 absolute size-3 -translate-x-1/2 -translate-y-1/2',
'bg-current rounded-full',
'shadow-[0_0_0_1px_var(--media-current-shadow-color-subtle,oklch(0_0_0/0.1)),0_1px_3px_0_oklch(0_0_0/0.15),0_1px_2px_-1px_oklch(0_0_0/0.15)]',
'shadow-[0_0_0_1px_var(--media-current-shadow-color,oklch(0_0_0/0.15)),0_1px_3px_0_oklch(0_0_0/0.15),0_1px_2px_-1px_oklch(0_0_0/0.15)]',
'transition-[opacity,scale,outline-offset] duration-150 ease-out select-none',
'outline-2 outline-transparent -outline-offset-2',
'focus-visible:outline-current focus-visible:outline-offset-2',
@@ -55,4 +55,21 @@ export const slider = {
'group-focus-within/slider:opacity-100 group-focus-within/slider:scale-100'
),
},
preview: cn(
'group/preview',
'before:block before:bg-current/35 before:opacity-0 before:scale-50',
'before:transition-[opacity,scale] before:duration-200 before:ease-out',
'data-pointing:not-data-dragging:before:opacity-100 data-pointing:not-data-dragging:before:scale-100',
'data-[orientation=horizontal]:before:min-w-px data-[orientation=horizontal]:before:h-5',
'data-[orientation=vertical]:before:w-5 data-[orientation=vertical]:before:min-h-px',
'peer-has-[[role=img]:not([data-hidden])]/thumbnail:*:hidden'
),
value: cn(
'absolute bottom-6 tabular-nums',
'-translate-x-1/2 translate-y-2 scale-50 opacity-0 blur-lg',
'text-shadow-2xs text-shadow-(color:--media-current-shadow-color)',
'transition-[filter,opacity,scale,translate] duration-200 ease-out',
'group-data-pointing/preview:translate-y-0 group-data-pointing/preview:scale-100',
'group-data-pointing/preview:opacity-100 group-data-pointing/preview:blur-none'
),
};
@@ -0,0 +1,13 @@
import { cn } from '@videojs/utils/style';
export const thumbnail = {
root: 'group/thumbnail peer/thumbnail pointer-events-none',
imageWrapper: 'relative rounded-lg bg-black/90',
image: cn('block rounded-[inherit] transition-opacity duration-150 ease-out', 'data-loading:opacity-0'),
time: 'mt-2 block text-center tabular-nums',
spinner: cn(
'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0',
'transition-opacity duration-150 ease-out',
'group-not-has-[[role=img][data-loading]]/thumbnail:[--media-spinner-animation:none] group-has-[[role=img][data-loading]]/thumbnail:opacity-100'
),
};
@@ -3,9 +3,9 @@ import { buttonGroup as baseButtonGroup } from './components/button-group';
import { controls as baseControls } from './components/controls';
import { error as baseError } from './components/error';
import { popup as basePopup } from './components/popup';
import { preview as basePreview } from './components/preview';
import { root as baseRoot } from './components/root';
import { slider as baseSlider } from './components/slider';
import { thumbnail as baseThumbnail } from './components/thumbnail';
import { time as baseTime } from './components/time';
/* ==========================================================================
@@ -139,28 +139,29 @@ export const error = {
};
/* ==========================================================================
Preview
Thumbnail
========================================================================== */
export const preview = {
...basePreview,
export const thumbnail = {
...baseThumbnail,
root: cn(
'[--media-preview-max-width:11rem] [--media-preview-padding:-0.5rem] [--media-preview-inset:calc(100cqi-100%)]',
'absolute [left:clamp(calc(var(--media-preview-max-width)/2+var(--media-preview-padding)),var(--media-slider-pointer),calc(100%-var(--media-preview-max-width)/2-var(--media-preview-padding)+var(--media-preview-inset)))] bottom-full -translate-x-1/2',
'@2xl/media-root:bottom-[calc(100%+0.25rem)] @2xl/media-root:[left:var(--media-slider-pointer)]',
baseThumbnail.root,
'[--media-slider-thumbnail-max-width:11rem] [--media-slider-thumbnail-padding:-0.5rem] [--media-slider-thumbnail-inset:calc(100cqi-100%)]',
'absolute [left:clamp(calc(var(--media-slider-thumbnail-max-width)/2+var(--media-slider-thumbnail-padding)),var(--media-slider-pointer),calc(100%-var(--media-slider-thumbnail-max-width)/2-var(--media-slider-thumbnail-padding)+var(--media-slider-thumbnail-inset)))] bottom-full -translate-x-1/2',
'@2xl/media-root:[left:var(--media-slider-pointer)]',
'opacity-0 scale-80 blur-sm origin-bottom',
'transition-[scale,opacity,filter] duration-150',
'group-data-pointing/slider:opacity-100 group-data-pointing/slider:scale-100 group-data-pointing/slider:blur-none',
'has-[[role=img][data-hidden]]:opacity-0 has-[[role=img][data-hidden]]:scale-80 has-[[role=img][data-hidden]]:blur-sm',
'has-[[role=img][data-loading]]:max-h-24',
basePreview.root
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:opacity-100',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:scale-100',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:blur-none',
'has-[[role=img][data-loading]]:max-h-24'
),
thumbnailWrapper: cn(
basePreview.thumbnailWrapper,
imageWrapper: cn(
baseThumbnail.imageWrapper,
'after:absolute after:inset-0 after:rounded-[inherit]',
'after:ring-1 after:ring-black/5 after:shadow-sm after:shadow-black/20'
),
thumbnail: cn(basePreview.thumbnail, 'max-w-(--media-preview-max-width)'),
image: cn(baseThumbnail.image, 'max-w-(--media-slider-thumbnail-max-width)'),
};
/* ==========================================================================