diff --git a/commitlint.config.js b/commitlint.config.js
index f5e52474..5300bb12 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -27,6 +27,7 @@ export default {
'react',
'rfc',
'root',
+ 'sandbox',
'site',
'store',
'test',
diff --git a/packages/sandbox/.gitignore b/packages/sandbox/.gitignore
new file mode 100644
index 00000000..c16e81c1
--- /dev/null
+++ b/packages/sandbox/.gitignore
@@ -0,0 +1,2 @@
+src/*
+!src/index.html
diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json
new file mode 100644
index 00000000..bc6d7c8d
--- /dev/null
+++ b/packages/sandbox/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@videojs/sandbox",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "tsx setup.ts && vite",
+ "build": "tsx setup.ts && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@videojs/core": "workspace:*",
+ "@videojs/html": "workspace:*",
+ "@videojs/react": "workspace:*",
+ "@videojs/store": "workspace:*",
+ "@videojs/utils": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.0",
+ "@types/react-dom": "^18.0.0",
+ "@tailwindcss/vite": "^4.1.17",
+ "@vitejs/plugin-react": "^4.0.0",
+ "tailwindcss": "^4.1.17",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "tsx": "^4.0.0",
+ "typescript": "^5.0.0",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/packages/sandbox/setup.ts b/packages/sandbox/setup.ts
new file mode 100644
index 00000000..5e687e09
--- /dev/null
+++ b/packages/sandbox/setup.ts
@@ -0,0 +1,26 @@
+import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+const root = import.meta.dirname;
+const templatesDir = resolve(root, 'templates');
+const srcDir = resolve(root, 'src');
+
+function mirror(dir: string) {
+ for (const entry of readdirSync(dir)) {
+ const templatePath = resolve(dir, entry);
+ const targetPath = resolve(srcDir, templatePath.slice(templatesDir.length + 1));
+
+ if (statSync(templatePath).isDirectory()) {
+ mkdirSync(targetPath, { recursive: true });
+ mirror(templatePath);
+ continue;
+ }
+
+ if (!existsSync(targetPath)) {
+ copyFileSync(templatePath, targetPath);
+ console.log(`Created ${targetPath.slice(root.length + 1)}`);
+ }
+ }
+}
+
+mirror(templatesDir);
diff --git a/packages/sandbox/src/index.html b/packages/sandbox/src/index.html
new file mode 100644
index 00000000..3d44cbce
--- /dev/null
+++ b/packages/sandbox/src/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Sandbox
+
+
+
+ Sandbox
+
+
+
diff --git a/packages/sandbox/templates/core/index.html b/packages/sandbox/templates/core/index.html
new file mode 100644
index 00000000..b90c8272
--- /dev/null
+++ b/packages/sandbox/templates/core/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Sandbox — Core
+
+
+
+
+
+
+
diff --git a/packages/sandbox/templates/core/main.ts b/packages/sandbox/templates/core/main.ts
new file mode 100644
index 00000000..df3154ac
--- /dev/null
+++ b/packages/sandbox/templates/core/main.ts
@@ -0,0 +1,3 @@
+// http://localhost:5173/core/
+
+// import { } from '@videojs/core';
diff --git a/packages/sandbox/templates/html/index.html b/packages/sandbox/templates/html/index.html
new file mode 100644
index 00000000..89b2c294
--- /dev/null
+++ b/packages/sandbox/templates/html/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Sandbox — HTML
+
+
+
+
+
+
+
diff --git a/packages/sandbox/templates/html/main.ts b/packages/sandbox/templates/html/main.ts
new file mode 100644
index 00000000..9fae361c
--- /dev/null
+++ b/packages/sandbox/templates/html/main.ts
@@ -0,0 +1,18 @@
+// HTML sandbox — Web player (DOM/Browser)
+// http://localhost:5173/html/
+
+import { createPlayer, features, MediaElement } from '@videojs/html';
+
+const { PlayerElement } = createPlayer({
+ features: features.video,
+});
+
+customElements.define('video-player', PlayerElement);
+
+const html = String.raw;
+
+document.getElementById('root')!.innerHTML = html`
+
+
+
+`;
diff --git a/packages/sandbox/templates/react/index.html b/packages/sandbox/templates/react/index.html
new file mode 100644
index 00000000..72bb6f58
--- /dev/null
+++ b/packages/sandbox/templates/react/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Sandbox — React
+
+
+
+
+
+
+
diff --git a/packages/sandbox/templates/react/main.tsx b/packages/sandbox/templates/react/main.tsx
new file mode 100644
index 00000000..7e67d936
--- /dev/null
+++ b/packages/sandbox/templates/react/main.tsx
@@ -0,0 +1,21 @@
+// React sandbox — React player
+// http://localhost:5173/react/
+
+import { Container, createPlayer, features, Video } from '@videojs/react';
+import { createRoot } from 'react-dom/client';
+
+const { Provider } = createPlayer({
+ features: features.video,
+});
+
+function App() {
+ return (
+
+
+
+
+
+ );
+}
+
+createRoot(document.getElementById('root')!).render();
diff --git a/packages/sandbox/templates/styles.css b/packages/sandbox/templates/styles.css
new file mode 100644
index 00000000..f1d8c73c
--- /dev/null
+++ b/packages/sandbox/templates/styles.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/packages/sandbox/tsconfig.json b/packages/sandbox/tsconfig.json
new file mode 100644
index 00000000..221e6c92
--- /dev/null
+++ b/packages/sandbox/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": false,
+ "noEmit": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx"
+ },
+ "include": ["src"]
+}
diff --git a/packages/sandbox/vite.config.ts b/packages/sandbox/vite.config.ts
new file mode 100644
index 00000000..2e7b2fcd
--- /dev/null
+++ b/packages/sandbox/vite.config.ts
@@ -0,0 +1,27 @@
+import { resolve } from 'node:path';
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ root: 'src',
+ plugins: [tailwindcss(), react()],
+ resolve: {
+ dedupe: ['react', 'react-dom'],
+ },
+ optimizeDeps: {
+ exclude: ['@videojs/core', '@videojs/html', '@videojs/react', '@videojs/store', '@videojs/utils'],
+ },
+ build: {
+ outDir: resolve(__dirname, 'dist'),
+ emptyOutDir: true,
+ rollupOptions: {
+ input: {
+ main: resolve(__dirname, 'src/index.html'),
+ core: resolve(__dirname, 'src/core/index.html'),
+ html: resolve(__dirname, 'src/html/index.html'),
+ react: resolve(__dirname, 'src/react/index.html'),
+ },
+ },
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 65e9b524..e720c7b4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -261,6 +261,55 @@ importers:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
+ packages/sandbox:
+ dependencies:
+ '@videojs/core':
+ specifier: workspace:*
+ version: link:../core
+ '@videojs/html':
+ specifier: workspace:*
+ version: link:../html
+ '@videojs/react':
+ specifier: workspace:*
+ version: link:../react
+ '@videojs/store':
+ specifier: workspace:*
+ version: link:../store
+ '@videojs/utils':
+ specifier: workspace:*
+ version: link:../utils
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.1.17
+ version: 4.1.18(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
+ '@types/react':
+ specifier: ^18.0.0
+ version: 18.3.27
+ '@types/react-dom':
+ specifier: ^18.0.0
+ version: 18.3.7(@types/react@18.3.27)
+ '@vitejs/plugin-react':
+ specifier: ^4.0.0
+ version: 4.7.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
+ react:
+ specifier: ^18.0.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.0.0
+ version: 18.3.1(react@18.3.1)
+ tailwindcss:
+ specifier: ^4.1.17
+ version: 4.1.18
+ tsx:
+ specifier: ^4.0.0
+ version: 4.21.0
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+ vite:
+ specifier: ^6.0.0
+ version: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
+
packages/store:
dependencies:
'@videojs/utils':
@@ -2646,6 +2695,11 @@ packages:
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+ '@types/react-dom@18.3.7':
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
+ peerDependencies:
+ '@types/react': ^18.0.0
+
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -8934,6 +8988,13 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
+ '@tailwindcss/vite@4.1.18(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
+ dependencies:
+ '@tailwindcss/node': 4.1.18
+ '@tailwindcss/oxide': 4.1.18
+ tailwindcss: 4.1.18
+ vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
+
'@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@tailwindcss/node': 4.1.18
@@ -9101,6 +9162,10 @@ snapshots:
'@types/prop-types@15.7.15': {}
+ '@types/react-dom@18.3.7(@types/react@18.3.27)':
+ dependencies:
+ '@types/react': 18.3.27
+
'@types/react-dom@19.2.3(@types/react@19.2.7)':
dependencies:
'@types/react': 19.2.7
@@ -9313,7 +9378,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.3)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
'@vitest/utils@3.2.4':
dependencies:
@@ -13328,7 +13393,7 @@ snapshots:
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.2.7(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
+ vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- '@types/node'
- jiti