diff --git a/packages/core/src/dom/ui/slider.ts b/packages/core/src/dom/ui/slider.ts
index 42733b5a..9fc0e95e 100644
--- a/packages/core/src/dom/ui/slider.ts
+++ b/packages/core/src/dom/ui/slider.ts
@@ -155,7 +155,11 @@ export function createSlider(options: SliderOptions): SliderApi {
options.onValueChange?.(percent);
// Focus the thumb for keyboard follow-up and screen reader tracking.
- options.getThumbElement?.()?.focus();
+ options.getThumbElement?.()?.focus({
+ preventScroll: true,
+ // @ts-expect-error -- focusVisible is not yet in TypeScript's lib.dom typings.
+ focusVisible: false,
+ });
},
onPointerMove(event) {
diff --git a/packages/core/src/dom/ui/tests/thumbnail.test.ts b/packages/core/src/dom/ui/tests/thumbnail.test.ts
index 58386c00..49007072 100644
--- a/packages/core/src/dom/ui/tests/thumbnail.test.ts
+++ b/packages/core/src/dom/ui/tests/thumbnail.test.ts
@@ -308,6 +308,104 @@ describe('createThumbnail', () => {
handle.destroy();
});
+
+ it('React lifecycle: updateSrc before img available, then connect after mount', () => {
+ let img: HTMLImageElement | null = null;
+ const onStateChange = vi.fn();
+
+ const handle = createThumbnail(
+ createOptions({
+ getImg: () => img,
+ onStateChange,
+ })
+ );
+
+ // 1. Render phase: updateSrc called but img ref is null (not mounted yet).
+ handle.updateSrc('sprite.jpg');
+ expect(handle.loading).toBe(true);
+
+ // 2. Commit phase: img becomes available (React sets ref) but image is still loading.
+ img = document.createElement('img');
+ // Simulate a loading image: in a real browser, an img with src set is !complete
+ // while the network request is in flight.
+ Object.defineProperty(img, 'complete', { value: false, configurable: true });
+
+ // 3. useEffect: connect() binds events and checks img.complete.
+ handle.connect();
+
+ // Image is not complete, so loading should remain true.
+ expect(handle.loading).toBe(true);
+
+ // 4. Image loads — event listener should catch it.
+ Object.defineProperty(img, 'naturalWidth', { value: 2560, configurable: true });
+ Object.defineProperty(img, 'naturalHeight', { value: 1600, configurable: true });
+ img.dispatchEvent(new Event('load'));
+
+ expect(handle.loading).toBe(false);
+ expect(handle.naturalWidth).toBe(2560);
+ expect(onStateChange).toHaveBeenCalled();
+
+ handle.destroy();
+ });
+
+ it('React lifecycle: handles already-loaded img when ref was null during updateSrc', () => {
+ let img: HTMLImageElement | null = null;
+ const onStateChange = vi.fn();
+
+ const handle = createThumbnail(
+ createOptions({
+ getImg: () => img,
+ onStateChange,
+ })
+ );
+
+ // 1. Render phase: updateSrc called but img ref is null.
+ handle.updateSrc('sprite.jpg');
+ expect(handle.loading).toBe(true);
+
+ // 2. Commit phase: img becomes available and is already cached/complete.
+ img = createMockImg();
+ Object.defineProperty(img, 'complete', { value: true, configurable: true });
+
+ // 3. useEffect: connect() should detect the already-loaded image.
+ handle.connect();
+
+ expect(handle.loading).toBe(false);
+ expect(handle.naturalWidth).toBe(2560);
+ expect(onStateChange).toHaveBeenCalled();
+
+ handle.destroy();
+ });
+
+ it('React lifecycle: handles errored img when ref was null during updateSrc', () => {
+ let img: HTMLImageElement | null = null;
+ const onStateChange = vi.fn();
+
+ const handle = createThumbnail(
+ createOptions({
+ getImg: () => img,
+ onStateChange,
+ })
+ );
+
+ // 1. Render phase: updateSrc called but img ref is null.
+ handle.updateSrc('bad.jpg');
+ expect(handle.loading).toBe(true);
+
+ // 2. Commit phase: img becomes available but image errored (complete but no dimensions).
+ img = document.createElement('img');
+ Object.defineProperty(img, 'complete', { value: true, configurable: true });
+ // naturalWidth defaults to 0 in jsdom — simulates an errored image.
+
+ // 3. useEffect: connect() should detect the errored image.
+ handle.connect();
+
+ expect(handle.loading).toBe(false);
+ expect(handle.error).toBe(true);
+ expect(onStateChange).toHaveBeenCalled();
+
+ handle.destroy();
+ });
});
describe('destroy', () => {
diff --git a/packages/core/src/dom/ui/thumbnail.ts b/packages/core/src/dom/ui/thumbnail.ts
index abec5125..8651834a 100644
--- a/packages/core/src/dom/ui/thumbnail.ts
+++ b/packages/core/src/dom/ui/thumbnail.ts
@@ -108,15 +108,21 @@ export function createThumbnail(options: CreateThumbnailOptions): ThumbnailApi {
function connect(): void {
ensureBindings();
- // Handle the case where the img already loaded before listeners were bound
- // (e.g., cached image in React where mount happens before useEffect).
+ // Handle the case where the img already loaded or errored before listeners
+ // were bound (e.g., cached image in React where mount happens before useEffect).
const img = getImg();
- if (img?.complete && img.naturalWidth > 0 && lastSrc) {
- naturalWidth = img.naturalWidth;
- naturalHeight = img.naturalHeight;
- loading = false;
- error = false;
+ if (img?.complete && lastSrc) {
+ if (img.naturalWidth > 0) {
+ naturalWidth = img.naturalWidth;
+ naturalHeight = img.naturalHeight;
+ loading = false;
+ error = false;
+ } else {
+ loading = false;
+ error = true;
+ }
+
onStateChange();
}
}
diff --git a/packages/html/src/define/video/minimal-skin.tailwind.ts b/packages/html/src/define/video/minimal-skin.tailwind.ts
index e6c11fc1..3fcf6390 100644
--- a/packages/html/src/define/video/minimal-skin.tailwind.ts
+++ b/packages/html/src/define/video/minimal-skin.tailwind.ts
@@ -11,6 +11,7 @@ import {
iconState,
overlay,
popup,
+ preview,
root,
seek,
slider,
@@ -100,6 +101,14 @@ function getTemplateHTML() {
+
+
+
+
+
+
+ ${renderIcon('spinner', { class: cn(icon, preview.spinner) })}
+
diff --git a/packages/html/src/define/video/minimal-skin.ts b/packages/html/src/define/video/minimal-skin.ts
index 724cb4ce..41a06f6f 100644
--- a/packages/html/src/define/video/minimal-skin.ts
+++ b/packages/html/src/define/video/minimal-skin.ts
@@ -79,6 +79,14 @@ function getTemplateHTML() {
+
+
diff --git a/packages/html/src/define/video/skin.tailwind.ts b/packages/html/src/define/video/skin.tailwind.ts
index b7c4c67b..13925279 100644
--- a/packages/html/src/define/video/skin.tailwind.ts
+++ b/packages/html/src/define/video/skin.tailwind.ts
@@ -11,6 +11,7 @@ import {
overlay,
playbackRate,
popup,
+ preview,
root,
seek,
slider,
@@ -94,6 +95,12 @@ function getTemplateHTML() {
+
+
+
+
+ ${renderIcon('spinner', { class: cn(icon, preview.spinner) })}
+
diff --git a/packages/html/src/define/video/skin.ts b/packages/html/src/define/video/skin.ts
index 2eb78e30..3aeef13f 100644
--- a/packages/html/src/define/video/skin.ts
+++ b/packages/html/src/define/video/skin.ts
@@ -75,6 +75,12 @@ function getTemplateHTML() {
+
+
+
+
+ ${renderIcon('spinner', { class: 'media-preview__spinner media-icon' })}
+
diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx
index 8e4ca266..b06a2307 100644
--- a/packages/react/src/presets/video/minimal-skin.tailwind.tsx
+++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx
@@ -26,6 +26,7 @@ import {
iconState,
overlay,
popup,
+ preview,
root,
seek,
slider,
@@ -44,6 +45,7 @@ import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { SeekButton } from '@/ui/seek-button';
+import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
@@ -233,6 +235,13 @@ 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 a1225614..ccbe4f00 100644
--- a/packages/react/src/presets/video/minimal-skin.tsx
+++ b/packages/react/src/presets/video/minimal-skin.tsx
@@ -26,6 +26,7 @@ import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { SeekButton } from '@/ui/seek-button';
+import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
@@ -164,6 +165,14 @@ 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 822fb885..c1d7404f 100644
--- a/packages/react/src/presets/video/skin.tailwind.tsx
+++ b/packages/react/src/presets/video/skin.tailwind.tsx
@@ -25,6 +25,7 @@ import {
overlay,
playbackRate,
popup,
+ preview,
root,
seek,
slider,
@@ -43,6 +44,7 @@ import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { SeekButton } from '@/ui/seek-button';
+import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
@@ -228,6 +230,11 @@ 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 462044b8..8e6fc9d6 100644
--- a/packages/react/src/presets/video/skin.tsx
+++ b/packages/react/src/presets/video/skin.tsx
@@ -26,6 +26,7 @@ import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { SeekButton } from '@/ui/seek-button';
+import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
@@ -159,6 +160,12 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
+
+
+
+
+
+
diff --git a/packages/react/src/ui/thumbnail/index.ts b/packages/react/src/ui/thumbnail/index.ts
new file mode 100644
index 00000000..d4ab7a50
--- /dev/null
+++ b/packages/react/src/ui/thumbnail/index.ts
@@ -0,0 +1 @@
+export * from './thumbnail';
diff --git a/packages/sandbox/app/shared/html/mux-storyboard.ts b/packages/sandbox/app/shared/html/mux-storyboard.ts
new file mode 100644
index 00000000..242dcc4a
--- /dev/null
+++ b/packages/sandbox/app/shared/html/mux-storyboard.ts
@@ -0,0 +1,9 @@
+import { getMuxAssetId } from '../mux';
+import type { SourceId } from '../sources';
+
+export function renderMuxStoryboard(source: SourceId): string {
+ const id = getMuxAssetId(source);
+ return id
+ ? ``
+ : '';
+}
diff --git a/packages/sandbox/app/shared/mux.ts b/packages/sandbox/app/shared/mux.ts
new file mode 100644
index 00000000..e4800134
--- /dev/null
+++ b/packages/sandbox/app/shared/mux.ts
@@ -0,0 +1,10 @@
+import { SOURCES, type SourceId } from './sources';
+
+export function getMuxAssetId(source: SourceId): string | undefined {
+ return SOURCES[source].url.match(/stream\.mux\.com\/([a-zA-Z0-9]+)/)?.[1];
+}
+
+export function getMuxPosterSrc(source: SourceId): string | undefined {
+ const id = getMuxAssetId(source);
+ return id ? `https://image.mux.com/${id}/thumbnail.jpg` : undefined;
+}
diff --git a/packages/sandbox/app/shared/react/mux-poster.tsx b/packages/sandbox/app/shared/react/mux-poster.tsx
new file mode 100644
index 00000000..ce7d2f02
--- /dev/null
+++ b/packages/sandbox/app/shared/react/mux-poster.tsx
@@ -0,0 +1,14 @@
+import { Poster } from '@videojs/react';
+
+import { getMuxAssetId } from '../mux';
+import type { SourceId } from '../sources';
+
+type MuxPosterProps = {
+ source: SourceId;
+};
+
+export function MuxPoster({ source }: MuxPosterProps) {
+ const id = getMuxAssetId(source);
+ if (!id) return null;
+ return ;
+}
diff --git a/packages/sandbox/app/shared/react/mux-storyboard.tsx b/packages/sandbox/app/shared/react/mux-storyboard.tsx
new file mode 100644
index 00000000..a075c0b6
--- /dev/null
+++ b/packages/sandbox/app/shared/react/mux-storyboard.tsx
@@ -0,0 +1,12 @@
+import { getMuxAssetId } from '../mux';
+import type { SourceId } from '../sources';
+
+type MuxStoryboardProps = {
+ source: SourceId;
+};
+
+export function MuxStoryboard({ source }: MuxStoryboardProps) {
+ const id = getMuxAssetId(source);
+ if (!id) return null;
+ return ;
+}
diff --git a/packages/sandbox/templates/html-hls-video/main.ts b/packages/sandbox/templates/html-hls-video/main.ts
index 1e2a7496..44c09b68 100644
--- a/packages/sandbox/templates/html-hls-video/main.ts
+++ b/packages/sandbox/templates/html-hls-video/main.ts
@@ -3,6 +3,7 @@ import '@videojs/html/video/player';
import '@videojs/html/media/hls-video';
import '@videojs/html/video/skin';
import '@videojs/html/video/minimal-skin';
+import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
@@ -23,7 +24,9 @@ function render() {
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
+
+ ${renderMuxStoryboard(currentSource)}
+
${tag}>
`;
diff --git a/packages/sandbox/templates/html-simple-hls-video/main.ts b/packages/sandbox/templates/html-simple-hls-video/main.ts
index bf8f676d..cb003f2a 100644
--- a/packages/sandbox/templates/html-simple-hls-video/main.ts
+++ b/packages/sandbox/templates/html-simple-hls-video/main.ts
@@ -3,6 +3,7 @@ import '@videojs/html/video/player';
import '@videojs/html/media/simple-hls-video';
import '@videojs/html/video/skin';
import '@videojs/html/video/minimal-skin';
+import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
@@ -23,7 +24,9 @@ function render() {
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
+
+ ${renderMuxStoryboard(currentSource)}
+
${tag}>
`;
diff --git a/packages/sandbox/templates/html-video-tailwind/main.ts b/packages/sandbox/templates/html-video-tailwind/main.ts
index 20a47644..edd47596 100644
--- a/packages/sandbox/templates/html-video-tailwind/main.ts
+++ b/packages/sandbox/templates/html-video-tailwind/main.ts
@@ -1,7 +1,9 @@
import '@app/styles.css';
+import '@videojs/html/ui/poster';
import '@videojs/html/video/player';
import '@videojs/html/video/skin.tailwind';
import '@videojs/html/video/minimal-skin.tailwind';
+import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
import { TAILWIND_SKIN_TAGS } from '@app/shared/html/skin-tags';
import { setupVideoTailwind } from '@app/shared/html/tailwind-setup';
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
@@ -22,7 +24,9 @@ function render() {
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
+
${tag}>
`;
diff --git a/packages/sandbox/templates/html-video/main.ts b/packages/sandbox/templates/html-video/main.ts
index 883b911f..86480193 100644
--- a/packages/sandbox/templates/html-video/main.ts
+++ b/packages/sandbox/templates/html-video/main.ts
@@ -2,6 +2,8 @@ import '@app/styles.css';
import '@videojs/html/video/player';
import '@videojs/html/video/skin';
import '@videojs/html/video/minimal-skin';
+import '@videojs/html/ui/poster';
+import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
@@ -22,7 +24,9 @@ function render() {
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
+
${tag}>
`;
diff --git a/packages/sandbox/templates/react-hls-video/main.tsx b/packages/sandbox/templates/react-hls-video/main.tsx
index 2fa205e5..572973aa 100644
--- a/packages/sandbox/templates/react-hls-video/main.tsx
+++ b/packages/sandbox/templates/react-hls-video/main.tsx
@@ -1,6 +1,8 @@
import '@app/styles.css';
import '@videojs/react/video/skin.css';
import '@videojs/react/video/minimal-skin.css';
+import { MuxPoster } from '@app/shared/react/mux-poster';
+import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
@@ -16,7 +18,10 @@ function App() {
return (
-
+
+
+
+
);
diff --git a/packages/sandbox/templates/react-simple-hls-video/main.tsx b/packages/sandbox/templates/react-simple-hls-video/main.tsx
index b1bcc103..33cbfe76 100644
--- a/packages/sandbox/templates/react-simple-hls-video/main.tsx
+++ b/packages/sandbox/templates/react-simple-hls-video/main.tsx
@@ -1,6 +1,8 @@
import '@app/styles.css';
import '@videojs/react/video/skin.css';
import '@videojs/react/video/minimal-skin.css';
+import { MuxPoster } from '@app/shared/react/mux-poster';
+import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
@@ -16,7 +18,10 @@ function App() {
return (
-
+
+
+
+
);
diff --git a/packages/sandbox/templates/react-video-tailwind/main.tsx b/packages/sandbox/templates/react-video-tailwind/main.tsx
index 7d88e38c..ded1f56d 100644
--- a/packages/sandbox/templates/react-video-tailwind/main.tsx
+++ b/packages/sandbox/templates/react-video-tailwind/main.tsx
@@ -1,4 +1,6 @@
import '@app/styles.css';
+import { MuxPoster } from '@app/shared/react/mux-poster';
+import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
@@ -14,7 +16,10 @@ function App() {
return (
-
+
+
);
diff --git a/packages/sandbox/templates/react-video/main.tsx b/packages/sandbox/templates/react-video/main.tsx
index 82920010..209396b3 100644
--- a/packages/sandbox/templates/react-video/main.tsx
+++ b/packages/sandbox/templates/react-video/main.tsx
@@ -1,6 +1,8 @@
import '@app/styles.css';
import '@videojs/react/video/skin.css';
import '@videojs/react/video/minimal-skin.css';
+import { MuxPoster } from '@app/shared/react/mux-poster';
+import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
@@ -16,7 +18,10 @@ function App() {
return (
-
+
+
);
diff --git a/packages/skins/src/default/css/components/buttons.css b/packages/skins/src/default/css/components/buttons.css
index 92df1abd..6784e32d 100644
--- a/packages/skins/src/default/css/components/buttons.css
+++ b/packages/skins/src/default/css/components/buttons.css
@@ -22,6 +22,7 @@
transition-timing-function: ease-out;
cursor: pointer;
user-select: none;
+ touch-action: manipulation;
&:focus-visible {
outline-color: oklch(62.3% 0.214 259.815);
diff --git a/packages/skins/src/default/css/components/media.css b/packages/skins/src/default/css/components/media.css
index a7d33e67..98d0ec8d 100644
--- a/packages/skins/src/default/css/components/media.css
+++ b/packages/skins/src/default/css/components/media.css
@@ -24,10 +24,10 @@
inset: 0;
width: 100%;
height: 100%;
- border-radius: inherit;
object-fit: cover;
transition: opacity 0.25s;
pointer-events: none;
+ border-radius: inherit;
&:not([data-visible]) {
opacity: 0;
diff --git a/packages/skins/src/default/css/components/popup.css b/packages/skins/src/default/css/components/popup.css
index ab28d3a3..15f5fce9 100644
--- a/packages/skins/src/default/css/components/popup.css
+++ b/packages/skins/src/default/css/components/popup.css
@@ -9,12 +9,12 @@
color: inherit;
overflow: visible;
transition-property: transform, scale, opacity, filter;
- transition-duration: 200ms;
+ transition-duration: 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
- transform: scale(0);
+ transform: scale(0.5);
filter: blur(8px);
}
@@ -49,5 +49,5 @@
border-radius: calc(infinity * 1px);
font-size: 0.75rem;
white-space: nowrap;
- --media-tooltip-side-offset: 0.5rem;
+ --media-tooltip-side-offset: 0.75rem;
}
diff --git a/packages/skins/src/default/css/components/preview.css b/packages/skins/src/default/css/components/preview.css
new file mode 100644
index 00000000..2e1cfbdd
--- /dev/null
+++ b/packages/skins/src/default/css/components/preview.css
@@ -0,0 +1,56 @@
+/* ==========================================================================
+ Media preview
+ ========================================================================== */
+.media-default-skin .media-preview {
+ background-color: oklch(0 0 0 / 0.9);
+ border-radius: 0.75rem;
+
+ & .media-preview__thumbnail {
+ display: block;
+ position: relative;
+ border-radius: inherit;
+ overflow: clip;
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklch(0 0 0 / 0.8), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
+ }
+ }
+
+ & .media-preview__timestamp {
+ position: absolute;
+ bottom: 0.5rem;
+ inset-inline: 0;
+ text-align: center;
+ font-variant-numeric: tabular-nums;
+ }
+
+ & .media-overlay {
+ opacity: 1;
+ }
+
+ & .media-preview__spinner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ }
+
+ & .media-preview__thumbnail,
+ & .media-preview__spinner {
+ transition: opacity 150ms ease-out;
+ }
+
+ &:has(.media-preview__thumbnail[data-loading]) {
+ & .media-preview__thumbnail {
+ opacity: 0;
+ }
+ & .media-preview__spinner {
+ opacity: 1;
+ }
+ }
+}
diff --git a/packages/skins/src/default/css/components/reset.css b/packages/skins/src/default/css/components/reset.css
index c7315d7d..99e1bebe 100644
--- a/packages/skins/src/default/css/components/reset.css
+++ b/packages/skins/src/default/css/components/reset.css
@@ -6,7 +6,6 @@
.media-default-skin *::before,
.media-default-skin *::after {
box-sizing: border-box;
- margin: 0;
}
.media-default-skin img,
.media-default-skin video,
diff --git a/packages/skins/src/default/css/components/slider.css b/packages/skins/src/default/css/components/slider.css
index 99a5be73..f767d126 100644
--- a/packages/skins/src/default/css/components/slider.css
+++ b/packages/skins/src/default/css/components/slider.css
@@ -10,6 +10,7 @@
flex: 1;
border-radius: calc(infinity * 1px);
outline: none;
+ cursor: pointer;
&[data-orientation="horizontal"] {
min-width: 5rem;
@@ -141,8 +142,3 @@
height: var(--media-slider-fill);
}
}
-
-/* Time display within slider */
-.media-default-skin .media-slider__time-display {
- font-variant-numeric: tabular-nums;
-}
diff --git a/packages/skins/src/default/css/components/surface.css b/packages/skins/src/default/css/components/surface.css
index 2773c329..1c8c1b0b 100644
--- a/packages/skins/src/default/css/components/surface.css
+++ b/packages/skins/src/default/css/components/surface.css
@@ -6,7 +6,7 @@
background-color: var(--media-surface-background-color);
backdrop-filter: var(--media-surface-backdrop-filter);
box-shadow:
- inset 0 0 0 1px var(--media-surface-inner-border-color),
+ 0 0 0 1px var(--media-surface-outer-border-color),
0 1px 3px 0 var(--media-surface-shadow-color),
0 1px 2px -1px var(--media-surface-shadow-color);
@@ -17,7 +17,7 @@
inset: 0;
z-index: 10;
border-radius: inherit;
- box-shadow: 0 0 0 1px var(--media-surface-outer-border-color);
+ box-shadow: inset 0 0 0 1px var(--media-surface-inner-border-color);
pointer-events: none;
}
diff --git a/packages/skins/src/default/css/video.css b/packages/skins/src/default/css/video.css
index 10f8f442..507e25e6 100644
--- a/packages/skins/src/default/css/video.css
+++ b/packages/skins/src/default/css/video.css
@@ -11,6 +11,7 @@
@import "./components/time.css";
@import "./components/buttons.css";
@import "./components/icons.css";
+@import "./components/preview.css";
@import "./components/slider.css";
@import "./components/popup.css";
@import "./components/captions.css";
@@ -99,3 +100,29 @@
background-color: oklch(1 0 0 / 0.2);
box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05);
}
+
+.media-default-skin .media-slider__preview {
+ position: absolute;
+ left: var(--media-slider-pointer);
+ bottom: calc(100% + 1.2rem);
+ translate: -50%;
+ opacity: 0;
+ scale: 0.8;
+ filter: blur(8px);
+ transition-property: scale, opacity, filter;
+ transition-duration: 150ms;
+ transform-origin: bottom;
+
+ & .media-preview__thumbnail {
+ max-width: 11rem;
+ }
+
+ &:has(.media-preview__thumbnail[data-loading]) {
+ max-height: 6rem;
+ }
+}
+.media-default-skin .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
+ opacity: 1;
+ scale: 1;
+ filter: blur(0);
+}
diff --git a/packages/skins/src/default/tailwind/components/button.ts b/packages/skins/src/default/tailwind/components/button.ts
index 84d86e6f..66c92273 100644
--- a/packages/skins/src/default/tailwind/components/button.ts
+++ b/packages/skins/src/default/tailwind/components/button.ts
@@ -2,7 +2,7 @@ import { cn } from '@videojs/utils/style';
export const button = {
base: cn(
- 'items-center justify-center shrink-0 border-none cursor-pointer select-none text-center',
+ 'items-center justify-center shrink-0 border-none cursor-pointer select-none text-center touch-manipulation',
'font-medium',
'outline-2 outline-transparent -outline-offset-2',
'transition-[background-color,color,outline-offset,scale] duration-150 ease-out',
diff --git a/packages/skins/src/default/tailwind/components/popup.ts b/packages/skins/src/default/tailwind/components/popup.ts
index 331d43b8..163d147d 100644
--- a/packages/skins/src/default/tailwind/components/popup.ts
+++ b/packages/skins/src/default/tailwind/components/popup.ts
@@ -4,9 +4,9 @@ const base = cn(
// Reset default popover styles
'm-0 border-0 text-inherit overflow-visible',
// Animation
- 'transition-[transform,scale,opacity,filter] duration-200',
- 'data-starting-style:opacity-0 data-starting-style:scale-0 data-starting-style:blur-sm',
- 'data-ending-style:opacity-0 data-ending-style:scale-0 data-ending-style:blur-sm',
+ 'transition-[transform,scale,opacity,filter] duration-150',
+ 'data-starting-style:opacity-0 data-starting-style:scale-50 data-starting-style:blur-sm',
+ 'data-ending-style:opacity-0 data-ending-style:scale-50 data-ending-style:blur-sm',
'data-instant:duration-0',
// Ensure we animate from the correct origin based on the side the popover is on
'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left'
@@ -17,7 +17,7 @@ export const popup = {
tooltip: cn(
base,
'py-1 px-2.5 rounded-full text-[0.75rem] whitespace-nowrap',
- '[--media-tooltip-side-offset:0.5rem]'
+ '[--media-tooltip-side-offset:0.75rem]'
),
volume: 'py-2.5 px-1 rounded-full',
};
diff --git a/packages/skins/src/default/tailwind/components/preview.ts b/packages/skins/src/default/tailwind/components/preview.ts
new file mode 100644
index 00000000..58ed996e
--- /dev/null
+++ b/packages/skins/src/default/tailwind/components/preview.ts
@@ -0,0 +1,18 @@
+import { cn } from '@videojs/utils/style';
+
+export const preview = {
+ root: cn('group/preview pointer-events-none bg-black/90 rounded-xl'),
+ thumbnail: cn(
+ 'block relative overflow-clip rounded-[inherit]',
+ 'transition-opacity duration-150 ease-out',
+ 'after:absolute after:inset-0 after:rounded-[inherit]',
+ 'after:bg-linear-to-t after:from-black/80 after:via-black/30 after:to-black/0',
+ 'data-loading:opacity-0'
+ ),
+ timestamp: 'absolute bottom-2 inset-x-0 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-has-[[role=img][data-loading]]/preview:opacity-100'
+ ),
+};
diff --git a/packages/skins/src/default/tailwind/components/root.ts b/packages/skins/src/default/tailwind/components/root.ts
index 1d8bc58c..ffcaa973 100644
--- a/packages/skins/src/default/tailwind/components/root.ts
+++ b/packages/skins/src/default/tailwind/components/root.ts
@@ -7,7 +7,7 @@ export const root = cn(
'rounded-(--media-border-radius,2rem)',
'font-[Inter_Variable,Inter,ui-sans-serif,system-ui,sans-serif] text-[0.8125rem] leading-normal subpixel-antialiased',
// Resets
- '**:box-border **:m-0',
+ '**:box-border',
'[&_button]:font-[inherit]',
'motion-safe:[interpolate-size:allow-keywords]'
);
diff --git a/packages/skins/src/default/tailwind/components/slider.ts b/packages/skins/src/default/tailwind/components/slider.ts
index d17623f2..9fc72188 100644
--- a/packages/skins/src/default/tailwind/components/slider.ts
+++ b/packages/skins/src/default/tailwind/components/slider.ts
@@ -2,7 +2,7 @@ import { cn } from '@videojs/utils/style';
export const slider = {
root: cn(
- 'group/slider relative flex flex-1 items-center justify-center rounded-full outline-none',
+ 'group/slider relative flex flex-1 items-center justify-center rounded-full outline-none cursor-pointer',
// Horizontal
'data-[orientation=horizontal]:min-w-20 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:h-5',
// Vertical
diff --git a/packages/skins/src/default/tailwind/components/surface.ts b/packages/skins/src/default/tailwind/components/surface.ts
index 3289d1fd..9639ef35 100644
--- a/packages/skins/src/default/tailwind/components/surface.ts
+++ b/packages/skins/src/default/tailwind/components/surface.ts
@@ -2,7 +2,7 @@ import { cn } from '@videojs/utils/style';
export const surface = cn(
// Border and shadow
- 'ring ring-inset shadow-sm',
+ 'ring shadow-sm',
// Border to enhance contrast on lighter videos
- 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:pointer-events-none after:z-10'
+ 'after:absolute after:inset-0 after:ring after:ring-inset after:rounded-[inherit] after:pointer-events-none after:z-10'
);
diff --git a/packages/skins/src/default/tailwind/video.tailwind.ts b/packages/skins/src/default/tailwind/video.tailwind.ts
index 7d3ab13c..6cbfad1b 100644
--- a/packages/skins/src/default/tailwind/video.tailwind.ts
+++ b/packages/skins/src/default/tailwind/video.tailwind.ts
@@ -3,6 +3,7 @@ import { bufferingIndicator as baseBufferingIndicator } from './components/buffe
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 { surface as baseSurface } from './components/surface';
@@ -66,9 +67,9 @@ export const surface = cn(
'bg-white/10',
'backdrop-saturate-150 backdrop-blur-lg',
// Border and shadow
- 'ring-white/5 shadow-black/10',
+ 'ring-black/15 shadow-black/10',
// Border to enhance contrast on lighter videos
- 'after:ring-black/15',
+ 'after:ring-white/5',
// Reduced transparency for users with preference
'[@media(prefers-reduced-transparency:reduce)]:bg-black/70',
// High contrast mode
@@ -101,6 +102,25 @@ export const controls = cn(
'motion-reduce:not-data-visible:scale-100'
);
+/* ==========================================================================
+ Preview (with video surface)
+ ========================================================================== */
+
+export const preview = {
+ ...basePreview,
+ root: cn(
+ 'absolute left-(--media-slider-pointer) 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
+ ),
+ thumbnail: cn(basePreview.thumbnail, 'max-w-44'),
+};
+
/* ==========================================================================
Sliders
========================================================================== */
diff --git a/packages/skins/src/minimal/css/components/buttons.css b/packages/skins/src/minimal/css/components/buttons.css
index 9ef1da14..378a1078 100644
--- a/packages/skins/src/minimal/css/components/buttons.css
+++ b/packages/skins/src/minimal/css/components/buttons.css
@@ -37,6 +37,7 @@
transition-timing-function: ease-out;
cursor: pointer;
user-select: none;
+ touch-action: manipulation;
&:focus-visible {
outline-color: currentColor;
diff --git a/packages/skins/src/minimal/css/components/popup.css b/packages/skins/src/minimal/css/components/popup.css
index cdafc961..09a70ef7 100644
--- a/packages/skins/src/minimal/css/components/popup.css
+++ b/packages/skins/src/minimal/css/components/popup.css
@@ -9,12 +9,12 @@
color: inherit;
overflow: visible;
transition-property: transform, scale, opacity, filter;
- transition-duration: 200ms;
+ transition-duration: 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
- transform: scale(0);
+ transform: scale(0.5);
filter: blur(8px);
}
@@ -46,7 +46,7 @@
0 2px 4px -2px oklch(0 0 0 / 0.1);
font-size: 0.75rem;
white-space: nowrap;
- --media-tooltip-side-offset: 0.5rem;
+ --media-tooltip-side-offset: 0.75rem;
@media (prefers-reduced-transparency: reduce) {
background-color: oklch(0 0 0 / 0.7);
diff --git a/packages/skins/src/minimal/css/components/preview.css b/packages/skins/src/minimal/css/components/preview.css
new file mode 100644
index 00000000..ac1618f8
--- /dev/null
+++ b/packages/skins/src/minimal/css/components/preview.css
@@ -0,0 +1,47 @@
+/* ==========================================================================
+ Media preview
+ ========================================================================== */
+.media-minimal-skin .media-preview {
+ & .media-preview__thumbnail-wrapper {
+ position: relative;
+ border-radius: 0.5rem;
+ background-color: oklch(0 0 0 / 0.9);
+ }
+ & .media-preview__thumbnail {
+ display: block;
+ border-radius: inherit;
+ }
+
+ & .media-preview__timestamp {
+ display: block;
+ font-variant-numeric: tabular-nums;
+ text-align: center;
+ margin-top: 0.5rem;
+ }
+
+ & .media-overlay {
+ opacity: 1;
+ }
+
+ & .media-preview__spinner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ }
+
+ & .media-preview__thumbnail,
+ & .media-preview__spinner {
+ transition: opacity 150ms ease-out;
+ }
+
+ &:has(.media-preview__thumbnail[data-loading]) {
+ & .media-preview__thumbnail {
+ opacity: 0;
+ }
+ & .media-preview__spinner {
+ opacity: 1;
+ }
+ }
+}
diff --git a/packages/skins/src/minimal/css/components/reset.css b/packages/skins/src/minimal/css/components/reset.css
index 3a59a221..95ae6b76 100644
--- a/packages/skins/src/minimal/css/components/reset.css
+++ b/packages/skins/src/minimal/css/components/reset.css
@@ -6,7 +6,6 @@
.media-minimal-skin *::before,
.media-minimal-skin *::after {
box-sizing: border-box;
- margin: 0;
}
.media-minimal-skin img,
.media-minimal-skin video,
diff --git a/packages/skins/src/minimal/css/components/slider.css b/packages/skins/src/minimal/css/components/slider.css
index 21f62bf9..294c76b4 100644
--- a/packages/skins/src/minimal/css/components/slider.css
+++ b/packages/skins/src/minimal/css/components/slider.css
@@ -10,6 +10,7 @@
flex: 1;
border-radius: calc(infinity * 1px);
outline: none;
+ cursor: pointer;
&[data-orientation="horizontal"] {
min-width: 5rem;
@@ -138,8 +139,3 @@
height: var(--media-slider-fill);
}
}
-
-/* Time display within slider */
-.media-minimal-skin .media-slider__time-display {
- font-variant-numeric: tabular-nums;
-}
diff --git a/packages/skins/src/minimal/css/video.css b/packages/skins/src/minimal/css/video.css
index 9e69fade..4016c537 100644
--- a/packages/skins/src/minimal/css/video.css
+++ b/packages/skins/src/minimal/css/video.css
@@ -10,6 +10,7 @@
@import "./components/time.css";
@import "./components/buttons.css";
@import "./components/icons.css";
+@import "./components/preview.css";
@import "./components/slider.css";
@import "./components/popup.css";
@import "./components/captions.css";
@@ -106,3 +107,48 @@
background: transparent;
padding: 0.25rem;
}
+
+/* ==========================================================================
+ Slider preview
+ ========================================================================== */
+
+.media-minimal-skin .media-slider__preview {
+ position: absolute;
+ left: var(--media-slider-pointer);
+ bottom: calc(100% + 0.5rem);
+ translate: -50%;
+ opacity: 0;
+ scale: 0.8;
+ filter: blur(8px);
+ transition-property: scale, opacity, filter;
+ transition-duration: 150ms;
+ transform-origin: bottom;
+
+ & .media-preview__thumbnail-wrapper {
+ position: relative;
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ box-shadow:
+ 0 0 0 1px oklch(0 0 0 / 0.05),
+ 0 1px 3px 0 oklch(0 0 0 / 0.2),
+ 0 1px 2px -1px oklch(0 0 0 / 0.2);
+ }
+ }
+
+ & .media-preview__thumbnail {
+ max-width: 11rem;
+ }
+
+ &:has(.media-preview__thumbnail[data-loading]) {
+ max-height: 6rem;
+ }
+}
+.media-minimal-skin .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
+ opacity: 1;
+ scale: 1;
+ filter: blur(0);
+}
diff --git a/packages/skins/src/minimal/tailwind/components/button.ts b/packages/skins/src/minimal/tailwind/components/button.ts
index 8d4bb58c..e3146c57 100644
--- a/packages/skins/src/minimal/tailwind/components/button.ts
+++ b/packages/skins/src/minimal/tailwind/components/button.ts
@@ -2,7 +2,7 @@ import { cn } from '@videojs/utils/style';
export const button = {
base: cn(
- 'items-center justify-center shrink-0 border-none cursor-pointer select-none text-center',
+ 'items-center justify-center shrink-0 border-none cursor-pointer select-none text-center touch-manipulation',
'outline-2 outline-transparent -outline-offset-2',
'font-medium text-shadow-inherit',
'transition-[background-color,color,outline-offset,scale] duration-150 ease-out',
diff --git a/packages/skins/src/minimal/tailwind/components/popup.ts b/packages/skins/src/minimal/tailwind/components/popup.ts
index 42865327..b94a2b6e 100644
--- a/packages/skins/src/minimal/tailwind/components/popup.ts
+++ b/packages/skins/src/minimal/tailwind/components/popup.ts
@@ -4,19 +4,19 @@ const base = cn(
// Reset default popover styles
'm-0 border-0 text-inherit overflow-visible',
// Animation
- 'transition-[transform,scale,opacity,filter] duration-200',
- 'data-starting-style:opacity-0 data-starting-style:scale-0 data-starting-style:blur-sm',
- 'data-ending-style:opacity-0 data-ending-style:scale-0 data-ending-style:blur-sm',
+ 'transition-[transform,scale,opacity,filter] duration-150',
+ 'data-starting-style:opacity-0 data-starting-style:scale-50 data-starting-style:blur-sm',
+ 'data-ending-style:opacity-0 data-ending-style:scale-50 data-ending-style:blur-sm',
'data-instant:duration-0',
// Ensure we animate from the correct origin based on the side the popover is on
'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left'
);
export const popup = {
+ base,
tooltip: cn(
base,
'px-2 py-1 rounded-sm shadow-md shadow-black/10 bg-white/10 backdrop-blur-lg backdrop-saturate-150 text-[0.75rem] whitespace-nowrap',
- '[--media-tooltip-side-offset:0.5rem]'
+ '[--media-tooltip-side-offset:0.75rem]'
),
- volume: cn(base, 'py-2.5 px-1 rounded-full'),
};
diff --git a/packages/skins/src/minimal/tailwind/components/preview.ts b/packages/skins/src/minimal/tailwind/components/preview.ts
new file mode 100644
index 00000000..60937d01
--- /dev/null
+++ b/packages/skins/src/minimal/tailwind/components/preview.ts
@@ -0,0 +1,13 @@
+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'),
+ timestamp: '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-has-[[role=img][data-loading]]/preview:opacity-100'
+ ),
+};
diff --git a/packages/skins/src/minimal/tailwind/components/root.ts b/packages/skins/src/minimal/tailwind/components/root.ts
index c37c379a..5a31ef0a 100644
--- a/packages/skins/src/minimal/tailwind/components/root.ts
+++ b/packages/skins/src/minimal/tailwind/components/root.ts
@@ -7,7 +7,7 @@ export const root = cn(
'rounded-(--media-border-radius,0.75rem)',
'font-[Inter_Variable,Inter,ui-sans-serif,system-ui,sans-serif] text-[0.8125rem] leading-normal subpixel-antialiased',
// Resets
- '**:box-border **:m-0',
+ '**:box-border',
'[&_button]:font-[inherit]',
'motion-safe:[interpolate-size:allow-keywords]'
);
diff --git a/packages/skins/src/minimal/tailwind/components/slider.ts b/packages/skins/src/minimal/tailwind/components/slider.ts
index d7d7f698..c0cb7c0b 100644
--- a/packages/skins/src/minimal/tailwind/components/slider.ts
+++ b/packages/skins/src/minimal/tailwind/components/slider.ts
@@ -2,7 +2,7 @@ import { cn } from '@videojs/utils/style';
export const slider = {
root: cn(
- 'group/slider relative flex flex-1 items-center justify-center rounded-full outline-none',
+ 'group/slider relative flex flex-1 items-center justify-center rounded-full outline-none cursor-pointer',
// Horizontal
'data-[orientation=horizontal]:min-w-20 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:h-5',
// Vertical
diff --git a/packages/skins/src/minimal/tailwind/video.tailwind.ts b/packages/skins/src/minimal/tailwind/video.tailwind.ts
index 75878365..416b5b49 100644
--- a/packages/skins/src/minimal/tailwind/video.tailwind.ts
+++ b/packages/skins/src/minimal/tailwind/video.tailwind.ts
@@ -1,6 +1,7 @@
import { cn } from '@videojs/utils/style';
import { controls as baseControls } from './components/controls';
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';
@@ -85,13 +86,36 @@ export const controls = cn(
'@sm/media-root:gap-3.5'
);
+/* ==========================================================================
+ Preview
+ ========================================================================== */
+
+export const preview = {
+ ...basePreview,
+ root: cn(
+ 'absolute left-(--media-slider-pointer) bottom-[calc(100%+0.5rem)] -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',
+ basePreview.root
+ ),
+ thumbnailWrapper: cn(
+ basePreview.thumbnailWrapper,
+ '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-44'),
+};
+
/* ==========================================================================
Sliders
========================================================================== */
export const slider = {
...baseSlider,
- track: cn(baseSlider.track, 'shadow-[0_0_0_1px_oklch(0_0_0/0.05)]'),
+ track: cn(baseSlider.track, 'ring-1 ring-black/5'),
};
/* ==========================================================================
@@ -100,7 +124,7 @@ export const slider = {
export const popup = {
...basePopup,
- volume: cn('[--media-popover-side-offset:0.5rem] p-1 bg-transparent'),
+ volume: cn(basePopup.base, '[--media-popover-side-offset:0.5rem] p-1 bg-transparent'),
};
/* ==========================================================================