feat(skin): add error dialogs (#603)

This commit is contained in:
Sam Potts
2026-02-25 14:38:57 +11:00
committed by GitHub
parent 78112fbefd
commit ba94f74b91
25 changed files with 1641 additions and 477 deletions
+4 -3
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@videojs/core": "workspace:*",
"@videojs/html": "workspace:*",
"@videojs/icons": "workspace:*",
"@videojs/react": "workspace:*",
"@videojs/store": "workspace:*",
"@videojs/utils": "workspace:*"
@@ -18,9 +19,9 @@
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@tailwindcss/vite": "^4.1.17",
"@vitejs/plugin-react": "^4.0.0",
"tailwindcss": "^4.1.17",
"@tailwindcss/vite": "^4.2.0",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsx": "^4.0.0",
+2 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox</title>
<link rel="stylesheet" href="./styles.css" />
<link rel="stylesheet" href="../styles.css" />
</head>
<body class="p-8 font-mono text-neutral-900">
<h1 class="text-2xl font-bold mb-6">Sandbox</h1>
@@ -12,6 +12,7 @@
<li><a href="/core/" class="text-blue-600 underline hover:text-blue-800">Core</a></li>
<li><a href="/html/" class="text-blue-600 underline hover:text-blue-800">HTML</a></li>
<li><a href="/react/" class="text-blue-600 underline hover:text-blue-800">React</a></li>
<li><a href="/react-tailwind/" class="text-blue-600 underline hover:text-blue-800">React & Tailwind</a></li>
</ul>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
export const SKINS = ['default', 'minimal'] as const;
+1 -1
View File
@@ -1,7 +1,7 @@
// HTML sandbox — Web player (DOM/Browser)
// http://localhost:5173/html/
import { createPlayer, features, MediaElement } from '@videojs/html';
import { createPlayer, features } from '@videojs/html';
const { PlayerElement } = createPlayer({
features: features.video,
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox</title>
<link rel="stylesheet" href="../styles.css" />
</head>
<body class="p-8 font-mono text-neutral-900">
<h1 class="text-2xl font-bold mb-6">Sandbox</h1>
<ul class="flex gap-4">
<li><a href="/core/" class="text-blue-600 underline hover:text-blue-800">Core</a></li>
<li><a href="/html/" class="text-blue-600 underline hover:text-blue-800">HTML</a></li>
<li><a href="/react/" class="text-blue-600 underline hover:text-blue-800">React</a></li>
<li><a href="/react-tailwind/" class="text-blue-600 underline hover:text-blue-800">React & Tailwind</a></li>
</ul>
</body>
</html>
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox — React & Tailwind</title>
<link rel="stylesheet" href="../styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,46 @@
// React & Tailwind Sandbox
// http://localhost:5173/react-tailwind/
import { createPlayer, features } from '@videojs/react';
import { MinimalVideoSkinTailwind, Video, type VideoSkinProps, VideoSkinTailwind } from '@videojs/react/video';
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { SKINS } from '../constants';
import type { Skin } from '../types';
const { Provider } = createPlayer({
features: features.video,
});
function SkinComponent({ skin, ...props }: { skin: Skin } & VideoSkinProps) {
switch (skin) {
case 'default':
return <VideoSkinTailwind {...props} />;
case 'minimal':
return <MinimalVideoSkinTailwind {...props} />;
}
}
function App() {
const [skin, setSkin] = useState<Skin>('default');
return (
<div className="flex flex-col justify-center items-center gap-4 my-4">
<select value={skin} onChange={(e) => setSkin(e.target.value as Skin)} aria-label="Skin">
{SKINS.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
<Provider>
<SkinComponent skin={skin} className="w-full aspect-video max-w-4xl mx-auto">
<Video src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4" />
</SkinComponent>
</Provider>
</div>
);
}
createRoot(document.getElementById('root')!).render(<App />);
+22 -6
View File
@@ -1,21 +1,37 @@
// React sandbox — React player
// http://localhost:5173/react/
import { Container, createPlayer, features } from '@videojs/react';
import { createPlayer, features } from '@videojs/react';
import { Video } from '@videojs/react/video';
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { SKINS } from '../constants';
import type { Skin } from '../types';
import { SkinComponent } from './skins';
const { Provider } = createPlayer({
features: features.video,
});
function App() {
const [skin, setSkin] = useState<Skin>('default');
return (
<Provider>
<Container>
<Video src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4" />
</Container>
</Provider>
<div className="flex flex-col justify-center items-center gap-4 my-4">
<select value={skin} onChange={(e) => setSkin(e.target.value as Skin)} aria-label="Skin">
{SKINS.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
<Provider>
<SkinComponent skin={skin} className="w-full aspect-video max-w-4xl mx-auto">
<Video src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4" />
</SkinComponent>
</Provider>
</div>
);
}
@@ -0,0 +1,34 @@
import type { VideoSkinProps } from '@videojs/react/video';
import { MinimalVideoSkin, VideoSkin } from '@videojs/react/video';
import minimalSkinUrl from '@videojs/react/video/minimal-skin.css?url';
import defaultSkinUrl from '@videojs/react/video/skin.css?url';
import { useEffect } from 'react';
import type { Skin } from '../types';
const stylesheets: Record<Skin, string> = {
default: defaultSkinUrl,
minimal: minimalSkinUrl,
};
function useStylesheet(url: string) {
useEffect(() => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
return () => {
link.remove();
};
}, [url]);
}
export function SkinComponent({ skin, ...props }: { skin: Skin } & VideoSkinProps) {
useStylesheet(stylesheets[skin]);
switch (skin) {
case 'default':
return <VideoSkin {...props} />;
case 'minimal':
return <MinimalVideoSkin {...props} />;
}
}
+2
View File
@@ -1 +1,3 @@
@import "tailwindcss";
@source "../templates";
@source "../node_modules/@videojs/react";
+3
View File
@@ -0,0 +1,3 @@
import type { SKINS } from './constants';
export type Skin = (typeof SKINS)[number];
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+2 -1
View File
@@ -10,7 +10,7 @@ export default defineConfig({
dedupe: ['react', 'react-dom'],
},
optimizeDeps: {
exclude: ['@videojs/core', '@videojs/html', '@videojs/react', '@videojs/store', '@videojs/utils'],
exclude: ['@videojs/core', '@videojs/html', '@videojs/icons', '@videojs/react', '@videojs/store', '@videojs/utils'],
},
build: {
outDir: resolve(__dirname, 'dist'),
@@ -21,6 +21,7 @@ export default defineConfig({
core: resolve(__dirname, 'src/core/index.html'),
html: resolve(__dirname, 'src/html/index.html'),
react: resolve(__dirname, 'src/react/index.html'),
'react-tailwind': resolve(__dirname, 'src/react-tailwind/index.html'),
},
},
},