From 9a6ee7f8b7f5ad53b6f80fb16ceaddd33efb89d1 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Tue, 9 Sep 2025 13:44:10 -0700 Subject: [PATCH 1/7] docs: add Media Chrome to VJS-10 migration guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive migration documentation covering: - State management transformation patterns - Component refactoring from Media Chrome to VJS-10 - Styling and theming approach changes - Icons and asset migration - React architecture adoption - Subcomponent pattern evolution 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- MEDIA_CHROME_MIGRATION.md | 908 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 908 insertions(+) create mode 100644 MEDIA_CHROME_MIGRATION.md diff --git a/MEDIA_CHROME_MIGRATION.md b/MEDIA_CHROME_MIGRATION.md new file mode 100644 index 00000000..6965c7f2 --- /dev/null +++ b/MEDIA_CHROME_MIGRATION.md @@ -0,0 +1,908 @@ +# Migration Guide: Media Chrome to VJS-10 Monorepo (Extended) + +## Overview + +This guide demonstrates how to migrate components and state management from Media Chrome's monolithic architecture to VJS-10's layered monorepo approach, using the mute button as a comprehensive example. + +## Architecture Comparison + +### Media Chrome: Monolithic Event-Driven Architecture +- **Single package** with all functionality +- **Web Components** with direct DOM state coupling +- **Custom event system** for state changes +- **Integrated state management** within MediaController + +### VJS-10: Layered Platform-Specific Architecture +- **Multi-package monorepo** organized by platform +- **Hook-style components** with state injection +- **Nanostores-based** reactive state management +- **Strict dependency hierarchy** preventing circular dependencies + +## Migration Pattern: 3-Phase Decomposition + +Based on commit history analysis (`ad7aa79` → `882019f` → `e1d326f`), the migration follows this pattern: + +1. **State Extraction** → Move state logic to core packages +2. **Component Refactoring** → Implement hook-style architecture +3. **Platform Specialization** → Create platform-specific implementations + +--- + +## Phase 1: State Management Migration + +### Before: Media Chrome Monolithic State + +```typescript +// media-chrome/src/js/media-store/request-map.ts +export const requestMap = { + [MediaUIEvents.MEDIA_MUTE_REQUEST](stateMediator, stateOwners) { + const key = 'mediaMuted'; + const value = true; + stateMediator[key].set(value, stateOwners); + }, + [MediaUIEvents.MEDIA_UNMUTE_REQUEST](stateMediator, stateOwners) { + const key = 'mediaMuted'; + const value = false; + stateMediator[key].set(value, stateOwners); + }, +}; + +// media-chrome/src/js/media-store/state-mediator.ts +export const stateMediator = { + mediaMuted: { + get: (stateOwners) => stateOwners.media?.muted ?? false, + set: (value, stateOwners) => { + if (!stateOwners.media) return; + stateOwners.media.muted = value; + if (!value && !stateOwners.media.volume) { + stateOwners.media.volume = 0.25; // Auto-unmute behavior + } + }, + mediaEvents: ['volumechange'], + }, + mediaVolumeLevel: { + get: (stateOwners) => { + const { media } = stateOwners; + if (media?.muted || media?.volume === 0) return 'off'; + if (media?.volume < 0.5) return 'low'; + if (media?.volume < 0.75) return 'medium'; + return 'high'; + }, + mediaEvents: ['volumechange'], + }, +}; +``` + +### After: VJS-10 Decomposed State Architecture + +#### 1. Core State Mediator +**Location**: `packages/core/media-store/src/state-mediators/audible.ts` + +```typescript +export const audible = { + muted: { + get(stateOwners: any) { + const { media } = stateOwners; + return media?.muted ?? false; + }, + set(value: boolean, stateOwners: any) { + const { media } = stateOwners; + if (!media) return; + media.muted = value; + if (!value && !media.volume) { + media.volume = 0.25; + } + }, + mediaEvents: ['volumechange'], + actions: { + muterequest: () => true, + unmuterequest: () => false, + }, + }, + volumeLevel: { + get(stateOwners: any) { + const { media } = stateOwners; + if (typeof media?.volume == 'undefined') return 'high'; + if (media.muted || media.volume === 0) return 'off'; + if (media.volume < 0.5) return 'low'; + if (media.volume < 0.75) return 'medium'; + return 'high'; + }, + mediaEvents: ['volumechange'], + }, +}; +``` + +#### 2. Component State Definition +**Location**: `packages/core/media-store/src/component-state-definitions/mute-button.ts` + +```typescript +export interface MuteButtonState { + muted: boolean; + volumeLevel: string; +} + +export interface MuteButtonMethods { + requestMute: () => void; + requestUnmute: () => void; +} + +export const muteButtonStateDefinition: MuteButtonStateDefinition = { + keys: ['muted', 'volumeLevel'], + + stateTransform: (rawState: any): MuteButtonState => ({ + muted: rawState.muted ?? false, + volumeLevel: rawState.volumeLevel ?? 'off', + }), + + createRequestMethods: (dispatch): MuteButtonMethods => ({ + requestMute: () => dispatch({ type: 'muterequest' }), + requestUnmute: () => dispatch({ type: 'unmuterequest' }), + }), +}; +``` + +--- + +## Phase 2: Component Refactoring + +### Before: Media Chrome Monolithic Component + +```typescript +// media-chrome/src/js/media-mute-button.ts +import { MediaChromeButton } from './media-chrome-button.js'; +import { MediaUIEvents, MediaUIAttributes } from './constants.js'; + +class MediaMuteButton extends MediaChromeButton { + static get observedAttributes(): string[] { + return [...super.observedAttributes, MediaUIAttributes.MEDIA_VOLUME_LEVEL]; + } + + handleClick(): void { + const eventName: string = + this.mediaVolumeLevel === 'off' + ? MediaUIEvents.MEDIA_UNMUTE_REQUEST + : MediaUIEvents.MEDIA_MUTE_REQUEST; + this.dispatchEvent( + new globalThis.CustomEvent(eventName, { composed: true, bubbles: true }) + ); + } +} +``` + +### After: VJS-10 Hook-Style Components + +#### HTML Implementation +**Location**: `packages/html/html/src/components/media-mute-button.ts` + +```typescript +import { muteButtonStateDefinition } from '@vjs-10/media-store'; + +export class MuteButtonBase extends MediaChromeButton { + _state: { + muted: boolean; + volumeLevel: string; + requestMute: () => void; + requestUnmute: () => void; + } | undefined; + + handleEvent(event: Event) { + const { type } = event; + const state = this._state; + if (state && type === 'click') { + if (state.volumeLevel === 'off') { + state.requestUnmute(); + } else { + state.requestMute(); + } + } + } + + _update(props: any, state: any) { + this._state = state; + this.toggleAttribute('data-muted', props['data-muted']); + this.setAttribute('data-volume-level', props['data-volume-level']); + this.setAttribute('aria-label', props['aria-label']); + } +} + +export const useMuteButtonState = { + keys: muteButtonStateDefinition.keys, + transform: (rawState, mediaStore) => ({ + ...muteButtonStateDefinition.stateTransform(rawState), + ...muteButtonStateDefinition.createRequestMethods(mediaStore.dispatch), + }), +}; + +export const useMuteButtonProps = (state, _element) => ({ + 'data-muted': state.muted, + 'data-volume-level': state.volumeLevel, + 'aria-label': state.muted ? 'unmute' : 'mute', + 'data-tooltip': state.muted ? 'Unmute' : 'Mute', +}); + +export const MuteButton = toConnectedHTMLComponent( + MuteButtonBase, + useMuteButtonState, + useMuteButtonProps, + 'MuteButton', +); +``` + +#### React Implementation +**Location**: `packages/react/react/src/components/MuteButton.tsx` + +```typescript +import { useMediaSelector, useMediaStore } from '@vjs-10/react-media-store'; +import { muteButtonStateDefinition } from '@vjs-10/media-store'; + +export const useMuteButtonState = (_props: any) => { + const mediaStore = useMediaStore(); + const mediaState = useMediaSelector( + muteButtonStateDefinition.stateTransform, + shallowEqual, + ); + + const methods = React.useMemo( + () => muteButtonStateDefinition.createRequestMethods(mediaStore.dispatch), + [mediaStore], + ); + + return { + volumeLevel: mediaState.volumeLevel, + muted: mediaState.muted, + requestMute: methods.requestMute, + requestUnmute: methods.requestUnmute, + } as const; +}; + +export const renderMuteButton = (props, state) => ( + +); + +export const MuteButton = toConnectedComponent( + useMuteButtonState, + useMuteButtonProps, + renderMuteButton, + 'MuteButton', +); +``` + +--- + +## Phase 3: Styling & Theming Migration + +### Media Chrome: Shadow DOM with CSS Custom Properties + +```typescript +// media-chrome/src/js/media-mute-button.ts +function getSlotTemplateHTML(_attrs: Record) { + return /*html*/ ` + + + + ${offIcon} + ${lowIcon} + ${lowIcon} + ${highIcon} + + `; +} +``` + +**CSS Custom Properties for Theming:** +```css +/* Media Chrome approach */ +media-mute-button { + --media-primary-color: #fff; + --media-secondary-color: rgba(20, 20, 30, 0.7); + --media-icon-color: var(--media-primary-color); + --media-control-background: var(--media-secondary-color); + --media-control-hover-background: rgba(50, 50, 70, 0.7); +} +``` + +### VJS-10: Platform-Specific Styling Approaches + +#### HTML Styling - Data Attributes + External CSS + +```typescript +// packages/html/html/src/components/media-mute-button.ts +export const useMuteButtonProps = (state, _element) => ({ + 'data-muted': state.muted, + 'data-volume-level': state.volumeLevel, + 'aria-label': state.muted ? 'unmute' : 'mute', + 'data-tooltip': state.muted ? 'Unmute' : 'Mute', +}); +``` + +**External CSS Targeting Data Attributes:** +```css +/* VJS-10 HTML approach - external stylesheets */ +media-mute-button { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--vjs-control-bg, rgba(0, 0, 0, 0.5)); + color: var(--vjs-control-color, #fff); + border: none; + padding: 8px; + cursor: pointer; +} + +media-mute-button[data-volume-level="off"] .volume-icon:not(.volume-off) { + display: none; +} + +media-mute-button[data-volume-level="low"] .volume-icon:not(.volume-low) { + display: none; +} + +media-mute-button[data-volume-level="medium"] .volume-icon:not(.volume-medium) { + display: none; +} + +media-mute-button[data-volume-level="high"] .volume-icon:not(.volume-high) { + display: none; +} +``` + +#### React Styling - CSS-in-JS or Styled Components + +```tsx +// packages/react/react/src/components/MuteButton.tsx +export const renderMuteButton = (props, state) => ( + +); +``` + +--- + +## Phase 4: Icon Management Migration + +### Media Chrome: Inline SVG with Slots + +```typescript +// media-chrome/src/js/media-mute-button.ts +const offIcon = ``; + +const lowIcon = ``; + +const highIcon = ``; + +// Usage in template with slot-based icon switching +function getSlotTemplateHTML() { + return ` + + ${offIcon} + ${lowIcon} + ${lowIcon} + ${highIcon} + + `; +} +``` + +### VJS-10: Centralized Icon System + +#### Core Icon Package +**Location**: `packages/core/icons/src/index.ts` + +```typescript +// Import SVG files as strings +import playSvg from '../assets/play.svg'; +import pauseSvg from '../assets/pause.svg'; +import volumeHighSvg from '../assets/volume-high.svg'; +import volumeLowSvg from '../assets/volume-low.svg'; +import volumeOffSvg from '../assets/volume-off.svg'; + +// Export SVG strings directly +export const SVG_ICONS = { + play: playSvg, + pause: pauseSvg, + volumeHigh: volumeHighSvg, + volumeLow: volumeLowSvg, + volumeOff: volumeOffSvg, +} as const; + +// Legacy interface for backward compatibility +export interface IconDefinition { + name: string; + viewBox: string; + paths: string[]; +} + +export const ICON_DEFINITIONS: Record = { + play: { + name: 'play', + viewBox: '0 0 24 24', + paths: ['M8 5v14l11-7z'] + }, + // ... other icons +}; +``` + +#### HTML Icon Components +**Location**: `packages/html/html-icons/src/media-volume-off-icon.ts` + +```typescript +import { MediaChromeIcon } from './media-chrome-icon.js'; +import { SVG_ICONS } from '@vjs-10/icons'; + +export function getTemplateHTML() { + return /* html */ ` + ${MediaChromeIcon.getTemplateHTML()} + + ${SVG_ICONS.volumeOff} + `; +} + +export class MediaVolumeOffIcon extends MediaChromeIcon { + static getTemplateHTML = getTemplateHTML; +} + +customElements.define('media-volume-off-icon', MediaVolumeOffIcon); +``` + +#### React Icon Components (Auto-Generated) +**Location**: `packages/react/react-icons/src/generated-icons/VolumeOff.tsx` + +```tsx +/** + * @fileoverview Auto-generated React component from SVG + * @generated + */ +import * as React from "react"; +import type { IconProps } from "../types"; + +const SvgVolumeOff = ({ color = "currentColor", ...props }: IconProps) => ( + +); +export default SvgVolumeOff; +``` + +--- + +## Phase 5: React Component Architecture Migration + +### Media Chrome: Auto-Generated Thin Wrappers + +Media Chrome uses **ce-la-react** to automatically generate React wrappers around web components: + +```typescript +// media-chrome/scripts/react/build.js +const toReactComponentStr = (config) => { + const { elementName } = config; + const ReactComponentName = toPascalCase(elementName); + return ` +export const ${ReactComponentName} = createComponent({ + tagName: "${elementName}", + elementClass: Modules.${ReactComponentName}, + react: React, + toAttributeValue: toAttributeValue, + defaultProps: { + suppressHydrationWarning: true, + }, +});`; +}; +``` + +**Generated React Component:** +```tsx +// Generated: media-chrome/dist/react/media-mute-button.js +import React from "react"; +import { createComponent } from 'ce-la-react'; +import * as Modules from "../index.js"; + +export const MediaMuteButton = createComponent({ + tagName: "media-mute-button", + elementClass: Modules.MediaMuteButton, + react: React, + toAttributeValue: toAttributeValue, + defaultProps: { + suppressHydrationWarning: true, + }, +}); +``` + +**Usage:** +```tsx +// Media Chrome approach - thin wrapper around web component +import { MediaMuteButton } from 'media-chrome/react'; + +function MyPlayer() { + return ( + + + ); +} +``` + +### VJS-10: Native React Components with Shared State + +VJS-10 creates **completely separate React components** that share only the core state logic: + +```tsx +// packages/react/react/src/components/MuteButton.tsx +export const useMuteButtonState = (_props: any) => { + const mediaStore = useMediaStore(); + const mediaState = useMediaSelector( + muteButtonStateDefinition.stateTransform, + shallowEqual, + ); + + const methods = React.useMemo( + () => muteButtonStateDefinition.createRequestMethods(mediaStore.dispatch), + [mediaStore], + ); + + return { ...mediaState, ...methods }; +}; + +export const useMuteButtonProps = (props, state) => { + const baseProps = { + 'data-volume-level': state.volumeLevel, + 'aria-label': state.muted ? 'unmute' : 'mute', + ...props, + }; + + if (state.muted) { + baseProps['data-muted'] = ''; + } + + return baseProps; +}; + +export const renderMuteButton = (props, state) => ( + +); + +export const MuteButton = toConnectedComponent( + useMuteButtonState, + useMuteButtonProps, + renderMuteButton, + 'MuteButton', +); +``` + +**Usage:** +```tsx +// VJS-10 approach - native React with shared state +import { MediaProvider } from '@vjs-10/react-media-store'; +import { MuteButton } from '@vjs-10/react'; +import { VolumeOffIcon } from '@vjs-10/react-icons'; + +function MyPlayer() { + return ( + + + ); +} +``` + +--- + +## Phase 6: Subcomponent & Slot Pattern Migration + +### Media Chrome: Web Component Slots + +Media Chrome uses **Shadow DOM slots** for composability: + +```typescript +// media-chrome/src/js/media-chrome-button.ts +function getTemplateHTML(_attrs, _props) { + return /*html*/ ` + + + ${this.getSlotTemplateHTML(_attrs, _props)} + + + + + `; +} + +// media-chrome/src/js/media-mute-button.ts +function getSlotTemplateHTML() { + return /*html*/ ` + + ${offIcon} + ${lowIcon} + ${lowIcon} + ${highIcon} + + `; +} + +function getTooltipContentHTML() { + return /*html*/ ` + ${t('Mute')} + ${t('Unmute')} + `; +} +``` + +**Usage:** +```html + + + Click to mute audio + Click to unmute audio + + + + +``` + +### VJS-10: React Children & Render Props + +VJS-10 uses **React children patterns** and **render props** for composability: + +#### HTML Implementation (Web Component Slots) +```typescript +// packages/html/html/src/components/media-mute-button.ts - Similar to Media Chrome +export class MuteButtonBase extends MediaChromeButton { + // Inherits slot-based template system from MediaChromeButton base +} +``` + +#### React Implementation (Children & Render Props) +```tsx +// packages/react/react/src/components/MuteButton.tsx +export const renderMuteButton = (props, state) => ( + +); + +// Advanced render prop pattern +export const MuteButtonRender = ({ children, ...props }) => { + const state = useMuteButtonState(props); + const buttonProps = useMuteButtonProps(props, state); + + return children(buttonProps, state); +}; +``` + +**Usage:** +```tsx +// VJS-10 React children patterns +import { MuteButton, MuteButtonRender } from '@vjs-10/react'; +import { VolumeOffIcon, VolumeLowIcon, VolumeHighIcon } from '@vjs-10/react-icons'; + +// Simple children +function SimpleUsage() { + return ( + + + + ); +} + +// Conditional rendering based on state +function ConditionalIcons() { + return ( + + {({ volumeLevel }) => { + switch (volumeLevel) { + case 'off': return ; + case 'low': return ; + default: return ; + } + }} + + ); +} + +// Render prop pattern for maximum control +function RenderPropUsage() { + return ( + + {(buttonProps, state) => ( +
+ + + {state.muted ? 'Unmute' : 'Mute'} + +
+ )} +
+ ); +} +``` + +--- + +## Migration Comparison Summary + +### Architecture Changes + +| Aspect | Media Chrome | VJS-10 | +|--------|-------------|--------| +| **State Management** | Monolithic MediaStore with custom events | Layered nanostores with mediators | +| **Component Style** | Web Components with Shadow DOM | Hook-style with platform adapters | +| **React Integration** | Auto-generated thin wrappers | Native React components | +| **Styling** | Shadow DOM + CSS custom properties | Data attributes + external CSS | +| **Icons** | Inline SVG strings with slots | Centralized icon packages | +| **Theming** | CSS custom properties | Platform-specific approaches | +| **Composition** | Slot-based (HTML) | Children/render props (React) | + +### Migration Benefits + +#### 1. **Platform Optimization** +- **HTML**: Web Components with Shadow DOM encapsulation +- **React**: Native React patterns with hooks and JSX +- **React Native**: Native mobile components (future) + +#### 2. **Bundle Size Control** +- **Tree-shakable**: Import only needed components/icons +- **Platform-specific builds**: No unused code +- **Selective state subscriptions**: Optimized reactivity + +#### 3. **Developer Experience** +- **Type safety**: Explicit interfaces and platform types +- **Framework familiarity**: Native patterns per platform +- **Customization**: More flexible composition patterns + +#### 4. **Maintainability** +- **Separation of concerns**: Core logic vs. UI implementation +- **Dependency hierarchy**: Clear, acyclic relationships +- **Shared testing**: Core state logic tested once + +### Migration Checklist (Extended) + +#### ✅ Phase 1: State Extraction +- [ ] Create state mediator in `packages/core/media-store/src/state-mediators/` +- [ ] Define component state interface in `packages/core/media-store/src/component-state-definitions/` +- [ ] Add state keys and transformation logic +- [ ] Implement request method factories + +#### ✅ Phase 2: Icon System Migration +- [ ] Add SVG assets to `packages/core/icons/assets/` +- [ ] Export icon strings from `packages/core/icons/src/index.ts` +- [ ] Create HTML icon components in `packages/html/html-icons/src/` +- [ ] Generate React icon components in `packages/react/react-icons/src/generated-icons/` + +#### ✅ Phase 3: HTML Component Implementation +- [ ] Create base component class in `packages/html/*/src/components/` +- [ ] Implement state hook with keys and transform function +- [ ] Implement props hook for attribute management +- [ ] Connect with `toConnectedHTMLComponent` factory +- [ ] Register custom element + +#### ✅ Phase 4: React Component Implementation +- [ ] Create React hooks in `packages/react/*/src/components/` +- [ ] Use `useMediaSelector` with `shallowEqual` optimization +- [ ] Implement props transformation for React patterns +- [ ] Create render function component +- [ ] Connect with `toConnectedComponent` factory + +#### ✅ Phase 5: Styling Migration +- [ ] Convert Shadow DOM styles to external CSS (HTML) +- [ ] Implement CSS-in-JS or styled-components (React) +- [ ] Update CSS custom property naming conventions +- [ ] Create platform-specific theming approaches + +#### ✅ Phase 6: Testing & Documentation +- [ ] Create platform-specific tests +- [ ] Verify dependency hierarchy compliance +- [ ] Document component API and usage patterns +- [ ] Create migration guide for users + +This comprehensive migration guide demonstrates how VJS-10's layered architecture enables **maximum reusability** while providing **platform-optimized developer experiences** and maintaining **strict separation of concerns**. \ No newline at end of file From 852dad717cc2a765f79f22078a965aae9a080d9a Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Tue, 9 Sep 2025 13:44:22 -0700 Subject: [PATCH 2/7] docs(claude): reference migration guide and enhance package structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update CLAUDE.md with: - Reference to MEDIA_CHROME_MIGRATION.md for architectural evolution context - Enhanced package structure documentation - Current monorepo workspace patterns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 170 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 58884dc3..de761a23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,12 +7,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is a Video.js 10 monorepo organized by platform/runtime with a clear dependency hierarchy: ### Package Structure + - **Core packages** (`packages/core/*`) - Runtime-agnostic packages that form the foundation -- **HTML packages** (`packages/html/*`) - DOM/Browser-specific implementations +- **HTML packages** (`packages/html/*`) - DOM/Browser-specific implementations - **React packages** (`packages/react/*`) - React-specific implementations - **React Native packages** (`packages/react-native/*`) - React Native implementations ### Dependency Hierarchy + - Core packages have no dependencies on other vjs-10 packages - HTML packages depend only on core packages - React packages depend only on core packages (with React peer deps) @@ -21,14 +23,41 @@ This is a Video.js 10 monorepo organized by platform/runtime with a clear depend This prevents circular dependencies and ensures maximum reusability. ### Key Core Packages -- `@vjs-10/media-store` - State management for media players + +- `@vjs-10/media-store` - State management for media players with specialized state mediators: + - `audible` - Volume, mute, and audio-related state + - `playable` - Play/pause and playback state + - `temporal` - Time-based controls (currentTime, duration, seeking) - `@vjs-10/playback-engine` - Abstraction layer for media engines (HLS.js, Dash.js, etc.) - `@vjs-10/media` - HTMLMediaElement contracts and utilities - `@vjs-10/icons` - SVG icon definitions and utilities +### Platform Packages + +Each platform has specialized packages for different concerns: + +**HTML Platform:** +- `@vjs-10/html` - Core HTML components (PlayButton, MuteButton, TimeRange, VolumeRange) +- `@vjs-10/html-icons` - HTML icon components +- `@vjs-10/html-media-elements` - HTML media element wrappers +- `@vjs-10/html-media-store` - HTML-specific MediaStore integration + +**React Platform:** +- `@vjs-10/react` - Native React components with hooks +- `@vjs-10/react-icons` - Auto-generated React icon components +- `@vjs-10/react-media-elements` - React media element wrappers +- `@vjs-10/react-media-store` - React Context and hooks for MediaStore + +**React Native Platform:** +- `@vjs-10/react-native` - React Native components +- `@vjs-10/react-native-icons` - React Native icon components +- `@vjs-10/react-native-media-elements` - React Native media wrappers +- `@vjs-10/react-native-media-store` - React Native MediaStore integration + ## Common Development Commands ### Monorepo Commands (run from root) + ```bash # Install all dependencies npm install @@ -50,6 +79,7 @@ npm run clean ``` ### Working with Specific Packages + ```bash # Build specific package npm run build --workspace=@vjs-10/media-store @@ -59,14 +89,34 @@ cd packages/core/media-store npm run build ``` +### Development & Examples + +```bash +# Run HTML example +npm run dev:html + +# Run React example +npm run dev:react + +# Build libraries only (exclude examples) +npm run build:libs +``` + +#### Example Applications +- `examples/html-demo` - HTML/TypeScript example using Vite +- `examples/react-demo` - React/TypeScript example using Vite +- `examples/react-native-demo` - React Native example (placeholder) + ## TypeScript Configuration The monorepo uses TypeScript project references for efficient compilation: + - `tsconfig.base.json` - Shared compiler options with strict settings - `tsconfig.json` - Root config with path mappings and project references - Each package has its own `tsconfig.json` extending the base Key TypeScript features: + - Strict mode enabled with additional checks (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`) - Path mappings for all `@vjs-10/*` packages point to source directories - Composite builds for incremental compilation @@ -74,7 +124,9 @@ Key TypeScript features: ## Package Development ### Individual Package Scripts + Most packages follow this pattern: + ```bash npm run build # Compile TypeScript (tsc) npm run test # Currently placeholder "No tests yet" @@ -82,6 +134,7 @@ npm run clean # Remove dist directory ``` ### Package Types + - **Core packages** - Pure TypeScript, no external dependencies - **HTML packages** - May include DOM-specific code, depend on core packages - **React packages** - Include React peer dependencies, depend on core packages @@ -90,18 +143,109 @@ npm run clean # Remove dist directory ## Workspace Management This uses npm workspaces with the following workspace patterns: + - `packages/core/*` -- `packages/html/*` +- `packages/html/*` - `packages/react/*` - `packages/react-native/*` Internal dependencies use `workspace:*` protocol for linking between packages. +## Build System + +### Turbo Build Pipeline + +The monorepo uses [Turbo](https://turbo.build/) for efficient task orchestration: + +- **Parallel builds** with dependency resolution +- **Incremental builds** with intelligent caching +- **Task dependencies** ensure proper build order +- **Development mode** with hot reloading for examples + +### Build Tooling by Package Type + +- **Core packages**: Rollup with TypeScript for dual ESM/CJS output +- **Platform packages**: TypeScript compilation with tsup +- **Examples**: Vite for fast development and building + +## State Management Architecture + +### Layered State Mediators + +The `@vjs-10/media-store` uses specialized state mediators: + +- **`audible`** - Volume, mute, and volume level state +- **`playable`** - Play/pause, loading, and playback state +- **`temporal`** - Time-based state (currentTime, duration, seeking) + +### Component State Definitions + +Shared component logic is defined in core and consumed by platforms: + +```typescript +// Core definition (platform-agnostic) +export const muteButtonStateDefinition = { + keys: ['muted', 'volumeLevel'], + stateTransform: (rawState) => ({ /* transform logic */ }), + createRequestMethods: (dispatch) => ({ /* request methods */ }), +}; + +// Platform implementations use the shared definition +const state = muteButtonStateDefinition.stateTransform(rawState); +const methods = muteButtonStateDefinition.createRequestMethods(dispatch); +``` + +## Component Development Patterns + +### Hook-Style Architecture + +All components follow a consistent hook-style pattern with three key parts: + +1. **State Hook** (`useXState`) - Manages MediaStore subscription and state transformation +2. **Props Hook** (`useXProps`) - Handles element attributes/properties based on state +3. **Render Function** (`renderX`) - Platform-specific rendering logic + +#### Example: MuteButton Implementation + +```typescript +// 1. State Hook - shared logic +export const useMuteButtonState = { + keys: ['muted', 'volumeLevel'], + transform: (rawState, mediaStore) => ({ + ...muteButtonStateDefinition.stateTransform(rawState), + ...muteButtonStateDefinition.createRequestMethods(mediaStore.dispatch), + }), +}; + +// 2. Props Hook - platform-specific attributes +export const useMuteButtonProps = (state, element) => ({ + 'data-muted': state.muted, + 'data-volume-level': state.volumeLevel, + 'aria-label': state.muted ? 'unmute' : 'mute', +}); + +// 3. Connected Component - factory combination +export const MuteButton = toConnectedComponent( + MuteButtonBase, + useMuteButtonState, + useMuteButtonProps, + 'MuteButton', +); +``` + +### Current Component Library + +- **Buttons**: PlayButton, MuteButton +- **Ranges**: TimeRange, VolumeRange +- **Display**: DurationDisplay, TimeDisplay +- **Icons**: Platform-specific icon components for all UI elements + ## Git Workflow This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) for commit messages. ### Commit Message Format + ``` [optional scope]: @@ -111,6 +255,7 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/en/ ``` ### Common Types + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation changes @@ -120,15 +265,36 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/en/ - `chore:` - Maintenance tasks, dependency updates ### Scope Examples + Use package names or areas of the codebase: + - `feat(media-store): add pause state management` - `fix(react-icons): resolve SVG rendering issue` - `docs(readme): update installation instructions` - `chore(deps): update typescript to 5.4.0` ### Breaking Changes + For breaking changes, add `!` after the type/scope: + ``` feat!: remove deprecated playback API feat(media-store)!: change state interface structure -``` \ No newline at end of file +``` + +## Migration from Media Chrome + +This project represents a significant architectural evolution from Media Chrome, drawing heavy inspiration from varioius other projects as well. For detailed information about migrating components, state management, styling, and React patterns from Media Chrome to VJS-10, see: + +**[MEDIA_CHROME_MIGRATION.md](./MEDIA_CHROME_MIGRATION.md)** + +Key migration topics covered: + +- **State Management**: From monolithic MediaStore to layered nanostores with mediators +- **Component Architecture**: From web components to hook-style with platform adapters +- **React Integration**: From auto-generated thin wrappers to native React components +- **Styling & Theming**: From Shadow DOM + CSS custom properties to platform-specific approaches +- **Icon Management**: From inline SVG with slots to centralized icon packages +- **Subcomponent Patterns**: From slots to React children/render props + +The migration guide includes detailed code examples, commit history analysis, and step-by-step transformation patterns using the mute button as a comprehensive case study. From 8c08e3b599b7f6061eab6d4ce10bd611d64728cd Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Tue, 9 Sep 2025 13:44:36 -0700 Subject: [PATCH 3/7] docs: add comprehensive VJS-10 architecture documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document architectural influences and design philosophy including: - Media Elements: Platform-agnostic HTMLMediaElement contract evolution - Media Chrome: Foundational state management and media architecture - VidStack: Multi-framework common core architecture patterns - Base UI: Component primitives and compound component philosophy - Adobe React Spectrum: Three-layer hook architecture separation Covers planned evolution including compound components, CLI tooling, and cross-platform state management patterns. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- ARCHITECTURE.md | 1133 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1133 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..f7c02935 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,1133 @@ +# VJS-10 Architecture & Design Philosophy + +## Overview + +VJS-10 represents a significant architectural evolution in media player component libraries, prioritizing platform-native development experiences while maintaining shared core logic. This document outlines the design philosophy, architectural influences, and key decisions that shape the VJS-10 ecosystem. + +## Architectural Influences & Inspirations + +### Media Elements: Platform-Agnostic HTMLMediaElement Contract + +VJS-10's media state management architecture draws significant inspiration from the [media-elements monorepo](https://github.com/muxinc/media-elements), which pioneered the concept of creating HTMLMediaElement-compatible elements that work across different media providers while maintaining consistent interfaces. + +#### 1. Extended HTMLMediaElement Contract Foundation + +**Media Elements Innovation**: The media-elements monorepo established the pattern of creating custom elements that "look like" HTMLMediaElement but can be extended for different media providers (HLS, DASH, YouTube, Vimeo, etc.). + +**Core Architecture Pattern**: +```typescript +// Media Elements: CustomVideoElement extends HTMLVideoElement +export class CustomVideoElement extends HTMLVideoElement implements HTMLVideoElement { + readonly nativeEl: HTMLVideoElement; + + // Maintains HTMLMediaElement contract + get currentTime() { return this.nativeEl?.currentTime ?? 0; } + set currentTime(val) { if (this.nativeEl) this.nativeEl.currentTime = val; } + + play(): Promise { return this.nativeEl?.play() ?? Promise.resolve(); } + pause(): void { this.nativeEl?.pause(); } +} + +// Provider-specific implementations +class HlsVideoElement extends CustomVideoElement { + api: Hls | null = null; + + async load() { + if (Hls.isSupported()) { + this.api = new Hls(this.config); + this.api.loadSource(this.src); + this.api.attachMedia(this.nativeEl); + } + } +} +``` + +**Key Architectural Assumptions**: +- Media state owner must be an `HTMLElement` (DOM-based) +- Must implement the complete `HTMLMediaElement` interface +- Provider-specific logic encapsulated in custom element classes +- Shadow DOM for consistent styling and behavior + +**Reference**: [`packages/custom-media-element/custom-media-element.ts`](https://github.com/muxinc/media-elements/blob/main/packages/custom-media-element/custom-media-element.ts) + +#### 2. VJS-10's Platform-Agnostic Evolution + +**VJS-10 Innovation**: Relaxed the HTMLElement requirement while maintaining the HTMLMediaElement contract, enabling true cross-platform compatibility. + +**Architectural Relaxation**: +```typescript +// VJS-10: MediaStateOwner - JavaScript interface only +export type MediaStateOwner = Partial & + Pick & + EventTarget & { // Only requires EventTarget, not HTMLElement + // HTMLMediaElement contract maintained + currentTime?: number; + duration?: number; + volume?: number; + muted?: boolean; + paused?: boolean; + + // Extended media-specific properties (Media Elements influence) + streamType?: StreamTypes; + targetLiveWindow?: number; + videoRenditions?: Rendition[] & EventTarget; + audioTracks?: AudioTrack[] & EventTarget; + + // Platform-specific extensions + webkitDisplayingFullscreen?: boolean; + webkitCurrentPlaybackTargetIsWireless?: boolean; + }; +``` + +**Cross-Platform Implementation**: + +**HTML Platform** (Media Elements Heritage): +```typescript +// Direct evolution from media-elements CustomVideoElement +export class MediaVideoElement extends HTMLVideoElement implements MediaStateOwner { + connectedCallback() { + // Media Elements pattern: delegate to native element + this.mediaStore = createMediaStore(this.nativeEl); + } +} +``` + +**React Platform** (Platform-Agnostic Contract): +```typescript +// Uses HTMLVideoElement but not as DOM element +export const useVideoElement = (): MediaStateOwner => { + const videoRef = useRef(null); + + return useMemo(() => ({ + // Maintains HTMLMediaElement contract without DOM assumptions + get currentTime() { return videoRef.current?.currentTime ?? 0; }, + set currentTime(val) { if (videoRef.current) videoRef.current.currentTime = val; }, + + play: () => videoRef.current?.play() ?? Promise.resolve(), + pause: () => videoRef.current?.pause(), + + addEventListener: (type, listener) => videoRef.current?.addEventListener(type, listener), + removeEventListener: (type, listener) => videoRef.current?.removeEventListener(type, listener), + }), []); +}; +``` + +**React Native Platform** (Contract Without HTMLMediaElement): +```typescript +// Implements MediaStateOwner contract with React Native Video +export const useVideoElementNative = (): MediaStateOwner => { + const videoRef = useRef