mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 13:48:14 +00:00
feat(react): port time slider styling into video skin presets (#666)
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user