chore: rename skins, minor style tweaks (#61)

This commit is contained in:
Sam Potts
2025-10-19 10:16:47 +11:00
committed by GitHub
parent cdebece1eb
commit b63c22ef9f
23 changed files with 292 additions and 120 deletions
+1 -1
View File
@@ -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>
+2
View File
@@ -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": {
+46 -19
View File
@@ -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,
};
}
@@ -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;
@@ -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}
@@ -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,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';