feat: migrate examples from prototype and add CSS modules support

- Migrate HTML and React demo applications from prototype to examples/ directory
- Add examples to workspace configuration with selective build support
- Update workspace scripts for library-only builds (build:libs) and example dev servers
- Fix React MediaProvider context conflicts by consolidating exports
- Add esbuild-css-modules-plugin for CSS modules support in React package
- Configure tsup for flattened output structure with CSS processing
- Update package exports and imports for new @vjs-10/* scoped packages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2025-07-29 15:51:40 -07:00
co-authored by Claude
parent 7403cc7281
commit 19c85fc2f8
17 changed files with 2133 additions and 190 deletions
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>VJS HTML Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
{
"name": "html-demo",
"version": "1.0.0",
"description": "HTML example for Video.js 10",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vjs-10/html": "*"
},
"devDependencies": {
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}
+11
View File
@@ -0,0 +1,11 @@
import { defineVjsPlayer } from '@vjs-10/html';
defineVjsPlayer();
document.body.innerHTML = `
<media-provider>
<media-skin-default>
<video slot="media" src="https://www.w3schools.com/html/mov_bbb.mp4" muted></video>
</media-skin-default>
</media-provider>
`;
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vjs/html": ["../../packages/html/src"],
"@vjs/react": ["../../packages/react/src"],
"@vjs/core": ["../../packages/core/src"]
}
},
"include": ["src"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vite';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export default defineConfig({
root: __dirname,
});
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>VJS React Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "react-demo",
"version": "1.0.0",
"description": "React example for Video.js 10",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vjs-10/react": "*",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { MediaProvider, Video, MediaSkinDefault } from '@vjs-10/react';
const DemoPlayer = () => {
return (
<MediaProvider>
<MediaSkinDefault>
{/* @ts-ignore */}
{/* <Video muted src="https://www.w3schools.com/html/mov_bbb.mp4" /> */}
<Video muted src="https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M.m3u8" />
</MediaSkinDefault>
</MediaProvider>
);
};
ReactDOM.createRoot(document.getElementById('root')!).render(
<DemoPlayer />
);
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vjs/react": ["../../packages/react/src"],
"@vjs/html": ["../../packages/html/src"],
"@vjs/core": ["../../packages/core/src"]
}
},
"include": ["src"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
+1967 -1
View File
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -7,21 +7,25 @@
"packages/core/*",
"packages/html/*",
"packages/react/*",
"packages/react-native/*"
"packages/react-native/*",
"examples/*"
],
"scripts": {
"build": "npm run build --workspaces --if-present",
"build:libs": "npm run build --workspace=packages/core/* --workspace=packages/html/* --workspace=packages/react/* --workspace=packages/react-native/*",
"test": "npm run test --workspaces --if-present",
"lint": "npm run lint --workspaces --if-present",
"typecheck": "npm run typecheck --workspaces --if-present",
"clean": "npm run clean --workspaces --if-present"
"clean": "npm run clean --workspaces --if-present",
"dev:html": "npm run dev --workspace=examples/html-demo",
"dev:react": "npm run dev --workspace=examples/react-demo"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"react": "^18.0.0",
"tsup": "^8.0.0"
"tsup": "^8.0.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18.0.0",
+1 -1
View File
@@ -18,4 +18,4 @@ export default defineConfig({
incremental: false,
},
},
});
});
+13 -181
View File
@@ -1,182 +1,14 @@
import React, { createContext, useContext, useEffect, useState, useRef, ReactNode } from 'react';
// Re-export everything from MediaProvider - this is the primary implementation
export {
MediaProvider,
MediaContext,
useMediaStore,
useMediaDispatch,
useMediaRef,
useMediaSelector
} from './MediaProvider.js';
// @ts-ignore - Placeholder types until media-store exports are updated
type MediaStore = any;
type MediaState = any;
type MediaStateOwner = any;
import { MediaElementRef } from '@vjs-10/react-media-elements';
const MediaStoreContext = createContext<MediaStore | null>(null);
export interface MediaStoreProviderProps {
children: ReactNode;
store?: MediaStore;
}
export const MediaStoreProvider: React.FC<MediaStoreProviderProps> = ({
children,
store = {} as MediaStore,
}) => {
return (
<MediaStoreContext.Provider value={store}>
{children}
</MediaStoreContext.Provider>
);
};
export function useMediaStore(): MediaStore {
const store = useContext(MediaStoreContext);
if (!store) {
throw new Error('useMediaStore must be used within a MediaStoreProvider');
}
return store;
}
export function useMediaState(): [MediaState, (updates: Partial<MediaState>) => void] {
const store = useMediaStore();
const [state, setState] = useState<MediaState>(store.getState());
useEffect(() => {
const owner: MediaStateOwner = {
getState: () => state,
setState: (newState: Partial<MediaState>) => {
setState((current: any) => ({ ...current, ...newState }));
},
};
store.addOwner(owner);
return () => {
store.removeOwner(owner);
};
}, [store, state]);
const updateState = (updates: Partial<MediaState>) => {
store.updateState(updates);
};
return [state, updateState];
}
export class ReactMediaStateOwner implements MediaStateOwner {
private elementRef: React.RefObject<MediaElementRef>;
private store: MediaStore;
private intervalId?: NodeJS.Timeout;
constructor(elementRef: React.RefObject<MediaElementRef>, store: MediaStore) {
this.elementRef = elementRef;
this.store = store;
this.startPolling();
}
getState(): MediaState {
const element = this.elementRef.current;
if (!element) {
return {
currentTime: 0,
duration: 0,
paused: true,
volume: 1,
muted: false,
};
}
return {
currentTime: element.currentTime,
duration: element.duration,
paused: element.paused,
volume: element.volume,
muted: element.muted,
};
}
setState(state: Partial<MediaState>): void {
const element = this.elementRef.current;
if (!element) return;
if (state.currentTime !== undefined && state.currentTime !== element.currentTime) {
element.currentTime = state.currentTime;
}
if (state.volume !== undefined && state.volume !== element.volume) {
element.volume = state.volume;
}
if (state.muted !== undefined && state.muted !== element.muted) {
element.muted = state.muted;
}
}
private startPolling() {
this.intervalId = setInterval(() => {
const currentState = this.getState();
this.store.updateState(currentState);
}, 100);
}
destroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
}
export function useMediaElementStore(elementRef: React.RefObject<MediaElementRef>): MediaStore {
const store = useMediaStore();
const ownerRef = useRef<ReactMediaStateOwner | null>(null);
useEffect(() => {
if (elementRef.current && !ownerRef.current) {
ownerRef.current = new ReactMediaStateOwner(elementRef, store);
store.addOwner(ownerRef.current);
}
return () => {
if (ownerRef.current) {
store.removeOwner(ownerRef.current);
ownerRef.current.destroy();
ownerRef.current = null;
}
};
}, [elementRef, store]);
return store;
}
export function useCurrentTime(): [number, (time: number) => void] {
const [state, updateState] = useMediaState();
const setCurrentTime = (time: number) => {
updateState({ currentTime: time });
};
return [state.currentTime, setCurrentTime];
}
export function useVolume(): [number, boolean, (volume: number) => void, (muted: boolean) => void] {
const [state, updateState] = useMediaState();
const setVolume = (volume: number) => {
updateState({ volume });
};
const setMuted = (muted: boolean) => {
updateState({ muted });
};
return [state.volume, state.muted, setVolume, setMuted];
}
export function usePlaybackState(): [boolean, () => void, () => void] {
const [state, updateState] = useMediaState();
const play = () => {
updateState({ paused: false });
};
const pause = () => {
updateState({ paused: true });
};
return [state.paused, play, pause];
}
// Re-export placeholder types
export type { MediaStore, MediaState, MediaStateOwner };
export type MediaStore = any;
export type MediaState = any;
export type MediaStateOwner = any;
+1
View File
@@ -37,6 +37,7 @@
"react": ">=16.8.0"
},
"devDependencies": {
"esbuild-css-modules-plugin": "^3.1.5",
"typescript": "^5.3.0",
"@types/react": "^18.0.0",
"react": "^18.0.0"
@@ -3,7 +3,6 @@ import { PauseIcon, PlayIcon } from '@vjs-10/react-icons';
import PlayButton from '../components/connected-with-defaults/PlayButton';
import MuteButton from '../components/connected-with-defaults/MuteButton';
import { VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@vjs-10/react-icons';
/** @ts-ignore */
import styles from './styles.module.css';
export const MediaSkinDefault: React.FC<{ children: React.ReactNode }> = ({
+10 -2
View File
@@ -1,7 +1,10 @@
import { defineConfig } from 'tsup';
import cssPlugin from 'esbuild-css-modules-plugin';
export default defineConfig({
entry: ['src/index.tsx'],
entry: {
index: 'src/index.tsx',
},
format: ['cjs', 'esm'],
clean: true,
sourcemap: true,
@@ -11,6 +14,11 @@ export default defineConfig({
/^@vjs-10\//,
/^[^.]/,
],
// Add esbuild plugin for CSS modules
esbuildPlugins: [cssPlugin()],
esbuildOptions(options, context) {
options.outbase = './src'; // Ensure 'src' is the base for output paths
},
// Generate TypeScript declarations with custom config
dts: {
compilerOptions: {
@@ -18,4 +26,4 @@ export default defineConfig({
incremental: false,
},
},
});
});