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
+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;