feat(monorepo): migrate prototype code to organized package structure

Migrate all prototype code from vjs10-prototype repository into the
monorepo structure following established dependency hierarchy and
package organization patterns.

Core packages migrated:
- @vjs-10/media-store: Complete media store with nanostores integration
- @vjs-10/playback-engine: HLS.js playback engine implementation
- @vjs-10/media: Core media state owner and interfaces

HTML packages migrated:
- @vjs-10/html-icons: Web component icons with proper base classes
- @vjs-10/html-media-elements: Context provider integration
- @vjs-10/html: Main HTML UI components, skins, and utilities

React packages migrated:
- @vjs-10/react-icons: React icon components
- @vjs-10/react-media-elements: Video component with state integration
- @vjs-10/react-media-store: MediaProvider and React hooks
- @vjs-10/react: Main React UI components and skins

Changes made:
- Updated all imports to use monorepo package names (@vjs-10/*)
- Distributed dependencies to specific packages that use them
- Preserved all prototype functionality and component structure
- Created proper index files for all packages
- Fixed workspace protocol usage for npm compatibility

🤖 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 13:17:46 -07:00
co-authored by Claude
parent fc5a0e46fd
commit 4b472ec49c
61 changed files with 1967 additions and 995 deletions
+1 -69
View File
@@ -1,69 +1 @@
export type MediaReadyState = 0 | 1 | 2 | 3 | 4;
export const READY_STATE = {
HAVE_NOTHING: 0 as MediaReadyState,
HAVE_METADATA: 1 as MediaReadyState,
HAVE_CURRENT_DATA: 2 as MediaReadyState,
HAVE_FUTURE_DATA: 3 as MediaReadyState,
HAVE_ENOUGH_DATA: 4 as MediaReadyState,
} as const;
export type MediaNetworkState = 0 | 1 | 2 | 3;
export const NETWORK_STATE = {
EMPTY: 0 as MediaNetworkState,
IDLE: 1 as MediaNetworkState,
LOADING: 2 as MediaNetworkState,
NO_SOURCE: 3 as MediaNetworkState,
} as const;
export interface MediaElementLike {
currentTime: number;
duration: number;
paused: boolean;
ended: boolean;
volume: number;
muted: boolean;
playbackRate: number;
readyState: MediaReadyState;
networkState: MediaNetworkState;
play(): Promise<void>;
pause(): void;
load(): void;
}
export function isMediaElement(element: any): element is HTMLMediaElement {
return element &&
typeof element.play === 'function' &&
typeof element.pause === 'function' &&
typeof element.load === 'function' &&
typeof element.currentTime === 'number';
}
export function createMediaElementAdapter(element: HTMLMediaElement): MediaElementLike {
return {
get currentTime() { return element.currentTime; },
set currentTime(value: number) { element.currentTime = value; },
get duration() { return element.duration; },
get paused() { return element.paused; },
get ended() { return element.ended; },
get volume() { return element.volume; },
set volume(value: number) { element.volume = value; },
get muted() { return element.muted; },
set muted(value: boolean) { element.muted = value; },
get playbackRate() { return element.playbackRate; },
set playbackRate(value: number) { element.playbackRate = value; },
get readyState() { return element.readyState as MediaReadyState; },
get networkState() { return element.networkState as MediaNetworkState; },
play: () => element.play(),
pause: () => element.pause(),
load: () => element.load(),
};
}
export * from './playable';
+112
View File
@@ -0,0 +1,112 @@
import type { IBasePlaybackEngine } from '@vjs-10/playback-engine';
import { createPlaybackEngine } from '@vjs-10/playback-engine';
/** @TODO Split out "playable" vs. "audible" and compose via factory (current mixin pattern or spreadable mixin pattern) (CJP) */
export const Events = ['volumechange', 'pause', 'play', 'playing'] as const;
export interface IBaseMediaStateOwner<
T extends Pick<HTMLMediaElement, 'src'> = Pick<HTMLMediaElement, 'src'>,
> extends EventTarget,
Pick<HTMLMediaElement, 'src'> {
mediaElement?: T;
}
export interface IPlayableMediaStateOwner
extends EventTarget,
IBaseMediaStateOwner,
Pick<HTMLMediaElement, 'play' | 'pause' | 'paused'> {}
export interface IAudibleMediaStateOwner
extends EventTarget,
IBaseMediaStateOwner,
Pick<HTMLMediaElement, 'muted' | 'volume'> {}
export class PlayableMediaStateOwner
extends EventTarget
implements IPlayableMediaStateOwner, IAudibleMediaStateOwner
{
protected _playbackEngine: IBasePlaybackEngine;
constructor() {
super();
this._playbackEngine = createPlaybackEngine();
}
get mediaElement() {
return this._playbackEngine.mediaElement;
}
set mediaElement(value) {
if (this.mediaElement === value) return;
if (this.mediaElement != null) {
Events.forEach((eventType) => {
this.mediaElement?.removeEventListener(eventType, this);
});
}
this._playbackEngine.mediaElement = value;
if (this.mediaElement) {
Events.forEach((eventType) => {
this.mediaElement?.addEventListener(eventType, this);
});
}
}
get paused() {
return this.mediaElement?.paused ?? false;
}
pause() {
/** @TODO implement deferred state etc. for cases where media has yet to be set */
if (!this.mediaElement) return;
return this.mediaElement.pause();
}
play() {
/** @TODO implement deferred state etc. for cases where media has yet to be set */
if (!this.mediaElement) return Promise.reject();
return this.mediaElement.play();
}
get muted() {
return this.mediaElement?.muted ?? false;
}
set muted(value) {
if (value === this.muted) return;
/** @TODO implement deferred state etc. for cases where media has yet to be set */
if (!this.mediaElement) return;
this.mediaElement.muted = value;
}
get volume() {
return this.mediaElement?.volume ?? 0;
}
set volume(value) {
if (value === this.volume) return;
/** @TODO implement deferred state etc. for cases where media has yet to be set */
if (!this.mediaElement) return;
this.mediaElement.volume = value;
}
get src() {
return this._playbackEngine.src ?? '';
}
set src(value) {
this._playbackEngine.src = value;
}
handleEvent(event: Event): void {
if (event.target === this.mediaElement) {
// @ts-ignore
const clonedEvent = new event.constructor(event.type, event);
this.dispatchEvent(clonedEvent);
}
}
destroy() {
this.mediaElement = undefined;
this._playbackEngine.destroy();
}
}
export const createMediaStateOwner = () => new PlayableMediaStateOwner();