feat(ui): add toasted skin

This commit is contained in:
Sam Potts
2025-10-04 19:26:02 +10:00
committed by GitHub
parent 5195c09af6
commit bdd2f36e13
17 changed files with 475 additions and 300 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>
<body class="bg-white text-stone-700 dark:bg-stone-900 dark:text-stone-200">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
+149
View File
@@ -0,0 +1,149 @@
import { MediaProvider, MediaSkinDefault, MediaSkinToasted, Video } from '@vjs-10/react';
import './globals.css';
import { useCallback, useMemo, useState, type ChangeEventHandler } from 'react';
const skins = [{
key: 'default',
name: 'Frosted',
component: MediaSkinDefault,
}, {
key: 'toasted',
name: 'Toasted',
component: MediaSkinToasted,
}] as const;
type SkinKey = (typeof skins)[number]['key'];
const mediaSources = [{
key: '1',
name: 'Mux 1',
value: 'https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M.m3u8'
}, {
key: '2',
name: 'Mux 2',
value: 'https://stream.mux.com/fXNzVtmtWuyz00xnSrJg4OJH6PyNo6D02UzmgeKGkP5YQ.m3u8'
}, {
key: '3',
name: 'Mux 3',
value: 'https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4'
}, {
key: '4',
name: 'Mux 4',
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 = 'toasted';
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 skinClassName = useMemo(() => {
switch (skinKey) {
case 'default':
return 'rounded-4xl shadow shadow-lg shadow-black/15';
case 'toasted':
return 'rounded-lg shadow shadow-lg shadow-black/15';
default:
return '';
}
}, [skinKey]);
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 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-4xl mx-auto p-6'>
<MediaProvider key={key}>
<Skin className={skinClassName}>
{/* @ts-ignore -- types are incorrect */}
<Video src={mediaSource} />
</Skin>
</MediaProvider>
</div>
</main>
</>
);
}
+5 -7
View File
@@ -4,18 +4,16 @@
@source "../../../packages/react";
@theme {
--font-sans:
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-sans: InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.body {
@apply antialiased font-[510] font-sans text-[0.8125rem] @7xl/root:text-[0.9375rem] leading-normal tracking-[-0.0125em];
}
:root {
font-feature-settings: 'cv01', 'ss01', 'ss03';
}
/* Make hocus (hover + focus-visible) variants */
@custom-variant hocus (&:is(:hover, :focus-visible));
@custom-variant group-hocus (&:is(:hover, :focus-visible) &);
@custom-variant peer-hocus (&:is(:hover, :focus-visible) ~ &);
/* Make reduced-transparency variant */
@custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce);
+8 -36
View File
@@ -1,38 +1,10 @@
import ReactDOM from 'react-dom/client';
import { MediaProvider, MediaSkinDefault, Video } from '@vjs-10/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './globals.css';
function DemoPlayer() {
return (
<div className="min-h-screen grid place-items-center px-6">
<div className="space-y-12 w-full">
<MediaProvider>
<div className="w-full max-w-4xl mx-auto">
<MediaSkinDefault className="rounded-4xl aspect-video">
{/* @ts-ignore -- types are incorrect */}
{/* <Video
muted
src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4"
/> */}
{/* @ts-ignore -- types are incorrect */}
<Video muted src="https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M.m3u8" />
</MediaSkinDefault>
</div>
</MediaProvider>
<MediaProvider>
<div className="w-full max-w-4xl mx-auto">
<MediaSkinDefault className="aspect-video">
{/* @ts-ignore -- types are incorrect */}
<Video muted src="https://stream.mux.com/fXNzVtmtWuyz00xnSrJg4OJH6PyNo6D02UzmgeKGkP5YQ.m3u8" />
</MediaSkinDefault>
</div>
</MediaProvider>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(<DemoPlayer />);
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);