From ebb75f5863f137511f6fc75bb7fbf528d45b0d9b Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 2 Mar 2026 08:34:19 +1100 Subject: [PATCH] feat(react): port time slider styling into video skin presets (#666) --- .../react/src/presets/video/minimal-skin.css | 54 +++++++++++-------- .../presets/video/minimal-skin.tailwind.tsx | 32 ++++++++++- .../react/src/presets/video/minimal-skin.tsx | 10 +++- packages/react/src/presets/video/skin.css | 51 ++++++++++-------- .../react/src/presets/video/skin.tailwind.tsx | 32 ++++++++++- packages/react/src/presets/video/skin.tsx | 10 +++- 6 files changed, 138 insertions(+), 51 deletions(-) diff --git a/packages/react/src/presets/video/minimal-skin.css b/packages/react/src/presets/video/minimal-skin.css index 97d5faf9..1ffe58bd 100644 --- a/packages/react/src/presets/video/minimal-skin.css +++ b/packages/react/src/presets/video/minimal-skin.css @@ -101,6 +101,7 @@ .media-minimal-skin .media-overlay { position: absolute; inset: 0; + z-index: 1; border-radius: inherit; background-image: linear-gradient(to top, oklch(0 0 0 / 0.7), oklch(0 0 0 / 0.5) 7.5rem, oklch(0 0 0 / 0)); backdrop-filter: blur(0) saturate(1.2) brightness(0.9); @@ -451,10 +452,7 @@ overflow: hidden; background-color: oklch(1 0 0 / 0.2); border-radius: inherit; - outline: 2px solid transparent; - outline-offset: -2px; box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05); - transition: outline-offset 150ms ease-out; user-select: none; } .media-minimal-skin .media-slider__track[data-orientation="horizontal"] { @@ -465,13 +463,13 @@ width: 0.1875rem; height: 100%; } -.media-minimal-skin .media-slider:focus-visible .media-slider__track { - outline-color: oklch(1 0 0); - outline-offset: 6px; -} /* Thumb */ .media-minimal-skin .media-slider__thumb { + position: absolute; + top: 50%; + left: var(--media-slider-fill, 0%); + transform: translate(-50%, -50%); z-index: 10; width: 0.75rem; height: 0.75rem; @@ -484,32 +482,44 @@ opacity: 0; scale: 0.7; transform-origin: center; - transition-property: opacity, scale; + transition-property: opacity, scale, outline-offset; transition-duration: 150ms; transition-timing-function: ease-out; user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; +} +.media-minimal-skin .media-slider__thumb:focus-visible { + outline-color: oklch(1 0 0); + outline-offset: 2px; } .media-minimal-skin .media-slider:hover .media-slider__thumb, .media-minimal-skin .media-slider:focus-within .media-slider__thumb { opacity: 1; scale: 1; } -.media-minimal-skin .media-slider__track[data-orientation="horizontal"] .media-slider__thumb { - cursor: ew-resize; -} -.media-minimal-skin .media-slider__track[data-orientation="vertical"] .media-slider__thumb { - cursor: ns-resize; -} -/* Pointer (hidden in minimal skin) */ -.media-minimal-skin .media-slider__pointer { - display: none; -} - -/* Progress fill */ -.media-minimal-skin .media-slider__progress { - background-color: oklch(1 0 0); +/* Shared track fills */ +.media-minimal-skin .media-slider__buffer, +.media-minimal-skin .media-slider__fill { + position: absolute; + inset-block: 0; + left: 0; border-radius: inherit; + pointer-events: none; +} + +/* Buffer */ +.media-minimal-skin .media-slider__buffer { + background-color: oklch(1 0 0 / 0.2); + width: var(--media-slider-buffer, 0%); + transition: width 0.25s ease-out; +} + +/* Fill */ +.media-minimal-skin .media-slider__fill { + background-color: oklch(1 0 0); + width: var(--media-slider-fill, 0%); } /* Time display within slider */ diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx index 9608c618..550cbbf5 100644 --- a/packages/react/src/presets/video/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx @@ -28,6 +28,7 @@ import { PlayButton } from '@/ui/play-button'; import { PlaybackRateButton } from '@/ui/playback-rate-button'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; +import { TimeSlider } from '@/ui/time-slider'; import type { MinimalVideoSkinProps } from './minimal-skin'; const SEEK_TIME = 10; @@ -293,8 +294,35 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod /> - {/* Temporary spacer */} - + + + + + + + diff --git a/packages/react/src/presets/video/minimal-skin.tsx b/packages/react/src/presets/video/minimal-skin.tsx index b38a4329..edb9120a 100644 --- a/packages/react/src/presets/video/minimal-skin.tsx +++ b/packages/react/src/presets/video/minimal-skin.tsx @@ -28,6 +28,7 @@ import { PlayButton } from '@/ui/play-button'; import { PlaybackRateButton } from '@/ui/playback-rate-button'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; +import { TimeSlider } from '@/ui/time-slider'; import type { BaseSkinProps } from '../types'; const SEEK_TIME = 10; @@ -159,8 +160,13 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { - {/* Temporary spacer */} - + + + + + + + diff --git a/packages/react/src/presets/video/skin.css b/packages/react/src/presets/video/skin.css index ac00e98f..76d86af4 100644 --- a/packages/react/src/presets/video/skin.css +++ b/packages/react/src/presets/video/skin.css @@ -104,6 +104,7 @@ .media-default-skin .media-overlay { position: absolute; inset: 0; + z-index: 1; border-radius: inherit; background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0)); backdrop-filter: blur(0) saturate(1.2) brightness(0.9); @@ -465,10 +466,7 @@ overflow: hidden; background-color: oklch(1 0 0 / 0.2); border-radius: inherit; - outline: 2px solid transparent; - outline-offset: -2px; box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05); - transition: outline-offset 150ms ease-out; user-select: none; } .media-default-skin .media-slider__track[data-orientation="horizontal"] { @@ -479,14 +477,14 @@ width: 0.25rem; height: 100%; } -.media-default-skin .media-slider:focus-visible .media-slider__track { - outline-color: oklch(62.3% 0.214 259.815); - outline-offset: 6px; -} /* Thumb */ .media-default-skin .media-slider__thumb { z-index: 10; + position: absolute; + top: 50%; + left: var(--media-slider-fill); + transform: translate(-50%, -50%); width: 0.625rem; height: 0.625rem; background-color: oklch(1 0 0); @@ -496,36 +494,47 @@ 0 1px 3px 0 oklch(0 0 0 / 0.15), 0 1px 2px -1px oklch(0 0 0 / 0.15); opacity: 0; - transition-property: opacity, height, width; + transition-property: opacity, height, width, outline-offset; transition-duration: 150ms; transition-timing-function: ease-out; user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; } -.media-default-skin .media-slider__thumb:active { +.media-default-skin .media-slider__thumb:focus-visible { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 2px; +} +.media-default-skin .media-slider:active .media-slider__thumb { width: 0.75rem; height: 0.75rem; } .media-default-skin .media-slider:hover .media-slider__thumb, -.media-default-skin .media-slider:focus-within .media-slider__thumb { +.media-default-skin .media-slider__thumb:focus-visible { opacity: 1; } -.media-default-skin .media-slider__track[data-orientation="horizontal"] .media-slider__thumb { - cursor: ew-resize; -} -.media-default-skin .media-slider__track[data-orientation="vertical"] .media-slider__thumb { - cursor: ns-resize; + +/* Shared track fills */ +.media-default-skin .media-slider__buffer, +.media-default-skin .media-slider__fill { + position: absolute; + inset-block: 0; + left: 0; + border-radius: inherit; + pointer-events: none; } -/* Pointer (hover preview) */ -.media-default-skin .media-slider__pointer { +/* Buffer */ +.media-default-skin .media-slider__buffer { background-color: oklch(1 0 0 / 0.2); - border-radius: inherit; + width: var(--media-slider-buffer); + transition: width 0.25s ease-out; } -/* Progress fill */ -.media-default-skin .media-slider__progress { +/* Fill */ +.media-default-skin .media-slider__fill { background-color: oklch(1 0 0); - border-radius: inherit; + width: var(--media-slider-fill); } /* Time display within slider */ diff --git a/packages/react/src/presets/video/skin.tailwind.tsx b/packages/react/src/presets/video/skin.tailwind.tsx index 0d3eb12f..776204a6 100644 --- a/packages/react/src/presets/video/skin.tailwind.tsx +++ b/packages/react/src/presets/video/skin.tailwind.tsx @@ -28,6 +28,7 @@ import { PlayButton } from '@/ui/play-button'; import { PlaybackRateButton } from '@/ui/playback-rate-button'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; +import { TimeSlider } from '@/ui/time-slider'; import type { VideoSkinProps } from './skin'; const SEEK_TIME = 10; @@ -300,8 +301,35 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { type="current" className="hidden @2xs/media-time:block text-shadow-2xs text-shadow-black/25 tabular-nums" /> - {/* Temporary spacer */} -
+ + + + + + + diff --git a/packages/react/src/presets/video/skin.tsx b/packages/react/src/presets/video/skin.tsx index 8b286752..c2ddb470 100644 --- a/packages/react/src/presets/video/skin.tsx +++ b/packages/react/src/presets/video/skin.tsx @@ -28,6 +28,7 @@ import { PlayButton } from '@/ui/play-button'; import { PlaybackRateButton } from '@/ui/playback-rate-button'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; +import { TimeSlider } from '@/ui/time-slider'; import type { BaseSkinProps } from '../types'; const SEEK_TIME = 10; @@ -154,8 +155,13 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { - {/* Temporary spacer */} -
+ + + + + + +