From 7453d538a8d25e56fd288ef64fc0af901250e459 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 3 Jun 2026 19:27:54 +1000 Subject: [PATCH] feat(skin): show scrubber preview timestamps (#1652) --- .../src/define/audio/minimal-skin.tailwind.ts | 3 ++ .../html/src/define/audio/minimal-skin.ts | 3 ++ .../html/src/define/audio/skin.tailwind.ts | 3 ++ packages/html/src/define/audio/skin.ts | 3 ++ .../src/define/video/minimal-skin.tailwind.ts | 15 +++--- .../html/src/define/video/minimal-skin.ts | 14 +++-- .../html/src/define/video/skin.tailwind.ts | 13 +++-- packages/html/src/define/video/skin.ts | 12 +++-- .../presets/audio/minimal-skin.tailwind.tsx | 7 ++- .../react/src/presets/audio/minimal-skin.tsx | 7 ++- .../react/src/presets/audio/skin.tailwind.tsx | 7 ++- packages/react/src/presets/audio/skin.tsx | 7 ++- .../live-audio/minimal-skin.tailwind.tsx | 2 +- .../src/presets/live-audio/skin.tailwind.tsx | 2 +- .../live-video/minimal-skin.tailwind.tsx | 10 ++-- .../src/presets/live-video/skin.tailwind.tsx | 10 ++-- .../presets/video/minimal-skin.tailwind.tsx | 15 +++--- .../react/src/presets/video/minimal-skin.tsx | 14 +++-- .../react/src/presets/video/skin.tailwind.tsx | 13 +++-- packages/react/src/presets/video/skin.tsx | 12 +++-- .../src/default/css/components/slider.css | 51 +++++++++++++++++-- .../components/{preview.css => thumbnail.css} | 29 +++++------ packages/skins/src/default/css/video.css | 39 ++++++++++---- .../src/default/tailwind/components/slider.ts | 22 ++++++-- .../components/{preview.ts => thumbnail.ts} | 8 +-- .../src/default/tailwind/video.tailwind.ts | 25 ++++----- packages/skins/src/minimal/css/audio.css | 4 ++ .../src/minimal/css/components/slider.css | 49 +++++++++++++++++- .../components/{preview.css => thumbnail.css} | 27 +++++----- packages/skins/src/minimal/css/video.css | 37 +++++++++----- .../src/minimal/tailwind/audio.tailwind.ts | 11 +++- .../minimal/tailwind/components/preview.ts | 13 ----- .../src/minimal/tailwind/components/slider.ts | 19 ++++++- .../minimal/tailwind/components/thumbnail.ts | 13 +++++ .../src/minimal/tailwind/video.tailwind.ts | 29 ++++++----- 35 files changed, 382 insertions(+), 166 deletions(-) rename packages/skins/src/default/css/components/{preview.css => thumbnail.css} (66%) rename packages/skins/src/default/tailwind/components/{preview.ts => thumbnail.ts} (65%) rename packages/skins/src/minimal/css/components/{preview.css => thumbnail.css} (60%) delete mode 100644 packages/skins/src/minimal/tailwind/components/preview.ts create mode 100644 packages/skins/src/minimal/tailwind/components/thumbnail.ts diff --git a/packages/html/src/define/audio/minimal-skin.tailwind.ts b/packages/html/src/define/audio/minimal-skin.tailwind.ts index 002921a5..f12eac3f 100644 --- a/packages/html/src/define/audio/minimal-skin.tailwind.ts +++ b/packages/html/src/define/audio/minimal-skin.tailwind.ts @@ -85,6 +85,9 @@ function getTemplateHTML() { + + + diff --git a/packages/html/src/define/audio/minimal-skin.ts b/packages/html/src/define/audio/minimal-skin.ts index e75e2c0f..d682c974 100644 --- a/packages/html/src/define/audio/minimal-skin.ts +++ b/packages/html/src/define/audio/minimal-skin.ts @@ -68,6 +68,9 @@ function getTemplateHTML() { + + + diff --git a/packages/html/src/define/audio/skin.tailwind.ts b/packages/html/src/define/audio/skin.tailwind.ts index fcd9698c..a2912858 100644 --- a/packages/html/src/define/audio/skin.tailwind.ts +++ b/packages/html/src/define/audio/skin.tailwind.ts @@ -80,6 +80,9 @@ function getTemplateHTML() { + + + diff --git a/packages/html/src/define/audio/skin.ts b/packages/html/src/define/audio/skin.ts index 632b9fe7..3b1154dc 100644 --- a/packages/html/src/define/audio/skin.ts +++ b/packages/html/src/define/audio/skin.ts @@ -63,6 +63,9 @@ function getTemplateHTML() { + + + diff --git a/packages/html/src/define/video/minimal-skin.tailwind.ts b/packages/html/src/define/video/minimal-skin.tailwind.ts index e8d30f42..229d783d 100644 --- a/packages/html/src/define/video/minimal-skin.tailwind.ts +++ b/packages/html/src/define/video/minimal-skin.tailwind.ts @@ -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() { -
-
- +
+
+
- - ${renderIcon('spinner', { class: cn(icon, preview.spinner) })} + + ${renderIcon('spinner', { class: cn(icon, thumbnail.spinner) })}
+ + +
diff --git a/packages/html/src/define/video/minimal-skin.ts b/packages/html/src/define/video/minimal-skin.ts index ea2b0545..600fd36c 100644 --- a/packages/html/src/define/video/minimal-skin.ts +++ b/packages/html/src/define/video/minimal-skin.ts @@ -77,13 +77,17 @@ function getTemplateHTML() { -
-
- +
+
+
- - ${renderIcon('spinner', { class: 'media-preview__spinner media-icon' })} + + ${renderIcon('spinner', { class: 'media-thumbnail__spinner media-icon' })}
+ + + +
diff --git a/packages/html/src/define/video/skin.tailwind.ts b/packages/html/src/define/video/skin.tailwind.ts index 076eef90..03dc4459 100644 --- a/packages/html/src/define/video/skin.tailwind.ts +++ b/packages/html/src/define/video/skin.tailwind.ts @@ -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() { -
- - - ${renderIcon('spinner', { class: cn(icon, preview.spinner) })} +
+ + + ${renderIcon('spinner', { class: cn(icon, thumbnail.spinner) })}
+ + +
diff --git a/packages/html/src/define/video/skin.ts b/packages/html/src/define/video/skin.ts index 2e53ffad..75fc5709 100644 --- a/packages/html/src/define/video/skin.ts +++ b/packages/html/src/define/video/skin.ts @@ -74,11 +74,15 @@ function getTemplateHTML() { -
- - - ${renderIcon('spinner', { class: 'media-preview__spinner media-icon' })} +
+ + + ${renderIcon('spinner', { class: 'media-thumbnail__spinner media-icon' })}
+ + + +
diff --git a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx index 4757b1d4..0f558209 100644 --- a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx @@ -187,7 +187,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod } /> - Seek backward {SEEK_TIME} seconds + @@ -201,7 +201,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod } /> - Seek forward {SEEK_TIME} seconds +
@@ -218,6 +218,9 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod } /> } /> + + +
diff --git a/packages/react/src/presets/audio/minimal-skin.tsx b/packages/react/src/presets/audio/minimal-skin.tsx index f2c35b29..d716e5f4 100644 --- a/packages/react/src/presets/audio/minimal-skin.tsx +++ b/packages/react/src/presets/audio/minimal-skin.tsx @@ -134,7 +134,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode { } /> - Seek backward {SEEK_TIME} seconds + @@ -148,7 +148,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode { } /> - Seek forward {SEEK_TIME} seconds + @@ -165,6 +165,9 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode { + + + diff --git a/packages/react/src/presets/audio/skin.tailwind.tsx b/packages/react/src/presets/audio/skin.tailwind.tsx index 2da1c678..3708d560 100644 --- a/packages/react/src/presets/audio/skin.tailwind.tsx +++ b/packages/react/src/presets/audio/skin.tailwind.tsx @@ -189,7 +189,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode { } /> - Seek backward {SEEK_TIME} seconds + @@ -203,7 +203,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode { } /> - Seek forward {SEEK_TIME} seconds + @@ -215,6 +215,9 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode { } /> } /> + + + diff --git a/packages/react/src/presets/audio/skin.tsx b/packages/react/src/presets/audio/skin.tsx index 97db7275..a650f203 100644 --- a/packages/react/src/presets/audio/skin.tsx +++ b/packages/react/src/presets/audio/skin.tsx @@ -134,7 +134,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode { } /> - Seek backward {SEEK_TIME} seconds + @@ -148,7 +148,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode { } /> - Seek forward {SEEK_TIME} seconds + @@ -160,6 +160,9 @@ export function AudioSkin(props: AudioSkinProps): ReactNode { + + + diff --git a/packages/react/src/presets/live-audio/minimal-skin.tailwind.tsx b/packages/react/src/presets/live-audio/minimal-skin.tailwind.tsx index ce253c32..9223280b 100644 --- a/packages/react/src/presets/live-audio/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/live-audio/minimal-skin.tailwind.tsx @@ -129,7 +129,7 @@ export function MinimalLiveAudioSkinTailwind(props: MinimalLiveAudioSkinProps): } /> - + diff --git a/packages/react/src/presets/live-audio/skin.tailwind.tsx b/packages/react/src/presets/live-audio/skin.tailwind.tsx index eb17d2b2..58706680 100644 --- a/packages/react/src/presets/live-audio/skin.tailwind.tsx +++ b/packages/react/src/presets/live-audio/skin.tailwind.tsx @@ -129,7 +129,7 @@ export function LiveAudioSkinTailwind(props: LiveAudioSkinProps): ReactNode { } /> - + diff --git a/packages/react/src/presets/live-video/minimal-skin.tailwind.tsx b/packages/react/src/presets/live-video/minimal-skin.tailwind.tsx index 7375cfe5..c76db477 100644 --- a/packages/react/src/presets/live-video/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/live-video/minimal-skin.tailwind.tsx @@ -187,7 +187,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps): } /> - + @@ -207,7 +207,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps): } /> - + @@ -219,7 +219,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps): } /> - + @@ -243,7 +243,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps): } /> - + @@ -255,7 +255,7 @@ export function MinimalLiveVideoSkinTailwind(props: MinimalLiveVideoSkinProps): } /> - + diff --git a/packages/react/src/presets/live-video/skin.tailwind.tsx b/packages/react/src/presets/live-video/skin.tailwind.tsx index 08076dee..5dbdd7de 100644 --- a/packages/react/src/presets/live-video/skin.tailwind.tsx +++ b/packages/react/src/presets/live-video/skin.tailwind.tsx @@ -189,7 +189,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode { } /> - + @@ -209,7 +209,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode { } /> - + @@ -221,7 +221,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode { } /> - + @@ -245,7 +245,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode { } /> - + @@ -257,7 +257,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode { } /> - + diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx index 9c1be2dc..88c532f9 100644 --- a/packages/react/src/presets/video/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx @@ -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 } /> } /> -
-
- +
+
+
- - + +
+ + +
diff --git a/packages/react/src/presets/video/minimal-skin.tsx b/packages/react/src/presets/video/minimal-skin.tsx index 4995d502..ffc8d6e3 100644 --- a/packages/react/src/presets/video/minimal-skin.tsx +++ b/packages/react/src/presets/video/minimal-skin.tsx @@ -207,13 +207,17 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { -
-
- +
+
+
- - + +
+ + + +
diff --git a/packages/react/src/presets/video/skin.tailwind.tsx b/packages/react/src/presets/video/skin.tailwind.tsx index bf39ede1..9b0382d0 100644 --- a/packages/react/src/presets/video/skin.tailwind.tsx +++ b/packages/react/src/presets/video/skin.tailwind.tsx @@ -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 { } /> } /> -
- - - +
+ + +
+ + +
diff --git a/packages/react/src/presets/video/skin.tsx b/packages/react/src/presets/video/skin.tsx index 6d129612..ae782a52 100644 --- a/packages/react/src/presets/video/skin.tsx +++ b/packages/react/src/presets/video/skin.tsx @@ -204,11 +204,15 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { -
- - - +
+ + +
+ + + +
diff --git a/packages/skins/src/default/css/components/slider.css b/packages/skins/src/default/css/components/slider.css index f2e18055..905017fa 100644 --- a/packages/skins/src/default/css/components/slider.css +++ b/packages/skins/src/default/css/components/slider.css @@ -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 { diff --git a/packages/skins/src/default/css/components/preview.css b/packages/skins/src/default/css/components/thumbnail.css similarity index 66% rename from packages/skins/src/default/css/components/preview.css rename to packages/skins/src/default/css/components/thumbnail.css index 25b13792..53596bed 100644 --- a/packages/skins/src/default/css/components/preview.css +++ b/packages/skins/src/default/css/components/thumbnail.css @@ -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; } } diff --git a/packages/skins/src/default/css/video.css b/packages/skins/src/default/css/video.css index 49121569..c3d006d4 100644 --- a/packages/skins/src/default/css/video.css +++ b/packages/skins/src/default/css/video.css @@ -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; +} diff --git a/packages/skins/src/default/tailwind/components/slider.ts b/packages/skins/src/default/tailwind/components/slider.ts index a6c64c36..15901ef5 100644 --- a/packages/skins/src/default/tailwind/components/slider.ts +++ b/packages/skins/src/default/tailwind/components/slider.ts @@ -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' + ), }; diff --git a/packages/skins/src/default/tailwind/components/preview.ts b/packages/skins/src/default/tailwind/components/thumbnail.ts similarity index 65% rename from packages/skins/src/default/tailwind/components/preview.ts rename to packages/skins/src/default/tailwind/components/thumbnail.ts index 0c892334..822a2270 100644 --- a/packages/skins/src/default/tailwind/components/preview.ts +++ b/packages/skins/src/default/tailwind/components/thumbnail.ts @@ -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' ), }; diff --git a/packages/skins/src/default/tailwind/video.tailwind.ts b/packages/skins/src/default/tailwind/video.tailwind.ts index ac4296b6..b9713a0a 100644 --- a/packages/skins/src/default/tailwind/video.tailwind.ts +++ b/packages/skins/src/default/tailwind/video.tailwind.ts @@ -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)'), }; /* ========================================================================== diff --git a/packages/skins/src/minimal/css/audio.css b/packages/skins/src/minimal/css/audio.css index 9c9379d2..e76908fe 100644 --- a/packages/skins/src/minimal/css/audio.css +++ b/packages/skins/src/minimal/css/audio.css @@ -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; +} diff --git a/packages/skins/src/minimal/css/components/slider.css b/packages/skins/src/minimal/css/components/slider.css index 4fc03bc3..a8639a52 100644 --- a/packages/skins/src/minimal/css/components/slider.css +++ b/packages/skins/src/minimal/css/components/slider.css @@ -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 { diff --git a/packages/skins/src/minimal/css/components/preview.css b/packages/skins/src/minimal/css/components/thumbnail.css similarity index 60% rename from packages/skins/src/minimal/css/components/preview.css rename to packages/skins/src/minimal/css/components/thumbnail.css index f4652f44..d53fbb14 100644 --- a/packages/skins/src/minimal/css/components/preview.css +++ b/packages/skins/src/minimal/css/components/thumbnail.css @@ -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; } } diff --git a/packages/skins/src/minimal/css/video.css b/packages/skins/src/minimal/css/video.css index 4796798d..bcae177d 100644 --- a/packages/skins/src/minimal/css/video.css +++ b/packages/skins/src/minimal/css/video.css @@ -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; +} diff --git a/packages/skins/src/minimal/tailwind/audio.tailwind.ts b/packages/skins/src/minimal/tailwind/audio.tailwind.ts index 75c9dbdf..e197c8f0 100644 --- a/packages/skins/src/minimal/tailwind/audio.tailwind.ts +++ b/packages/skins/src/minimal/tailwind/audio.tailwind.ts @@ -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'; diff --git a/packages/skins/src/minimal/tailwind/components/preview.ts b/packages/skins/src/minimal/tailwind/components/preview.ts deleted file mode 100644 index 2689c3a4..00000000 --- a/packages/skins/src/minimal/tailwind/components/preview.ts +++ /dev/null @@ -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' - ), -}; diff --git a/packages/skins/src/minimal/tailwind/components/slider.ts b/packages/skins/src/minimal/tailwind/components/slider.ts index 0a50dcc6..12ba821a 100644 --- a/packages/skins/src/minimal/tailwind/components/slider.ts +++ b/packages/skins/src/minimal/tailwind/components/slider.ts @@ -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' + ), }; diff --git a/packages/skins/src/minimal/tailwind/components/thumbnail.ts b/packages/skins/src/minimal/tailwind/components/thumbnail.ts new file mode 100644 index 00000000..6861cf23 --- /dev/null +++ b/packages/skins/src/minimal/tailwind/components/thumbnail.ts @@ -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' + ), +}; diff --git a/packages/skins/src/minimal/tailwind/video.tailwind.ts b/packages/skins/src/minimal/tailwind/video.tailwind.ts index 64cffd64..00b7fd10 100644 --- a/packages/skins/src/minimal/tailwind/video.tailwind.ts +++ b/packages/skins/src/minimal/tailwind/video.tailwind.ts @@ -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)'), }; /* ==========================================================================