mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore(examples): remove -demo suffix on dir names
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import { FrostedSkin, HlsVideo, MinimalSkin, VideoProvider } from '@videojs/react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
// NOTE: Commented out imports are for testing locally/externally defined skins.
|
||||
// import { VideoProvider, Video } from '@videojs/react';
|
||||
// import FrostedSkin from './skins/frosted/FrostedSkin';
|
||||
// import MinimalSkin from './skins/toasted/MinimalSkin';
|
||||
|
||||
import FrostedEjectedSkin from './skins/frosted-eject/FrostedSkin';
|
||||
import MinimalEjectedSkin from './skins/minimal-eject/MinimalSkin';
|
||||
import '@videojs/react/skins/frosted.css';
|
||||
import '@videojs/react/skins/minimal.css';
|
||||
import './globals.css';
|
||||
|
||||
const skins = [
|
||||
{
|
||||
key: 'frosted',
|
||||
name: 'Frosted (imported)',
|
||||
component: FrostedSkin,
|
||||
},
|
||||
{
|
||||
key: 'frosted-eject',
|
||||
name: 'Frosted (ejected)',
|
||||
component: FrostedEjectedSkin,
|
||||
},
|
||||
{
|
||||
key: 'minimal',
|
||||
name: 'Minimal (imported)',
|
||||
component: MinimalSkin,
|
||||
},
|
||||
{
|
||||
key: 'minimal-eject',
|
||||
name: 'Minimal (ejected)',
|
||||
component: MinimalEjectedSkin,
|
||||
},
|
||||
] as const;
|
||||
|
||||
type SkinKey = (typeof skins)[number]['key'];
|
||||
|
||||
const mediaSources = [
|
||||
{
|
||||
key: '1',
|
||||
name: 'Mux 1 (HLS)',
|
||||
value: 'https://stream.mux.com/fXNzVtmtWuyz00xnSrJg4OJH6PyNo6D02UzmgeKGkP5YQ.m3u8',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: 'Mux 2 (HLS)',
|
||||
value: 'https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M.m3u8',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: 'Mux 3 (MP4)',
|
||||
value: 'https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
name: 'Mux 4 (HLS)',
|
||||
value: 'https://stream.mux.com/lyrKpPcGfqyzeI00jZAfW6MvP6GNPrkML.m3u8',
|
||||
},
|
||||
] as const;
|
||||
|
||||
type MediaSourceKey = (typeof mediaSources)[number]['key'];
|
||||
|
||||
function getParam<T>(key: string, defaultValue: T): T {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return (params.get(key) as T) || defaultValue;
|
||||
}
|
||||
function setParam(key: string, value: string) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set(key, value);
|
||||
const search = params.toString();
|
||||
const url = window.location.pathname + (search ? `?${search}` : '');
|
||||
window.history.replaceState(null, '', url);
|
||||
}
|
||||
|
||||
const DEFAULT_SKIN: SkinKey = 'frosted';
|
||||
const DEFAULT_MEDIA_SOURCE: MediaSourceKey = '1';
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const [skinKey, setSkinKey] = useState<SkinKey>(() => getParam('skin', DEFAULT_SKIN));
|
||||
const [mediaSourceKey, setMediaSourceKey] = useState<MediaSourceKey>(() => getParam('source', DEFAULT_MEDIA_SOURCE));
|
||||
|
||||
const mediaSource = useMemo(() => {
|
||||
let match = mediaSources.find(m => m.key === mediaSourceKey);
|
||||
if (!match) {
|
||||
match = mediaSources.find(m => m.key === DEFAULT_MEDIA_SOURCE)!;
|
||||
setMediaSourceKey(match.key);
|
||||
}
|
||||
return match.value;
|
||||
}, [mediaSourceKey]);
|
||||
|
||||
const Skin = useMemo(() => {
|
||||
let match = skins.find(s => s.key === skinKey);
|
||||
if (!match) {
|
||||
match = skins.find(s => s.key === DEFAULT_SKIN)!;
|
||||
setSkinKey(match.key);
|
||||
}
|
||||
return match.component;
|
||||
}, [skinKey]);
|
||||
|
||||
const onChangeSkin: ChangeEventHandler<HTMLSelectElement> = useCallback((event) => {
|
||||
const value = event.target.value as SkinKey;
|
||||
setSkinKey(value);
|
||||
setParam('skin', value);
|
||||
}, []);
|
||||
const onChangeMediaSource: ChangeEventHandler<HTMLSelectElement> = useCallback((event) => {
|
||||
const value = event.target.value as MediaSourceKey;
|
||||
setMediaSourceKey(value);
|
||||
setParam('source', value);
|
||||
}, []);
|
||||
|
||||
// Force a re-render on changes.
|
||||
const key = `${skinKey}-${mediaSourceKey}`;
|
||||
|
||||
const playbackId = mediaSource.match(/stream\.mux\.com\/([^./]+)/)?.[1];
|
||||
const poster = playbackId ? `https://image.mux.com/${playbackId}/thumbnail.webp` : undefined;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-stone-700 dark:bg-stone-900 dark:text-stone-200">
|
||||
<header className="fixed top-0 z-10 inset-x-0 bg-white dark:bg-stone-800 shadow shadow-black/10 after:h-px after:absolute after:inset-x-0 after:top-full after:bg-black/5 transition-transform">
|
||||
<div className="grid grid-cols-5 h-2" aria-hidden="true">
|
||||
<div className="bg-yellow-500"></div>
|
||||
<div className="bg-orange-500"></div>
|
||||
<div className="bg-red-500"></div>
|
||||
<div className="bg-purple-500"></div>
|
||||
<div className="bg-blue-500"></div>
|
||||
</div>
|
||||
|
||||
<div className="py-3 px-6 flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-medium text-lg tracking-tight leading-tight dark:text-white">Playground</h1>
|
||||
<small className="block text-stone-400 text-sm">Test out the various skins for Video.js.</small>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center gap-3">
|
||||
<select value={mediaSourceKey} onChange={onChangeMediaSource}>
|
||||
{mediaSources.map(({ key, name }) => (
|
||||
<option key={key} value={key}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={skinKey} onChange={onChangeSkin}>
|
||||
{skins.map(({ key, name }) => (
|
||||
<option key={key} value={key}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="min-h-screen flex justify-center items-center bg-radial bg-size-[16px_16px] from-stone-300 dark:from-stone-700 via-10% via-transparent to-transparent">
|
||||
<div className="w-full max-w-5xl mx-auto p-6">
|
||||
<VideoProvider key={key}>
|
||||
<Skin className="aspect-video shadow-lg shadow-black/15">
|
||||
{/* @ts-expect-error -- types are incorrect */}
|
||||
<HlsVideo src={mediaSource} poster={poster} playsInline />
|
||||
</Skin>
|
||||
</VideoProvider>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--font-sans:
|
||||
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
|
||||
/* Make reduced-transparency variant */
|
||||
@custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
|
||||
import {
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/react/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}`}>
|
||||
{children}
|
||||
|
||||
<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,358 @@
|
||||
.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-delay: 500ms;
|
||||
opacity: 0;
|
||||
}
|
||||
.vjs-frosted-skin:hover > .overlay,
|
||||
.vjs-frosted-skin:has([data-paused]) > .overlay,
|
||||
.vjs-frosted-skin:has([aria-expanded='true']) > .overlay {
|
||||
opacity: 1;
|
||||
transition-duration: 100ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
/* 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.15),
|
||||
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;
|
||||
border-radius: calc(infinity * 1px);
|
||||
will-change: scale, transform, filter, opacity;
|
||||
scale: 0.9;
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition-property: scale, transform, filter, opacity;
|
||||
transition-delay: 500ms;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease-out;
|
||||
transform-origin: bottom;
|
||||
color: oklab(1 0 0);
|
||||
}
|
||||
.vjs-frosted-skin:hover > .control-bar,
|
||||
.vjs-frosted-skin:has([data-paused]) > .control-bar,
|
||||
.vjs-frosted-skin:has([aria-expanded='true']) > .control-bar {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
filter: blur(0px);
|
||||
transition-delay: 0ms;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
||||
/* 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,130 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import {
|
||||
CurrentTimeDisplay,
|
||||
DurationDisplay,
|
||||
FullscreenButton,
|
||||
MediaContainer,
|
||||
MuteButton,
|
||||
PlayButton,
|
||||
Popover,
|
||||
TimeSlider,
|
||||
Tooltip,
|
||||
VolumeSlider,
|
||||
} from '@videojs/react';
|
||||
|
||||
import {
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type SkinProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element {
|
||||
return (
|
||||
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
|
||||
{children}
|
||||
|
||||
{/* Background gradient to help with controls contrast. */}
|
||||
<div className={styles.Overlay} aria-hidden="true" />
|
||||
|
||||
<div className={styles.Controls} data-testid="media-controls">
|
||||
<Tooltip.Root delay={600} closeDelay={0}>
|
||||
<Tooltip.Trigger>
|
||||
<PlayButton className={`${styles.Button} ${styles.IconButton} ${styles.PlayButton}`}>
|
||||
<PlayIcon className={styles.PlayIcon}></PlayIcon>
|
||||
<PauseIcon className={styles.PauseIcon}></PauseIcon>
|
||||
</PlayButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.PlayTooltipPopup}`}>
|
||||
<span className={styles.PlayTooltip}>Play</span>
|
||||
<span className={styles.PauseTooltip}>Pause</span>
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Root>
|
||||
|
||||
<div className={styles.TimeControls}>
|
||||
<CurrentTimeDisplay
|
||||
// Use showRemaining to show count down/remaining time
|
||||
// showRemaining
|
||||
className={styles.TimeDisplay}
|
||||
/>
|
||||
|
||||
<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} />
|
||||
</TimeSlider.Root>
|
||||
|
||||
<DurationDisplay className={styles.TimeDisplay} />
|
||||
</div>
|
||||
|
||||
<Popover.Root openOnHover delay={200} closeDelay={100}>
|
||||
<Popover.Trigger>
|
||||
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.MuteButton}`}>
|
||||
<VolumeHighIcon className={styles.VolumeHighIcon} />
|
||||
<VolumeLowIcon className={styles.VolumeLowIcon} />
|
||||
<VolumeOffIcon className={styles.VolumeOffIcon} />
|
||||
</MuteButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Positioner side="top" sideOffset={12}>
|
||||
<Popover.Popup className={styles.PopoverPopup}>
|
||||
<VolumeSlider.Root className={styles.SliderRoot} orientation="vertical">
|
||||
<VolumeSlider.Track className={styles.SliderTrack}>
|
||||
<VolumeSlider.Progress className={styles.SliderProgress} />
|
||||
</VolumeSlider.Track>
|
||||
<VolumeSlider.Thumb className={styles.SliderThumb} />
|
||||
</VolumeSlider.Root>
|
||||
</Popover.Popup>
|
||||
</Popover.Positioner>
|
||||
</Popover.Root>
|
||||
|
||||
<Tooltip.Root delay={600} closeDelay={0}>
|
||||
<Tooltip.Trigger>
|
||||
<FullscreenButton className={`${styles.Button} ${styles.IconButton} ${styles.FullScreenButton}`}>
|
||||
<FullscreenEnterIcon className={styles.FullScreenEnterIcon} />
|
||||
<FullscreenExitIcon className={styles.FullScreenExitIcon} />
|
||||
</FullscreenButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className={`${styles.TooltipPopup} ${styles.FullScreenTooltipPopup}`}>
|
||||
<span className={styles.FullScreenEnterTooltip}>Enter Fullscreen</span>
|
||||
<span className={styles.FullScreenExitTooltip}>Exit Fullscreen</span>
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</MediaContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// function ArrowSvg(props: React.ComponentProps<'svg'>) {
|
||||
// return (
|
||||
// <svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
|
||||
// <path
|
||||
// d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
|
||||
// className={styles.ArrowFill}
|
||||
// />
|
||||
// <path
|
||||
// d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
|
||||
// className={styles.ArrowOuterStroke}
|
||||
// />
|
||||
// <path
|
||||
// d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
|
||||
// className={styles.ArrowInnerStroke}
|
||||
// />
|
||||
// </svg>
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,171 @@
|
||||
import type { FrostedSkinStyles } 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: FrostedSkinStyles = {
|
||||
MediaContainer: cn(
|
||||
'relative @container/root group/root overflow-clip',
|
||||
// Base typography
|
||||
'text-sm',
|
||||
// Prevent rounded corners in fullscreen.
|
||||
'[&:fullscreen]:rounded-none [&:fullscreen]:[&_video]:h-full [&:fullscreen]:[&_video]:w-full',
|
||||
// Fancy borders.
|
||||
'after:absolute after:inset-0 after:ring-black/10 after:ring-1 dark:after:ring-black/40 after:ring-inset after:z-10 after:pointer-events-none after:rounded-[inherit]',
|
||||
'before:absolute before:inset-px before:rounded-[inherit] before:ring-white/15 before:ring-1 before:ring-inset before:z-10 before:pointer-events-none',
|
||||
// Ensure the nested video inherits the radius.
|
||||
'[&_video]:rounded-[inherit] [&_video]:w-full [&_video]:h-auto',
|
||||
),
|
||||
Overlay: cn(
|
||||
'opacity-0 delay-500 rounded-[inherit] absolute inset-0 pointer-events-none bg-gradient-to-t from-black/50 via-black/20 to-transparent transition-opacity backdrop-saturate-150 backdrop-brightness-90',
|
||||
// Hide when playing (for now).
|
||||
// FIXME: This is crude temporary logic, we’ll improve it later I guess with a [data-show-controls] attribute or something?
|
||||
'has-[+.controls_[data-paused]]:opacity-100 has-[+.controls_[data-paused]]:delay-0',
|
||||
'group-hover/root:opacity-100 group-hover/root:delay-0',
|
||||
),
|
||||
Controls: cn(
|
||||
'controls', // FIXME: Temporary className hook for above logic in the overlay. Can be removed once have a proper way to handle controls visibility.
|
||||
'@container/controls absolute inset-x-3 bottom-3 rounded-full flex items-center p-1 ring ring-white/10 ring-inset gap-0.5 text-white text-shadow',
|
||||
'shadow-sm shadow-black/15',
|
||||
// Background
|
||||
'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90',
|
||||
// Animation
|
||||
'transition will-change-transform origin-bottom ease-out',
|
||||
// FIXME: Temporary hide/show logic
|
||||
'scale-90 opacity-0 delay-500',
|
||||
'has-[[data-paused]]:scale-100 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0',
|
||||
'group-hover/root:scale-100 group-hover/root:opacity-100 group-hover/root:delay-0',
|
||||
// Border to enhance contrast on lighter videos
|
||||
'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10',
|
||||
// Reduced transparency for users with preference
|
||||
// XXX: This requires a Tailwind custom variant (see 1 below)
|
||||
'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20',
|
||||
// High contrast mode
|
||||
'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20',
|
||||
),
|
||||
Button: cn(
|
||||
'group/button cursor-pointer relative shrink-0 transition select-none p-2 rounded-full',
|
||||
// Background/foreground
|
||||
'bg-transparent text-white/90',
|
||||
// Hover and focus states
|
||||
'hover:no-underline hover:bg-white/10 hover:text-white focus-visible:no-underline focus-visible:bg-white/10 focus-visible:text-white',
|
||||
// Focus state
|
||||
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-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-white/10 aria-expanded:text-white',
|
||||
// Pressed state
|
||||
'active:scale-95',
|
||||
),
|
||||
IconButton: cn(
|
||||
'grid [&_svg]:[grid-area:1/1]',
|
||||
'[&_svg]:shrink-0 [&_svg]:transition-opacity [&_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'),
|
||||
TooltipPopup: cn(
|
||||
'whitespace-nowrap flex origin-[var(--transform-origin)] flex-col rounded-md text-white text-xs @7xl/root:text-sm px-2 py-1',
|
||||
// Background
|
||||
'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90',
|
||||
// Animation
|
||||
'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0',
|
||||
// Ring
|
||||
'ring-1 ring-white/10 ring-inset',
|
||||
// Text shadow
|
||||
'text-shadow shadow-black/10',
|
||||
// Border to enhance contrast on lighter videos
|
||||
'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none',
|
||||
),
|
||||
PlayTooltipPopup: cn(
|
||||
'[&_.pause-tooltip]:inline [&[data-paused]_.pause-tooltip]:hidden',
|
||||
'[&_.play-tooltip]:hidden [&[data-paused]_.play-tooltip]:inline',
|
||||
),
|
||||
PlayTooltip: cn('play-tooltip'),
|
||||
PauseTooltip: cn('pause-tooltip'),
|
||||
MuteButton: cn(
|
||||
'[&_svg]:opacity-0',
|
||||
'[&[data-volume-level="high"]_.volume-high-icon]:opacity-100',
|
||||
'[&[data-volume-level="medium"]_.volume-low-icon]:opacity-100',
|
||||
'[&[data-volume-level="low"]_.volume-low-icon]:opacity-100',
|
||||
'[&[data-volume-level="off"]_.volume-off-icon]:opacity-100',
|
||||
),
|
||||
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',
|
||||
),
|
||||
FullScreenTooltipPopup: cn(
|
||||
'[&_.fullscreen-enter-tooltip]:inline [&[data-fullscreen]_.fullscreen-enter-tooltip]:hidden',
|
||||
'[&_.fullscreen-exit-tooltip]:hidden [&[data-fullscreen]_.fullscreen-exit-tooltip]:inline',
|
||||
),
|
||||
FullScreenEnterTooltip: cn('fullscreen-enter-tooltip'),
|
||||
FullScreenExitTooltip: cn('fullscreen-exit-tooltip'),
|
||||
TimeControls: cn('flex-1 flex items-center gap-3 px-1.5'),
|
||||
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(
|
||||
'w-full relative select-none rounded-full bg-white/20 ring-1 ring-black/5',
|
||||
'[&[data-orientation="horizontal"]]:h-1',
|
||||
'[&[data-orientation="vertical"]]:w-1',
|
||||
),
|
||||
SliderProgress: cn('bg-white 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',
|
||||
'opacity-0 transition-[opacity,height,width] ease-in-out',
|
||||
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500',
|
||||
'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100',
|
||||
'size-2.5 active:size-3 group-active/slider:size-3 hover:cursor-ew-resize',
|
||||
),
|
||||
PopoverPopup: cn(
|
||||
'relative px-2 py-4 rounded-2xl',
|
||||
'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90',
|
||||
'ring ring-white/10 ring-inset shadow-sm shadow-black/15',
|
||||
// Border to enhance contrast on lighter videos
|
||||
'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10',
|
||||
// Reduced transparency for users with preference
|
||||
// XXX: This requires a Tailwind custom variant (see 1 below)
|
||||
'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20',
|
||||
// High contrast mode
|
||||
'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20',
|
||||
),
|
||||
};
|
||||
|
||||
/*
|
||||
[1] @custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce);
|
||||
*/
|
||||
|
||||
export default styles;
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface FrostedSkinStyles {
|
||||
readonly MediaContainer: string;
|
||||
readonly Overlay: string;
|
||||
readonly Controls: string;
|
||||
readonly Button: string;
|
||||
readonly IconButton: string;
|
||||
readonly PlayButton: string;
|
||||
readonly PlayIcon: string;
|
||||
readonly PauseIcon: string;
|
||||
readonly PlayTooltipPopup: string;
|
||||
readonly PlayTooltip: string;
|
||||
readonly PauseTooltip: string;
|
||||
readonly TooltipPopup: string;
|
||||
readonly MuteButton: string;
|
||||
readonly VolumeHighIcon: string;
|
||||
readonly VolumeLowIcon: string;
|
||||
readonly VolumeOffIcon: string;
|
||||
readonly FullScreenButton: string;
|
||||
readonly FullScreenEnterIcon: string;
|
||||
readonly FullScreenExitIcon: string;
|
||||
readonly FullScreenTooltipPopup: string;
|
||||
readonly FullScreenEnterTooltip: string;
|
||||
readonly FullScreenExitTooltip: string;
|
||||
readonly SliderRoot: string;
|
||||
readonly SliderTrack: string;
|
||||
readonly SliderProgress: string;
|
||||
readonly SliderPointer: string;
|
||||
readonly SliderThumb: string;
|
||||
readonly PopoverPopup: string;
|
||||
readonly TimeControls: string;
|
||||
readonly TimeDisplay: string;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
|
||||
import {
|
||||
FullscreenEnterAltIcon,
|
||||
FullscreenExitAltIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/react/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}`}>
|
||||
{children}
|
||||
|
||||
<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,352 @@
|
||||
.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, 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-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));
|
||||
transform: translateY(100%);
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
transition-delay: 500ms;
|
||||
opacity: 0;
|
||||
}
|
||||
.vjs-minimal-skin:hover > .overlay,
|
||||
.vjs-minimal-skin:has([data-paused]) > .overlay,
|
||||
.vjs-minimal-skin:has([aria-expanded='true']) > .overlay {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition-duration: 100ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
/* Media Control Bar UI/Styles */
|
||||
.vjs-minimal-skin .control-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
padding: 2.5rem 0.75rem 0.75rem 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
will-change: transform, filter, opacity;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition-property: transform, filter, opacity;
|
||||
transition-delay: 500ms;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease-out;
|
||||
color: oklab(1 0 0);
|
||||
}
|
||||
.vjs-minimal-skin:hover > .control-bar,
|
||||
.vjs-minimal-skin:has([data-paused]) > .control-bar,
|
||||
.vjs-minimal-skin:has([aria-expanded='true']) > .control-bar {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0px);
|
||||
transition-delay: 0ms;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
||||
/* 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,75 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import {
|
||||
CurrentTimeDisplay,
|
||||
DurationDisplay,
|
||||
FullscreenButton,
|
||||
MediaContainer,
|
||||
MuteButton,
|
||||
PlayButton,
|
||||
TimeSlider,
|
||||
} from '@videojs/react';
|
||||
|
||||
import {
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type SkinProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element {
|
||||
return (
|
||||
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
|
||||
{children}
|
||||
|
||||
<div className={styles.Overlay} aria-hidden="true" />
|
||||
|
||||
<div className={styles.Controls}>
|
||||
<PlayButton className={`${styles.Button} ${styles.IconButton} ${styles.PlayButton}`}>
|
||||
<PlayIcon className={`${styles.PlayIcon} ${styles.Icon}`} />
|
||||
<PauseIcon className={`${styles.PauseIcon} ${styles.Icon}`} />
|
||||
</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>
|
||||
|
||||
<TimeSlider.Root className={`${styles.SliderRoot} ${styles.TimeSliderRoot}`}>
|
||||
<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 className={styles.ButtonGroup}>
|
||||
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.MuteButton}`}>
|
||||
<VolumeHighIcon className={`${styles.VolumeHighIcon} ${styles.Icon}`} />
|
||||
<VolumeLowIcon className={`${styles.VolumeLowIcon} ${styles.Icon}`} />
|
||||
<VolumeOffIcon className={`${styles.VolumeOffIcon} ${styles.Icon}`} />
|
||||
</MuteButton>
|
||||
|
||||
<FullscreenButton className={`${styles.Button} ${styles.IconButton} ${styles.FullscreenButton}`}>
|
||||
<FullscreenEnterIcon className={`${styles.FullscreenEnterIcon} ${styles.Icon}`} />
|
||||
<FullscreenExitIcon className={`${styles.FullscreenExitIcon} ${styles.Icon}`} />
|
||||
</FullscreenButton>
|
||||
</div>
|
||||
</div>
|
||||
</MediaContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { MinimalSkinStyles } 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: MinimalSkinStyles = {
|
||||
MediaContainer: cn(
|
||||
'relative @container/root group/root overflow-clip bg-black',
|
||||
// Base typography
|
||||
'text-[0.8125rem]', // 13px
|
||||
// 'ring-1 ring-inset ring-black/10 dark:ring-white/10',
|
||||
'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 [&:fullscreen]:[&_video]:h-full [&:fullscreen]:[&_video]:w-full',
|
||||
// Ensure the nested video inherits the radius.
|
||||
'[&_video]:rounded-[inherit] [&_video]:w-full [&_video]:h-auto',
|
||||
),
|
||||
Overlay: cn(
|
||||
'absolute inset-0 rounded-[inherit] bg-black/30',
|
||||
'bg-gradient-to-t from-black/30 to-transparent to-[120px]',
|
||||
'opacity-0 delay-500 duration-300',
|
||||
// FIXME: Temporary hide/show logic
|
||||
'has-[+.controls_[data-paused]]:opacity-100 has-[+.controls_[data-paused]]:delay-0 has-[+.controls_[data-paused]]:duration-75',
|
||||
'group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:duration-75',
|
||||
),
|
||||
Controls: cn(
|
||||
'controls', // FIXME: Temporary className hook for above logic in the overlay. Can be removed once have a proper way to handle controls visibility.
|
||||
'@container/controls absolute inset-x-0 bottom-0 flex items-center gap-3.5 z-20 px-6 pb-6 pt-10 text-white text-shadow',
|
||||
'shadow-sm shadow-black/15',
|
||||
// Animation
|
||||
'transition ease-in-out',
|
||||
// FIXME: Temporary hide/show logic
|
||||
'translate-y-full opacity-0 delay-500 duration-300',
|
||||
'has-[[data-paused]]:translate-y-0 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0 has-[[data-paused]]:duration-75',
|
||||
'group-hover/root:translate-y-0 group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:duration-75',
|
||||
),
|
||||
Icon: cn('icon'),
|
||||
Button: cn(
|
||||
'group/button cursor-pointer relative shrink-0 transition select-none p-2 rounded-md',
|
||||
// Background/foreground
|
||||
'bg-transparent text-white',
|
||||
// Hover and focus states
|
||||
'hover:text-white/70 focus-visible:text-white/70',
|
||||
// Focus state
|
||||
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white',
|
||||
// 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:text-white/70',
|
||||
// Pressed state
|
||||
'active:scale-95',
|
||||
),
|
||||
ButtonGroup: cn('flex items-center gap-1.5'),
|
||||
IconButton: cn(
|
||||
'grid [&_.icon]:[grid-area:1/1]',
|
||||
'[&_.icon]:shrink-0 [&_.icon]:transition [&_.icon]:duration-300 [&_.icon]:ease-out [&_.icon]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] [&_.icon]: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'),
|
||||
MuteButton: cn(
|
||||
'[&_.icon]: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',
|
||||
),
|
||||
TimeSliderRoot: cn('mx-2'),
|
||||
TimeSliderThumb: cn('opacity-0'),
|
||||
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/10',
|
||||
'[&[data-orientation="horizontal"]]:w-full [&[data-orientation="horizontal"]]:h-1',
|
||||
'[&[data-orientation="vertical"]]:w-1',
|
||||
),
|
||||
SliderProgress: cn('bg-white rounded-[inherit]'),
|
||||
SliderPointer: cn('hidden'),
|
||||
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,27 @@
|
||||
export interface MinimalSkinStyles {
|
||||
readonly MediaContainer: string;
|
||||
readonly Overlay: string;
|
||||
readonly Controls: string;
|
||||
readonly Icon: string;
|
||||
readonly Button: string;
|
||||
readonly ButtonGroup: string;
|
||||
readonly IconButton: string;
|
||||
readonly PlayButton: string;
|
||||
readonly PlayIcon: string;
|
||||
readonly PauseIcon: string;
|
||||
readonly MuteButton: string;
|
||||
readonly VolumeHighIcon: string;
|
||||
readonly VolumeLowIcon: string;
|
||||
readonly VolumeOffIcon: string;
|
||||
readonly FullscreenButton: string;
|
||||
readonly FullscreenEnterIcon: string;
|
||||
readonly FullscreenExitIcon: string;
|
||||
readonly TimeSliderRoot: string;
|
||||
readonly TimeSliderThumb: string;
|
||||
readonly TimeDisplay: string;
|
||||
readonly SliderRoot: string;
|
||||
readonly SliderTrack: string;
|
||||
readonly SliderProgress: string;
|
||||
readonly SliderPointer: string;
|
||||
readonly SliderThumb: string;
|
||||
}
|
||||
Reference in New Issue
Block a user