mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore: rename skins, minor style tweaks (#61)
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body class="bg-white text-stone-700 dark:bg-stone-900 dark:text-stone-200">
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
"dependencies": {
|
||||
"@vjs-10/react": "workspace:*",
|
||||
"@vjs-10/react-icons": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"tailwindcss": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
import { MediaProvider, MediaSkinDefault, MediaSkinToasted, Video } from '@vjs-10/react';
|
||||
import { FrostedSkin, MediaProvider, MinimalSkin, Video } from '@vjs-10/react';
|
||||
import { FullscreenEnterAltIcon, FullscreenExitAltIcon } from '@vjs-10/react-icons';
|
||||
|
||||
// import FrostedSkin from './skins/frosted/FrostedSkin';
|
||||
// import MinimalSkin from './skins/toasted/MinimalSkin';
|
||||
|
||||
import clsx from 'clsx';
|
||||
// NOTE: Commented out imports are for testing locally/externally defined skins.
|
||||
// import { MediaProvider, Video } from '@vjs-10/react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
// import MediaSkinDefault from './skins/frosted/MediaSkinDefault';
|
||||
// import MediaSkinToasted from './skins/toasted/MediaSkinToasted';
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useFullscreen } from './hooks/useFullscreen';
|
||||
import './globals.css';
|
||||
|
||||
const skins = [
|
||||
{
|
||||
key: 'default',
|
||||
key: 'frosted',
|
||||
name: 'Frosted',
|
||||
component: MediaSkinDefault,
|
||||
component: FrostedSkin,
|
||||
},
|
||||
{
|
||||
key: 'toasted',
|
||||
name: 'Toasted',
|
||||
component: MediaSkinToasted,
|
||||
key: 'minimal',
|
||||
name: 'Minimal',
|
||||
component: MinimalSkin,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -62,12 +65,14 @@ function setParam(key: string, value: string) {
|
||||
window.history.replaceState(null, '', url);
|
||||
}
|
||||
|
||||
const DEFAULT_SKIN: SkinKey = 'toasted';
|
||||
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 containerRef = useRef<HTMLDivElement>(null);
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(containerRef);
|
||||
|
||||
const mediaSource = useMemo(() => {
|
||||
let match = mediaSources.find(m => m.key === mediaSourceKey);
|
||||
@@ -103,10 +108,10 @@ export default function App(): JSX.Element {
|
||||
|
||||
const skinClassName = useMemo(() => {
|
||||
switch (skinKey) {
|
||||
case 'default':
|
||||
case 'frosted':
|
||||
return 'rounded-4xl shadow shadow-lg shadow-black/15';
|
||||
case 'toasted':
|
||||
return 'rounded-lg shadow shadow-lg shadow-black/15';
|
||||
case 'minimal':
|
||||
return 'rounded-2xl shadow shadow-lg shadow-black/15';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -116,8 +121,20 @@ export default function App(): JSX.Element {
|
||||
const poster = playbackId ? `https://image.mux.com/${playbackId}/thumbnail.webp` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="fixed top-0 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">
|
||||
<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="grid grid-cols-5 h-2" aria-hidden="true">
|
||||
<div className="bg-yellow-500"></div>
|
||||
<div className="bg-orange-500"></div>
|
||||
@@ -147,11 +164,21 @@ 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="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">
|
||||
<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,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="w-full max-w-5xl mx-auto p-6">
|
||||
<MediaProvider key={key}>
|
||||
<Skin className={skinClassName}>
|
||||
@@ -161,6 +188,6 @@ export default function App(): JSX.Element {
|
||||
</MediaProvider>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ type SkinProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default function MediaSkinDefault({ children, className = '' }: SkinProps): JSX.Element {
|
||||
export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element {
|
||||
return (
|
||||
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
|
||||
{children}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MediaDefaultSkinStyles } from './types';
|
||||
import type { FrostedSkinStyles } from './types';
|
||||
|
||||
// NOTE: Removing import to sidestep for compiler complexity (CJP)
|
||||
// import { cn } from '../../utils/cn';
|
||||
@@ -9,7 +9,7 @@ function cn(...classes: (string | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
const styles: MediaDefaultSkinStyles = {
|
||||
const styles: FrostedSkinStyles = {
|
||||
MediaContainer: cn(
|
||||
'relative @container/root group/root overflow-clip',
|
||||
// Base typography
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface MediaDefaultSkinStyles {
|
||||
export interface FrostedSkinStyles {
|
||||
readonly MediaContainer: string;
|
||||
readonly Overlay: string;
|
||||
readonly Controls: string;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ type SkinProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default function MediaSkinToasted({ children, className = '' }: SkinProps): JSX.Element {
|
||||
export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element {
|
||||
return (
|
||||
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
|
||||
{children}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import type { MediaToastedSkinStyles } from './types';
|
||||
import type { MinimalSkinStyles } from './types';
|
||||
|
||||
// NOTE: Removing import to sidestep for compiler complexity (CJP)
|
||||
// import { cn } from '../../utils/cn';
|
||||
@@ -9,7 +9,7 @@ function cn(...classes: (string | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
const styles: MediaToastedSkinStyles = {
|
||||
const styles: MinimalSkinStyles = {
|
||||
MediaContainer: cn(
|
||||
'relative @container/root group/root overflow-clip bg-black',
|
||||
// Base typography
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface MediaToastedSkinStyles {
|
||||
export interface MinimalSkinStyles {
|
||||
readonly MediaContainer: string;
|
||||
readonly Overlay: string;
|
||||
readonly Controls: string;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as MediaSkinToasted } from './MediaSkinToasted';
|
||||
Reference in New Issue
Block a user