chore(root): archive examples into tech-preview (#315)

This commit is contained in:
rahim
2026-01-20 14:35:51 +11:00
committed by GitHub
parent 78695d4bb6
commit 35c6cb1e16
62 changed files with 33 additions and 1519 deletions
@@ -0,0 +1,366 @@
import type { ButtonProps as ButtonPrimitiveProps } from '@base-ui-components/react/button';
import type { SliderRootProps } from '@base-ui-components/react/slider';
import type { ComponentProps, PropsWithChildren } from 'react';
import { Button as ButtonPrimitive } from '@base-ui-components/react/button';
import { Popover as PopoverPrimitive } from '@base-ui-components/react/popover';
import { Slider as SliderPrimitive } from '@base-ui-components/react/slider';
import { Tooltip as TooltipPrimitive } from '@base-ui-components/react/tooltip';
import {
ArrowDownLeftIcon,
ArrowUpRightIcon,
} from '@heroicons/react/16/solid';
import {
PauseIcon,
PlayIcon,
SpeakerWaveIcon,
SpeakerXMarkIcon,
XMarkIcon,
} from '@heroicons/react/20/solid';
import {
MediaContainer,
useCurrentTimeDisplayState,
useFullscreenButtonState,
useMuteButtonState,
usePlayButtonState,
useTimeSliderRootState,
useVolumeSliderRootState,
} from '@videojs/react-preview';
import { formatDisplayTime } from '@videojs/utils-preview';
import clsx from 'clsx';
import { useCallback, useState } from 'react';
type SkinProps = PropsWithChildren<{
className?: string;
}>;
function Surface(props: ComponentProps<'div'>) {
const { className, ...rest } = props;
return (
<div
className={clsx(
// Background
'bg-linear-to-t to-zinc-50/80 from-zinc-100/80 backdrop-blur-md dark:from-zinc-950/80 dark:to-zinc-900/80',
// Border & shadow
'ring-1 ring-black/5 shadow-xs shadow-black/15 dark:ring-white/15',
// Text
'text-zinc-900 dark:text-zinc-50',
className,
)}
{...rest}
/>
);
}
function Popup(props: ComponentProps<typeof Surface>) {
const { className, ...rest } = props;
return (
<Surface
className={clsx(
'origin-(--transform-origin) transition data-instant:duration-0 translate-y-0',
'data-starting-style:scale-90 data-starting-style:translate-y-1 data-starting-style:opacity-0',
'data-ending-style:scale-90 data-ending-style:translate-y-1 data-ending-style:opacity-0',
className,
)}
{...rest}
/>
);
}
function TooltipPopup(props: ComponentProps<typeof TooltipPrimitive.Popup>) {
const { className, ...rest } = props;
return (
<TooltipPrimitive.Popup
render={props => <Popup {...props} className="rounded-md px-2 py-1 text-xs" />}
{...rest}
/>
);
}
const Tooltip = {
...TooltipPrimitive,
Popup: TooltipPopup,
};
function PopoverPopup(props: ComponentProps<typeof PopoverPrimitive.Popup>) {
const { className, ...rest } = props;
return (
<PopoverPrimitive.Popup
render={props => <Popup {...props} className="rounded-lg py-3 px-1.5" />}
{...rest}
/>
);
}
const Popover = {
...PopoverPrimitive,
Popup: PopoverPopup,
};
type ButtonProps = Omit<ButtonPrimitiveProps, 'className'>;
function Button(props: ButtonProps) {
return (
<ButtonPrimitive
className={clsx(
'flex p-1.75 hover:bg-black/5 text-zinc-700 hover:text-zinc-800 transition-colors rounded-md outline-2 outline-transparent',
// Focus styles
'focus-visible:outline-zinc-700',
// Dark styles
'dark:text-zinc-100 dark:hover:text-white dark:hover:bg-white/10 dark:focus-visible:outline-zinc-300',
)}
{...props}
/>
);
}
function PlayButton() {
const { paused, requestPause, requestPlay } = usePlayButtonState();
const onClick = useCallback(() => {
if (paused) {
requestPlay();
} else {
requestPause();
}
}, [paused, requestPlay, requestPause]);
const label = paused ? 'Play' : 'Pause';
return (
<Tooltip.Root>
<Tooltip.Trigger render={(
<Button onClick={onClick} data-paused={paused ? '' : undefined}>
{paused ? <PlayIcon className="size-5" /> : <PauseIcon className="size-5" />}
<span className="sr-only">{label}</span>
</Button>
)}
/>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={12} align="start">
<Tooltip.Popup>
{label}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
);
}
function MuteButton() {
const { muted, requestMute, requestUnmute } = useMuteButtonState();
const [open, setOpen] = useState(false);
const onClick = useCallback(() => {
if (muted) {
requestUnmute();
} else {
requestMute();
}
}, [muted, requestMute, requestUnmute]);
const handleOpenChange = useCallback((isOpen: boolean, eventDetails: PopoverPrimitive.Root.ChangeEventDetails) => {
// Don't close when clicking the trigger button - only allow hover to control it
if (!isOpen && eventDetails?.reason === 'trigger-press') return;
setOpen(isOpen);
}, []);
const label = muted ? 'Unmute' : 'Mute';
return (
<Popover.Root open={open} onOpenChange={handleOpenChange}>
<Popover.Trigger
openOnHover
delay={20}
closeDelay={200}
render={(
<Button onClick={onClick}>
{muted ? <SpeakerXMarkIcon className="size-5" /> : <SpeakerWaveIcon className="size-5" />}
<span className="sr-only">{label}</span>
</Button>
)}
/>
<Popover.Portal>
<Popover.Backdrop />
<Popover.Positioner sideOffset={12} side="top">
<Popover.Popup className="flex items-center justify-center">
<VolumeSlider />
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
function FullscreenButton() {
const { fullscreen, requestEnterFullscreen, requestExitFullscreen } = useFullscreenButtonState();
const onClick = useCallback(() => {
if (fullscreen) {
requestExitFullscreen();
} else {
requestEnterFullscreen();
}
}, [fullscreen, requestEnterFullscreen, requestExitFullscreen]);
const label = fullscreen ? 'Exit fullscreen' : 'Enter fullscreen';
return (
<Tooltip.Root>
<Tooltip.Trigger render={(
<Button onClick={onClick}>
{fullscreen
? <XMarkIcon className="size-5" />
: (
<div className="grid [&_svg]:[grid-area:1/1] size-5 place-content-center">
<ArrowDownLeftIcon className="size-4 -translate-x-0.5 translate-y-0.5" />
<ArrowUpRightIcon className="size-4 translate-x-0.5 -translate-y-0.5" />
</div>
)}
<span className="sr-only">{label}</span>
</Button>
)}
/>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={12} align="end">
<Tooltip.Popup>
{label}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
);
}
type SliderProps = Pick<SliderRootProps<number>, 'min' | 'max' | 'step' | 'value' | 'onValueChange' | 'orientation'>;
function Slider(props: SliderProps) {
return (
<SliderPrimitive.Root
{...props}
thumbAlignment="edge"
className="flex-1 group/slider"
>
<SliderPrimitive.Control className={clsx(
'flex items-center',
'data-[orientation="horizontal"]:h-5 data-[orientation="horizontal"]:min-w-20',
'data-[orientation="vertical"]:w-5 data-[orientation="vertical"]:h-20 data-[orientation="vertical"]:justify-center',
)}
>
<SliderPrimitive.Track className={clsx(
'relative select-none rounded-full bg-black/10 dark:bg-white/10',
'data-[orientation="horizontal"]:w-full data-[orientation="horizontal"]:h-1',
'data-[orientation="vertical"]:w-1 data-[orientation="vertical"]:h-full',
)}
>
<SliderPrimitive.Indicator className="bg-zinc-700 dark:bg-white rounded-[inherit]" />
<SliderPrimitive.Thumb className={clsx(
'z-10 bg-white size-3 select-none ring ring-black/10 rounded-full shadow-xs shadow-black/15 transition-opacity ease-in-out',
// Focus styles (the hidden input inside the thumb gets focus)
'-outline-offset-2 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-zinc-500',
)}
/>
</SliderPrimitive.Track>
</SliderPrimitive.Control>
</SliderPrimitive.Root>
);
}
function TimeSlider() {
// TODO: Expose something like _fillWidth to facilitate debouncing etc.
const { currentTime, duration, requestSeek } = useTimeSliderRootState();
return (
<Slider
// FIXME: If seems that currentTime can be undefined initially
value={currentTime ?? 0}
// FIXME: If seems that duration can be undefined initially
// Base UI will throw an error if max is less than min (which is 0)
max={!duration ? 1 : duration}
step={0.1}
onValueChange={requestSeek}
/>
);
}
function VolumeSlider() {
const { volume, muted, requestVolumeChange } = useVolumeSliderRootState();
// FIXME: If seems that volume can be undefined initially, despite the types.
const value = muted ? 0 : volume ?? 0;
return (
<Slider
value={value}
max={1}
step={0.1}
onValueChange={requestVolumeChange}
orientation="vertical"
/>
);
}
interface TimeDisplayProps {
type: 'current' | 'duration';
className?: string;
}
function TimeDisplay(props: TimeDisplayProps) {
const { type, className } = props;
const { duration, currentTime } = useCurrentTimeDisplayState();
const value = formatDisplayTime(type === 'current' ? currentTime : duration);
return <span className={clsx('tabular-nums', className)}>{value}</span>;
}
export default function CustomBaseUISkin({ children, className }: SkinProps): JSX.Element {
return (
<MediaContainer className={clsx(
'relative isolate @container/root group/root overflow-clip bg-black rounded-xl',
// Base typography
'font-sans text-[0.8125rem] subpixel-antialiased',
// Fancy borders
'after:absolute after:inset-0 after:ring-black/10 after:ring-1 dark:after:ring-white/10 after:ring-inset after:z-10 after:pointer-events-none after:rounded-[inherit]',
// Prevent rounded corners in fullscreen
'[&:fullscreen]:rounded-none',
// Ensure the nested video inherits the radius
'[&_video]:w-full [&_video]:h-full',
className,
)}
>
{/* FIXME: Mismatch between React 18 & 19 types */}
{children as any}
<Surface className={clsx(
'@container/controls absolute inset-x-2 bottom-2 justify-end z-20 p-1 rounded-lg flex items-center gap-0.5',
// Animation
'transition ease-in-out',
// FIXME: Temporary hide/show logic
'opacity-0 scale-[0.98] blur-sm delay-500 pointer-events-none origin-bottom',
'has-data-paused:opacity-100 has-data-paused:scale-100 has-data-paused:blur-none has-data-paused:delay-0 has-data-paused:pointer-events-auto',
'group-hover/root:opacity-100 group-hover/root:scale-100 group-hover/root:blur-none group-hover/root:delay-0 group-hover/root:pointer-events-auto',
)}
>
<Tooltip.Provider>
<PlayButton />
<div className="flex items-center gap-2.5 px-2 flex-1">
<div className="flex items-center gap-1">
<TimeDisplay type="current" />
<span className="opacity-50">/</span>
<TimeDisplay type="duration" className="opacity-50" />
</div>
<TimeSlider />
</div>
<MuteButton />
<FullscreenButton />
</Tooltip.Provider>
</Surface>
</MediaContainer>
);
};
@@ -0,0 +1,90 @@
import type { PropsWithChildren } from 'react';
import {
CurrentTimeDisplay,
DurationDisplay,
FullscreenButton,
MediaContainer,
MuteButton,
PlayButton,
TimeSlider,
VolumeSlider,
} from '@videojs/react-preview';
import {
FullscreenEnterIcon,
FullscreenExitIcon,
PauseIcon,
PlayIcon,
VolumeHighIcon,
VolumeLowIcon,
VolumeOffIcon,
} from '@videojs/react-preview/icons';
import styles from './styles';
type SkinProps = PropsWithChildren<{
className?: string;
}>;
export default function CustomNativeSkin({ children, className = '' }: SkinProps): JSX.Element {
return (
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
{/* FIXME: Mismatch between React 18 & 19 types */}
{children as any}
<div className={styles.Controls}>
<div className={styles.ControlsRow}>
<TimeSlider.Root className={styles.SliderRoot}>
<TimeSlider.Track className={styles.SliderTrack}>
<TimeSlider.Progress className={styles.SliderProgress} />
<TimeSlider.Pointer className={styles.SliderPointer} />
</TimeSlider.Track>
<TimeSlider.Thumb className={`${styles.SliderThumb} ${styles.TimeSliderThumb}`} />
</TimeSlider.Root>
</div>
<div className={styles.ControlsRow}>
<div className="flex items-center gap-3">
<PlayButton className={`${styles.Button} ${styles.IconButton} ${styles.PlayButton}`}>
<PlayIcon className={styles.PlayIcon}></PlayIcon>
<PauseIcon className={styles.PauseIcon}></PauseIcon>
</PlayButton>
<div className="flex items-center gap-1">
<CurrentTimeDisplay
// Use showRemaining to show count down/remaining time
// showRemaining
className={styles.TimeDisplay}
/>
<span className="opacity-50">/</span>
<DurationDisplay className={`${styles.TimeDisplay} opacity-50`} />
</div>
</div>
<div className="flex items-center gap-0.5">
<div className={styles.VolumeControls}>
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.VolumeButton}`}>
<VolumeHighIcon className={styles.VolumeHighIcon} />
<VolumeLowIcon className={styles.VolumeLowIcon} />
<VolumeOffIcon className={styles.VolumeOffIcon} />
</MuteButton>
<div className={styles.VolumeSlider}>
<VolumeSlider.Root className={styles.SliderRoot}>
<VolumeSlider.Track className={styles.SliderTrack}>
<VolumeSlider.Progress className={styles.SliderProgress} />
</VolumeSlider.Track>
<VolumeSlider.Thumb className={styles.SliderThumb} />
</VolumeSlider.Root>
</div>
</div>
<FullscreenButton className={`${styles.Button} ${styles.IconButton} ${styles.FullScreenButton}`}>
<FullscreenEnterIcon className={styles.FullScreenEnterIcon} />
<FullscreenExitIcon className={styles.FullScreenExitIcon} />
</FullscreenButton>
</div>
</div>
</div>
</MediaContainer>
);
};
@@ -0,0 +1,122 @@
import type { CustomNativeSkinStyles } from './types';
// NOTE: Removing import to sidestep for compiler complexity (CJP)
// import { cn } from '../../utils/cn';
// A (very crude) utility to merge class names
// Usually I'd use something like `clsx` or `classnames` but this is ok for our simple use case.
// It just makes the billions of Tailwind classes a little easier to read.
function cn(...classes: (string | undefined)[]): string {
return classes.filter(Boolean).join(' ');
}
const styles: CustomNativeSkinStyles = {
MediaContainer: cn(
'relative isolate @container/root group/root overflow-clip bg-black rounded-xl',
// Base typography
'font-sans text-[0.8125rem] subpixel-antialiased',
// Fancy borders
'after:absolute after:inset-0 after:ring-black/10 after:ring-1 dark:after:ring-white/10 after:ring-inset after:z-10 after:pointer-events-none after:rounded-[inherit]',
// Prevent rounded corners in fullscreen
'[&:fullscreen]:rounded-none',
// Ensure the nested video inherits the radius
'[&_video]:w-full [&_video]:h-full',
),
Controls: cn(
'@container/controls absolute inset-x-0 bottom-0 top-1/3 flex flex-col justify-end z-20 px-2.5 pb-2.5 text-white text-shadow',
'shadow-sm shadow-black/15',
// Background
'bg-linear-to-t from-stone-950/70 via-stone-950/60 via-35% to-transparent',
// Animation
'transition ease-in-out',
// FIXME: Temporary hide/show logic
'translate-y-full opacity-0 delay-500 pointer-events-none',
'has-data-paused:translate-y-0 has-data-paused:opacity-100 has-data-paused:delay-0 has-data-paused:pointer-events-auto',
'group-hover/root:translate-y-0 group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:pointer-events-auto',
),
ControlsRow: cn('flex items-center justify-between'),
Button: cn(
'group/button cursor-pointer relative shrink-0 transition select-none p-2 rounded-md',
// Background/foreground
'bg-transparent text-white/90',
// Hover and focus states
'hover:no-underline hover:bg-stone-100/10 hover:backdrop-blur-md hover:text-white focus-visible:no-underline focus-visible:bg-stone-100/10 focus-visible:text-white',
// Focus state
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500',
// Disabled state
'aria-disabled:grayscale aria-disabled:opacity-50 aria-disabled:cursor-not-allowed',
// Loading state
'aria-busy:pointer-events-none aria-busy:cursor-not-allowed',
// Expanded state
'aria-expanded:bg-stone-100/10 aria-expanded:text-white',
// Pressed state
'active:scale-95',
),
IconButton: cn(
'grid [&_svg]:[grid-area:1/1]',
'[&_svg]:shrink-0 [&_svg]:transition [&_svg]:duration-300 [&_svg]:ease-out [&_svg]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] [&_svg]:shadow-black/20',
),
PlayButton: cn(
'[&_.pause-icon]:opacity-100 [&[data-paused]_.pause-icon]:opacity-0',
'[&_.play-icon]:opacity-0 [&[data-paused]_.play-icon]:opacity-100',
),
PlayIcon: cn('play-icon'),
PauseIcon: cn('pause-icon'),
VolumeControls: cn('flex items-center flex-row-reverse group/volume'),
VolumeSlider: cn(
'w-0 px-3 overflow-hidden pointer-events-none transition-[opacity,width] opacity-0 ease-out delay-500',
'group-hover/volume:w-28 group-hover/volume:pointer-events-auto group-hover/volume:opacity-100 group-hover/volume:delay-0',
'group-focus-within/volume:w-28 group-focus-within/volume:pointer-events-auto group-focus-within/volume:opacity-100 group-focus-within/volume:delay-0',
),
VolumeButton: cn(
'[&_svg]:hidden',
'[&[data-volume-level="high"]_.volume-high-icon]:inline',
'[&[data-volume-level="medium"]_.volume-low-icon]:inline',
'[&[data-volume-level="low"]_.volume-low-icon]:inline',
'[&[data-volume-level="off"]_.volume-off-icon]:inline',
),
VolumeHighIcon: cn('volume-high-icon'),
VolumeLowIcon: cn('volume-low-icon'),
VolumeOffIcon: cn('volume-off-icon'),
FullScreenButton: cn(
'[&_.fullscreen-enter-icon]:opacity-100 [&[data-fullscreen]_.fullscreen-enter-icon]:opacity-0',
'[&_.fullscreen-exit-icon]:opacity-0 [&[data-fullscreen]_.fullscreen-exit-icon]:opacity-100',
'[&_path]:transition-transform ease-out',
),
FullScreenEnterIcon: cn(
'fullscreen-enter-icon',
'group-hover/button:[&_.arrow-1]:-translate-x-px group-hover/button:[&_.arrow-1]:-translate-y-px',
'group-hover/button:[&_.arrow-2]:translate-x-px group-hover/button:[&_.arrow-2]:translate-y-px',
),
FullScreenExitIcon: cn(
'fullscreen-exit-icon',
'[&_.arrow-1]:-translate-x-px [&_.arrow-1]:-translate-y-px',
'[&_.arrow-2]:translate-x-px [&_.arrow-2]:translate-y-px',
'group-hover/button:[&_.arrow-1]:translate-0',
'group-hover/button:[&_.arrow-2]:translate-0',
),
TimeSliderThumb: cn(
'opacity-0',
'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100',
),
TimeDisplay: cn('tabular-nums text-shadow-2xs shadow-black/50'),
SliderRoot: cn(
'flex items-center justify-center flex-1 group/slider relative',
'[&[data-orientation="horizontal"]]:h-5 [&[data-orientation="horizontal"]]:min-w-20',
'[&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-20',
),
SliderTrack: cn(
'relative select-none rounded-full bg-white/25 backdrop-blur-sm backdrop-brightness-90 backdrop-saturate-150 shadow-sm shadow-black/10',
'[&[data-orientation="horizontal"]]:w-full [&[data-orientation="horizontal"]]:h-1',
'[&[data-orientation="vertical"]]:w-1',
),
SliderProgress: cn('bg-amber-500 rounded-[inherit]'),
// TODO: Work out what we want to do here.
SliderPointer: cn('rounded-[inherit]'),
SliderThumb: cn(
'bg-white z-10 select-none ring ring-black/10 rounded-full shadow-sm shadow-black/15 transition-[opacity,height,width] ease-in-out',
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500',
'size-3 active:size-3.5 group-active/slider:size-3.5 hover:cursor-ew-resize',
),
};
export default styles;
@@ -0,0 +1,26 @@
export interface CustomNativeSkinStyles {
readonly MediaContainer: string;
readonly Controls: string;
readonly ControlsRow: string;
readonly Button: string;
readonly IconButton: string;
readonly PlayButton: string;
readonly PlayIcon: string;
readonly PauseIcon: string;
readonly VolumeControls: string;
readonly VolumeSlider: string;
readonly VolumeButton: string;
readonly VolumeHighIcon: string;
readonly VolumeLowIcon: string;
readonly VolumeOffIcon: string;
readonly FullScreenButton: string;
readonly FullScreenEnterIcon: string;
readonly FullScreenExitIcon: string;
readonly TimeSliderThumb: string;
readonly TimeDisplay: string;
readonly SliderRoot: string;
readonly SliderTrack: string;
readonly SliderProgress: string;
readonly SliderPointer: string;
readonly SliderThumb: string;
}
@@ -0,0 +1,120 @@
import type { PropsWithChildren } from 'react';
import {
CurrentTimeDisplay,
DurationDisplay,
FullscreenButton,
MediaContainer,
MuteButton,
PlayButton,
Popover,
PreviewTimeDisplay,
TimeSlider,
Tooltip,
VolumeSlider,
} from '@videojs/react-preview';
import {
FullscreenEnterIcon,
FullscreenExitIcon,
PauseIcon,
PlayIcon,
VolumeHighIcon,
VolumeLowIcon,
VolumeOffIcon,
} from '@videojs/react-preview/icons';
import './frosted.css';
type SkinProps = PropsWithChildren<{
className?: string;
}>;
export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element {
return (
<MediaContainer className={`vjs-frosted-skin ${className}`}>
{/* FIXME: Mismatch between React 18 & 19 types */}
{children as any}
<div className="overlay" />
<div className="surface control-bar" data-testid="media-controls">
<Tooltip.Root delay={500}>
<Tooltip.Trigger>
<PlayButton className="button play-button">
<PlayIcon className="icon play-icon" />
<PauseIcon className="icon pause-icon" />
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="tooltip-popup surface popup-animation">
<span className="tooltip play-tooltip">Play</span>
<span className="tooltip pause-tooltip">Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className="time-controls">
<CurrentTimeDisplay
// Use showRemaining to show count down/remaining time
// showRemaining
className="time-display"
/>
<Tooltip.Root trackCursorAxis="x">
<Tooltip.Trigger>
<TimeSlider.Root className="slider">
<TimeSlider.Track className="slider-track">
<TimeSlider.Progress className="slider-progress" />
<TimeSlider.Pointer className="slider-pointer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="slider-thumb" />
</TimeSlider.Root>
</Tooltip.Trigger>
<Tooltip.Positioner side="top" sideOffset={18} collisionPadding={12}>
<Tooltip.Popup className="surface popup-animation tooltip-popup">
<PreviewTimeDisplay className="time-display media-preview-time-display" />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<DurationDisplay className="time-display" />
</div>
<Popover.Root openOnHover delay={200} closeDelay={100}>
<Popover.Trigger>
<MuteButton className="button mute-button">
<VolumeHighIcon className="icon volume-high-icon" />
<VolumeLowIcon className="icon volume-low-icon" />
<VolumeOffIcon className="icon volume-off-icon" />
</MuteButton>
</Popover.Trigger>
<Popover.Positioner side="top" sideOffset={12}>
<Popover.Popup className="surface popup-animation popover-popup">
<VolumeSlider.Root className="slider" orientation="vertical">
<VolumeSlider.Track className="slider-track">
<VolumeSlider.Progress className="slider-progress" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="slider-thumb" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={500}>
<Tooltip.Trigger>
<FullscreenButton className="button fullscreen-button">
<FullscreenEnterIcon className="icon fullscreen-enter-icon" />
<FullscreenExitIcon className="icon fullscreen-exit-icon" />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="surface popup-animation tooltip-popup">
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</MediaContainer>
);
}
@@ -0,0 +1,352 @@
.vjs-frosted-skin * {
box-sizing: border-box;
}
.vjs-frosted-skin {
position: relative;
isolation: isolate;
container: root / inline-size;
overflow: clip;
border-radius: var(--frosted-border-radius, 2rem);
background: oklab(0 0 0);
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
.vjs-frosted-skin::before,
.vjs-frosted-skin::after {
content: '';
position: absolute;
pointer-events: none;
border-radius: inherit;
z-index: 10;
}
.vjs-frosted-skin::before {
inset: 1px;
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
}
.vjs-frosted-skin::after {
inset: 0;
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
}
/* Fullscreen */
.vjs-frosted-skin:fullscreen {
border-radius: 0;
}
.vjs-frosted-skin > ::slotted([slot='media']) {
display: block;
width: 100%;
height: 100%;
}
/* Media Container UI Overlay Styling */
.vjs-frosted-skin > .overlay {
position: absolute;
inset: 0;
display: flex;
flex-flow: column nowrap;
align-items: start;
pointer-events: none;
border-radius: inherit;
background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
backdrop-filter: saturate(1.5) brightness(0.9);
transition: opacity 0.15s ease-out;
transition-duration: 100ms;
transition-delay: 0ms;
}
.vjs-frosted-skin[data-controls='hidden'] > .overlay {
transition-delay: 500ms;
transition-duration: 300ms;
opacity: 0;
}
/* Common Surface Styles - e.g. tooltips, popovers, controls */
.vjs-frosted-skin .surface {
background-color: oklab(1 0 0 / 0.1);
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
box-shadow:
inset 0 0 0 1px oklab(1 0 0 / 0.05),
0 0 0 1px oklab(0 0 0 / 0.15),
oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
}
@media (prefers-reduced-transparency: reduce) {
.vjs-frosted-skin .surface {
background-color: oklab(0 0 0 / 0.7);
box-shadow:
inset 0 0 0 1px oklab(0 0 0),
0 0 0 1px oklab(1 0 0 / 0.2);
}
}
@media (prefers-contrast: more) {
.vjs-frosted-skin .surface {
background-color: oklab(0 0 0 / 0.9);
box-shadow:
inset 0 0 0 1px oklab(0 0 0),
0 0 0 1px oklab(1 0 0 / 0.2);
}
}
/* Media Control Bar UI/Styles */
.vjs-frosted-skin .control-bar {
position: absolute;
bottom: 0.75rem;
inset-inline: 0.75rem;
padding: 0.25rem;
display: flex;
align-items: center;
gap: 0.125rem;
color: oklab(1 0 0);
border-radius: calc(infinity * 1px);
will-change: scale, transform, filter, opacity;
transition-property: scale, transform, filter, opacity;
transition-timing-function: ease-out;
transition-delay: 0ms;
transition-duration: 100ms;
transform-origin: bottom;
}
.vjs-frosted-skin[data-controls='hidden'] .control-bar {
pointer-events: none;
filter: blur(8px);
scale: 0.9;
opacity: 0;
transition-delay: 500ms;
transition-duration: 300ms;
}
/* Time Display Styling */
.vjs-frosted-skin .time-controls {
display: flex;
align-items: center;
flex: 1;
gap: 0.75rem;
padding-inline: 0.375rem;
}
.vjs-frosted-skin .time-display {
text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
font-variant-numeric: tabular-nums;
}
/* Generic Media Button Styling */
.vjs-frosted-skin .button {
display: grid;
flex-shrink: 0;
padding: 0.5rem;
cursor: pointer;
background: transparent;
border: none;
border-radius: calc(infinity * 1px);
color: oklab(1 0 0 / 0.9);
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
transition-property: background-color, color, outline-offset;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.vjs-frosted-skin .button:hover,
.vjs-frosted-skin .button:focus-visible,
.vjs-frosted-skin .button[aria-expanded='true'] {
background-color: oklab(1 0 0 / 0.1);
color: oklab(1 0 0);
text-decoration: none;
}
.vjs-frosted-skin .button:focus-visible {
outline-color: oklch(62.3% 0.214 259.815);
outline-offset: 2px;
}
.vjs-frosted-skin .button[disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
.vjs-frosted-skin .button .icon {
grid-area: 1 / 1;
width: 18px;
height: 18px;
filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
}
/* Media Play Button UI/Styles */
.vjs-frosted-skin .play-button .icon {
opacity: 0;
transition: opacity 150ms linear;
}
.vjs-frosted-skin .play-button:not([data-paused]) .pause-icon,
.vjs-frosted-skin .play-button[data-paused] .play-icon {
opacity: 1;
}
/* Media Fullscreen Button UI/Styles */
.vjs-frosted-skin .fullscreen-button .icon {
display: none;
}
.vjs-frosted-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
.vjs-frosted-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
display: inline;
}
/* One way to define the "default visible" icon (CJP) */
.vjs-frosted-skin .mute-button .icon {
display: none;
}
.vjs-frosted-skin .mute-button:not([data-volume-level]) .volume-low-icon,
.vjs-frosted-skin .mute-button[data-volume-level='high'] .volume-high-icon,
.vjs-frosted-skin .mute-button[data-volume-level='medium'] .volume-high-icon,
.vjs-frosted-skin .mute-button[data-volume-level='low'] .volume-low-icon,
.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon {
display: inline;
}
/* TimeSlider Component Styles */
.vjs-frosted-skin .slider {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: calc(infinity * 1px);
outline: none;
}
/* Horizontal orientation styles */
.vjs-frosted-skin .slider[data-orientation='horizontal'] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
/* Vertical orientation styles */
.vjs-frosted-skin .slider[data-orientation='vertical'] {
height: 5rem;
width: 1.25rem;
}
.vjs-frosted-skin .slider-track {
position: relative;
isolation: isolate;
background-color: oklab(1 0 0 / 0.2);
border-radius: inherit;
overflow: hidden;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
transition: outline-offset 150ms ease-out;
box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
}
/* Horizontal track styles */
.vjs-frosted-skin .slider-track[data-orientation='horizontal'] {
width: 100%;
height: 0.25rem;
}
/* Vertical track styles */
.vjs-frosted-skin .slider-track[data-orientation='vertical'] {
width: 0.25rem;
height: 100%;
}
.vjs-frosted-skin .slider:focus-visible .slider-track {
outline-color: oklch(62.3% 0.214 259.815);
outline-offset: 6px;
}
.vjs-frosted-skin .slider-thumb {
width: 0.625rem;
height: 0.625rem;
background-color: oklab(1 0 0);
border-radius: calc(infinity * 1px);
user-select: none;
z-index: 10;
box-shadow:
0 0 0 1px oklab(0 0 0 / 0.1),
0 1px 3px 0 oklab(0 0 0 / 0.15),
0 1px 2px -1px oklab(0 0 0 / 0.15);
opacity: 0;
transition-property: opacity, height, width;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.vjs-frosted-skin .slider-thumb:active {
width: 0.75rem;
height: 0.75rem;
}
.vjs-frosted-skin .slider:hover .slider-thumb,
.vjs-frosted-skin .slider:focus-within .slider-thumb {
opacity: 1;
}
.vjs-frosted-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
cursor: ew-resize;
}
.vjs-frosted-skin .slider-track[data-orientation='vertical'] .slider-thumb {
cursor: ns-resize;
}
.vjs-frosted-skin .slider-pointer {
background-color: oklab(1 0 0 / 0.2);
border-radius: inherit;
}
.vjs-frosted-skin .slider-progress {
background-color: oklab(1 0 0);
border-radius: inherit;
}
.vjs-frosted-skin .media-preview-time-display {
font-variant-numeric: tabular-nums;
}
.vjs-frosted-skin .popup-animation {
transition-property: transform, scale, opacity, filter;
transition-duration: 200ms;
transform: scale(1);
transform-origin: bottom;
opacity: 1;
filter: blur(0px);
}
.vjs-frosted-skin .popup-animation[data-starting-style],
.vjs-frosted-skin .popup-animation[data-ending-style] {
transform: scale(0);
opacity: 0;
filter: blur(8px);
}
.vjs-frosted-skin .popup-animation[data-instant] {
transition-duration: 0ms;
}
.vjs-frosted-skin .popover-popup {
padding: 0.75rem 0.25rem;
border-radius: calc(infinity * 1px);
margin: 0;
border: 0;
}
/* Tooltip Component Styles */
.vjs-frosted-skin .tooltip-popup {
color: oklab(1 0 0);
padding: 0.25rem 0.625rem;
border-radius: calc(infinity * 1px);
font-size: 0.75rem;
}
.vjs-frosted-skin .tooltip {
display: none;
white-space: nowrap;
}
.vjs-frosted-skin .tooltip-popup[data-paused] .play-tooltip,
.vjs-frosted-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
display: block;
}
.vjs-frosted-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
.vjs-frosted-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
display: block;
}
@@ -0,0 +1,125 @@
import type { PropsWithChildren } from 'react';
import {
CurrentTimeDisplay,
DurationDisplay,
FullscreenButton,
MediaContainer,
MuteButton,
PlayButton,
Popover,
PreviewTimeDisplay,
TimeSlider,
Tooltip,
VolumeSlider,
} from '@videojs/react-preview';
import {
FullscreenEnterAltIcon,
FullscreenExitAltIcon,
PauseIcon,
PlayIcon,
VolumeHighIcon,
VolumeLowIcon,
VolumeOffIcon,
} from '@videojs/react-preview/icons';
import './minimal.css';
type SkinProps = PropsWithChildren<{
className?: string;
}>;
export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element {
return (
<MediaContainer className={`vjs-minimal-skin ${className}`}>
{/* FIXME: Mismatch between React 18 & 19 types */}
{children as any}
<div className="overlay" />
<div className="control-bar">
<Tooltip.Root delay={500} closeDelay={0}>
<Tooltip.Trigger>
<PlayButton className="button play-button">
<PlayIcon className="icon play-icon" />
<PauseIcon className="icon pause-icon" />
</PlayButton>
</Tooltip.Trigger>
<Tooltip.Positioner side="top-start" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<span className="tooltip play-tooltip">Play</span>
<span className="tooltip pause-tooltip">Pause</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className="time-display-group">
<CurrentTimeDisplay
// Use showRemaining to show count down/remaining time
// showRemaining
className="time-display"
/>
<span className="duration-display">
/
<DurationDisplay className="time-display" />
</span>
</div>
<Tooltip.Root trackCursorAxis="x">
<Tooltip.Trigger>
<TimeSlider.Root className="slider">
<TimeSlider.Track className="slider-track">
<TimeSlider.Progress className="slider-progress" />
<TimeSlider.Pointer className="slider-pointer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="slider-thumb" />
</TimeSlider.Root>
</Tooltip.Trigger>
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<PreviewTimeDisplay className="time-display media-preview-time-display" />
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
<div className="button-group">
<Popover.Root openOnHover delay={200} closeDelay={300}>
<Popover.Trigger>
<MuteButton className="button mute-button">
<VolumeHighIcon className="icon volume-high-icon" />
<VolumeLowIcon className="icon volume-low-icon" />
<VolumeOffIcon className="icon volume-off-icon" />
</MuteButton>
</Popover.Trigger>
<Popover.Positioner side="top" sideOffset={2}>
<Popover.Popup className="popup-animation popover-popup">
<VolumeSlider.Root className="slider" orientation="vertical">
<VolumeSlider.Track className="slider-track">
<VolumeSlider.Progress className="slider-progress" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="slider-thumb" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Positioner>
</Popover.Root>
<Tooltip.Root delay={500} closeDelay={0}>
<Tooltip.Trigger>
<FullscreenButton className="button fullscreen-button">
<FullscreenEnterAltIcon className="icon fullscreen-enter-icon" />
<FullscreenExitAltIcon className="icon fullscreen-exit-icon" />
</FullscreenButton>
</Tooltip.Trigger>
<Tooltip.Positioner side="top-end" sideOffset={6} collisionPadding={12}>
<Tooltip.Popup className="popup-animation tooltip-popup">
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</div>
</MediaContainer>
);
}
@@ -0,0 +1,347 @@
.vjs-minimal-skin * {
box-sizing: border-box;
}
.vjs-minimal-skin {
position: relative;
isolation: isolate;
container: root / inline-size;
overflow: clip;
border-radius: var(--minimal-border-radius, 0.75rem);
background: oklab(0 0 0);
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
.vjs-minimal-skin::after {
content: '';
position: absolute;
pointer-events: none;
border-radius: inherit;
z-index: 10;
inset: 0;
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.15);
}
@media (prefers-color-scheme: dark) {
.vjs-minimal-skin::after {
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
}
}
/* Fullscreen */
.vjs-minimal-skin:fullscreen {
border-radius: 0;
}
.vjs-minimal-skin > ::slotted([slot='media']) {
display: block;
width: 100%;
height: 100%;
}
/* Media Container UI Overlay Styling */
.vjs-minimal-skin > .overlay {
position: absolute;
inset: 0;
display: flex;
flex-flow: column nowrap;
align-items: start;
pointer-events: none;
border-radius: inherit;
background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
transition-property: opacity;
transition-duration: 75ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
}
.vjs-minimal-skin[data-controls='hidden'] > .overlay {
opacity: 0;
transition-duration: 500ms;
transition-delay: 500ms;
}
/* Media Control Bar UI/Styles */
.vjs-minimal-skin .control-bar {
position: absolute;
bottom: 0;
inset-inline: 0;
padding: 2rem 0.375rem 0.375rem 0.375rem;
display: flex;
align-items: center;
gap: 0.5rem;
will-change: transform, filter, opacity;
transition-property: transform, filter, opacity;
transition-delay: 0ms;
transition-duration: 75ms;
transition-timing-function: ease-out;
color: oklab(1 0 0);
}
.vjs-minimal-skin[data-controls='hidden'] > .control-bar {
opacity: 0;
transform: translateY(100%);
filter: blur(8px);
transition-delay: 500ms;
transition-duration: 500ms;
pointer-events: none;
}
@container root (width > 640px) {
.vjs-minimal-skin .control-bar {
padding: 2.5rem 0.75rem 0.75rem 0.75rem;
gap: 0.875rem;
}
}
/* Time Display Styling */
.vjs-minimal-skin .time-display-group {
display: flex;
align-items: center;
gap: 0.25rem;
}
.vjs-minimal-skin .duration-display {
display: contents;
color: oklab(1 0 0 / 0.5);
}
.vjs-minimal-skin .time-display {
text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
font-variant-numeric: tabular-nums;
}
.vjs-minimal-skin .button-group {
display: flex;
align-items: center;
gap: 0.375rem;
}
/* Generic Media Button Styling */
.vjs-minimal-skin .button {
display: grid;
flex-shrink: 0;
padding: 0.625rem;
cursor: pointer;
background: transparent;
border: none;
border-radius: 0.375rem;
color: oklab(1 0 0);
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
transition-property: background-color, color, outline-offset;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.vjs-minimal-skin .button:hover,
.vjs-minimal-skin .button:focus-visible,
.vjs-minimal-skin .button[aria-expanded='true'] {
color: oklab(1 0 0 / 0.8);
text-decoration: none;
}
.vjs-minimal-skin .button:focus-visible {
outline-color: oklab(1 0 0);
outline-offset: 2px;
}
.vjs-minimal-skin .button[disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
.vjs-minimal-skin .button .icon {
grid-area: 1 / 1;
width: 18px;
height: 18px;
filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
}
/* Media Play Button UI/Styles */
.vjs-minimal-skin .play-button .icon {
opacity: 0;
transition: opacity 150ms linear;
}
.vjs-minimal-skin .play-button:not([data-paused]) .pause-icon,
.vjs-minimal-skin .play-button[data-paused] .play-icon {
opacity: 1;
}
/* Media Fullscreen Button UI/Styles */
.vjs-minimal-skin .fullscreen-button .icon {
display: none;
}
.vjs-minimal-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
.vjs-minimal-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
display: inline;
}
/* One way to define the "default visible" icon (CJP) */
.vjs-minimal-skin .mute-button .icon {
display: none;
}
.vjs-minimal-skin .mute-button:not([data-volume-level]) .volume-low-icon,
.vjs-minimal-skin .mute-button[data-volume-level='high'] .volume-high-icon,
.vjs-minimal-skin .mute-button[data-volume-level='medium'] .volume-high-icon,
.vjs-minimal-skin .mute-button[data-volume-level='low'] .volume-low-icon,
.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon {
display: inline;
}
/* TimeSlider Component Styles */
.vjs-minimal-skin .slider {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: calc(infinity * 1px);
outline: none;
}
/* Horizontal orientation styles */
.vjs-minimal-skin .slider[data-orientation='horizontal'] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
/* Vertical orientation styles */
.vjs-minimal-skin .slider[data-orientation='vertical'] {
height: 4.5rem;
width: 1.25rem;
}
.vjs-minimal-skin .slider-track {
position: relative;
isolation: isolate;
background-color: oklab(1 0 0 / 0.2);
border-radius: inherit;
overflow: hidden;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
transition: outline-offset 150ms ease-out;
box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
}
/* Horizontal track styles */
.vjs-minimal-skin .slider-track[data-orientation='horizontal'] {
width: 100%;
height: 0.1875rem;
}
/* Vertical track styles */
.vjs-minimal-skin .slider-track[data-orientation='vertical'] {
width: 0.1875rem;
height: 100%;
}
.vjs-minimal-skin .slider:focus-visible .slider-track {
outline-color: oklab(1 0 0);
outline-offset: 6px;
}
.vjs-minimal-skin .slider-thumb {
width: 0.75rem;
height: 0.75rem;
background-color: oklab(1 0 0);
border-radius: calc(infinity * 1px);
user-select: none;
z-index: 10;
box-shadow:
0 0 0 1px oklab(0 0 0 / 0.1),
0 1px 3px 0 oklab(0 0 0 / 0.15),
0 1px 2px -1px oklab(0 0 0 / 0.15);
opacity: 0;
scale: 0.7;
transform-origin: center;
transition-property: opacity, scale;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.vjs-minimal-skin .slider:hover .slider-thumb,
.vjs-minimal-skin .slider:focus-within .slider-thumb {
opacity: 1;
scale: 1;
}
.vjs-minimal-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
cursor: ew-resize;
}
.vjs-minimal-skin .slider-track[data-orientation='vertical'] .slider-thumb {
cursor: ns-resize;
}
.vjs-minimal-skin .slider-pointer {
display: none;
}
.vjs-minimal-skin .slider-progress {
background-color: oklab(1 0 0);
border-radius: inherit;
}
.vjs-minimal-skin .media-preview-time-display {
font-variant-numeric: tabular-nums;
}
.vjs-minimal-skin .popup-animation {
transition-property: transform, scale, opacity, filter;
transition-duration: 200ms;
transform: scale(1);
transform-origin: bottom;
opacity: 1;
filter: blur(0px);
}
.vjs-minimal-skin .popup-animation[data-starting-style],
.vjs-minimal-skin .popup-animation[data-ending-style] {
transform: scale(0);
opacity: 0;
filter: blur(8px);
}
.vjs-minimal-skin .popup-animation[data-instant] {
transition-duration: 0ms;
}
.vjs-minimal-skin .popover-popup {
padding: 0.5rem;
margin: 0;
border: 0;
background: transparent;
}
/* Tooltip Component Styles */
.vjs-minimal-skin .tooltip-popup {
color: oklab(1 0 0);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
background-color: oklab(1 0 0 / 0.1);
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
box-shadow:
0 4px 6px -1px oklab(0 0 0 / 0.1),
0 2px 4px -2px oklab(0 0 0 / 0.1);
}
@media (prefers-reduced-transparency: reduce) {
.vjs-minimal-skin .tooltip-popup {
background-color: oklab(0 0 0 / 0.7);
}
}
@media (prefers-contrast: more) {
.vjs-minimal-skin .tooltip-popup {
background-color: oklab(0 0 0 / 0.9);
}
}
.vjs-minimal-skin .tooltip {
display: none;
white-space: nowrap;
}
.vjs-minimal-skin .tooltip-popup[data-paused] .play-tooltip,
.vjs-minimal-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
display: block;
}
.vjs-minimal-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
.vjs-minimal-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
display: block;
}
@@ -0,0 +1,3 @@
import '@videojs/react-preview/skins/frosted.css';
export { FrostedSkin as default } from '@videojs/react-preview';
@@ -0,0 +1,3 @@
import '@videojs/react-preview/skins/minimal.css';
export { MinimalSkin as default } from '@videojs/react-preview';