diff --git a/packages/sandbox/app/main.tsx b/packages/sandbox/app/main.tsx
index 38834311..8aaf6380 100644
--- a/packages/sandbox/app/main.tsx
+++ b/packages/sandbox/app/main.tsx
@@ -1,16 +1,5 @@
-import type { SourceId } from '@app/shared/sources';
import '@app/styles.css';
import { App } from '@app/shell/app';
-import type { Skin } from '@app/types';
-import { skinStore, sourceStore } from '@app/utils/stores';
import { createRoot } from 'react-dom/client';
-// Sync query-param overrides to localStorage BEFORE React mounts,
-// so useWebStorage hooks pick up the correct initial values.
-const params = new URLSearchParams(location.search);
-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);
-
createRoot(document.getElementById('root')!).render();
diff --git a/packages/sandbox/app/shared/html/sandbox-state.ts b/packages/sandbox/app/shared/html/sandbox-state.ts
new file mode 100644
index 00000000..0906da5a
--- /dev/null
+++ b/packages/sandbox/app/shared/html/sandbox-state.ts
@@ -0,0 +1,39 @@
+import { getInitialSkin, getInitialSource } from '@app/shared/sandbox-listener';
+import type { SourceId } from '@app/shared/sources';
+import type { Skin, Styling } from '@app/types';
+
+function getInitialStyling(): Styling {
+ return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
+}
+
+export type HtmlSandboxState = {
+ skin: Skin;
+ source: SourceId;
+ styling: Styling;
+};
+
+export function createHtmlSandboxState(audioOnly?: boolean): HtmlSandboxState {
+ return {
+ skin: getInitialSkin(),
+ source: getInitialSource(audioOnly),
+ styling: getInitialStyling(),
+ };
+}
+
+export function createLatestLoader() {
+ let loadVersion = 0;
+
+ return async (load: () => Promise): Promise => {
+ const version = ++loadVersion;
+ try {
+ const result = await load();
+ return version === loadVersion ? result : undefined;
+ } catch (error) {
+ // Swallow load errors to avoid unhandled promise rejections in callers
+ // that do not await the returned promise. Callers can treat `undefined`
+ // as a signal that no valid result is available.
+ console.error('Failed to load latest result', error);
+ return undefined;
+ }
+ };
+}
diff --git a/packages/sandbox/app/shared/html/skins.ts b/packages/sandbox/app/shared/html/skins.ts
new file mode 100644
index 00000000..8b68e14c
--- /dev/null
+++ b/packages/sandbox/app/shared/html/skins.ts
@@ -0,0 +1,67 @@
+import type { Skin, Styling } from '@app/types';
+import { CSS_SKIN_TAGS, TAILWIND_SKIN_TAGS } from './skin-tags';
+import { loadAudioStylesheets, loadVideoStylesheets } from './stylesheets';
+
+async function loadVideoCssSkin(skin: Skin): Promise {
+ if (skin === 'default') {
+ await import('@videojs/html/video/skin');
+ } else {
+ await import('@videojs/html/video/minimal-skin');
+ }
+
+ loadVideoStylesheets(skin);
+
+ return CSS_SKIN_TAGS[skin].video;
+}
+
+async function loadAudioCssSkin(skin: Skin): Promise {
+ if (skin === 'default') {
+ await import('@videojs/html/audio/skin');
+ } else {
+ await import('@videojs/html/audio/minimal-skin');
+ }
+
+ loadAudioStylesheets(skin);
+
+ return CSS_SKIN_TAGS[skin].audio;
+}
+
+async function loadVideoTailwindSkin(skin: Skin): Promise {
+ if (skin === 'default') {
+ const { VideoSkinTailwindElement } = await import('@videojs/html/video/skin.tailwind');
+ const { getTailwindStyles } = await import('./tailwind-setup');
+
+ VideoSkinTailwindElement.styles = getTailwindStyles();
+ } else {
+ const { MinimalVideoSkinTailwindElement } = await import('@videojs/html/video/minimal-skin.tailwind');
+ const { getTailwindStyles } = await import('./tailwind-setup');
+
+ MinimalVideoSkinTailwindElement.styles = getTailwindStyles();
+ }
+
+ return TAILWIND_SKIN_TAGS[skin].video;
+}
+
+async function loadAudioTailwindSkin(skin: Skin): Promise {
+ if (skin === 'default') {
+ const { AudioSkinTailwindElement } = await import('@videojs/html/audio/skin.tailwind');
+ const { getTailwindStyles } = await import('./tailwind-setup');
+
+ AudioSkinTailwindElement.styles = getTailwindStyles();
+ } else {
+ const { MinimalAudioSkinTailwindElement } = await import('@videojs/html/audio/minimal-skin.tailwind');
+ const { getTailwindStyles } = await import('./tailwind-setup');
+
+ MinimalAudioSkinTailwindElement.styles = getTailwindStyles();
+ }
+
+ return TAILWIND_SKIN_TAGS[skin].audio;
+}
+
+export function loadVideoSkinTag(skin: Skin, styling: Styling): Promise {
+ return styling === 'tailwind' ? loadVideoTailwindSkin(skin) : loadVideoCssSkin(skin);
+}
+
+export function loadAudioSkinTag(skin: Skin, styling: Styling): Promise {
+ return styling === 'tailwind' ? loadAudioTailwindSkin(skin) : loadAudioCssSkin(skin);
+}
diff --git a/packages/sandbox/app/shared/html/stylesheets.ts b/packages/sandbox/app/shared/html/stylesheets.ts
index c4563643..c89fdeee 100644
--- a/packages/sandbox/app/shared/html/stylesheets.ts
+++ b/packages/sandbox/app/shared/html/stylesheets.ts
@@ -10,21 +10,21 @@ const audioStylesheets: Record = {
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}"]`);
+function loadStylesheet(id: string, url: string) {
+ const existing = document.querySelector(`link[rel="stylesheet"][data-sandbox-stylesheet="${id}"]`);
existing?.remove();
const link = document.createElement('link');
+ link.dataset.sandboxStylesheet = id;
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
}
export function loadVideoStylesheets(skin: Skin) {
- loadStylesheet(videoStylesheets[skin]);
+ loadStylesheet('video-skin', videoStylesheets[skin]);
}
export function loadAudioStylesheets(skin: Skin) {
- loadStylesheet(audioStylesheets[skin]);
+ loadStylesheet('audio-skin', audioStylesheets[skin]);
}
diff --git a/packages/sandbox/app/shared/html/tailwind-setup.ts b/packages/sandbox/app/shared/html/tailwind-setup.ts
index 9c0336a8..1774c06d 100644
--- a/packages/sandbox/app/shared/html/tailwind-setup.ts
+++ b/packages/sandbox/app/shared/html/tailwind-setup.ts
@@ -1,18 +1,8 @@
import tailwindCSS from '@app/styles.css?inline';
-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';
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;
+export function getTailwindStyles(): CSSStyleSheet {
+ return tailwindStyles;
}
diff --git a/packages/sandbox/app/shared/react/skins.tsx b/packages/sandbox/app/shared/react/skins.tsx
index 2314371a..cf8c2cf3 100644
--- a/packages/sandbox/app/shared/react/skins.tsx
+++ b/packages/sandbox/app/shared/react/skins.tsx
@@ -1,45 +1,87 @@
import type { Skin, Styling } from '@app/types';
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 { ComponentType } from 'react';
+import { useEffect, useState } from 'react';
+
+async function loadVideoSkinComponent(skin: Skin, styling: Styling): Promise> {
+ const module = await import('@videojs/react/video');
+
+ if (styling === 'tailwind') {
+ return skin === 'default' ? module.VideoSkinTailwind : module.MinimalVideoSkinTailwind;
+ }
+
+ if (skin === 'default') {
+ await import('@videojs/react/video/skin.css');
+ return module.VideoSkin;
+ }
+
+ await import('@videojs/react/video/minimal-skin.css');
+ return module.MinimalVideoSkin;
+}
+
+async function loadAudioSkinComponent(skin: Skin, styling: Styling): Promise> {
+ const module = await import('@videojs/react/audio');
+
+ if (styling === 'tailwind') {
+ return skin === 'default' ? module.AudioSkinTailwind : module.MinimalAudioSkinTailwind;
+ }
+
+ if (skin === 'default') {
+ await import('@videojs/react/audio/skin.css');
+ return module.AudioSkin;
+ }
+
+ await import('@videojs/react/audio/minimal-skin.css');
+ return module.MinimalAudioSkin;
+}
+
+function useLoadedComponent(
+ load: () => Promise>,
+ deps: readonly unknown[]
+): ComponentType | null {
+ const [component, setComponent] = useState | null>(null);
+
+ useEffect(() => {
+ let active = true;
+
+ void load()
+ .then((resolved) => {
+ if (!active) return;
+
+ setComponent(() => resolved);
+ })
+ .catch(() => {
+ if (!active) return;
+ // Intentionally ignore load errors to avoid unhandled promise rejections.
+ // The component will remain null, and callers can handle absence as needed.
+ });
+
+ return () => {
+ active = false;
+ };
+ // biome-ignore lint/correctness/useExhaustiveDependencies: we're proxying the deps
+ }, deps);
+
+ return component;
+}
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 ;
- }
- }
+ const Component = useLoadedComponent(() => loadVideoSkinComponent(skin, styling), [skin, styling]);
- switch (skin) {
- case 'default':
- return ;
- case 'minimal':
- return ;
- }
+ if (!Component) return null;
+
+ 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 ;
- }
- }
+ const Component = useLoadedComponent(() => loadAudioSkinComponent(skin, styling), [skin, styling]);
- switch (skin) {
- case 'default':
- return ;
- case 'minimal':
- return ;
- }
+ if (!Component) return null;
+
+ return ;
}
diff --git a/packages/sandbox/app/shared/sandbox-listener.ts b/packages/sandbox/app/shared/sandbox-listener.ts
index 8ec0b86a..b2d38aa9 100644
--- a/packages/sandbox/app/shared/sandbox-listener.ts
+++ b/packages/sandbox/app/shared/sandbox-listener.ts
@@ -1,42 +1,45 @@
+import { SKINS } from '@app/constants';
import type { Skin } from '@app/types';
-import { createWebStorageStore } from '@app/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');
+function readSkin(): Skin {
+ const skin = params.get('skin');
-// Apply query param overrides to localStorage so they persist
-const skinParam = params.get('skin') as Skin | null;
-if (skinParam) skinStore.setValue(skinParam);
+ return skin && SKINS.includes(skin as Skin) ? (skin as Skin) : 'default';
+}
-const sourceParam = params.get('source') as SourceId | null;
-if (sourceParam) sourceStore.setValue(sourceParam);
+function readSource(): SourceId {
+ const source = params.get('source');
+
+ return source && source in SOURCES ? (source as SourceId) : 'hls-1';
+}
+
+let currentSkin = readSkin();
+let currentSource = readSource();
export function getInitialSkin(): Skin {
- return skinStore.getSnapshot();
+ return currentSkin;
}
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);
- }
+ if (event.data?.type !== 'skin-change' || !SKINS.includes(event.data.skin)) return;
+
+ currentSkin = event.data.skin;
+ callback(currentSkin);
};
window.addEventListener('message', handler);
return () => {
- unsubStore();
window.removeEventListener('message', handler);
};
}
export function getInitialSource(audioOnly?: boolean): SourceId {
- const stored = sourceStore.getSnapshot();
+ const stored = currentSource;
if (audioOnly && SOURCES[stored].type !== 'mp4') {
return DEFAULT_AUDIO_SOURCE;
@@ -46,18 +49,16 @@ export function getInitialSource(audioOnly?: boolean): SourceId {
}
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);
- }
+ if (event.data?.type !== 'source-change' || !(event.data.source in SOURCES)) return;
+
+ currentSource = event.data.source;
+ callback(currentSource);
};
window.addEventListener('message', handler);
return () => {
- unsubStore();
window.removeEventListener('message', handler);
};
}
diff --git a/packages/sandbox/app/shell/app.tsx b/packages/sandbox/app/shell/app.tsx
index 1fc2b2f7..4f91ac78 100644
--- a/packages/sandbox/app/shell/app.tsx
+++ b/packages/sandbox/app/shell/app.tsx
@@ -9,15 +9,12 @@ import {
SOURCES,
} from '@app/shared/sources';
import type { Platform, Preset, Styling } from '@app/types';
-import { useSkinSwitcher } from '@app/utils/use-skin-switcher';
-import { useSourceSwitcher } from '@app/utils/use-source-switcher';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Navbar } from './navbar';
import { Preview } from './preview';
-function getPagePath(platform: Platform, styling: Styling, preset: Preset): string {
+function getPagePath(platform: Platform, preset: Preset): string {
if (preset === 'background-video') return `/${platform}-background-video/`;
- if (styling === 'tailwind') return `/${platform}-${preset}-tailwind/`;
return `/${platform}-${preset}/`;
}
@@ -27,19 +24,21 @@ function readParams() {
platform: (params.get('platform') ?? 'html') as Platform,
styling: (params.get('styling') ?? 'css') as Styling,
preset: (params.get('preset') ?? 'video') as Preset,
+ skin: (params.get('skin') ?? 'default') as 'default' | 'minimal',
+ source: (params.get('source') ?? 'hls-1') as SourceId,
};
}
export function App() {
const initial = useMemo(readParams, []);
const [platform, setPlatform] = useState(initial.platform);
- const [styling, setStyling] = useState(initial.styling);
+ const [styling, setStyling] = useState(initial.styling);
const [preset, setPreset] = useState(initial.preset);
- const [skin, setSkin] = useSkinSwitcher();
- const [source, setSource] = useSourceSwitcher();
+ const [skin, setSkin] = useState(initial.skin);
+ const [source, setSource] = useState(initial.source);
const iframeRef = useRef(null);
- const pagePath = getPagePath(platform, styling, preset);
+ const pagePath = getPagePath(platform, preset);
// Keep the URL in sync with all state (including skin + source)
useEffect(() => {
@@ -47,23 +46,11 @@ export function App() {
history.replaceState(null, '', `/?${params}`);
}, [platform, styling, preset, skin, source]);
- // Send postMessage to iframe for skin changes (skip initial mount — iframe reads localStorage)
- const skinMountRef = useRef(true);
useEffect(() => {
- if (skinMountRef.current) {
- skinMountRef.current = false;
- return;
- }
iframeRef.current?.contentWindow?.postMessage({ type: 'skin-change', skin }, '*');
}, [skin]);
- // Send postMessage to iframe for source changes (skip initial mount — iframe reads localStorage)
- const sourceMountRef = useRef(true);
useEffect(() => {
- if (sourceMountRef.current) {
- sourceMountRef.current = false;
- return;
- }
iframeRef.current?.contentWindow?.postMessage({ type: 'source-change', source }, '*');
}, [source]);
@@ -81,9 +68,9 @@ export function App() {
}
}, [preset, source, setSource]);
- // Constrain styling when switching to a preset that has no tailwind template
+ // Background video does not have a Tailwind skin variant.
useEffect(() => {
- if ((preset === 'background-video' || preset === 'dash-video') && styling === 'tailwind') {
+ if (preset === 'background-video' && styling === 'tailwind') {
setStyling('css');
}
}, [preset, styling]);
@@ -108,13 +95,19 @@ export function App() {
availableSources={availableSources}
isBackgroundVideo={preset === 'background-video'}
isSimpleHlsVideo={preset === 'simple-hls-video'}
- isDashVideo={preset === 'dash-video'}
platforms={PLATFORMS}
stylings={STYLINGS}
presets={PRESETS}
sources={SOURCES}
/>
-
+
);
}
diff --git a/packages/sandbox/app/shell/navbar.tsx b/packages/sandbox/app/shell/navbar.tsx
index 36b11970..9b0fd1a0 100644
--- a/packages/sandbox/app/shell/navbar.tsx
+++ b/packages/sandbox/app/shell/navbar.tsx
@@ -16,7 +16,6 @@ type NavbarProps = {
availableSources: readonly SourceId[];
isBackgroundVideo: boolean;
isSimpleHlsVideo: boolean;
- isDashVideo: boolean;
platforms: readonly Platform[];
stylings: readonly Styling[];
presets: readonly Preset[];
@@ -53,7 +52,6 @@ export function Navbar({
availableSources,
isBackgroundVideo,
isSimpleHlsVideo,
- isDashVideo,
platforms,
stylings,
presets,
@@ -82,7 +80,7 @@ export function Navbar({
options={stylings.map((s) => ({
value: s,
label: s === 'css' ? 'CSS' : 'Tailwind',
- disabled: s === 'tailwind' && (isBackgroundVideo || isDashVideo),
+ disabled: s === 'tailwind' && isBackgroundVideo,
}))}
/>
diff --git a/packages/sandbox/app/shell/preview.tsx b/packages/sandbox/app/shell/preview.tsx
index 19f755e1..9442ed16 100644
--- a/packages/sandbox/app/shell/preview.tsx
+++ b/packages/sandbox/app/shell/preview.tsx
@@ -1,15 +1,25 @@
import type { SourceId } from '@app/shared/sources';
-import type { Skin } from '@app/types';
-import { forwardRef } from 'react';
+import type { Skin, Styling } from '@app/types';
+import { forwardRef, useState } from 'react';
type PreviewProps = {
pagePath: string;
skin: Skin;
+ styling: Styling;
source: SourceId;
};
-export const Preview = forwardRef(function Preview({ pagePath, skin, source }, ref) {
- const openUrl = `${pagePath}?skin=${encodeURIComponent(skin)}&source=${encodeURIComponent(source)}`;
+export const Preview = forwardRef(function Preview(
+ { pagePath, skin, styling, source },
+ ref
+) {
+ const [iframeUrl] = useState(
+ () =>
+ `${pagePath}?skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}&source=${encodeURIComponent(source)}`
+ );
+ const openUrl =
+ `${pagePath}?skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}` +
+ `&source=${encodeURIComponent(source)}`;
return (
@@ -37,7 +47,7 @@ export const Preview = forwardRef(function Prev
-
+
);
});
diff --git a/packages/sandbox/app/utils/create-web-storage-store.ts b/packages/sandbox/app/utils/create-web-storage-store.ts
deleted file mode 100644
index 5186f953..00000000
--- a/packages/sandbox/app/utils/create-web-storage-store.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-export type WebStorageType = 'local' | 'session';
-
-export type WebStorageSerializableValue =
- | string
- | boolean
- | number
- | null
- | Record
- | WebStorageSerializableValue[];
-
-type Subscriber = () => void;
-
-export function createWebStorageStore(
- type: WebStorageType,
- key: string,
- initialValue: T
-) {
- const listeners = new Set();
-
- const getSnapshot = (): T => {
- const data = type === 'local' ? localStorage.getItem(key) : sessionStorage.getItem(key);
- return data ? JSON.parse(data) : initialValue;
- };
-
- const setValue = (value: T) => {
- if (type === 'local') {
- localStorage.setItem(key, JSON.stringify(value));
- } else {
- sessionStorage.setItem(key, JSON.stringify(value));
- }
- listeners.forEach((l) => l());
- };
-
- const subscribe = (listener: Subscriber) => {
- listeners.add(listener);
-
- const onStorage = (event: StorageEvent) => {
- if (event.key === key) listener();
- };
-
- window.addEventListener('storage', onStorage);
-
- return () => {
- listeners.delete(listener);
- window.removeEventListener('storage', onStorage);
- };
- };
-
- return { getSnapshot, setValue, subscribe };
-}
diff --git a/packages/sandbox/app/utils/stores.ts b/packages/sandbox/app/utils/stores.ts
deleted file mode 100644
index ede6d8c5..00000000
--- a/packages/sandbox/app/utils/stores.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { SourceId } from '@app/shared/sources';
-import type { Skin } from '@app/types';
-import { createWebStorageStore } from './create-web-storage-store';
-
-export const skinStore = createWebStorageStore('local', 'skin', 'default');
-export const sourceStore = createWebStorageStore('local', 'source', 'hls-1');
diff --git a/packages/sandbox/app/utils/use-external-store.ts b/packages/sandbox/app/utils/use-external-store.ts
deleted file mode 100644
index 034bac08..00000000
--- a/packages/sandbox/app/utils/use-external-store.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { useSyncExternalStore } from 'react';
-
-export function useExternalStore(store: { subscribe: (listener: () => void) => () => void; getSnapshot: () => T }) {
- return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
-}
diff --git a/packages/sandbox/app/utils/use-skin-switcher.ts b/packages/sandbox/app/utils/use-skin-switcher.ts
deleted file mode 100644
index 821b5a31..00000000
--- a/packages/sandbox/app/utils/use-skin-switcher.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { Skin } from '@app/types';
-import { skinStore } from './stores';
-import { useExternalStore } from './use-external-store';
-
-export function useSkinSwitcher(): [Skin, (value: Skin) => void] {
- const value = useExternalStore(skinStore);
- return [value, skinStore.setValue];
-}
diff --git a/packages/sandbox/app/utils/use-source-switcher.ts b/packages/sandbox/app/utils/use-source-switcher.ts
deleted file mode 100644
index 0a8150c6..00000000
--- a/packages/sandbox/app/utils/use-source-switcher.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { SourceId } from '@app/shared/sources';
-import { sourceStore } from './stores';
-import { useExternalStore } from './use-external-store';
-
-export function useSourceSwitcher(): [SourceId, (value: SourceId) => void] {
- const value = useExternalStore(sourceStore);
- return [value, sourceStore.setValue];
-}
diff --git a/packages/sandbox/app/utils/use-web-storage.ts b/packages/sandbox/app/utils/use-web-storage.ts
deleted file mode 100644
index 1505a251..00000000
--- a/packages/sandbox/app/utils/use-web-storage.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useState } from 'react';
-import {
- createWebStorageStore,
- type WebStorageSerializableValue,
- type WebStorageType,
-} from './create-web-storage-store';
-import { useExternalStore } from './use-external-store';
-
-export function useWebStorage(
- type: WebStorageType,
- key: string,
- defaultValue: T
-): [T, (value: T) => void] {
- const [store] = useState(() => createWebStorageStore(type, key, defaultValue));
- const value = useExternalStore(store);
- return [value, store.setValue];
-}
diff --git a/packages/sandbox/templates/html-audio-tailwind/index.html b/packages/sandbox/templates/html-audio-tailwind/index.html
deleted file mode 100644
index ab51a4ad..00000000
--- a/packages/sandbox/templates/html-audio-tailwind/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Sandbox — HTML Audio Tailwind
-
-
-
-
-
-
-
-
diff --git a/packages/sandbox/templates/html-audio-tailwind/main.ts b/packages/sandbox/templates/html-audio-tailwind/main.ts
deleted file mode 100644
index d90fd12d..00000000
--- a/packages/sandbox/templates/html-audio-tailwind/main.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import '@app/styles.css';
-import '@videojs/html/audio/player';
-import '@videojs/html/audio/skin.tailwind';
-import '@videojs/html/audio/minimal-skin.tailwind';
-import { TAILWIND_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { setupAudioTailwind } from '@app/shared/html/tailwind-setup';
-import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
-import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/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}>
-
- ${tag}>
-
-
- `;
-}
-
-render();
-
-onSkinChange((skin) => {
- currentSkin = skin;
- render();
-});
-
-onSourceChange((source) => {
- currentSource = source;
- render();
-});
diff --git a/packages/sandbox/templates/html-audio/main.ts b/packages/sandbox/templates/html-audio/main.ts
index a32f5a3f..ed5dc664 100644
--- a/packages/sandbox/templates/html-audio/main.ts
+++ b/packages/sandbox/templates/html-audio/main.ts
@@ -1,29 +1,24 @@
import '@app/styles.css';
import '@videojs/html/audio/player';
-import '@videojs/html/audio/skin';
-import '@videojs/html/audio/minimal-skin';
-import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { loadAudioStylesheets } from '@app/shared/html/stylesheets';
-import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
+import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
+import { loadAudioSkinTag } from '@app/shared/html/skins';
+import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/types';
const html = String.raw;
-let currentSkin: Skin = getInitialSkin();
-let currentSource: SourceId = getInitialSource(true);
+const state = createHtmlSandboxState(true);
+const loadLatest = createLatestLoader();
-function render() {
- const tag = CSS_SKIN_TAGS[currentSkin].audio;
-
- loadAudioStylesheets(currentSkin);
+async function render() {
+ const tag = await loadLatest(() => loadAudioSkinTag(state.skin, state.styling));
+ if (!tag) return;
document.getElementById('root')!.innerHTML = html`
@@ -33,11 +28,11 @@ function render() {
render();
onSkinChange((skin) => {
- currentSkin = skin;
+ state.skin = skin;
render();
});
onSourceChange((source) => {
- currentSource = source;
+ state.source = source;
render();
});
diff --git a/packages/sandbox/templates/html-dash-video/main.ts b/packages/sandbox/templates/html-dash-video/main.ts
index 56bcefc4..9288a51f 100644
--- a/packages/sandbox/templates/html-dash-video/main.ts
+++ b/packages/sandbox/templates/html-dash-video/main.ts
@@ -1,29 +1,24 @@
import '@app/styles.css';
import '@videojs/html/video/player';
import '@videojs/html/media/dash-video';
-import '@videojs/html/video/skin';
-import '@videojs/html/video/minimal-skin';
-import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
-import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
+import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
+import { loadVideoSkinTag } from '@app/shared/html/skins';
+import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/types';
const html = String.raw;
-let currentSkin: Skin = getInitialSkin();
-let currentSource: SourceId = getInitialSource();
+const state = createHtmlSandboxState();
+const loadLatest = createLatestLoader();
-function render() {
- const tag = CSS_SKIN_TAGS[currentSkin].video;
-
- loadVideoStylesheets(currentSkin);
+async function render() {
+ const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
+ if (!tag) return;
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
+
${tag}>
`;
@@ -32,11 +27,11 @@ function render() {
render();
onSkinChange((skin) => {
- currentSkin = skin;
+ state.skin = skin;
render();
});
onSourceChange((source) => {
- currentSource = source;
+ state.source = source;
render();
});
diff --git a/packages/sandbox/templates/html-hls-video/main.ts b/packages/sandbox/templates/html-hls-video/main.ts
index 44c09b68..5722fa17 100644
--- a/packages/sandbox/templates/html-hls-video/main.ts
+++ b/packages/sandbox/templates/html-hls-video/main.ts
@@ -1,31 +1,26 @@
import '@app/styles.css';
import '@videojs/html/video/player';
import '@videojs/html/media/hls-video';
-import '@videojs/html/video/skin';
-import '@videojs/html/video/minimal-skin';
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
-import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
-import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
+import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
+import { loadVideoSkinTag } from '@app/shared/html/skins';
+import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/types';
const html = String.raw;
-let currentSkin: Skin = getInitialSkin();
-let currentSource: SourceId = getInitialSource();
+const state = createHtmlSandboxState();
+const loadLatest = createLatestLoader();
-function render() {
- const tag = CSS_SKIN_TAGS[currentSkin].video;
-
- loadVideoStylesheets(currentSkin);
+async function render() {
+ const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
+ if (!tag) return;
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
- ${renderMuxStoryboard(currentSource)}
+
+ ${renderMuxStoryboard(state.source)}
${tag}>
@@ -35,11 +30,11 @@ function render() {
render();
onSkinChange((skin) => {
- currentSkin = skin;
+ state.skin = skin;
render();
});
onSourceChange((source) => {
- currentSource = source;
+ state.source = source;
render();
});
diff --git a/packages/sandbox/templates/html-simple-hls-video/main.ts b/packages/sandbox/templates/html-simple-hls-video/main.ts
index cb003f2a..907a746c 100644
--- a/packages/sandbox/templates/html-simple-hls-video/main.ts
+++ b/packages/sandbox/templates/html-simple-hls-video/main.ts
@@ -1,31 +1,26 @@
import '@app/styles.css';
import '@videojs/html/video/player';
import '@videojs/html/media/simple-hls-video';
-import '@videojs/html/video/skin';
-import '@videojs/html/video/minimal-skin';
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
-import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
-import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
+import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
+import { loadVideoSkinTag } from '@app/shared/html/skins';
+import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/types';
const html = String.raw;
-let currentSkin: Skin = getInitialSkin();
-let currentSource: SourceId = getInitialSource();
+const state = createHtmlSandboxState();
+const loadLatest = createLatestLoader();
-function render() {
- const tag = CSS_SKIN_TAGS[currentSkin].video;
-
- loadVideoStylesheets(currentSkin);
+async function render() {
+ const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
+ if (!tag) return;
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
- ${renderMuxStoryboard(currentSource)}
+
+ ${renderMuxStoryboard(state.source)}
${tag}>
@@ -35,11 +30,11 @@ function render() {
render();
onSkinChange((skin) => {
- currentSkin = skin;
+ state.skin = skin;
render();
});
onSourceChange((source) => {
- currentSource = source;
+ state.source = source;
render();
});
diff --git a/packages/sandbox/templates/html-video-tailwind/index.html b/packages/sandbox/templates/html-video-tailwind/index.html
deleted file mode 100644
index 1e28faa2..00000000
--- a/packages/sandbox/templates/html-video-tailwind/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Sandbox — HTML Video Tailwind
-
-
-
-
-
-
-
-
diff --git a/packages/sandbox/templates/html-video-tailwind/main.ts b/packages/sandbox/templates/html-video-tailwind/main.ts
deleted file mode 100644
index edd47596..00000000
--- a/packages/sandbox/templates/html-video-tailwind/main.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import '@app/styles.css';
-import '@videojs/html/ui/poster';
-import '@videojs/html/video/player';
-import '@videojs/html/video/skin.tailwind';
-import '@videojs/html/video/minimal-skin.tailwind';
-import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
-import { TAILWIND_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { setupVideoTailwind } from '@app/shared/html/tailwind-setup';
-import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
-import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/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">
-
- ${tag}>
-
- `;
-}
-
-render();
-
-onSkinChange((skin) => {
- currentSkin = skin;
- render();
-});
-
-onSourceChange((source) => {
- currentSource = source;
- render();
-});
diff --git a/packages/sandbox/templates/html-video/main.ts b/packages/sandbox/templates/html-video/main.ts
index 86480193..6bfd3d05 100644
--- a/packages/sandbox/templates/html-video/main.ts
+++ b/packages/sandbox/templates/html-video/main.ts
@@ -1,31 +1,26 @@
import '@app/styles.css';
import '@videojs/html/video/player';
-import '@videojs/html/video/skin';
-import '@videojs/html/video/minimal-skin';
import '@videojs/html/ui/poster';
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
-import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
-import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
+import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
+import { loadVideoSkinTag } from '@app/shared/html/skins';
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
-import type { SourceId } from '@app/shared/sources';
import { SOURCES } from '@app/shared/sources';
-import type { Skin } from '@app/types';
const html = String.raw;
-let currentSkin: Skin = getInitialSkin();
-let currentSource: SourceId = getInitialSource();
+const state = createHtmlSandboxState();
+const loadLatest = createLatestLoader();
-function render() {
- const tag = CSS_SKIN_TAGS[currentSkin].video;
-
- loadVideoStylesheets(currentSkin);
+async function render() {
+ const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
+ if (!tag) return;
document.getElementById('root')!.innerHTML = html`
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
-
@@ -35,11 +30,11 @@ function render() {
render();
onSkinChange((skin) => {
- currentSkin = skin;
+ state.skin = skin;
render();
});
onSourceChange((source) => {
- currentSource = source;
+ state.source = source;
render();
});
diff --git a/packages/sandbox/templates/react-audio-tailwind/index.html b/packages/sandbox/templates/react-audio-tailwind/index.html
deleted file mode 100644
index ebc897a2..00000000
--- a/packages/sandbox/templates/react-audio-tailwind/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Sandbox — React Audio Tailwind
-
-
-
-
-
-
-
-
diff --git a/packages/sandbox/templates/react-audio-tailwind/main.tsx b/packages/sandbox/templates/react-audio-tailwind/main.tsx
deleted file mode 100644
index f8af9bb0..00000000
--- a/packages/sandbox/templates/react-audio-tailwind/main.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import '@app/styles.css';
-import { AudioProvider } from '@app/shared/react/providers';
-import { AudioSkinComponent } from '@app/shared/react/skins';
-import { useSkin } from '@app/shared/react/use-skin';
-import { useSource } from '@app/shared/react/use-source';
-import { SOURCES } from '@app/shared/sources';
-import { Audio } from '@videojs/react/audio';
-import { createRoot } from 'react-dom/client';
-
-function App() {
- const skin = useSkin();
- const source = useSource(true);
-
- return (
-
-
-
-
-
- );
-}
-
-createRoot(document.getElementById('root')!).render();
diff --git a/packages/sandbox/templates/react-audio/main.tsx b/packages/sandbox/templates/react-audio/main.tsx
index e7d31697..23ecc94e 100644
--- a/packages/sandbox/templates/react-audio/main.tsx
+++ b/packages/sandbox/templates/react-audio/main.tsx
@@ -1,21 +1,26 @@
import '@app/styles.css';
-import '@videojs/react/audio/skin.css';
-import '@videojs/react/audio/minimal-skin.css';
import { AudioProvider } from '@app/shared/react/providers';
import { AudioSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
import { useSource } from '@app/shared/react/use-source';
import { SOURCES } from '@app/shared/sources';
+import type { Styling } from '@app/types';
import { Audio } from '@videojs/react/audio';
+import { useMemo } from 'react';
import { createRoot } from 'react-dom/client';
+function readStyling(): Styling {
+ return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
+}
+
function App() {
const skin = useSkin();
const source = useSource(true);
+ const styling = useMemo(readStyling, []);
return (
-
+
diff --git a/packages/sandbox/templates/react-dash-video/main.tsx b/packages/sandbox/templates/react-dash-video/main.tsx
index 9c985972..185127a1 100644
--- a/packages/sandbox/templates/react-dash-video/main.tsx
+++ b/packages/sandbox/templates/react-dash-video/main.tsx
@@ -1,21 +1,26 @@
import '@app/styles.css';
-import '@videojs/react/video/skin.css';
-import '@videojs/react/video/minimal-skin.css';
import { VideoProvider } from '@app/shared/react/providers';
import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
import { useSource } from '@app/shared/react/use-source';
import { SOURCES } from '@app/shared/sources';
+import type { Styling } from '@app/types';
import { DashVideo } from '@videojs/react/media/dash-video';
+import { useMemo } from 'react';
import { createRoot } from 'react-dom/client';
+function readStyling(): Styling {
+ return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
+}
+
function App() {
const skin = useSkin();
const source = useSource();
+ const styling = useMemo(readStyling, []);
return (
-
+
diff --git a/packages/sandbox/templates/react-hls-video/main.tsx b/packages/sandbox/templates/react-hls-video/main.tsx
index 572973aa..97ccbc8d 100644
--- a/packages/sandbox/templates/react-hls-video/main.tsx
+++ b/packages/sandbox/templates/react-hls-video/main.tsx
@@ -1,6 +1,4 @@
import '@app/styles.css';
-import '@videojs/react/video/skin.css';
-import '@videojs/react/video/minimal-skin.css';
import { MuxPoster } from '@app/shared/react/mux-poster';
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
@@ -8,16 +6,23 @@ import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
import { useSource } from '@app/shared/react/use-source';
import { SOURCES } from '@app/shared/sources';
+import type { Styling } from '@app/types';
import { HlsVideo } from '@videojs/react/media/hls-video';
+import { useMemo } from 'react';
import { createRoot } from 'react-dom/client';
+function readStyling(): Styling {
+ return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
+}
+
function App() {
const skin = useSkin();
const source = useSource();
+ const styling = useMemo(readStyling, []);
return (
-
+
diff --git a/packages/sandbox/templates/react-simple-hls-video/main.tsx b/packages/sandbox/templates/react-simple-hls-video/main.tsx
index 33cbfe76..c2a9aa59 100644
--- a/packages/sandbox/templates/react-simple-hls-video/main.tsx
+++ b/packages/sandbox/templates/react-simple-hls-video/main.tsx
@@ -1,6 +1,4 @@
import '@app/styles.css';
-import '@videojs/react/video/skin.css';
-import '@videojs/react/video/minimal-skin.css';
import { MuxPoster } from '@app/shared/react/mux-poster';
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
@@ -8,16 +6,23 @@ import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
import { useSource } from '@app/shared/react/use-source';
import { SOURCES } from '@app/shared/sources';
+import type { Styling } from '@app/types';
import { SimpleHlsVideo } from '@videojs/react/media/simple-hls-video';
+import { useMemo } from 'react';
import { createRoot } from 'react-dom/client';
+function readStyling(): Styling {
+ return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
+}
+
function App() {
const skin = useSkin();
const source = useSource();
+ const styling = useMemo(readStyling, []);
return (
-
+
diff --git a/packages/sandbox/templates/react-video-tailwind/index.html b/packages/sandbox/templates/react-video-tailwind/index.html
deleted file mode 100644
index ac6f661d..00000000
--- a/packages/sandbox/templates/react-video-tailwind/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Sandbox — React Video Tailwind
-
-
-
-
-
-
-
-
diff --git a/packages/sandbox/templates/react-video-tailwind/main.tsx b/packages/sandbox/templates/react-video-tailwind/main.tsx
deleted file mode 100644
index ded1f56d..00000000
--- a/packages/sandbox/templates/react-video-tailwind/main.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import '@app/styles.css';
-import { MuxPoster } from '@app/shared/react/mux-poster';
-import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
-import { VideoProvider } from '@app/shared/react/providers';
-import { VideoSkinComponent } from '@app/shared/react/skins';
-import { useSkin } from '@app/shared/react/use-skin';
-import { useSource } from '@app/shared/react/use-source';
-import { SOURCES } from '@app/shared/sources';
-import { Video } from '@videojs/react/video';
-import { createRoot } from 'react-dom/client';
-
-function App() {
- const skin = useSkin();
- const source = useSource();
-
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-createRoot(document.getElementById('root')!).render();
diff --git a/packages/sandbox/templates/react-video/main.tsx b/packages/sandbox/templates/react-video/main.tsx
index 209396b3..36618647 100644
--- a/packages/sandbox/templates/react-video/main.tsx
+++ b/packages/sandbox/templates/react-video/main.tsx
@@ -1,6 +1,4 @@
import '@app/styles.css';
-import '@videojs/react/video/skin.css';
-import '@videojs/react/video/minimal-skin.css';
import { MuxPoster } from '@app/shared/react/mux-poster';
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
import { VideoProvider } from '@app/shared/react/providers';
@@ -8,16 +6,23 @@ import { VideoSkinComponent } from '@app/shared/react/skins';
import { useSkin } from '@app/shared/react/use-skin';
import { useSource } from '@app/shared/react/use-source';
import { SOURCES } from '@app/shared/sources';
+import type { Styling } from '@app/types';
import { Video } from '@videojs/react/video';
+import { useMemo } from 'react';
import { createRoot } from 'react-dom/client';
+function readStyling(): Styling {
+ return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
+}
+
function App() {
const skin = useSkin();
const source = useSource();
+ const styling = useMemo(readStyling, []);
return (
-
+