From 2cb39d0304bd07b379df3698441bd612b440eff0 Mon Sep 17 00:00:00 2001 From: Rahim Date: Mon, 5 Jan 2026 14:56:24 +1100 Subject: [PATCH] docs(plan): remove old file --- .claude/plans/store-factory-hooks.md | 827 --------------------------- 1 file changed, 827 deletions(-) delete mode 100644 .claude/plans/store-factory-hooks.md diff --git a/.claude/plans/store-factory-hooks.md b/.claude/plans/store-factory-hooks.md deleted file mode 100644 index cca0afe3..00000000 --- a/.claude/plans/store-factory-hooks.md +++ /dev/null @@ -1,827 +0,0 @@ -# Store React/DOM Bindings - -## Goal - -Implement React and DOM bindings for Video.js 10's store, enabling: - -- Simple `createStore()` API that returns Provider + hooks/controllers -- Skins define their own store configs and export Provider + Skin + hooks -- Consumers can extend skin configs with additional slices -- Base hooks/controllers for testing and advanced use cases - -## Key Decisions - -| Decision | Resolution | -| ---------------------- | ----------------------------------------------------------------------- | -| Store creation | `createStore({ slices, displayName? })` - types inferred from slices | -| Hook naming | `useStore`, `useSelector`, `useRequest`, `usePending`, `useSlice` | -| Controller naming | `StoreController`, `SelectorController`, `RequestController`, etc | -| Selector hook | `useSelector(selector)` - requires selector (Redux-style) | -| Store hook | `useStore()` - returns store instance | -| Pending hook | `usePending()` - returns `store.queue.pending` (reactive) | -| Slice hook return | `{ state, request, isAvailable }` - state/request null when unavailable | -| Skin exports | `Provider`, `Skin`, hooks/controllers | -| Slice namespace | `export * as media` → `media.playbackSlice` | -| Video component | Generic, exported from `@videojs/react` (not from skins) | -| DOM provider | `withStoreProvider` mixin | -| Base hooks/controllers | Take store explicitly, for testing/advanced use | -| displayName | For React DevTools component naming | - ---- - -## Phase 1: React Bindings (`@videojs/store/react`) - -### 1.1 `createStore` - -**File:** `packages/store/src/react/create-store.ts` - -```typescript -import type { AnySlice, StoreConfig } from '../core'; -import type { ReactNode } from 'react'; - -export interface CreateStoreConfig { - slices: Slices; - displayName?: string; -} - -export interface CreateStoreResult { - Provider: FC<{ children: ReactNode }>; - useStore: () => Store, Slices>; - useSelector: (selector: (state: UnionSliceState) => T) => T; - useRequest: () => UnionSliceRequests; - usePending: () => PendingRecord>; - useSlice: (slice: S) => SliceResult; -} - -export function createStore(config: CreateStoreConfig): CreateStoreResult; -``` - -### 1.2 Types - -**File:** `packages/store/src/react/types.ts` - -```typescript -export type SliceResult = - | { state: InferSliceState; request: InferSliceRequests; isAvailable: true } - | { state: null; request: null; isAvailable: false }; -``` - -### 1.3 Base hooks - -**File:** `packages/store/src/react/hooks.ts` - -```typescript -// Base hooks - take store explicitly (for testing/advanced use) -export function useSelector(store: S, selector: (state: InferStoreState) => T): T; - -export function useRequest(store: S): InferStoreRequests; - -export function usePending(store: S): PendingRecord>; - -export function useSlice(store: S, slice: Slice): SliceResult; -``` - -**Implementation details:** - -- `Provider`: Creates store via `useState(() => new Store(config))`, provides via context -- `useStore()`: Returns store instance from context -- `useSelector(selector)`: Uses `useSyncExternalStore` with selector -- `useRequest()`: Returns stable `store.request` from context -- `usePending()`: Subscribes to `store.queue`, returns `queue.pending` -- `useSlice(slice)`: Returns `{ state, request, isAvailable }` with null narrowing -- Cleanup: `useEffect` calls `store.destroy()` on unmount - -### 1.4 Exports - -**File:** `packages/store/src/react/index.ts` - -```typescript -export { createStore } from './create-store'; -// Base hooks for testing/advanced use -export { usePending, useRequest, useSelector, useSlice } from './hooks'; - -export type { CreateStoreConfig, CreateStoreResult, SliceResult } from './types'; -``` - ---- - -## Phase 2: DOM Bindings (`@videojs/store/dom`) - -### 2.0 @lit/context Research - -Key findings from analyzing `@lit/context@1.1.6`: - -**Dynamic Value Updates:** - -- `ContextProvider.setValue(newValue, force?)` notifies all subscribed consumers -- Uses `Object.is()` for equality - swapping store objects triggers updates automatically -- `force = true` needed only for in-place mutations (same reference) - -**Subscription Model:** - -- Consumers must opt-in: `subscribe: true` in `@consume()` or `ContextConsumer` -- Without subscription, consumers only receive initial value -- Provider stores callbacks in `Map` -- `updateObservers()` iterates all callbacks on value change - -**Store Swapping Pattern (validated):** - -```typescript -class MySkin extends HTMLElement { - #provider = new ContextProvider(this, { context: storeContext }); - - set store(newStore: Store) { - this.#provider.setValue(newStore); // Notifies all subscribers - } -} -``` - -### 2.1 `createStore` - -**File:** `packages/store/src/dom/create-store.ts` - -```typescript -import type { Context } from '@lit/context'; -import type { ReactiveControllerHost } from '@lit/reactive-element'; - -import { createContext } from '@lit/context'; - -export interface CreateStoreConfig { - slices: Slices; -} - -export interface CreateStoreResult { - defineStoreProvider: (tagName: string) => void; - withStoreProvider: >(Base: T) => T; - StoreController: new (host: ReactiveControllerHost) => StoreControllerInstance; - SelectorController: ( - host: ReactiveControllerHost, - selector: (state: UnionSliceState) => T - ) => SelectorControllerInstance; - RequestController: new (host: ReactiveControllerHost) => RequestControllerInstance; - PendingController: new (host: ReactiveControllerHost) => PendingControllerInstance; - SliceController: (host: ReactiveControllerHost, slice: S) => SliceControllerInstance; - context: Context, Slices>>; -} - -export function createStore(config: CreateStoreConfig): CreateStoreResult; -``` - -### 2.2 Base controllers - -**File:** `packages/store/src/dom/controllers.ts` - -```typescript -import type { ReactiveController, ReactiveControllerHost } from '@lit/reactive-element'; - -// Base controllers - take store explicitly (for testing/advanced use) -export class SelectorController implements ReactiveController { - constructor(host: ReactiveControllerHost, store: S, selector: (state: InferStoreState) => T); - get value(): T; -} - -export class RequestController implements ReactiveController { - constructor(host: ReactiveControllerHost, store: S); - get value(): InferStoreRequests; -} - -export class PendingController implements ReactiveController { - constructor(host: ReactiveControllerHost, store: S); - get value(): PendingRecord>; -} - -export class SliceController implements ReactiveController { - constructor(host: ReactiveControllerHost, store: S, slice: Slice); - get state(): InferSliceState | null; - get request(): InferSliceRequests | null; - get isAvailable(): this is this & { state: InferSliceState; request: InferSliceRequests }; -} -``` - -**Implementation details:** - -- Uses `@lit/context` for W3C Context Protocol -- Context key auto-generated per `createStore()` call (unique Symbol) -- `withStoreProvider`: Mixin that: - - Creates store instance - - Uses `ContextProvider` internally - - Exposes `store` setter that calls `provider.setValue(newStore)` -- Controllers: - - Consume store via `ContextConsumer` with `subscribe: true` - - Subscribe to store changes, call `host.requestUpdate()` on change - - Cleanup on `hostDisconnected` (automatic via ContextConsumer) -- `SelectorController`: One-step instantiation `new SelectorController(this, selector)` - -### 2.3 Exports - -**File:** `packages/store/src/dom/index.ts` - -```typescript -// Base controllers for testing/advanced use -export { PendingController, RequestController, SelectorController, SliceController } from './controllers'; -export { createStore } from './create-store'; - -export type { CreateStoreConfig, CreateStoreResult } from './types'; -``` - ---- - -## Phase 3: Playback Slice (`@videojs/core/dom`) - -Based on [Issue #239](https://github.com/videojs/v10/issues/239). - -### 3.1 Playback slice - -**File:** `packages/core/src/dom/slices/playback.ts` - -```typescript -interface PlaybackState { - paused: boolean; - ended: boolean; - started: boolean; - waiting: boolean; - currentTime: number; - duration: number; - buffered: Array<[number, number]>; - seekable: Array<[number, number]>; - volume: number; - muted: boolean; - canPlay: boolean; - source: unknown; - streamType: 'on-demand' | 'live' | 'live-dvr' | 'unknown'; -} - -interface PlaybackRequests { - play: Request; - pause: Request; - seek: Request; - changeVolume: Request; - toggleMute: Request; - changeSource: Request; -} - -export const playbackSlice = createSlice()({ - initialState: { - /* ... */ - }, - getSnapshot: ({ target }) => ({ - /* ... */ - }), - subscribe: ({ target, update, signal }) => { - /* ... */ - }, - request: { - /* ... */ - }, -}); -``` - -### 3.2 Namespace export - -**File:** `packages/core/src/dom/slices/index.ts` - -```typescript -export { playbackSlice } from './playback'; - -// Namespace export -export * as media from './index'; -``` - -Usage: - -```typescript -import { media, playbackSlice } from '@videojs/core/dom'; - -media.playbackSlice; // via namespace -playbackSlice; // standalone export -``` - -### 3.3 Utilities - -**File:** `packages/core/src/dom/slices/utils.ts` - -```typescript -export function serializeTimeRanges(ranges: TimeRanges): Array<[number, number]>; -``` - -### 3.4 Type guards - -**File:** `packages/core/src/dom/guards.ts` - -```typescript -export function isHTMLVideo(target: unknown): target is HTMLVideoElement; -export function isHTMLAudio(target: unknown): target is HTMLAudioElement; -export function isHTMLMedia(target: unknown): target is HTMLMediaElement; -``` - ---- - -## Phase 4: React Package Setup (`@videojs/react`) - -### 4.1 Video component - -**File:** `packages/react/src/media/Video.tsx` - -```typescript -import type { VideoHTMLAttributes, RefCallback } from 'react'; -import { useCallback } from 'react'; -import { useStore } from '../store'; -import { useComposedRefs } from '../utils/use-composed-refs'; - -export interface VideoProps extends VideoHTMLAttributes { - ref?: RefCallback | React.RefObject; -} - -/** - * Video element that automatically attaches to the store. - * Uses React 19 ref cleanup pattern. - */ -export function Video({ children, ref, ...props }: VideoProps): JSX.Element { - const store = useStore(); - - const attachRef: RefCallback = useCallback((el) => { - if (el) { - const detach = store.attach(el); - // React 19: return cleanup function - return detach; - } - }, [store]); - - const composedRef = useComposedRefs(ref, attachRef); - - return ( - - ); -} -``` - -### 4.2 Package exports - -**File:** `packages/react/src/index.ts` - -```typescript -// Media elements -export { Video } from './media/Video'; -export type { VideoProps } from './media/Video'; - -export { media } from '@videojs/core/dom'; -// Re-export for extension -export { createStore } from '@videojs/store/react'; -``` - ---- - -## Phase 5: Frosted Skin (React) - -### 5.1 Store config - -**File:** `packages/react/src/skins/frosted/store.ts` - -```typescript -import { media } from '@videojs/core/dom'; -import { createStore } from '@videojs/store/react'; - -export const storeConfig = { - slices: [media.playback] as const, - displayName: 'FrostedSkin', -}; - -export const { Provider, useStore, useSelector, useRequest, usePending, useSlice } = createStore(storeConfig); -``` - -### 5.2 Skin component - -**File:** `packages/react/src/skins/frosted/Skin.tsx` - -```typescript -import type { PropsWithChildren } from 'react'; - -import { useRequest, useSelector } from './store'; - -export interface SkinProps extends PropsWithChildren<{ - className?: string; - theme?: 'light' | 'dark'; -}> {} - -export function Skin({ children, className, theme }: SkinProps): JSX.Element { - return ( -
- {children} - -
- ); -} - -function Controls() { - const paused = useSelector((s) => s.paused); - const { play, pause } = useRequest(); - // ... render controls -} -``` - -### 5.3 Exports - -**File:** `packages/react/src/skins/frosted/index.ts` - -```typescript -export { Skin } from './Skin'; -export type { SkinProps } from './Skin'; -export { Provider, storeConfig, usePending, useRequest, useSelector, useSlice, useStore } from './store'; -``` - ---- - -## Phase 6: Frosted Skin (HTML) - -### 6.1 Store config - -**File:** `packages/html/src/skins/frosted/store.ts` - -```typescript -import { media } from '@videojs/core/dom'; -import { createStore } from '@videojs/store/dom'; - -export const storeConfig = { - slices: [media.playback] as const, -}; - -export const { - defineStoreProvider, - withStoreProvider, - StoreController, - SelectorController, - RequestController, - PendingController, - SliceController, - context, -} = createStore(storeConfig); -``` - -### 6.2 Skin component - -**File:** `packages/html/src/skins/frosted/Skin.ts` - -```typescript -import { RequestController, SelectorController, withStoreProvider } from './store'; - -export class FrostedSkinElement extends HTMLElement { - #paused = new SelectorController(this, (s) => s.paused); - #request = new RequestController(this); - - /** - * Define a custom element with this skin, optionally with a custom provider mixin. - * Useful for extending the skin with additional slices. - */ - static define(tagName: string, providerMixin = withStoreProvider) { - const SkinWithProvider = providerMixin(this); - customElements.define(tagName, SkinWithProvider); - } - - connectedCallback() { - this.innerHTML = ` - -
- -
- `; - this.#updatePlayButton(); - this.querySelector('button')?.addEventListener('click', this.#handleClick); - } - - #handleClick = () => { - this.#paused.value ? this.#request.value.play() : this.#request.value.pause(); - }; - - #updatePlayButton() { - const btn = this.querySelector('.play-pause'); - if (btn) btn.textContent = this.#paused.value ? 'Play' : 'Pause'; - } -} - - #handleClick = () => { - this.#paused.value ? this.#request.value.play() : this.#request.value.pause(); - }; - - #updatePlayButton() { - const btn = this.querySelector('.play-pause'); - if (btn) btn.textContent = this.#paused.value ? 'Play' : 'Pause'; - } -} -``` - -### 6.3 Define export - -**File:** `packages/html/src/define/vjs-frosted-skin.ts` - -```typescript -import { FrostedSkinElement } from '../skins/frosted/Skin'; - -customElements.define('vjs-frosted-skin', FrostedSkinElement); -``` - -### 6.4 Exports - -**File:** `packages/html/src/skins/frosted/index.ts` - -```typescript -export { FrostedSkinElement } from './Skin'; -export { - context, - PendingController, - RequestController, - SelectorController, - SliceController, - storeConfig, - StoreController, - withStoreProvider, -} from './store'; -``` - ---- - -## Usage Examples - -### React: Custom UI - -```tsx -import { createStore, media, Video } from '@videojs/react'; - -const { Provider, useSelector, useRequest } = createStore({ - slices: [media.playbackSlice], -}); - -function App() { - return ( - - - ); -} - -function MyCustomControls() { - const currentTime = useSelector((s) => s.currentTime); - const { seek } = useRequest(); - return ; -} -``` - -### React: Frosted skin - -```tsx -import { Video } from '@videojs/react'; -import { Provider, Skin } from '@videojs/react/skins/frosted'; - -function App() { - return ( - - - - - ); -} -``` - -### React: Extending frosted with custom slices - -```tsx -import { createStore, Video } from '@videojs/react'; -import { Skin, storeConfig } from '@videojs/react/skins/frosted'; - -import { chaptersSlice } from './slices/chapters'; - -// Extend frosted config with custom slice -const { Provider, useSlice } = createStore({ - ...storeConfig, - slices: [...storeConfig.slices, chaptersSlice], -}); - -function App() { - return ( - - - - - - ); -} - -function ChaptersPanel() { - const chapters = useSlice(chaptersSlice); - if (!chapters.isAvailable) return null; - return
{chapters.state.markers.map(...)}
; -} -``` - -### HTML: Frosted skin (CDN) - -```html - - - - - -``` - -### HTML: Custom provider element (CDN) - -```html - - - - - - -``` - -Where `my-player.js` contains: - -```typescript -import { createStore, playbackSlice } from '@videojs/html'; - -const { defineStoreProvider } = createStore({ - slices: [playbackSlice], -}); - -defineStoreProvider('my-player'); -``` - -### HTML: Extending frosted with custom slices - -```html - - - - - -``` - -Where `my-extended-skin.js` contains: - -```typescript -import { FrostedSkinElement, storeConfig } from '@videojs/html/skins/frosted'; -import { createStore } from '@videojs/store/dom'; - -import { chaptersSlice } from './slices/chapters.js'; - -// Extend frosted config with custom slice -const { withStoreProvider } = createStore({ - ...storeConfig, - slices: [...storeConfig.slices, chaptersSlice], -}); - -FrostedSkinElement.define('my-extended-skin', withStoreProvider); -``` - ---- - -## File Structure - -``` -packages/store/src/ -├── core/ -│ ├── store.ts # existing -│ ├── slice.ts # existing -│ ├── queue.ts # existing -│ └── index.ts -├── react/ -│ ├── create-store.ts # NEW -│ ├── hooks.ts # NEW (base hooks) -│ ├── types.ts # NEW -│ └── index.ts -└── dom/ - ├── create-store.ts # NEW - ├── controllers.ts # NEW (base controllers) - ├── types.ts # NEW - └── index.ts - -packages/core/src/dom/ -├── slices/ -│ ├── playback.ts # NEW -│ ├── utils.ts # NEW -│ └── index.ts # NEW (+ media namespace) -├── guards.ts # NEW -└── index.ts - -packages/react/src/ -├── media/ -│ └── Video.tsx # NEW -├── skins/ -│ ├── frosted/ -│ │ ├── store.ts # NEW -│ │ ├── Skin.tsx # NEW -│ │ └── index.ts # NEW -│ └── minimal/ -│ └── ... -└── index.ts - -packages/html/src/ -├── define/ -│ └── vjs-frosted-skin.ts # NEW -├── skins/ -│ ├── frosted/ -│ │ ├── store.ts # NEW -│ │ ├── Skin.ts # NEW -│ │ ├── styles.css # NEW -│ │ └── index.ts # NEW -│ └── minimal/ -│ └── ... -└── index.ts -``` - ---- - -## Implementation Order - -1. **Phase 1**: React bindings (`@videojs/store/react`) - - `createStore()`, base hooks, types -2. **Phase 2**: DOM bindings (`@videojs/store/dom`) - - `createStore()`, `defineStoreProvider()`, base controllers, types, `@lit/context` integration -3. **Phase 3**: Playback slice (`@videojs/core/dom`) - - `media.playback`, utils, guards -4. **Phase 4**: React package setup (`@videojs/react`) - - `Video`, package exports -5. **Phase 5**: Frosted skin React (`@videojs/react/skins/frosted`) -6. **Phase 6**: Frosted skin HTML (`@videojs/html/skins/frosted`) - -Each phase includes tests. - ---- - -## PR Coordination - -### Related Issues - -| Issue | Title | Description | -| ----- | ------------------------ | ----------------------------- | -| #218 | Store | Parent tracking issue | -| #229 | React Bindings | `createStore`, hooks, context | -| #230 | ReactiveElement Bindings | Controllers, context | -| #231 | Frosted Skin | Frosted skin implementation | -| #239 | Playback Feature | Consolidated playback slice | - -### Existing PR - -- **PR #281** (`videojs-store-lit`) - Draft with WIP Lit controllers - - Has: `StoreController`, `SliceController`, `PendingController` - - Missing: `SelectorController`, `RequestController`, context-based factory, `withStoreProvider` mixin - - Action: Update with new API design - -### PR Strategy - -``` -PR 1: Store React Bindings -├── createStore (store/react) -├── Base hooks (store/react) -├── Types -├── References #218 -└── Closes #229 - -PR 2: Store DOM Bindings (update PR #281) -├── createStore (store/dom) -├── defineStoreProvider -├── Base controllers (store/dom) -├── withStoreProvider mixin -├── @lit/context integration -├── References #218 -└── Closes #230 - -PR 3: Playback Slice -├── media.playbackSlice (core/dom/slices) -├── Utils (serializeTimeRanges) -├── Type guards -├── References #218 -└── Closes #239 - -PR 4: React Package Setup -├── Video component -├── Package exports -└── References #218 - -PR 5: Frosted Skin -├── React skin (Provider, Skin, storeConfig, hooks) -├── HTML skin (FrostedSkinElement.define, storeConfig, controllers) -├── define/vjs-frosted-skin.ts -├── References #218 -└── Closes #231 -``` - -### Dependency Graph - -``` -PR 1 ──┬──> PR 2 (can parallel) - └──> PR 3 (can parallel) - └──> PR 4 (needs PR 1, PR 3) - └──> PR 5 (needs PR 2, PR 4) -``` - ---- - -## Deferred - -- Testing utilities (`@videojs/store/testing`) - separate plan -- Minimal skin implementation