mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(skins): slightly more idiomatic Tailwind, added custom properties (#175)
This commit is contained in:
@@ -4,19 +4,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VJS HTML Demo</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family:
|
||||
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
margin: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="display: flex; flex-direction: column; min-height: 100vh; max-width: 960px; padding: 1rem">
|
||||
<h1>Video.js 10 HTML Demos</h1>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2rem;
|
||||
border-radius: 0.75rem;
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
aspect-ratio: 16/9;
|
||||
|
||||
@@ -7,10 +7,11 @@ media-container {
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
border-radius: var(--vjs-border-radius, 2rem);
|
||||
background: oklab(0 0 0);
|
||||
font-family: var(--vjs-font-family, InterVariable, Inter, ui-sans-serif, system-ui, sans-serif);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
media-container::before,
|
||||
media-container::after {
|
||||
@@ -204,8 +205,8 @@ media-mute-button .icon {
|
||||
}
|
||||
media-mute-button:not([data-volume-level]) .volume-low-icon,
|
||||
media-mute-button[data-volume-level='high'] .volume-high-icon,
|
||||
media-mute-button[data-volume-level='medium'] .volume-high-icon,
|
||||
media-mute-button[data-volume-level='low'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='medium'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ media-container {
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
border-radius: var(--vjs-border-radius, 2rem);
|
||||
background: oklab(0 0 0);
|
||||
font-family: var(--vjs-font-family, InterVariable, Inter, ui-sans-serif, system-ui, sans-serif);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
media-container::after {
|
||||
content: '';
|
||||
@@ -19,11 +20,11 @@ media-container::after {
|
||||
border-radius: inherit;
|
||||
z-index: 10;
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.15);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
media-container::after {
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +184,8 @@ media-mute-button .icon {
|
||||
}
|
||||
media-mute-button:not([data-volume-level]) .volume-low-icon,
|
||||
media-mute-button[data-volume-level='high'] .volume-high-icon,
|
||||
media-mute-button[data-volume-level='medium'] .volume-high-icon,
|
||||
media-mute-button[data-volume-level='low'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='medium'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"tailwindcss": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import { FrostedSkin, HlsVideo, MinimalSkin, VideoProvider } from '@videojs/react';
|
||||
import { FullscreenEnterAltIcon, FullscreenExitAltIcon } from '@videojs/react/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useFullscreen } from './hooks/useFullscreen';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
// NOTE: Commented out imports are for testing locally/externally defined skins.
|
||||
// import { VideoProvider, Video } from '@videojs/react';
|
||||
@@ -45,22 +42,22 @@ type SkinKey = (typeof skins)[number]['key'];
|
||||
const mediaSources = [
|
||||
{
|
||||
key: '1',
|
||||
name: 'Mux 1',
|
||||
name: 'Mux 1 (HLS)',
|
||||
value: 'https://stream.mux.com/fXNzVtmtWuyz00xnSrJg4OJH6PyNo6D02UzmgeKGkP5YQ.m3u8',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: 'Mux 2',
|
||||
name: 'Mux 2 (HLS)',
|
||||
value: 'https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M.m3u8',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: 'Mux 3',
|
||||
name: 'Mux 3 (MP4)',
|
||||
value: 'https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
name: 'Mux 4',
|
||||
name: 'Mux 4 (HLS)',
|
||||
value: 'https://stream.mux.com/lyrKpPcGfqyzeI00jZAfW6MvP6GNPrkML.m3u8',
|
||||
},
|
||||
] as const;
|
||||
@@ -85,8 +82,6 @@ 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 containerRef = useRef<HTMLDivElement>(null);
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(containerRef);
|
||||
|
||||
const mediaSource = useMemo(() => {
|
||||
let match = mediaSources.find(m => m.key === mediaSourceKey);
|
||||
@@ -120,37 +115,12 @@ export default function App(): JSX.Element {
|
||||
// Force a re-render on changes.
|
||||
const key = `${skinKey}-${mediaSourceKey}`;
|
||||
|
||||
const skinClassName = useMemo(() => {
|
||||
switch (skinKey) {
|
||||
case 'frosted':
|
||||
case 'frosted-eject':
|
||||
return 'aspect-video !rounded-4xl shadow shadow-lg shadow-black/15';
|
||||
case 'minimal':
|
||||
case 'minimal-eject':
|
||||
return 'aspect-video !rounded-2xl shadow shadow-lg shadow-black/15';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, [skinKey]);
|
||||
|
||||
const playbackId = mediaSource.match(/stream\.mux\.com\/([^./]+)/)?.[1];
|
||||
const poster = playbackId ? `https://image.mux.com/${playbackId}/thumbnail.webp` : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx('', {
|
||||
'bg-black text-stone-200 h-screen': isFullscreen,
|
||||
'min-h-screen bg-white text-stone-700 dark:bg-black dark:text-stone-200': !isFullscreen,
|
||||
})}
|
||||
>
|
||||
<header className={clsx(
|
||||
'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',
|
||||
{
|
||||
'-translate-y-full': isFullscreen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
@@ -180,24 +150,14 @@ export default function App(): JSX.Element {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" className="p-2" onClick={toggleFullscreen}>
|
||||
{isFullscreen ? <FullscreenExitAltIcon /> : <FullscreenEnterAltIcon />}
|
||||
<span className="sr-only">Toggle fullscreen</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className={clsx(
|
||||
'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': !isFullscreen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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={skinClassName}>
|
||||
<Skin className="aspect-video shadow-lg shadow-black/15">
|
||||
{/* @ts-expect-error -- types are incorrect */}
|
||||
<HlsVideo src={mediaSource} poster={poster} playsInline />
|
||||
</Skin>
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import screenfull from 'screenfull';
|
||||
|
||||
export interface UseFullscreenOptions {
|
||||
/**
|
||||
* When true, automatically handles fullscreen change events
|
||||
* to synchronize the state with browser fullscreen status
|
||||
* @default true
|
||||
*/
|
||||
trackChanges?: boolean;
|
||||
/**
|
||||
* Callback fired when fullscreen state changes
|
||||
*/
|
||||
onFullscreenChange?: (isFullscreen: boolean) => void;
|
||||
/**
|
||||
* Callback fired when fullscreen encounters an error
|
||||
*/
|
||||
onFullscreenError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export interface UseFullscreenResult {
|
||||
/**
|
||||
* Whether the element is currently in fullscreen mode
|
||||
*/
|
||||
isFullscreen: boolean;
|
||||
/**
|
||||
* Whether fullscreen is supported in the current environment
|
||||
*/
|
||||
isSupported: boolean;
|
||||
/**
|
||||
* Request fullscreen for the target element
|
||||
* @returns Promise that resolves when fullscreen is entered
|
||||
*/
|
||||
enter: () => Promise<void>;
|
||||
/**
|
||||
* Exit fullscreen mode
|
||||
* @returns Promise that resolves when fullscreen is exited
|
||||
*/
|
||||
exit: () => Promise<void>;
|
||||
/**
|
||||
* Toggle fullscreen mode for the target element
|
||||
* @returns Promise that resolves when the toggle is complete
|
||||
*/
|
||||
toggle: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that provides fullscreen functionality for a DOM element
|
||||
*
|
||||
* @param elementRef - Ref to the element to make fullscreen
|
||||
* @param options - Configuration options
|
||||
* @returns Methods and state for managing fullscreen
|
||||
*/
|
||||
export function useFullscreen(
|
||||
elementRef: RefObject<HTMLElement | null>,
|
||||
options: UseFullscreenOptions = {},
|
||||
): UseFullscreenResult {
|
||||
const {
|
||||
trackChanges = true,
|
||||
onFullscreenChange,
|
||||
onFullscreenError,
|
||||
} = options;
|
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(
|
||||
screenfull.isEnabled ? screenfull.isFullscreen : false,
|
||||
);
|
||||
|
||||
// Handle fullscreen change events
|
||||
useEffect(() => {
|
||||
if (!screenfull.isEnabled || !trackChanges) return;
|
||||
|
||||
const handleChange = () => {
|
||||
const newIsFullscreen = screenfull.isFullscreen;
|
||||
setIsFullscreen(newIsFullscreen);
|
||||
onFullscreenChange?.(newIsFullscreen);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
onFullscreenError?.(new Error('Fullscreen error occurred'));
|
||||
};
|
||||
|
||||
screenfull.on('change', handleChange);
|
||||
screenfull.on('error', handleError);
|
||||
|
||||
return () => {
|
||||
screenfull.off('change', handleChange);
|
||||
screenfull.off('error', handleError);
|
||||
};
|
||||
}, [trackChanges, onFullscreenChange, onFullscreenError]);
|
||||
|
||||
// Request fullscreen for the target element
|
||||
const enter = useCallback(async (): Promise<void> => {
|
||||
if (!screenfull.isEnabled || !elementRef.current) return;
|
||||
|
||||
try {
|
||||
await screenfull.request(elementRef.current);
|
||||
} catch (error) {
|
||||
onFullscreenError?.(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}, [elementRef, onFullscreenError]);
|
||||
|
||||
// Exit fullscreen mode
|
||||
const exit = useCallback(async (): Promise<void> => {
|
||||
if (!screenfull.isEnabled) return;
|
||||
|
||||
try {
|
||||
await screenfull.exit();
|
||||
} catch (error) {
|
||||
onFullscreenError?.(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}, [onFullscreenError]);
|
||||
|
||||
// Toggle fullscreen mode for the target element
|
||||
const toggle = useCallback(async (): Promise<void> => {
|
||||
if (!screenfull.isEnabled || !elementRef.current) return;
|
||||
|
||||
try {
|
||||
await screenfull.toggle(elementRef.current);
|
||||
} catch (error) {
|
||||
onFullscreenError?.(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}, [elementRef, onFullscreenError]);
|
||||
|
||||
return {
|
||||
isFullscreen,
|
||||
isSupported: screenfull.isEnabled,
|
||||
enter,
|
||||
exit,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
border-radius: var(--vjs-border-radius, 2rem);
|
||||
background: oklab(0 0 0);
|
||||
font-family: var(--vjs-font-family, InterVariable, Inter, ui-sans-serif, system-ui, sans-serif);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
.vjs-frosted-skin::before,
|
||||
.vjs-frosted-skin::after {
|
||||
@@ -201,8 +202,8 @@
|
||||
}
|
||||
.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='medium'] .volume-low-icon,
|
||||
.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
border-radius: var(--vjs-border-radius, 2rem);
|
||||
background: oklab(0 0 0);
|
||||
font-family: var(--vjs-font-family, InterVariable, Inter, ui-sans-serif, system-ui, sans-serif);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
.vjs-minimal-skin::after {
|
||||
content: '';
|
||||
@@ -19,11 +20,11 @@
|
||||
border-radius: inherit;
|
||||
z-index: 10;
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
|
||||
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.2);
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +181,8 @@
|
||||
}
|
||||
.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='medium'] .volume-low-icon,
|
||||
.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user