feat(react): port time slider styling into video skin presets (#666)

This commit is contained in:
Sam Potts
2026-03-02 08:34:19 +11:00
committed by GitHub
parent 50bf91ed87
commit ebb75f5863
6 changed files with 138 additions and 51 deletions
@@ -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 */
@@ -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
/>
</Time.Group>
{/* Temporary spacer */}
<span className="flex-1 h-[3px] rounded-full bg-white/20" />
<TimeSlider.Root
className={cn(
'group/slider relative flex items-center justify-center flex-1 rounded-full outline-none',
'min-w-20 w-full h-5'
)}
>
<TimeSlider.Track
className={cn(
'relative isolate overflow-hidden bg-white/20 rounded-[inherit]',
'shadow-[0_0_0_1px_oklch(0_0_0/0.05)] select-none',
'w-full h-0.75'
)}
>
<TimeSlider.Fill className="absolute inset-y-0 left-0 rounded-[inherit] pointer-events-none bg-white w-(--media-slider-fill)" />
<TimeSlider.Buffer className="absolute inset-y-0 left-0 rounded-[inherit] pointer-events-none bg-white/20 w-(--media-slider-buffer) transition-[width] duration-250 ease-out" />
</TimeSlider.Track>
<TimeSlider.Thumb
className={cn(
'z-10 absolute top-1/2 left-(--media-slider-fill) -translate-x-1/2 -translate-y-1/2',
'size-3 bg-white rounded-full origin-center',
'ring ring-black/10 shadow-sm shadow-black/15',
'opacity-0 scale-70 transition-[opacity,scale,outline-offset] duration-150 ease-out select-none',
'outline-2 outline-transparent -outline-offset-2',
'focus-visible:outline-white focus-visible:outline-offset-2',
'group-hover/slider:opacity-100 group-hover/slider:scale-100',
'group-focus-within/slider:opacity-100 group-focus-within/slider:scale-100'
)}
/>
</TimeSlider.Root>
</span>
<span className={cn('flex items-center gap-[0.075rem]', '@2xl/media-root:gap-0.5')}>
@@ -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 {
<Time.Value type="duration" className="media-time__value media-time__value--duration" />
</Time.Group>
{/* Temporary spacer */}
<span className="media-slider" style={{ height: '4px', background: 'oklch(1 0 0 / 0.2)' }} />
<TimeSlider.Root className="media-slider">
<TimeSlider.Track className="media-slider__track">
<TimeSlider.Fill className="media-slider__fill" />
<TimeSlider.Buffer className="media-slider__buffer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
</TimeSlider.Root>
</span>
<span className="media-button-group">
+30 -21
View File
@@ -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 */
@@ -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 */}
<div className="flex-1 h-1 rounded-full bg-white/20" />
<TimeSlider.Root
className={cn(
'group/slider relative flex items-center justify-center flex-1 rounded-full outline-none',
'min-w-20 w-full h-5'
)}
>
<TimeSlider.Track
className={cn(
'relative isolate overflow-hidden bg-white/20 rounded-[inherit]',
'shadow-[0_0_0_1px_oklch(0_0_0/0.05)] select-none',
'w-full h-1'
)}
>
<TimeSlider.Fill className="absolute inset-y-0 left-0 rounded-[inherit] pointer-events-none bg-white w-(--media-slider-fill)" />
<TimeSlider.Buffer className="absolute inset-y-0 left-0 rounded-[inherit] pointer-events-none bg-white/20 w-(--media-slider-buffer) transition-[width] duration-250 ease-out" />
</TimeSlider.Track>
<TimeSlider.Thumb
className={cn(
'z-10 absolute top-1/2 left-(--media-slider-fill) -translate-x-1/2 -translate-y-1/2',
'size-2.5 bg-white rounded-full',
'ring ring-black/10 shadow-sm shadow-black/15',
'opacity-0 transition-[opacity,height,width,outline-offset] duration-150 ease-out select-none',
'outline-2 outline-transparent -outline-offset-2',
'focus-visible:outline-blue-500 focus-visible:outline-offset-2 focus-visible:opacity-100',
'group-hover/slider:opacity-100',
'group-active/slider:size-3'
)}
/>
</TimeSlider.Root>
<Time.Value type="duration" className="text-shadow-2xs text-shadow-black/25 tabular-nums" />
</Time.Group>
+8 -2
View File
@@ -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 {
<Time.Group className="media-time">
<Time.Value type="current" className="media-time__value" />
{/* Temporary spacer */}
<div className="media-slider" style={{ height: '4px', background: 'oklab(1 0 0 / 0.2)' }} />
<TimeSlider.Root className="media-slider">
<TimeSlider.Track className="media-slider__track">
<TimeSlider.Fill className="media-slider__fill" />
<TimeSlider.Buffer className="media-slider__buffer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
</TimeSlider.Root>
<Time.Value type="duration" className="media-time__value" />
</Time.Group>