From b4e01250b404ca266a44f17e23d18e29fa9f02e6 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 9 Mar 2026 14:20:53 +1100 Subject: [PATCH] feat(sandbox): rebuild sandbox with shell UI and expanded templates (#773) --- packages/sandbox/.gitignore | 1 + packages/sandbox/package.json | 3 +- packages/sandbox/scripts/sync.ts | 9 +- packages/sandbox/src/index.html | 119 +----------- packages/sandbox/src/main.tsx | 4 + packages/sandbox/templates/constants.ts | 3 + .../templates/html-audio-tailwind/index.html | 15 ++ .../templates/html-audio-tailwind/main.ts | 42 +++++ .../sandbox/templates/html-audio/index.html | 15 ++ packages/sandbox/templates/html-audio/main.ts | 42 +++++ .../html-background-video/index.html | 15 ++ .../templates/html-background-video/main.ts | 15 ++ .../templates/html-background/index.html | 25 --- .../sandbox/templates/html-background/main.ts | 21 --- .../templates/html-tailwind/index.html | 29 --- .../sandbox/templates/html-tailwind/main.ts | 44 ----- .../templates/html-video-tailwind/index.html | 15 ++ .../templates/html-video-tailwind/main.ts | 40 ++++ .../sandbox/templates/html-video/index.html | 15 ++ packages/sandbox/templates/html-video/main.ts | 40 ++++ packages/sandbox/templates/html/index.html | 28 --- packages/sandbox/templates/html/main.ts | 62 ------ packages/sandbox/templates/index.html | 124 ------------ .../templates/react-audio-tailwind/index.html | 15 ++ .../templates/react-audio-tailwind/main.tsx | 22 +++ .../sandbox/templates/react-audio/index.html | 15 ++ .../sandbox/templates/react-audio/main.tsx | 24 +++ .../react-background-video/index.html | 15 ++ .../templates/react-background-video/main.tsx | 17 ++ .../templates/react-background/background.tsx | 20 -- .../templates/react-background/index.html | 22 --- .../templates/react-tailwind/index.html | 25 --- .../sandbox/templates/react-tailwind/main.tsx | 53 ------ .../templates/react-video-tailwind/index.html | 15 ++ .../templates/react-video-tailwind/main.tsx | 22 +++ .../sandbox/templates/react-video/index.html | 15 ++ .../sandbox/templates/react-video/main.tsx | 24 +++ packages/sandbox/templates/react/index.html | 26 --- packages/sandbox/templates/react/main.tsx | 55 ------ packages/sandbox/templates/react/skins.tsx | 34 ---- .../templates/shared/html/skin-tags.ts | 13 ++ .../templates/shared/html/stylesheets.ts | 30 +++ .../templates/shared/html/tailwind-setup.ts | 18 ++ .../templates/shared/react/providers.ts | 16 ++ .../sandbox/templates/shared/react/skins.tsx | 45 +++++ .../templates/shared/react/use-skin.ts | 9 + .../templates/shared/react/use-source.ts | 9 + .../templates/shared/sandbox-listener.ts | 63 +++++++ packages/sandbox/templates/shared/sources.ts | 36 ++++ packages/sandbox/templates/shell/app.tsx | 102 ++++++++++ packages/sandbox/templates/shell/navbar.tsx | 178 ++++++++++++++++++ packages/sandbox/templates/shell/preview.tsx | 43 +++++ packages/sandbox/templates/types.ts | 5 +- .../utils/create-web-storage-store.ts | 50 +++++ .../templates/utils/use-external-store.ts | 5 + .../templates/utils/use-source-switcher.ts | 6 + packages/sandbox/tsconfig.json | 2 +- packages/sandbox/vite.config.ts | 16 +- pnpm-lock.yaml | 28 ++- 59 files changed, 1126 insertions(+), 693 deletions(-) create mode 100644 packages/sandbox/src/main.tsx create mode 100644 packages/sandbox/templates/html-audio-tailwind/index.html create mode 100644 packages/sandbox/templates/html-audio-tailwind/main.ts create mode 100644 packages/sandbox/templates/html-audio/index.html create mode 100644 packages/sandbox/templates/html-audio/main.ts create mode 100644 packages/sandbox/templates/html-background-video/index.html create mode 100644 packages/sandbox/templates/html-background-video/main.ts delete mode 100644 packages/sandbox/templates/html-background/index.html delete mode 100644 packages/sandbox/templates/html-background/main.ts delete mode 100644 packages/sandbox/templates/html-tailwind/index.html delete mode 100644 packages/sandbox/templates/html-tailwind/main.ts create mode 100644 packages/sandbox/templates/html-video-tailwind/index.html create mode 100644 packages/sandbox/templates/html-video-tailwind/main.ts create mode 100644 packages/sandbox/templates/html-video/index.html create mode 100644 packages/sandbox/templates/html-video/main.ts delete mode 100644 packages/sandbox/templates/html/index.html delete mode 100644 packages/sandbox/templates/html/main.ts delete mode 100644 packages/sandbox/templates/index.html create mode 100644 packages/sandbox/templates/react-audio-tailwind/index.html create mode 100644 packages/sandbox/templates/react-audio-tailwind/main.tsx create mode 100644 packages/sandbox/templates/react-audio/index.html create mode 100644 packages/sandbox/templates/react-audio/main.tsx create mode 100644 packages/sandbox/templates/react-background-video/index.html create mode 100644 packages/sandbox/templates/react-background-video/main.tsx delete mode 100644 packages/sandbox/templates/react-background/background.tsx delete mode 100644 packages/sandbox/templates/react-background/index.html delete mode 100644 packages/sandbox/templates/react-tailwind/index.html delete mode 100644 packages/sandbox/templates/react-tailwind/main.tsx create mode 100644 packages/sandbox/templates/react-video-tailwind/index.html create mode 100644 packages/sandbox/templates/react-video-tailwind/main.tsx create mode 100644 packages/sandbox/templates/react-video/index.html create mode 100644 packages/sandbox/templates/react-video/main.tsx delete mode 100644 packages/sandbox/templates/react/index.html delete mode 100644 packages/sandbox/templates/react/main.tsx delete mode 100644 packages/sandbox/templates/react/skins.tsx create mode 100644 packages/sandbox/templates/shared/html/skin-tags.ts create mode 100644 packages/sandbox/templates/shared/html/stylesheets.ts create mode 100644 packages/sandbox/templates/shared/html/tailwind-setup.ts create mode 100644 packages/sandbox/templates/shared/react/providers.ts create mode 100644 packages/sandbox/templates/shared/react/skins.tsx create mode 100644 packages/sandbox/templates/shared/react/use-skin.ts create mode 100644 packages/sandbox/templates/shared/react/use-source.ts create mode 100644 packages/sandbox/templates/shared/sandbox-listener.ts create mode 100644 packages/sandbox/templates/shared/sources.ts create mode 100644 packages/sandbox/templates/shell/app.tsx create mode 100644 packages/sandbox/templates/shell/navbar.tsx create mode 100644 packages/sandbox/templates/shell/preview.tsx create mode 100644 packages/sandbox/templates/utils/create-web-storage-store.ts create mode 100644 packages/sandbox/templates/utils/use-external-store.ts create mode 100644 packages/sandbox/templates/utils/use-source-switcher.ts diff --git a/packages/sandbox/.gitignore b/packages/sandbox/.gitignore index c16e81c1..e71bfe66 100644 --- a/packages/sandbox/.gitignore +++ b/packages/sandbox/.gitignore @@ -1,2 +1,3 @@ src/* !src/index.html +!src/main.tsx diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 29a1a666..6d2c237a 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -16,7 +16,8 @@ "@videojs/react": "workspace:*", "@videojs/skins": "workspace:*", "@videojs/store": "workspace:*", - "@videojs/utils": "workspace:*" + "@videojs/utils": "workspace:*", + "wouter": "^3.0.0" }, "devDependencies": { "@tailwindcss/vite": "^4.2.0", diff --git a/packages/sandbox/scripts/sync.ts b/packages/sandbox/scripts/sync.ts index ab7df404..aa49f47c 100644 --- a/packages/sandbox/scripts/sync.ts +++ b/packages/sandbox/scripts/sync.ts @@ -7,6 +7,8 @@ import prompts from 'prompts'; const SOURCE = './src'; const TEMPLATES = './templates'; +// Some file always live in src +const IGNORE = new Set(['index.html', 'main.tsx']); function colorizePatch(patch: string): string { return patch @@ -38,7 +40,12 @@ console.log(chalk.bold(`\nComparing ${SOURCE} → ${TEMPLATES}\n`)); const res = compareSync(SOURCE, TEMPLATES, { compareContent: true }); const changes = (res.diffSet ?? []).filter( - (d) => d.state !== 'equal' && d.type1 !== 'directory' && d.type2 !== 'directory' + (d) => + d.state !== 'equal' && + d.type1 !== 'directory' && + d.type2 !== 'directory' && + !IGNORE.has(d.name1 ?? '') && + !IGNORE.has(d.name2 ?? '') ); if (changes.length === 0) { diff --git a/packages/sandbox/src/index.html b/packages/sandbox/src/index.html index aeaba95b..a2779a90 100644 --- a/packages/sandbox/src/index.html +++ b/packages/sandbox/src/index.html @@ -4,121 +4,12 @@ Sandbox - - + + - -
- Video.js v10 - - - -
-
- -
- -
-
- + +
+ diff --git a/packages/sandbox/src/main.tsx b/packages/sandbox/src/main.tsx new file mode 100644 index 00000000..53004947 --- /dev/null +++ b/packages/sandbox/src/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client'; +import { App } from './shell/app'; + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/constants.ts b/packages/sandbox/templates/constants.ts index c7e91ff8..e806428f 100644 --- a/packages/sandbox/templates/constants.ts +++ b/packages/sandbox/templates/constants.ts @@ -1 +1,4 @@ export const SKINS = ['default', 'minimal'] as const; +export const PLATFORMS = ['html', 'react'] as const; +export const STYLINGS = ['css', 'tailwind'] as const; +export const PRESETS = ['video', 'audio', 'background-video'] as const; diff --git a/packages/sandbox/templates/html-audio-tailwind/index.html b/packages/sandbox/templates/html-audio-tailwind/index.html new file mode 100644 index 00000000..dbdec75b --- /dev/null +++ b/packages/sandbox/templates/html-audio-tailwind/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — HTML Audio Tailwind + + + + + +
+ + + diff --git a/packages/sandbox/templates/html-audio-tailwind/main.ts b/packages/sandbox/templates/html-audio-tailwind/main.ts new file mode 100644 index 00000000..b7fa7b9d --- /dev/null +++ b/packages/sandbox/templates/html-audio-tailwind/main.ts @@ -0,0 +1,42 @@ +import '@videojs/html/audio/player'; +import '@videojs/html/audio/skin.tailwind'; +import '@videojs/html/audio/minimal-skin.tailwind'; +import { TAILWIND_SKIN_TAGS } from '../shared/html/skin-tags'; +import { setupAudioTailwind } from '../shared/html/tailwind-setup'; +import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '../shared/sandbox-listener'; +import type { SourceId } from '../shared/sources'; +import { SOURCES } from '../shared/sources'; +import type { Skin } from '../types'; + +setupAudioTailwind(); + +const html = String.raw; + +let currentSkin: Skin = getInitialSkin(); +let currentSource: SourceId = getInitialSource(true); + +function render() { + const tag = TAILWIND_SKIN_TAGS[currentSkin].audio; + + document.getElementById('root')!.innerHTML = html` +
+ + <${tag}> + + + +
+ `; +} + +render(); + +onSkinChange((skin) => { + currentSkin = skin; + render(); +}); + +onSourceChange((source) => { + currentSource = source; + render(); +}); diff --git a/packages/sandbox/templates/html-audio/index.html b/packages/sandbox/templates/html-audio/index.html new file mode 100644 index 00000000..b1618bd2 --- /dev/null +++ b/packages/sandbox/templates/html-audio/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — HTML Audio + + + + + +
+ + + diff --git a/packages/sandbox/templates/html-audio/main.ts b/packages/sandbox/templates/html-audio/main.ts new file mode 100644 index 00000000..e7fa6650 --- /dev/null +++ b/packages/sandbox/templates/html-audio/main.ts @@ -0,0 +1,42 @@ +import '@videojs/html/audio/player'; +import '@videojs/html/audio/skin'; +import '@videojs/html/audio/minimal-skin'; +import { CSS_SKIN_TAGS } from '../shared/html/skin-tags'; +import { loadAudioStylesheets } from '../shared/html/stylesheets'; +import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '../shared/sandbox-listener'; +import type { SourceId } from '../shared/sources'; +import { SOURCES } from '../shared/sources'; +import type { Skin } from '../types'; + +const html = String.raw; + +let currentSkin: Skin = getInitialSkin(); +let currentSource: SourceId = getInitialSource(true); + +function render() { + const tag = CSS_SKIN_TAGS[currentSkin].audio; + + loadAudioStylesheets(currentSkin); + + document.getElementById('root')!.innerHTML = html` +
+ + <${tag}> + + + +
+ `; +} + +render(); + +onSkinChange((skin) => { + currentSkin = skin; + render(); +}); + +onSourceChange((source) => { + currentSource = source; + render(); +}); diff --git a/packages/sandbox/templates/html-background-video/index.html b/packages/sandbox/templates/html-background-video/index.html new file mode 100644 index 00000000..648f155b --- /dev/null +++ b/packages/sandbox/templates/html-background-video/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — HTML Background Video + + + + + +
+ + + diff --git a/packages/sandbox/templates/html-background-video/main.ts b/packages/sandbox/templates/html-background-video/main.ts new file mode 100644 index 00000000..9ddf0823 --- /dev/null +++ b/packages/sandbox/templates/html-background-video/main.ts @@ -0,0 +1,15 @@ +import '@videojs/html/background/skin.css'; +import '@videojs/html/background/player'; +import '@videojs/html/background/skin'; +import '@videojs/html/background/video'; +import { BACKGROUND_VIDEO_SRC } from '../shared/sources'; + +const html = String.raw; + +document.getElementById('root')!.innerHTML = html` + + + + + +`; diff --git a/packages/sandbox/templates/html-background/index.html b/packages/sandbox/templates/html-background/index.html deleted file mode 100644 index 119c3d25..00000000 --- a/packages/sandbox/templates/html-background/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Sandbox — HTML Background - - - - - -
- - - diff --git a/packages/sandbox/templates/html-background/main.ts b/packages/sandbox/templates/html-background/main.ts deleted file mode 100644 index 91a597a3..00000000 --- a/packages/sandbox/templates/html-background/main.ts +++ /dev/null @@ -1,21 +0,0 @@ -// HTML sandbox — Background video player -// http://localhost:5173/html-background/ - -import '@videojs/html/background/skin.css'; -import '@videojs/html/background/player'; -import '@videojs/html/background/skin'; -import '@videojs/html/background/video'; - -const html = String.raw; - -const root = document.getElementById('root'); - -if (root) { - root.innerHTML = html` - - - - - - `; -} diff --git a/packages/sandbox/templates/html-tailwind/index.html b/packages/sandbox/templates/html-tailwind/index.html deleted file mode 100644 index 2ce1b0dc..00000000 --- a/packages/sandbox/templates/html-tailwind/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - Sandbox — HTML & Tailwind - - - - - -
- -
-
- - - diff --git a/packages/sandbox/templates/html-tailwind/main.ts b/packages/sandbox/templates/html-tailwind/main.ts deleted file mode 100644 index cb66acc3..00000000 --- a/packages/sandbox/templates/html-tailwind/main.ts +++ /dev/null @@ -1,44 +0,0 @@ -// HTML & Tailwind sandbox — Web player with Tailwind skins -// http://localhost:5173/html-tailwind/ - -import '@videojs/html/video/player'; -import '@videojs/html/video/skin.tailwind'; -import '@videojs/html/video/minimal-skin.tailwind'; -import { SKINS } from '../constants'; -import type { Skin } from '../types'; - -const player = document.getElementById('player'); -const select = document.getElementById('skin-select') as HTMLSelectElement | null; - -if (player && select) { - const html = String.raw; - - const VIDEO_SRC = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4'; - - const skinTags: Record = { - default: 'video-skin-tailwind', - minimal: 'video-minimal-skin-tailwind', - }; - - function render(skin: Skin) { - const tag = skinTags[skin]; - - player!.innerHTML = html` - - <${tag}> - - - - `; - } - - for (const skin of SKINS) { - const option = document.createElement('option'); - option.value = skin; - option.textContent = skin; - select.appendChild(option); - } - select.addEventListener('change', () => render(select.value as Skin)); - - render('default'); -} diff --git a/packages/sandbox/templates/html-video-tailwind/index.html b/packages/sandbox/templates/html-video-tailwind/index.html new file mode 100644 index 00000000..da7a0139 --- /dev/null +++ b/packages/sandbox/templates/html-video-tailwind/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — HTML Video Tailwind + + + + + +
+ + + diff --git a/packages/sandbox/templates/html-video-tailwind/main.ts b/packages/sandbox/templates/html-video-tailwind/main.ts new file mode 100644 index 00000000..0547a67f --- /dev/null +++ b/packages/sandbox/templates/html-video-tailwind/main.ts @@ -0,0 +1,40 @@ +import '@videojs/html/video/player'; +import '@videojs/html/video/skin.tailwind'; +import '@videojs/html/video/minimal-skin.tailwind'; +import { TAILWIND_SKIN_TAGS } from '../shared/html/skin-tags'; +import { setupVideoTailwind } from '../shared/html/tailwind-setup'; +import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '../shared/sandbox-listener'; +import type { SourceId } from '../shared/sources'; +import { SOURCES } from '../shared/sources'; +import type { Skin } from '../types'; + +setupVideoTailwind(); + +const html = String.raw; + +let currentSkin: Skin = getInitialSkin(); +let currentSource: SourceId = getInitialSource(); + +function render() { + const tag = TAILWIND_SKIN_TAGS[currentSkin].video; + + document.getElementById('root')!.innerHTML = html` + + <${tag} class="w-full aspect-video max-w-4xl mx-auto"> + + + + `; +} + +render(); + +onSkinChange((skin) => { + currentSkin = skin; + render(); +}); + +onSourceChange((source) => { + currentSource = source; + render(); +}); diff --git a/packages/sandbox/templates/html-video/index.html b/packages/sandbox/templates/html-video/index.html new file mode 100644 index 00000000..70e9ffcf --- /dev/null +++ b/packages/sandbox/templates/html-video/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — HTML Video + + + + + +
+ + + diff --git a/packages/sandbox/templates/html-video/main.ts b/packages/sandbox/templates/html-video/main.ts new file mode 100644 index 00000000..139504d4 --- /dev/null +++ b/packages/sandbox/templates/html-video/main.ts @@ -0,0 +1,40 @@ +import '@videojs/html/video/player'; +import '@videojs/html/video/skin'; +import '@videojs/html/video/minimal-skin'; +import { CSS_SKIN_TAGS } from '../shared/html/skin-tags'; +import { loadVideoStylesheets } from '../shared/html/stylesheets'; +import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '../shared/sandbox-listener'; +import type { SourceId } from '../shared/sources'; +import { SOURCES } from '../shared/sources'; +import type { Skin } from '../types'; + +const html = String.raw; + +let currentSkin: Skin = getInitialSkin(); +let currentSource: SourceId = getInitialSource(); + +function render() { + const tag = CSS_SKIN_TAGS[currentSkin].video; + + loadVideoStylesheets(currentSkin); + + document.getElementById('root')!.innerHTML = html` + + <${tag} class="w-full aspect-video max-w-4xl mx-auto"> + + + + `; +} + +render(); + +onSkinChange((skin) => { + currentSkin = skin; + render(); +}); + +onSourceChange((source) => { + currentSource = source; + render(); +}); diff --git a/packages/sandbox/templates/html/index.html b/packages/sandbox/templates/html/index.html deleted file mode 100644 index 12510f47..00000000 --- a/packages/sandbox/templates/html/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - Sandbox — HTML - - - - - -
- -
-
- - - diff --git a/packages/sandbox/templates/html/main.ts b/packages/sandbox/templates/html/main.ts deleted file mode 100644 index 2f3518d3..00000000 --- a/packages/sandbox/templates/html/main.ts +++ /dev/null @@ -1,62 +0,0 @@ -// HTML sandbox — Web player with skin switching -// http://localhost:5173/html/ - -import '@videojs/html/video/player'; -import '@videojs/html/video/skin'; -import '@videojs/html/video/minimal-skin'; -import { SKINS } from '../constants'; -import type { Skin } from '../types'; - -const player = document.getElementById('player'); -const select = document.getElementById('skin-select') as HTMLSelectElement | null; - -if (player && select) { - const html = String.raw; - - const VIDEO_SRC = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4'; - - const skinTags: Record = { - default: 'video-skin', - minimal: 'video-minimal-skin', - }; - - const stylesheets: Record = { - default: new URL('@videojs/html/video/skin.css', import.meta.url).href, - minimal: new URL('@videojs/html/video/minimal-skin.css', import.meta.url).href, - }; - - let currentLink: HTMLLinkElement | null = null; - - function loadStylesheet(url: string) { - currentLink?.remove(); - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = url; - document.head.appendChild(link); - currentLink = link; - } - - function render(skin: Skin) { - const tag = skinTags[skin]; - - loadStylesheet(stylesheets[skin]); - - player!.innerHTML = html` - - <${tag}> - - - - `; - } - - for (const skin of SKINS) { - const option = document.createElement('option'); - option.value = skin; - option.textContent = skin; - select.appendChild(option); - } - select.addEventListener('change', () => render(select.value as Skin)); - - render('default'); -} diff --git a/packages/sandbox/templates/index.html b/packages/sandbox/templates/index.html deleted file mode 100644 index aeaba95b..00000000 --- a/packages/sandbox/templates/index.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - Sandbox - - - - - -
- Video.js v10 - - - -
- - - - diff --git a/packages/sandbox/templates/react-audio-tailwind/index.html b/packages/sandbox/templates/react-audio-tailwind/index.html new file mode 100644 index 00000000..1b58a419 --- /dev/null +++ b/packages/sandbox/templates/react-audio-tailwind/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — React Audio Tailwind + + + + + +
+ + + diff --git a/packages/sandbox/templates/react-audio-tailwind/main.tsx b/packages/sandbox/templates/react-audio-tailwind/main.tsx new file mode 100644 index 00000000..9948c4d1 --- /dev/null +++ b/packages/sandbox/templates/react-audio-tailwind/main.tsx @@ -0,0 +1,22 @@ +import { Audio } from '@videojs/react/audio'; +import { createRoot } from 'react-dom/client'; +import { AudioProvider } from '../shared/react/providers'; +import { AudioSkinComponent } from '../shared/react/skins'; +import { useSkin } from '../shared/react/use-skin'; +import { useSource } from '../shared/react/use-source'; +import { SOURCES } from '../shared/sources'; + +function App() { + const skin = useSkin(); + const source = useSource(true); + + return ( + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react-audio/index.html b/packages/sandbox/templates/react-audio/index.html new file mode 100644 index 00000000..496b0b8f --- /dev/null +++ b/packages/sandbox/templates/react-audio/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — React Audio + + + + + +
+ + + diff --git a/packages/sandbox/templates/react-audio/main.tsx b/packages/sandbox/templates/react-audio/main.tsx new file mode 100644 index 00000000..897cc6b8 --- /dev/null +++ b/packages/sandbox/templates/react-audio/main.tsx @@ -0,0 +1,24 @@ +import '@videojs/react/audio/skin.css'; +import '@videojs/react/audio/minimal-skin.css'; +import { Audio } from '@videojs/react/audio'; +import { createRoot } from 'react-dom/client'; +import { AudioProvider } from '../shared/react/providers'; +import { AudioSkinComponent } from '../shared/react/skins'; +import { useSkin } from '../shared/react/use-skin'; +import { useSource } from '../shared/react/use-source'; +import { SOURCES } from '../shared/sources'; + +function App() { + const skin = useSkin(); + const source = useSource(true); + + return ( + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react-background-video/index.html b/packages/sandbox/templates/react-background-video/index.html new file mode 100644 index 00000000..cfabe4c1 --- /dev/null +++ b/packages/sandbox/templates/react-background-video/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — React Background Video + + + + + +
+ + + diff --git a/packages/sandbox/templates/react-background-video/main.tsx b/packages/sandbox/templates/react-background-video/main.tsx new file mode 100644 index 00000000..567d6572 --- /dev/null +++ b/packages/sandbox/templates/react-background-video/main.tsx @@ -0,0 +1,17 @@ +import '@videojs/react/background/skin.css'; +import { BackgroundVideo, BackgroundVideoSkin } from '@videojs/react/background'; +import { createRoot } from 'react-dom/client'; +import { BackgroundVideoProvider } from '../shared/react/providers'; +import { BACKGROUND_VIDEO_SRC } from '../shared/sources'; + +function App() { + return ( + + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react-background/background.tsx b/packages/sandbox/templates/react-background/background.tsx deleted file mode 100644 index 7ed60e6f..00000000 --- a/packages/sandbox/templates/react-background/background.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import '@videojs/react/background/skin.css'; -import { createPlayer } from '@videojs/react'; -import { BackgroundVideo, BackgroundVideoSkin, backgroundFeatures } from '@videojs/react/background'; -import { createRoot } from 'react-dom/client'; - -const { Provider: BackgroundVideoProvider } = createPlayer({ - features: backgroundFeatures, -}); - -export function App() { - return ( - - - - - - ); -} - -createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react-background/index.html b/packages/sandbox/templates/react-background/index.html deleted file mode 100644 index 24649407..00000000 --- a/packages/sandbox/templates/react-background/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Sandbox — React Background - - - - - -
- - - diff --git a/packages/sandbox/templates/react-tailwind/index.html b/packages/sandbox/templates/react-tailwind/index.html deleted file mode 100644 index 7ea6512c..00000000 --- a/packages/sandbox/templates/react-tailwind/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Sandbox — React & Tailwind - - - - - -
- - - diff --git a/packages/sandbox/templates/react-tailwind/main.tsx b/packages/sandbox/templates/react-tailwind/main.tsx deleted file mode 100644 index 998f83fb..00000000 --- a/packages/sandbox/templates/react-tailwind/main.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// React & Tailwind Sandbox -// http://localhost:5173/react-tailwind/ - -import { createPlayer } from '@videojs/react'; -import { - MinimalVideoSkinTailwind, - Video, - type VideoSkinProps, - VideoSkinTailwind, - videoFeatures, -} from '@videojs/react/video'; -import { createRoot } from 'react-dom/client'; -import { SKINS } from '../constants'; -import type { Skin } from '../types'; -import { useSkinSwitcher } from '../utils/use-skin-switcher'; - -const { Provider } = createPlayer({ - features: videoFeatures, -}); - -function SkinComponent({ skin, ...props }: { skin: Skin } & VideoSkinProps) { - switch (skin) { - case 'default': - return ; - case 'minimal': - return ; - } -} - -function App() { - const [skin, setSkin] = useSkinSwitcher(); - - return ( -
- - - - - - -
- ); -} - -const root = document.getElementById('root'); -if (root) createRoot(root).render(); diff --git a/packages/sandbox/templates/react-video-tailwind/index.html b/packages/sandbox/templates/react-video-tailwind/index.html new file mode 100644 index 00000000..20753fb3 --- /dev/null +++ b/packages/sandbox/templates/react-video-tailwind/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — React Video Tailwind + + + + + +
+ + + diff --git a/packages/sandbox/templates/react-video-tailwind/main.tsx b/packages/sandbox/templates/react-video-tailwind/main.tsx new file mode 100644 index 00000000..be43d2c6 --- /dev/null +++ b/packages/sandbox/templates/react-video-tailwind/main.tsx @@ -0,0 +1,22 @@ +import { Video } from '@videojs/react/video'; +import { createRoot } from 'react-dom/client'; +import { VideoProvider } from '../shared/react/providers'; +import { VideoSkinComponent } from '../shared/react/skins'; +import { useSkin } from '../shared/react/use-skin'; +import { useSource } from '../shared/react/use-source'; +import { SOURCES } from '../shared/sources'; + +function App() { + const skin = useSkin(); + const source = useSource(); + + return ( + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react-video/index.html b/packages/sandbox/templates/react-video/index.html new file mode 100644 index 00000000..29ec7003 --- /dev/null +++ b/packages/sandbox/templates/react-video/index.html @@ -0,0 +1,15 @@ + + + + + + Sandbox — React Video + + + + + +
+ + + diff --git a/packages/sandbox/templates/react-video/main.tsx b/packages/sandbox/templates/react-video/main.tsx new file mode 100644 index 00000000..a21208fc --- /dev/null +++ b/packages/sandbox/templates/react-video/main.tsx @@ -0,0 +1,24 @@ +import '@videojs/react/video/skin.css'; +import '@videojs/react/video/minimal-skin.css'; +import { Video } from '@videojs/react/video'; +import { createRoot } from 'react-dom/client'; +import { VideoProvider } from '../shared/react/providers'; +import { VideoSkinComponent } from '../shared/react/skins'; +import { useSkin } from '../shared/react/use-skin'; +import { useSource } from '../shared/react/use-source'; +import { SOURCES } from '../shared/sources'; + +function App() { + const skin = useSkin(); + const source = useSource(); + + return ( + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/sandbox/templates/react/index.html b/packages/sandbox/templates/react/index.html deleted file mode 100644 index 93733687..00000000 --- a/packages/sandbox/templates/react/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - Sandbox — React - - - - - -
- - - diff --git a/packages/sandbox/templates/react/main.tsx b/packages/sandbox/templates/react/main.tsx deleted file mode 100644 index 243a3507..00000000 --- a/packages/sandbox/templates/react/main.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// React sandbox — React player -// http://localhost:5173/react/ - -import { createPlayer } from '@videojs/react'; -import { HlsVideo } from '@videojs/react/media/hls-video'; -import { videoFeatures } from '@videojs/react/video'; -import { useState } from 'react'; -import { createRoot } from 'react-dom/client'; -import { SKINS } from '../constants'; -import type { Skin } from '../types'; -import { useSkinSwitcher } from '../utils/use-skin-switcher'; -import { SkinComponent } from './skins'; - -const { Provider } = createPlayer({ - features: videoFeatures, -}); - -const HLS_SOURCES = [ - 'https://stream.mux.com/VcmKA6aqzIzlg3MayLJDnbF55kX00mds028Z65QxvBYaA.m3u8', - 'https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008.m3u8', -]; - -function App() { - const [skin, setSkin] = useSkinSwitcher(); - const [src, setSrc] = useState(HLS_SOURCES[0]); - - return ( -
- - - - - - - - - -
- ); -} - -const root = document.getElementById('root'); -if (root) createRoot(root).render(); diff --git a/packages/sandbox/templates/react/skins.tsx b/packages/sandbox/templates/react/skins.tsx deleted file mode 100644 index ae7e4074..00000000 --- a/packages/sandbox/templates/react/skins.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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 = { - 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 ; - case 'minimal': - return ; - } -} diff --git a/packages/sandbox/templates/shared/html/skin-tags.ts b/packages/sandbox/templates/shared/html/skin-tags.ts new file mode 100644 index 00000000..1adb95d5 --- /dev/null +++ b/packages/sandbox/templates/shared/html/skin-tags.ts @@ -0,0 +1,13 @@ +import type { Skin } from '../../types'; + +type SkinTagMap = Record; + +export const CSS_SKIN_TAGS: SkinTagMap = { + default: { video: 'video-skin', audio: 'audio-skin' }, + minimal: { video: 'video-minimal-skin', audio: 'audio-minimal-skin' }, +}; + +export const TAILWIND_SKIN_TAGS: SkinTagMap = { + default: { video: 'video-skin-tailwind', audio: 'audio-skin-tailwind' }, + minimal: { video: 'video-minimal-skin-tailwind', audio: 'audio-minimal-skin-tailwind' }, +}; diff --git a/packages/sandbox/templates/shared/html/stylesheets.ts b/packages/sandbox/templates/shared/html/stylesheets.ts new file mode 100644 index 00000000..c4563643 --- /dev/null +++ b/packages/sandbox/templates/shared/html/stylesheets.ts @@ -0,0 +1,30 @@ +import type { Skin } from '../../types'; + +const videoStylesheets: Record = { + default: new URL('@videojs/html/video/skin.css', import.meta.url).href, + minimal: new URL('@videojs/html/video/minimal-skin.css', import.meta.url).href, +}; + +const audioStylesheets: Record = { + default: new URL('@videojs/html/audio/skin.css', import.meta.url).href, + minimal: new URL('@videojs/html/audio/minimal-skin.css', import.meta.url).href, +}; + +function loadStylesheet(url: string) { + // Remove any existing link for this URL to force reload + const existing = document.querySelector(`link[rel="stylesheet"][href="${url}"]`); + existing?.remove(); + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + document.head.appendChild(link); +} + +export function loadVideoStylesheets(skin: Skin) { + loadStylesheet(videoStylesheets[skin]); +} + +export function loadAudioStylesheets(skin: Skin) { + loadStylesheet(audioStylesheets[skin]); +} diff --git a/packages/sandbox/templates/shared/html/tailwind-setup.ts b/packages/sandbox/templates/shared/html/tailwind-setup.ts new file mode 100644 index 00000000..d899c1b1 --- /dev/null +++ b/packages/sandbox/templates/shared/html/tailwind-setup.ts @@ -0,0 +1,18 @@ +import { MinimalAudioSkinTailwindElement } from '@videojs/html/audio/minimal-skin.tailwind'; +import { AudioSkinTailwindElement } from '@videojs/html/audio/skin.tailwind'; +import { MinimalVideoSkinTailwindElement } from '@videojs/html/video/minimal-skin.tailwind'; +import { VideoSkinTailwindElement } from '@videojs/html/video/skin.tailwind'; +import tailwindCSS from '../../styles.css?inline'; + +const tailwindStyles = new CSSStyleSheet(); +tailwindStyles.replaceSync(tailwindCSS); + +export function setupVideoTailwind() { + VideoSkinTailwindElement.styles = tailwindStyles; + MinimalVideoSkinTailwindElement.styles = tailwindStyles; +} + +export function setupAudioTailwind() { + AudioSkinTailwindElement.styles = tailwindStyles; + MinimalAudioSkinTailwindElement.styles = tailwindStyles; +} diff --git a/packages/sandbox/templates/shared/react/providers.ts b/packages/sandbox/templates/shared/react/providers.ts new file mode 100644 index 00000000..8e6980f9 --- /dev/null +++ b/packages/sandbox/templates/shared/react/providers.ts @@ -0,0 +1,16 @@ +import { createPlayer } from '@videojs/react'; +import { audioFeatures } from '@videojs/react/audio'; +import { backgroundFeatures } from '@videojs/react/background'; +import { videoFeatures } from '@videojs/react/video'; + +export const { Provider: VideoProvider } = createPlayer({ + features: videoFeatures, +}); + +export const { Provider: AudioProvider } = createPlayer({ + features: audioFeatures, +}); + +export const { Provider: BackgroundVideoProvider } = createPlayer({ + features: backgroundFeatures, +}); diff --git a/packages/sandbox/templates/shared/react/skins.tsx b/packages/sandbox/templates/shared/react/skins.tsx new file mode 100644 index 00000000..adaa2540 --- /dev/null +++ b/packages/sandbox/templates/shared/react/skins.tsx @@ -0,0 +1,45 @@ +import type { AudioSkinProps } from '@videojs/react/audio'; +import { AudioSkin, AudioSkinTailwind, MinimalAudioSkin, MinimalAudioSkinTailwind } from '@videojs/react/audio'; +import type { VideoSkinProps } from '@videojs/react/video'; +import { MinimalVideoSkin, MinimalVideoSkinTailwind, VideoSkin, VideoSkinTailwind } from '@videojs/react/video'; +import type { Skin, Styling } from '../../types'; + +type VideoSkinComponentProps = { skin: Skin; styling: Styling } & VideoSkinProps; + +export function VideoSkinComponent({ skin, styling, ...props }: VideoSkinComponentProps) { + if (styling === 'tailwind') { + switch (skin) { + case 'default': + return ; + case 'minimal': + return ; + } + } + + switch (skin) { + case 'default': + return ; + case 'minimal': + return ; + } +} + +type AudioSkinComponentProps = { skin: Skin; styling: Styling } & AudioSkinProps; + +export function AudioSkinComponent({ skin, styling, ...props }: AudioSkinComponentProps) { + if (styling === 'tailwind') { + switch (skin) { + case 'default': + return ; + case 'minimal': + return ; + } + } + + switch (skin) { + case 'default': + return ; + case 'minimal': + return ; + } +} diff --git a/packages/sandbox/templates/shared/react/use-skin.ts b/packages/sandbox/templates/shared/react/use-skin.ts new file mode 100644 index 00000000..f82b2d4a --- /dev/null +++ b/packages/sandbox/templates/shared/react/use-skin.ts @@ -0,0 +1,9 @@ +import { useEffect, useState } from 'react'; +import type { Skin } from '../../types'; +import { getInitialSkin, onSkinChange } from '../sandbox-listener'; + +export function useSkin(): Skin { + const [skin, setSkin] = useState(getInitialSkin); + useEffect(() => onSkinChange(setSkin), []); + return skin; +} diff --git a/packages/sandbox/templates/shared/react/use-source.ts b/packages/sandbox/templates/shared/react/use-source.ts new file mode 100644 index 00000000..ca55f148 --- /dev/null +++ b/packages/sandbox/templates/shared/react/use-source.ts @@ -0,0 +1,9 @@ +import { useEffect, useState } from 'react'; +import { getInitialSource, onSourceChange } from '../sandbox-listener'; +import type { SourceId } from '../sources'; + +export function useSource(audioOnly?: boolean): SourceId { + const [source, setSource] = useState(() => getInitialSource(audioOnly)); + useEffect(() => onSourceChange(setSource), []); + return source; +} diff --git a/packages/sandbox/templates/shared/sandbox-listener.ts b/packages/sandbox/templates/shared/sandbox-listener.ts new file mode 100644 index 00000000..0108eeb4 --- /dev/null +++ b/packages/sandbox/templates/shared/sandbox-listener.ts @@ -0,0 +1,63 @@ +import type { Skin } from '../types'; +import { createWebStorageStore } from '../utils/create-web-storage-store'; +import { DEFAULT_AUDIO_SOURCE, SOURCES, type SourceId } from './sources'; + +const params = new URLSearchParams(window.location.search); + +const skinStore = createWebStorageStore('local', 'skin', 'default'); +const sourceStore = createWebStorageStore('local', 'source', 'hls-1'); + +// Apply query param overrides to localStorage so they persist +const skinParam = params.get('skin') as Skin | null; +if (skinParam) skinStore.setValue(skinParam); + +const sourceParam = params.get('source') as SourceId | null; +if (sourceParam) sourceStore.setValue(sourceParam); + +export function getInitialSkin(): Skin { + return skinStore.getSnapshot(); +} + +export function onSkinChange(callback: (skin: Skin) => void): () => void { + const unsubStore = skinStore.subscribe(() => callback(skinStore.getSnapshot())); + + const handler = (event: MessageEvent) => { + if (event.data?.type === 'skin-change' && event.data.skin !== skinStore.getSnapshot()) { + skinStore.setValue(event.data.skin); + } + }; + + window.addEventListener('message', handler); + + return () => { + unsubStore(); + window.removeEventListener('message', handler); + }; +} + +export function getInitialSource(audioOnly?: boolean): SourceId { + const stored = sourceStore.getSnapshot(); + + if (audioOnly && SOURCES[stored].type !== 'mp4') { + return DEFAULT_AUDIO_SOURCE; + } + + return stored; +} + +export function onSourceChange(callback: (source: SourceId) => void): () => void { + const unsubStore = sourceStore.subscribe(() => callback(sourceStore.getSnapshot())); + + const handler = (event: MessageEvent) => { + if (event.data?.type === 'source-change' && event.data.source !== sourceStore.getSnapshot()) { + sourceStore.setValue(event.data.source); + } + }; + + window.addEventListener('message', handler); + + return () => { + unsubStore(); + window.removeEventListener('message', handler); + }; +} diff --git a/packages/sandbox/templates/shared/sources.ts b/packages/sandbox/templates/shared/sources.ts new file mode 100644 index 00000000..561055c1 --- /dev/null +++ b/packages/sandbox/templates/shared/sources.ts @@ -0,0 +1,36 @@ +export type SourceId = 'hls-1' | 'hls-2' | 'hls-3' | 'hls-4' | 'mp4-1'; + +export const SOURCES: Record = { + 'hls-1': { + label: 'HLS - Big Buck Bunny', + url: 'https://stream.mux.com/VcmKA6aqzIzlg3MayLJDnbF55kX00mds028Z65QxvBYaA.m3u8', + type: 'hls', + }, + 'hls-2': { + label: 'HLS - 2', + url: 'https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008.m3u8', + type: 'hls', + }, + 'hls-3': { + label: 'HLS - Dancing Dude', + url: 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8', + type: 'hls', + }, + 'hls-4': { + label: 'HLS - Plyr', + url: 'https://stream.mux.com/lyrKpPcGfqyzeI00jZAfW6MvP6GNPrkML.m3u8', + type: 'hls', + }, + 'mp4-1': { + label: 'MP4 - Dancing Dude', + url: 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4', + type: 'mp4', + }, +}; + +export const SOURCE_IDS = Object.keys(SOURCES) as SourceId[]; +export const MP4_SOURCE_IDS = SOURCE_IDS.filter((id) => SOURCES[id].type === 'mp4'); +export const DEFAULT_SOURCE: SourceId = 'hls-1'; +export const DEFAULT_AUDIO_SOURCE: SourceId = 'mp4-1'; + +export const BACKGROUND_VIDEO_SRC = 'https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008/low.mp4'; diff --git a/packages/sandbox/templates/shell/app.tsx b/packages/sandbox/templates/shell/app.tsx new file mode 100644 index 00000000..b705fc8e --- /dev/null +++ b/packages/sandbox/templates/shell/app.tsx @@ -0,0 +1,102 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { PLATFORMS, PRESETS, STYLINGS } from '../constants'; +import type { SourceId } from '../shared/sources'; +import { DEFAULT_AUDIO_SOURCE, MP4_SOURCE_IDS, SOURCE_IDS, SOURCES } from '../shared/sources'; +import type { Platform, Preset, Styling } from '../types'; +import { useSkinSwitcher } from '../utils/use-skin-switcher'; +import { useSourceSwitcher } from '../utils/use-source-switcher'; +import { Navbar } from './navbar'; +import { Preview } from './preview'; + +function getPagePath(platform: Platform, styling: Styling, preset: Preset): string { + if (preset === 'background-video') return `/${platform}-background-video/`; + if (styling === 'tailwind') return `/${platform}-${preset}-tailwind/`; + return `/${platform}-${preset}/`; +} + +function readParams() { + const params = new URLSearchParams(location.search); + return { + platform: (params.get('platform') ?? 'html') as Platform, + styling: (params.get('styling') ?? 'css') as Styling, + preset: (params.get('preset') ?? 'video') as Preset, + }; +} + +export function App() { + const initial = useMemo(readParams, []); + const [platform, setPlatform] = useState(initial.platform); + const [styling, setStyling] = useState(initial.styling); + const [preset, setPreset] = useState(initial.preset); + const [skin, setSkin] = useSkinSwitcher(); + const [source, setSource] = useSourceSwitcher(); + const iframeRef = useRef(null); + + // Derive the page URL + const pagePath = getPagePath(platform, styling, preset); + + // Update URL when filters change + useEffect(() => { + const params = new URLSearchParams({ platform, styling, preset }); + const url = `/?${params.toString()}`; + history.replaceState(null, '', url); + }, [platform, styling, preset]); + + // Send postMessage to iframe for skin changes (skip initial mount — iframe reads localStorage) + const skinRef = useRef(skin); + useEffect(() => { + if (skinRef.current === skin) return; + skinRef.current = skin; + iframeRef.current?.contentWindow?.postMessage({ type: 'skin-change', skin }, '*'); + }, [skin]); + + // Send postMessage to iframe for source changes (skip initial mount — iframe reads localStorage) + const sourceRef = useRef(source); + useEffect(() => { + if (sourceRef.current === source) return; + sourceRef.current = source; + iframeRef.current?.contentWindow?.postMessage({ type: 'source-change', source }, '*'); + }, [source]); + + // Constrain source to MP4 when switching to audio + useEffect(() => { + if (preset === 'audio' && SOURCES[source].type !== 'mp4') { + setSource(DEFAULT_AUDIO_SOURCE); + } + }, [preset, source, setSource]); + + // Constrain styling when switching to background-video + useEffect(() => { + if (preset === 'background-video' && styling === 'tailwind') { + setStyling('css'); + } + }, [preset, styling]); + + const availableSources = preset === 'audio' ? MP4_SOURCE_IDS : SOURCE_IDS; + + const handleSourceChange = useCallback((value: string) => setSource(value as SourceId), [setSource]); + + return ( +
+ + +
+ ); +} diff --git a/packages/sandbox/templates/shell/navbar.tsx b/packages/sandbox/templates/shell/navbar.tsx new file mode 100644 index 00000000..9b4d9ee7 --- /dev/null +++ b/packages/sandbox/templates/shell/navbar.tsx @@ -0,0 +1,178 @@ +import type { SKINS } from '../constants'; +import type { SourceId } from '../shared/sources'; +import type { Platform, Preset, Skin, Styling } from '../types'; + +type NavbarProps = { + platform: Platform; + onPlatformChange: (value: Platform) => void; + styling: Styling; + onStylingChange: (value: Styling) => void; + preset: Preset; + onPresetChange: (value: Preset) => void; + skin: Skin; + onSkinChange: (value: Skin) => void; + source: SourceId; + onSourceChange: (value: string) => void; + availableSources: readonly SourceId[]; + isBackgroundVideo: boolean; + platforms: readonly Platform[]; + stylings: readonly Styling[]; + presets: readonly Preset[]; + sources: Record; +}; + +const SKIN_OPTIONS: readonly Skin[] = ['default', 'minimal'] satisfies readonly (typeof SKINS)[number][]; + +const PLATFORM_LABELS: Record = { + html: 'HTML', + react: 'React', +}; + +const PRESET_LABELS: Record = { + video: 'Video', + audio: 'Audio', + 'background-video': 'Background Video', +}; + +export function Navbar({ + platform, + onPlatformChange, + styling, + onStylingChange, + preset, + onPresetChange, + skin, + onSkinChange, + source, + onSourceChange, + availableSources, + isBackgroundVideo, + platforms, + stylings, + presets, + sources, +}: NavbarProps) { + return ( +
+ Video.js v10 + +
+ +
+ onStylingChange(v as Styling)} + options={stylings.map((s) => ({ + value: s, + label: s === 'css' ? 'CSS' : 'Tailwind', + disabled: s === 'tailwind' && isBackgroundVideo, + }))} + /> + + onSkinChange(v as Skin)} + options={SKIN_OPTIONS.map((s) => ({ value: s, label: capitalize(s) }))} + disabled={isBackgroundVideo} + /> + + onChange(e.target.value)} + disabled={disabled} + className="h-8 appearance-none rounded-md border-none bg-clip-border ring ring-zinc-800/10 bg-white pl-3 pr-8 text-[13px] font-medium text-zinc-950 shadow-xs shadow-black/20 transition-colors hover:bg-zinc-50 focus:outline-2 focus:outline-zinc-950 focus:outline-offset-2 disabled:pointer-events-none disabled:opacity-50" + > + {options.map((opt) => ( + + ))} + + +
+
+ ); +} diff --git a/packages/sandbox/templates/shell/preview.tsx b/packages/sandbox/templates/shell/preview.tsx new file mode 100644 index 00000000..35aa384e --- /dev/null +++ b/packages/sandbox/templates/shell/preview.tsx @@ -0,0 +1,43 @@ +import { forwardRef } from 'react'; +import type { SourceId } from '../shared/sources'; +import type { Skin } from '../types'; + +type PreviewProps = { + pagePath: string; + skin: Skin; + source: SourceId; +}; + +export const Preview = forwardRef(function Preview({ pagePath, skin, source }, ref) { + const openUrl = `${pagePath}?skin=${encodeURIComponent(skin)}&source=${encodeURIComponent(source)}`; + + return ( +
+ + Open + + +