diff --git a/.claude/plans/store-bindings.md b/.claude/plans/store-bindings.md
index bb41ade8..a8469ad2 100644
--- a/.claude/plans/store-bindings.md
+++ b/.claude/plans/store-bindings.md
@@ -36,7 +36,8 @@ Implement React and DOM bindings for Video.js 10's store, enabling:
| Primitives context | `useStoreContext()` internal hook for primitive UI components |
| displayName | For React DevTools component naming |
| Component types | Namespace pattern: `Skin.Props` via `namespace Skin { export type Props }` |
-| Element define | `FrostedSkinElement.define(tagName, { mixins })` for declarative setup |
+| Element define | `FrostedSkinElement.define(tagName?, mixin?)` - pass mixin directly |
+| Slot handling | Default slot (``) - no `slot="media"` attribute needed |
| Config extension | `extendConfig()` uses `uniqBy` + `composeCallbacks` from utils |
| Provider resolution | Isolated by default; `inherit` prop to use parent store from context |
| Store instance | `create()` method for imperative store creation |
@@ -232,108 +233,18 @@ export { extendConfig, Provider } from './store';
---
-## Phase 6: Frosted Skin (HTML)
+## Phase 6: Frosted Skin [DONE]
-### 6.1 Store config
+> Refer to [PR #298](https://github.com/videojs/v10/pull/298) for implementation details.
-**File:** `packages/html/src/skins/frosted/store.ts`
+React and HTML frosted skin store setup:
-```typescript
-import type { AnySlice, StoreConfig } from '@videojs/store';
+- React: `Provider`, `Skin`, `extendConfig`, `createStore` in `@videojs/react/skins/frosted`
+- HTML: `FrostedSkinElement`, `StoreMixin`, `extendConfig` in `@videojs/html/skins/frosted`
+- Auto-define entry: `@videojs/html/define/vjs-frosted-skin` for CDN usage
+- `StoreAttachMixin` uses default slot (no `slot="media"` attribute needed)
-import { extendConfig as extendBaseConfig } from '@videojs/store';
-import { createStore } from '@videojs/store/lit';
-
-import { media } from './slices'; // internal - re-exported from @videojs/html
-
-/** Base config for frosted skin. */
-const baseConfig = {
- slices: [media.playback] as const,
-};
-
-/**
- * Extends frosted skin config with additional slices/hooks.
- * Composes lifecycle hooks (both called, base first).
- */
-export function extendConfig(extension?: Partial>) {
- return extendBaseConfig(baseConfig, extension);
-}
-
-export const { StoreMixin, StoreProviderMixin, StoreAttachMixin, context } = createStore(extendConfig());
-```
-
-### 6.2 Skin component
-
-**File:** `packages/html/src/skins/frosted/skin.ts`
-
-```typescript
-import { StoreAttachMixin, StoreMixin, StoreProviderMixin } from './store';
-
-type Mixin = >(Base: T) => T;
-
-export interface DefineOptions {
- /** Mixins to apply. Defaults to [StoreMixin] (combined provider + attach). */
- mixins?: Mixin[];
-}
-
-/**
- * Frosted skin element. Empty for now - controls will be added later.
- * Uses shadow DOM with slot for video element.
- */
-export class FrostedSkinElement extends HTMLElement {
- /** Default tag name for this element. */
- static tagName = 'vjs-frosted-skin';
-
- /**
- * Define this element with the custom elements registry.
- *
- * @example
- * // Default: combined provider + attach
- * FrostedSkinElement.define('vjs-frosted-skin');
- *
- * @example
- * // Granular mixin control (e.g., attach only, inherit provider from parent)
- * FrostedSkinElement.define('vjs-thumbnail', { mixins: [StoreAttachMixin] });
- *
- * @example
- * // Custom store with extended slices
- * const { StoreMixin } = createStore(extendConfig({ slices: [chaptersSlice] }));
- * FrostedSkinElement.define('my-extended-player', { mixins: [StoreMixin] });
- */
- static define(tagName: string, options: DefineOptions = {}) {
- const { mixins = [StoreMixin] } = options;
-
- // Apply mixins in order (right to left composition)
- const Mixed = mixins.reduceRight((Base, mixin) => mixin(Base), this as typeof FrostedSkinElement);
- customElements.define(tagName, Mixed);
- }
-
- connectedCallback() {
- const shadow = this.attachShadow({ mode: 'open' });
- shadow.innerHTML = ``;
- }
-}
-```
-
-### 6.3 Define export
-
-**File:** `packages/html/src/define/vjs-frosted-skin.ts`
-
-```typescript
-import { FrostedSkinElement } from '../skins/frosted/skin';
-
-FrostedSkinElement.define('vjs-frosted-skin');
-```
-
-### 6.4 Exports
-
-**File:** `packages/html/src/skins/frosted/index.ts`
-
-```typescript
-export { FrostedSkinElement } from './skin';
-export type { DefineOptions } from './skin';
-export { context, extendConfig, StoreAttachMixin, StoreMixin, StoreProviderMixin } from './store';
-```
+**Note:** Don't export `context` from skins (causes unique symbol type issues). Users can import from `@videojs/store/lit` if needed.
---
@@ -501,17 +412,20 @@ function ChaptersPanel() {
Where `my-player.js` contains:
```typescript
-import { createStore, media } from '@videojs/html';
+import { ReactiveElement } from '@lit/reactive-element';
+import { media } from '@videojs/core/dom';
+import { createStore } from '@videojs/store/lit';
const { StoreMixin } = createStore({
- slices: [media.playback],
+ slices: [...media.all],
});
// Create custom element with store provider and auto-attach
-class MyPlayer extends StoreMixin(HTMLElement) {
- connectedCallback() {
+class MyPlayer extends StoreMixin(ReactiveElement) {
+ constructor() {
+ super();
const shadow = this.attachShadow({ mode: 'open' });
- shadow.innerHTML = ``;
+ shadow.innerHTML = '';
}
}
@@ -539,7 +453,7 @@ import { chaptersSlice } from './slices/chapters.js';
// Extend frosted config with custom slice (merges with base slices)
const { StoreMixin } = createStore(extendConfig({ slices: [chaptersSlice] }));
-FrostedSkinElement.define('my-extended-skin', { mixins: [StoreMixin] });
+FrostedSkinElement.define('my-extended-skin', StoreMixin);
```
---
@@ -683,9 +597,9 @@ packages/html/src/
- Shared types: `OptimisticResult` discriminated union in `src/shared/types.ts` ✓
- Tests: 13 new tests for React hook ✓
-8. **Phase 6**: Skins
- - React skin (Provider, Skin, extendConfig)
- - HTML skin (FrostedSkinElement, extendConfig)
+8. **Phase 6**: Skins **[DONE - PR #298]**
+ - React skin (Provider, Skin, extendConfig) ✓
+ - HTML skin (FrostedSkinElement, extendConfig) ✓
Each phase includes tests.
@@ -814,15 +728,15 @@ Use `types` + `default` format to match existing packages.
### Related Issues
-| Issue | Title | Description | Status |
-| ----- | ------------------- | --------------------------------------- | ------ |
-| #218 | Store | Parent tracking issue | Open |
-| #285 | Queue Task Refactor | Unified tasks map, status discriminator | Closed |
-| #228 | Optimistic Updates | useMutation, useOptimistic | Open |
-| #229 | React Bindings | createStore, hooks, context | Closed |
-| #230 | Lit Bindings | Controllers, mixins, context | Closed |
-| #239 | DOM Media Slices | media slices | Closed |
-| #231 | Skin Stores | Skin store configuration | Open |
+| Issue | Title | Description | Status |
+| ----- | ------------------- | --------------------------------------- | ---------------- |
+| #218 | Store | Parent tracking issue | Open |
+| #285 | Queue Task Refactor | Unified tasks map, status discriminator | Closed |
+| #228 | Optimistic Updates | useMutation, useOptimistic | Open |
+| #229 | React Bindings | createStore, hooks, context | Closed |
+| #230 | Lit Bindings | Controllers, mixins, context | Closed |
+| #239 | DOM Media Slices | media slices | Closed |
+| #231 | Skin Stores | Skin store configuration | Closed (PR #298) |
### PR Strategy
@@ -875,9 +789,9 @@ PR #291: Optimistic Hooks/Controllers [DONE]
├── Tests: 13 new tests for React hook ✓
└── Closes #228
-PR F: Skins
-├── React skin (Provider, Skin, extendConfig)
-├── HTML skin (FrostedSkinElement, extendConfig)
+PR #298: Skins [DONE]
+├── React skin (Provider, Skin, extendConfig) ✓
+├── HTML skin (FrostedSkinElement, extendConfig) ✓
├── References #218
└── Closes #231
```
@@ -885,12 +799,12 @@ PR F: Skins
### Dependency Graph
```
-PR #283 ───> PR #287 ───> PR #288 ───> PR #290 ───> PR #291 ───> PR F (Skins)
+PR #283 ───> PR #287 ───> PR #288 ───> PR #290 ───> PR #291 ───> PR #298 (Skins)
└──> PR #289 (done) ────────────────────────┘
└──> PR #292 (done) ────────────────────────┘
```
-PRs are sequential. PR #288, #289, #292 can technically parallel after PR #287, but we'll do them sequentially for easier review.
+All PRs merged. Store bindings implementation complete.
---
diff --git a/packages/html/package.json b/packages/html/package.json
index 6124411b..29a320e2 100644
--- a/packages/html/package.json
+++ b/packages/html/package.json
@@ -16,6 +16,14 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
+ },
+ "./skins/frosted": {
+ "types": "./dist/skins/frosted.d.ts",
+ "default": "./dist/skins/frosted.js"
+ },
+ "./define/vjs-frosted-skin": {
+ "types": "./dist/define/vjs-frosted-skin.d.ts",
+ "default": "./dist/define/vjs-frosted-skin.js"
}
},
"main": "dist/index.js",
@@ -32,6 +40,7 @@
"clean": "rm -rf dist types"
},
"dependencies": {
+ "@lit/reactive-element": "^2.1.2",
"@videojs/core": "workspace:*",
"@videojs/store": "workspace:*",
"@videojs/utils": "workspace:*"
diff --git a/packages/html/src/define/vjs-frosted-skin.ts b/packages/html/src/define/vjs-frosted-skin.ts
new file mode 100644
index 00000000..da7471d6
--- /dev/null
+++ b/packages/html/src/define/vjs-frosted-skin.ts
@@ -0,0 +1,3 @@
+import { FrostedSkinElement } from '../skins/frosted';
+
+FrostedSkinElement.define();
diff --git a/packages/html/src/skins/frosted/index.ts b/packages/html/src/skins/frosted/index.ts
new file mode 100644
index 00000000..da2336ef
--- /dev/null
+++ b/packages/html/src/skins/frosted/index.ts
@@ -0,0 +1,12 @@
+export { FrostedSkinElement } from './skin';
+
+export {
+ create as createStore,
+ extendConfig,
+ RequestController,
+ SelectorController,
+ StoreAttachMixin,
+ StoreMixin,
+ StoreProviderMixin,
+ TasksController,
+} from './store';
diff --git a/packages/html/src/skins/frosted/skin.ts b/packages/html/src/skins/frosted/skin.ts
new file mode 100644
index 00000000..d61a024d
--- /dev/null
+++ b/packages/html/src/skins/frosted/skin.ts
@@ -0,0 +1,57 @@
+import type { Mixin } from '@videojs/utils/types';
+
+import { ReactiveElement } from '@lit/reactive-element';
+
+import { StoreMixin } from './store';
+
+/**
+ * Frosted skin custom element.
+ *
+ * Uses shadow DOM with a slot for video elements. Controls will be added in future updates.
+ *
+ * @example Basic usage (after calling define)
+ * ```html
+ *
+ *
+ *
+ * ```
+ *
+ * @example Define with default tag
+ * ```ts
+ * import { FrostedSkinElement } from '@videojs/html/skins/frosted';
+ * FrostedSkinElement.define();
+ * ```
+ *
+ * @example Define with custom tag
+ * ```ts
+ * FrostedSkinElement.define('my-player');
+ * ```
+ *
+ * @example Define with extended store
+ * ```ts
+ * import { createStore } from '@videojs/store/lit';
+ * import { extendConfig, FrostedSkinElement } from '@videojs/html/skins/frosted';
+ *
+ * const { StoreMixin } = createStore(extendConfig({ slices: [chaptersSlice] }));
+ * FrostedSkinElement.define('my-player', StoreMixin);
+ * ```
+ */
+export class FrostedSkinElement extends ReactiveElement {
+ static tagName = 'vjs-frosted-skin';
+
+ /**
+ * Registers this element with the custom elements registry.
+ *
+ * @param tagName - Custom element tag name (defaults to 'vjs-frosted-skin')
+ * @param mixin - Mixin to apply (defaults to StoreMixin)
+ */
+ static define(tagName = this.tagName, mixin: Mixin = StoreMixin): void {
+ customElements.define(tagName, mixin(this));
+ }
+
+ constructor() {
+ super();
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ }
+}
diff --git a/packages/html/src/skins/frosted/store.ts b/packages/html/src/skins/frosted/store.ts
new file mode 100644
index 00000000..7735a23c
--- /dev/null
+++ b/packages/html/src/skins/frosted/store.ts
@@ -0,0 +1,41 @@
+import type { AnySlice, StoreConfig } from '@videojs/store';
+
+import { media } from '@videojs/core/dom';
+import { extendConfig as extendBaseConfig } from '@videojs/store';
+import { createStore } from '@videojs/store/lit';
+
+const baseConfig = {
+ slices: [...media.all],
+};
+
+/**
+ * Extends frosted skin config.
+ *
+ * @example
+ * ```ts
+ * import { createStore } from '@videojs/store/lit';
+ * import { extendConfig, FrostedSkinElement } from '@videojs/html/skins/frosted';
+ * import { chaptersSlice } from './slices/chapters';
+ *
+ * const { StoreMixin } = createStore(
+ * extendConfig({ slices: [chaptersSlice] })
+ * );
+ *
+ * FrostedSkinElement.define('my-player', El => StoreMixin(El));
+ * ```
+ */
+export function extendConfig[] = []>(
+ extension?: Partial>,
+) {
+ return extendBaseConfig(baseConfig, extension);
+}
+
+export const {
+ StoreMixin,
+ StoreProviderMixin,
+ StoreAttachMixin,
+ SelectorController,
+ RequestController,
+ TasksController,
+ create,
+} = createStore(baseConfig);
diff --git a/packages/html/tsdown.config.ts b/packages/html/tsdown.config.ts
index c387a2ae..9c9b64c9 100644
--- a/packages/html/tsdown.config.ts
+++ b/packages/html/tsdown.config.ts
@@ -6,13 +6,9 @@ import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
index: 'src/index.ts',
- // 'skins/frosted': 'src/skins/frosted/index.ts',
+ 'skins/frosted': 'src/skins/frosted/index.ts',
// 'skins/minimal': 'src/skins/minimal/index.ts',
- // ...defineFiles.reduce((entries, file) => {
- // const name = file.replace(/\.ts$/, '');
- // entries[`define/${name}`] = `src/define/${file}`;
- // return entries;
- // }, {} as Record),
+ 'define/vjs-frosted-skin': 'src/define/vjs-frosted-skin.ts',
},
platform: 'browser',
format: 'es',
diff --git a/packages/react/package.json b/packages/react/package.json
index aa4de8bb..932a6456 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -16,6 +16,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
+ },
+ "./skins/frosted": {
+ "types": "./dist/skins/frosted.d.ts",
+ "default": "./dist/skins/frosted.js"
}
},
"main": "dist/index.js",
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index b827556f..97428b63 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -3,5 +3,8 @@
// Media
export { Video, type VideoProps } from './media/video';
+// Slices (re-export for convenience)
+export { media } from '@videojs/core/dom';
+
// Store
export * from '@videojs/store/react';
diff --git a/packages/react/src/skins/frosted/index.ts b/packages/react/src/skins/frosted/index.ts
new file mode 100644
index 00000000..fc347c14
--- /dev/null
+++ b/packages/react/src/skins/frosted/index.ts
@@ -0,0 +1,13 @@
+'use client';
+
+export { Skin, type SkinProps } from './skin';
+
+export {
+ create as createStore,
+ extendConfig,
+ Provider,
+ useRequest,
+ useSelector,
+ useStore,
+ useTasks,
+} from './store';
diff --git a/packages/react/src/skins/frosted/skin.tsx b/packages/react/src/skins/frosted/skin.tsx
new file mode 100644
index 00000000..534cf3b4
--- /dev/null
+++ b/packages/react/src/skins/frosted/skin.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import type { ReactNode } from 'react';
+
+export interface SkinProps {
+ children?: ReactNode;
+ className?: string;
+}
+
+/**
+ * @example
+ * ```tsx
+ * import { Video } from '@videojs/react';
+ * import { Provider, Skin } from '@videojs/react/skins/frosted';
+ *
+ * function App() {
+ * return (
+ *
+ *
+ *
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function Skin({ children, className }: SkinProps): React.JSX.Element {
+ return {children}
;
+}
+
+export namespace Skin {
+ export type Props = SkinProps;
+}
diff --git a/packages/react/src/skins/frosted/store.ts b/packages/react/src/skins/frosted/store.ts
new file mode 100644
index 00000000..c275573a
--- /dev/null
+++ b/packages/react/src/skins/frosted/store.ts
@@ -0,0 +1,41 @@
+'use client';
+
+import type { AnySlice, StoreConfig } from '@videojs/store';
+
+import { media } from '@videojs/core/dom';
+import { extendConfig as extendBaseConfig } from '@videojs/store';
+import { createStore } from '@videojs/store/react';
+
+const baseConfig = {
+ slices: [...media.all],
+ displayName: 'FrostedSkin',
+};
+
+/**
+ * Extends frosted skin config.
+ *
+ * @example
+ * ```ts
+ * import { createStore } from '@videojs/store/react';
+ * import { extendConfig } from '@videojs/react/skins/frosted';
+ * import { chaptersSlice } from './slices/chapters';
+ *
+ * const { Provider, useSelector } = createStore(
+ * extendConfig({ slices: [chaptersSlice] })
+ * );
+ * ```
+ */
+export function extendConfig[] = []>(
+ extension?: Partial>,
+) {
+ return extendBaseConfig(baseConfig, extension);
+}
+
+export const {
+ Provider,
+ create,
+ useStore,
+ useSelector,
+ useRequest,
+ useTasks,
+} = createStore(baseConfig);
diff --git a/packages/react/tsdown.config.ts b/packages/react/tsdown.config.ts
index 32cb06ce..5d037b37 100644
--- a/packages/react/tsdown.config.ts
+++ b/packages/react/tsdown.config.ts
@@ -3,8 +3,7 @@ import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
index: './src/index.ts',
- // 'skins/frosted': './src/skins/frosted/index.ts',
- // 'skins/minimal': './src/skins/minimal/index.ts',
+ 'skins/frosted': './src/skins/frosted/index.ts',
},
platform: 'browser',
format: 'es',
diff --git a/packages/store/src/lit/create-store.ts b/packages/store/src/lit/create-store.ts
index 6012136b..1eb393ce 100644
--- a/packages/store/src/lit/create-store.ts
+++ b/packages/store/src/lit/create-store.ts
@@ -208,7 +208,6 @@ export function createStore(config: CreateStoreConfig
const StoreAttachMixin = createStoreAttachMixin(context);
const StoreMixin = createStoreMixin(context, create);
- // Bound controllers - context is pre-bound
class SelectorController extends SelectorControllerBase {
constructor(host: CreateStoreHost, selector: (state: State) => Value) {
super(host, context, selector);
diff --git a/packages/store/src/lit/mixins/attach-mixin.ts b/packages/store/src/lit/mixins/attach-mixin.ts
index 4939ad44..4b63b5ce 100644
--- a/packages/store/src/lit/mixins/attach-mixin.ts
+++ b/packages/store/src/lit/mixins/attach-mixin.ts
@@ -14,7 +14,7 @@ import { isNull } from '@videojs/utils/predicate';
* Creates a mixin that consumes a store from context and auto-attaches media elements.
*
* - Requests store from context (must have a provider ancestor)
- * - Observes slotted elements for `