feat(site): feature and preset reference UI + docs integration (#1258)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-04-14 17:19:01 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 5301202a18
commit d4b805ea69
35 changed files with 643 additions and 234 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ import A from '../typography/A.astro';
interface Props extends Omit<HTMLAttributes<'a'>, 'href'> {
slug: string;
}
const { slug } = Astro.props;
const { slug, class: className } = Astro.props;
const { framework: paramFramework } = Astro.params;
if (!paramFramework || !isValidFramework(paramFramework)) {
@@ -20,4 +20,4 @@ const { url: href } = resolveDocsLinkUrl({
});
---
<A href={href}><slot /></A>
<A href={href} class={className}><slot /></A>
@@ -0,0 +1,42 @@
---
import Table from '@/components/typography/Table.astro';
import Tbody from '@/components/typography/Tbody.astro';
import Th from '@/components/typography/Th.astro';
import Thead from '@/components/typography/Thead.astro';
import Tr from '@/components/typography/Tr.astro';
import type { FeatureActionDef } from '@/types/feature-reference';
import StateRow from './StateRow.astro';
interface Props {
actions: Record<string, FeatureActionDef>;
featureName: string;
}
const { actions, featureName } = Astro.props;
const actionEntries = Object.entries(actions);
---
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Action</Th>
<Th>Type</Th>
<Th>Details</Th>
</Tr>
</Thead>
<Tbody>
{
actionEntries.map(([name, def]) => (
<StateRow
name={name}
type={def.type}
detailedType={def.detailedType}
description={def.description}
componentName={featureName}
kind="action"
/>
))
}
</Tbody>
</Table>
@@ -32,7 +32,7 @@ const hasDetail = Boolean(attributeName || detailedType || description);
<Tr
id={id}
data-detail-row
data-apiref-row
class={clsx(
hasDetail &&
"group cursor-pointer data-expanded:border-transparent dark:data-expanded:border-transparent intent:bg-manila-75 dark:intent:bg-soot",
@@ -46,7 +46,7 @@ const hasDetail = Boolean(attributeName || detailedType || description);
aria-expanded="false"
aria-controls={`${id}-detail`}
aria-label={`Details for ${name}`}
data-detail-toggle
data-apiref-toggle
>
<span
aria-hidden="true"
@@ -66,8 +66,8 @@ const hasDetail = Boolean(attributeName || detailedType || description);
<Td colspan={String(colspan)} class="p-0">
<div class="px-6 py-4">
<dl
class="grid grid-cols-(--cols) gap-x-6 gap-y-3"
style="--cols: auto 1fr"
class="grid grid-cols-(--grid-cols) gap-x-6 gap-y-3"
style="--grid-cols: auto 1fr"
>
{attributeName && (
<>
@@ -104,13 +104,13 @@ const hasDetail = Boolean(attributeName || detailedType || description);
<script>
document
.querySelectorAll<HTMLButtonElement>("[data-detail-toggle]")
.querySelectorAll<HTMLButtonElement>("[data-apiref-toggle]")
.forEach((button) => {
button.addEventListener("click", () => {
const expanded = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!expanded));
const row = button.closest<HTMLTableRowElement>("[data-detail-row]");
const row = button.closest<HTMLTableRowElement>("[data-apiref-row]");
if (row) row.toggleAttribute("data-expanded", !expanded);
const detail = document.getElementById(
@@ -121,11 +121,11 @@ const hasDetail = Boolean(attributeName || detailedType || description);
});
document
.querySelectorAll<HTMLTableRowElement>("[data-detail-row]")
.querySelectorAll<HTMLTableRowElement>("[data-apiref-row]")
.forEach((row) => {
row.addEventListener("click", (e) => {
if ((e.target as Element).closest("a, button")) return;
row.querySelector<HTMLButtonElement>("[data-detail-toggle]")?.click();
row.querySelector<HTMLButtonElement>("[data-apiref-toggle]")?.click();
});
});
</script>
@@ -0,0 +1,48 @@
---
import { getEntry } from 'astro:content';
import ContentWidth from '@/components/frames/ContentWidth.astro';
import H2 from '@/components/typography/H2Markdown.astro';
import H3 from '@/components/typography/H3Markdown.astro';
import type { FeatureReference } from '@/types/feature-reference';
import { createFeatureReferenceModel } from '@/utils/featureReferenceModel';
import ApiActionsTable from './ApiActionsTable.astro';
import ApiStateTable from './ApiStateTable.astro';
interface Props {
feature: string;
}
const { feature } = Astro.props;
const entry = await getEntry('featureReference', feature);
const ref: FeatureReference | null = entry?.data ?? null;
if (!ref) return;
const model = createFeatureReferenceModel(feature, ref);
if (!model) return;
const stateSection = model.sections.find((s) => s.key === 'state');
const actionsSection = model.sections.find((s) => s.key === 'actions');
---
<ContentWidth>
<H2 id={model.heading.id}>{model.heading.text}</H2>
{
stateSection && (
<>
<H3 id={stateSection.id}>{stateSection.title}</H3>
<ApiStateTable state={model.data.state} componentName={feature} />
</>
)
}
{
actionsSection && (
<>
<H3 id={actionsSection.id}>{actionsSection.title}</H3>
<ApiActionsTable actions={model.data.actions} featureName={feature} />
</>
)
}
</ContentWidth>
@@ -0,0 +1,228 @@
---
import { getCollection } from 'astro:content';
import ContentWidth from '@/components/frames/ContentWidth.astro';
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import Table from '@/components/typography/Table.astro';
import Tbody from '@/components/typography/Tbody.astro';
import Td from '@/components/typography/Td.astro';
import Th from '@/components/typography/Th.astro';
import Thead from '@/components/typography/Thead.astro';
import Tr from '@/components/typography/Tr.astro';
import { isValidFramework } from '@/types/docs';
import type { PresetReference as PresetReferenceType } from '@/types/preset-reference';
import DocsLink from '../DocsLink.astro';
import FrameworkCase from '../FrameworkCase.astro';
import InlineMarkdown from './InlineMarkdown.astro';
const entries = await getCollection('presetReference');
const PINNED_PRESETS = ['video', 'audio'];
const presets: PresetReferenceType[] = entries
.map((e) => e.data)
.sort((a, b) => {
const aPin = PINNED_PRESETS.indexOf(a.name);
const bPin = PINNED_PRESETS.indexOf(b.name);
if (aPin !== -1 && bPin !== -1) return aPin - bPin;
if (aPin !== -1) return -1;
if (bPin !== -1) return 1;
return a.name.localeCompare(b.name);
});
const { framework } = Astro.params;
const pkg = framework && isValidFramework(framework) ? `@videojs/${framework}` : '@videojs/html';
/**
* Feature slug overrides for cases where kebabCase(featureName) doesn't
* match the docs page slug.
*/
const FEATURE_SLUG_OVERRIDES: Record<string, string> = {
textTrack: 'text-tracks',
};
/**
* Native media elements used by presets that don't have a custom element.
* The pipeline can't detect these since they're not classes with `static tagName`.
*/
const NATIVE_MEDIA_ELEMENTS: Record<string, string> = {
video: 'video',
audio: 'audio',
};
function featureDocsSlug(featureName: string): string {
const override = FEATURE_SLUG_OVERRIDES[featureName];
if (override) return `reference/feature-${override}`;
const kebab = featureName.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
return `reference/feature-${kebab}`;
}
function htmlMediaElement(preset: PresetReferenceType): string | undefined {
return preset.html.mediaElement ?? NATIVE_MEDIA_ELEMENTS[preset.name];
}
const listFormat = new Intl.ListFormat('en', { style: 'long', type: 'unit' });
---
<ContentWidth>
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Import</Th>
<Th>Description</Th>
<Th><span class="sr-only">Details</span></Th>
</Tr>
</Thead>
<Tbody>
{
presets.map((preset) => {
const id = `preset-${preset.name}`;
const htmlMedia = htmlMediaElement(preset);
return (
<>
<Tr
id={id}
data-preset-row
class="group cursor-pointer data-expanded:border-transparent dark:data-expanded:border-transparent intent:bg-manila-75 dark:intent:bg-soot"
>
<Td class="align-top">
<MarkdownCode>{`${pkg}/${preset.name}`}</MarkdownCode>
</Td>
<Td class="align-top">
{preset.description && (
<InlineMarkdown content={preset.description} />
)}
</Td>
<Td class="align-top">
<button
aria-expanded="false"
aria-controls={`${id}-detail`}
aria-label={`Details for ${preset.name} preset`}
data-preset-toggle
>
<span
aria-hidden="true"
class="inline-block group-data-expanded:rotate-90"
>
</span>
</button>
</Td>
</Tr>
<Tr id={`${id}-detail`} hidden>
<Td colspan="3" class="p-0">
<div class="px-6 py-4">
<dl
class="grid grid-cols-(--grid-cols) gap-x-6 gap-y-3"
style="--grid-cols: auto 1fr"
>
<dt class="font-bold">Feature bundle</dt>
<dd>
<MarkdownCode>{preset.featureBundle}</MarkdownCode>
</dd>
<dt class="font-bold">Features</dt>
<dd>
{preset.features.length > 0
? listFormat
.formatToParts(preset.features)
.map((part) =>
part.type === "element" ? (
<DocsLink
class="inline-block whitespace-nowrap"
slug={featureDocsSlug(part.value)}
>
{part.value}
</DocsLink>
) : (
<span>{part.value}</span>
),
)
: ""}
</dd>
<dt class="font-bold">Skins</dt>
<dd>
<FrameworkCase frameworks={["react"]}>
{preset.react.skins.length > 0
? listFormat
.formatToParts(
preset.react.skins.map((s) => s.name),
)
.map((part) =>
part.type === "element" ? (
<MarkdownCode class="inline-block whitespace-nowrap">{`<${part.value}>`}</MarkdownCode>
) : (
<span>{part.value}</span>
),
)
: ""}
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
{preset.html.skins.length > 0
? listFormat
.formatToParts(
preset.html.skins.map(
(s) => s.tagName ?? s.name,
),
)
.map((part) =>
part.type === "element" ? (
<MarkdownCode class="inline-block whitespace-nowrap">{`<${part.value}>`}</MarkdownCode>
) : (
<span>{part.value}</span>
),
)
: ""}
</FrameworkCase>
</dd>
<dt class="font-bold">Default media element</dt>
<dd>
<FrameworkCase frameworks={["react"]}>
<MarkdownCode>{`<${preset.react.mediaElement}>`}</MarkdownCode>
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
{htmlMedia ? (
<MarkdownCode>{`<${htmlMedia}>`}</MarkdownCode>
) : (
""
)}
</FrameworkCase>
</dd>
</dl>
</div>
</Td>
</Tr>
</>
);
})
}
</Tbody>
</Table>
</ContentWidth>
<script>
document
.querySelectorAll<HTMLButtonElement>("[data-preset-toggle]")
.forEach((button) => {
button.addEventListener("click", () => {
const expanded = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!expanded));
const row = button.closest<HTMLTableRowElement>("[data-preset-row]");
if (row) row.toggleAttribute("data-expanded", !expanded);
const detail = document.getElementById(
button.getAttribute("aria-controls")!,
);
if (detail) detail.hidden = expanded;
});
});
document
.querySelectorAll<HTMLTableRowElement>("[data-preset-row]")
.forEach((row) => {
row.addEventListener("click", (e) => {
if ((e.target as Element).closest("a, button")) return;
row.querySelector<HTMLButtonElement>("[data-preset-toggle]")?.click();
});
});
</script>
@@ -9,11 +9,12 @@ interface Props {
detailedType?: string;
description?: string;
componentName: string;
kind?: string;
}
const { name, type, detailedType, description, componentName } = Astro.props;
const { name, type, detailedType, description, componentName, kind = 'state' } = Astro.props;
const id = `${componentName}-state-${name}`;
const id = `${componentName}-${kind}-${name}`;
---
<DetailRow
+28 -1
View File
@@ -3,6 +3,8 @@ import { file, glob } from 'astro/loaders';
import { z } from 'astro/zod';
import { ComponentReferenceSchema } from './types/component-reference';
import { SUPPORTED_FRAMEWORKS } from './types/docs';
import { FeatureReferenceSchema } from './types/feature-reference';
import { PresetReferenceSchema } from './types/preset-reference';
import { UtilReferenceSchema } from './types/util-reference';
import { defaultGitService } from './utils/gitService';
import { globWithParser } from './utils/globWithParser';
@@ -127,6 +129,22 @@ const utilReference = defineCollection({
schema: UtilReferenceSchema,
});
const featureReference = defineCollection({
loader: glob({
pattern: '*.json',
base: './src/content/generated-feature-reference',
}),
schema: FeatureReferenceSchema,
});
const presetReference = defineCollection({
loader: glob({
pattern: '*.json',
base: './src/content/generated-preset-reference',
}),
schema: PresetReferenceSchema,
});
const ejectedSkins = defineCollection({
loader: file('./src/content/ejected-skins.json'),
schema: z.object({
@@ -141,4 +159,13 @@ const ejectedSkins = defineCollection({
}),
});
export const collections = { blog, docs, authors, componentReference, utilReference, ejectedSkins };
export const collections = {
blog,
docs,
authors,
componentReference,
utilReference,
featureReference,
presetReference,
ejectedSkins,
};
+7 -1
View File
@@ -144,7 +144,13 @@ DASH, YouTube, Vimeo, Mux, and more media elements are currently under developme
**Presets** preconfigure these parts for a specific use case.
The default presets are `/video` and `/audio`, covering the baseline set of controls you'd expect from the HTML `<video>` and `<audio>` tags.
<FrameworkCase frameworks={["react"]}>
The default presets are `@videojs/react/video` and `@videojs/react/audio`, covering the baseline set of controls you'd expect from the HTML `<video>` and `<audio>` tags.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
The default presets are `@videojs/html/video` and `@videojs/html/audio`, covering the baseline set of controls you'd expect from the HTML `<video>` and `<audio>` tags.
</FrameworkCase>
Beyond the defaults, presets target more specific use cases. For example, `/background` includes a media element with autoplay, mute, and loop built in, a skin with no controls, and just the features needed to power it:
+6 -20
View File
@@ -3,15 +3,16 @@ title: Presets
description: Pre-packaged player configurations that bundle state management, skins, and media elements for specific use cases.
---
import PresetReference from '@/components/docs/api-reference/PresetReference.astro';
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import Aside from '@/components/Aside.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
A **preset** packages what you need for a specific player use case. It can include special <DocsLink slug="concepts/features">state management</DocsLink>, one or more <DocsLink slug="concepts/skins">skins</DocsLink> for UI, and specific media elements. Instead of assembling these pieces individually, you pick a preset that matches what you're building.
For example, the `/background` preset includes a media element with autoplay, mute, and loop built in, a skin with no controls, and just the features needed to power them:
<FrameworkCase frameworks={["react"]}>
For example, the `@videojs/react/background` preset includes a media element with autoplay, mute, and loop built in, a skin with no controls, and just the features needed to power them:
```tsx title="App.tsx"
import { createPlayer } from '@videojs/react';
import { backgroundFeatures, BackgroundVideo, BackgroundVideoSkin } from '@videojs/react/background'; // [!code focus]
@@ -30,6 +31,8 @@ function Hero() {
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
For example, the `@videojs/html/background` preset includes a media element with autoplay, mute, and loop built in, a skin with no controls, and just the features needed to power them:
```html title="index.html"
<script type="module">
import '@videojs/html/background/player';
@@ -49,24 +52,7 @@ function Hero() {
The default presets are `/video` and `/audio`. These cover the baseline controls you'd expect from the HTML `<video>` and `<audio>` tags. Beyond the defaults, presets target more specific use cases — the `/background` preset, for example, needs layout but not controls. Over time we'll add more: short-form players, podcast players, TV streaming players, and others.
<FrameworkCase frameworks={["react"]}>
| Preset | Feature bundle | Skins | Default media element |
|--------|---------------|-------|-----------------------|
| `/video` | `videoFeatures` | `<VideoSkin>`, `<MinimalVideoSkin>` | `<Video>` |
| `/audio` | `audioFeatures` | `<AudioSkin>`, `<MinimalAudioSkin>` | `<Audio>` |
| `/background` | `backgroundFeatures` | `<BackgroundVideoSkin>` | `<BackgroundVideo>` |
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
| Preset | Skins | Default media element |
|--------|-------|-----------------------|
| `/video` | `<video-skin>`, `<video-minimal-skin>` | `<video>` |
| `/audio` | `<audio-skin>`, `<audio-minimal-skin>` | `<audio>` |
| `/background` | `<background-video-skin>` | `<background-video>` |
</FrameworkCase>
<PresetReference />
## What's in a preset
+6 -21
View File
@@ -3,6 +3,7 @@ title: Skins
description: Packaged player designs that include both UI components and their styles.
---
import PresetReference from '@/components/docs/api-reference/PresetReference.astro';
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import { FrostedSkinDemo } from '@/examples/react/FrostedSkin/FrostedSkinDemo';
import { MinimalSkinDemo } from '@/examples/react/MinimalSkin/MinimalSkinDemo';
@@ -88,31 +89,15 @@ When you choose a skin you have two options for how you use it: **packaged** or
## Skins, features, and presets
Each skin is built with specific <DocsLink slug="concepts/features">features</DocsLink> in mind. For example, a video skin renders fullscreen and picture-in-picture controls. An audio skin doesn't. The features associated with a skin are called a **feature bundle**.
Each skin is built with specific <DocsLink slug="concepts/features">features</DocsLink> in mind. For example, a video skin renders fullscreen and picture-in-picture controls. An audio skin doesn't.
<FrameworkCase frameworks={["html"]}>
You'll find both a skin and the feature bundle it expects exported from the same path. We call these paths **presets**.
| Player with feature bundle | Available skins | Import |
| ---------- | ----------------- | -------- |
| `<video-player>` | `<video-skin>`, `<video-minimal-skin>` | `@videojs/html/video/*` |
| `<audio-player>` | `<audio-skin>`, `<audio-minimal-skin>` | `@videojs/html/audio/*` |
| `<background-video-player>` | `<background-video-skin>` | `@videojs/html/background/*` |
<PresetReference />
</FrameworkCase>
Presets are a topic quite a bit bigger than just this guide. To learn more, check out the guide:
<FrameworkCase frameworks={["react"]}>
| Feature bundle | Available skins | Import |
| ---------- | ----------------- | -------- |
| `videoFeatures` | `<VideoSkin>`, `<MinimalVideoSkin>` | `@videojs/react/video` |
| `audioFeatures` | `<AudioSkin>`, `<MinimalAudioSkin>` | `@videojs/react/audio` |
| `backgroundFeatures` | `<BackgroundVideoSkin>` | `@videojs/react/background` |
</FrameworkCase>
Want to learn more about skins and feature bundles? Check out the presets guide:
<DocsLinkCard slug="concepts/presets">Learn more about presets</DocsLinkCard>
<DocsLinkCard slug="concepts/presets">Read about presets</DocsLinkCard>
## Styling
@@ -3,20 +3,15 @@ title: Buffer
description: Buffered and seekable time range state for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Read-only — tracks buffered and seekable time ranges.
## State
<FeatureReference feature="buffer" />
| State | Type | Description |
|---|---|---|
| `buffered` | `[number, number][]` | Buffered time ranges as `[start, end]` tuples |
| `seekable` | `[number, number][]` | Seekable time ranges as `[start, end]` tuples |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectBuffer` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to buffer state. Returns `undefined` if the buffer feature is not configured.
@@ -53,5 +48,3 @@ class BufferBar extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectBuffer" />
@@ -3,20 +3,15 @@ title: Controls
description: User activity and controls visibility state for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Read-only — tracks user activity for showing and hiding controls.
## State
<FeatureReference feature="controls" />
| State | Type | Description |
|---|---|---|
| `userActive` | `boolean` | Whether the user has recently interacted |
| `controlsVisible` | `boolean` | Whether controls should be visible (active or paused) |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectControls` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to controls state. Returns `undefined` if the controls feature is not configured.
@@ -55,5 +50,3 @@ class ControlsOverlay extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectControls" />
@@ -3,25 +3,15 @@ title: Error
description: Media error state and actions for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Tracks media errors.
## State
<FeatureReference feature="error" />
| State | Type | Description |
|---|---|---|
| `error` | `MediaError \| null` | The current error, or `null` |
## Actions
| Action | Description |
|---|---|
| `dismissError()` | Clear the current error |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectError` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to error state. Returns `undefined` if the error feature is not configured.
@@ -61,5 +51,3 @@ class ErrorDisplay extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectError" />
@@ -3,27 +3,15 @@ title: Fullscreen
description: Fullscreen state and actions for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Controls fullscreen mode. Tries the container element first, falls back to the media element.
## State
<FeatureReference feature="fullscreen" />
| State | Type | Description |
|---|---|---|
| `fullscreen` | `boolean` | Whether fullscreen is active |
| `fullscreenAvailability` | `MediaFeatureAvailability` | Whether fullscreen is supported |
## Actions
| Action | Description |
|---|---|
| `requestFullscreen()` | Enter fullscreen (returns a `Promise`) |
| `exitFullscreen()` | Exit fullscreen (returns a `Promise`) |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectFullscreen` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to fullscreen state. Returns `undefined` if the fullscreen feature is not configured.
@@ -62,5 +50,3 @@ class FullscreenButton extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectFullscreen" />
@@ -3,27 +3,15 @@ title: Picture-in-picture
description: Picture-in-picture state and actions for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Controls picture-in-picture mode.
## State
<FeatureReference feature="pip" />
| State | Type | Description |
|---|---|---|
| `pip` | `boolean` | Whether picture-in-picture is active |
| `pipAvailability` | `MediaFeatureAvailability` | Whether PiP is supported |
## Actions
| Action | Description |
|---|---|
| `requestPictureInPicture()` | Enter picture-in-picture (returns a `Promise`) |
| `exitPictureInPicture()` | Exit picture-in-picture (returns a `Promise`) |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectPiP` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to picture-in-picture state. Returns `undefined` if the PiP feature is not configured.
@@ -62,5 +50,3 @@ class PiPButton extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectPiP" />
@@ -3,26 +3,15 @@ title: Playback rate
description: Playback speed state and actions for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Controls speed of playback.
## State
<FeatureReference feature="playbackRate" />
| State | Type | Description |
|---|---|---|
| `playbackRate` | `number` | Current playback speed (1 = normal) |
| `playbackRates` | `readonly number[]` | Available playback rates |
## Actions
| Action | Description |
|---|---|
| `setPlaybackRate(rate)` | Set the playback speed |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectPlaybackRate` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to playback rate state. Returns `undefined` if the playback rate feature is not configured.
@@ -57,5 +46,3 @@ class RateDisplay extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectPlaybackRate" />
@@ -3,29 +3,15 @@ title: Playback
description: Play/pause state and actions for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Controls play/pause state and tracks whether playback has started or is stalled.
## State
<FeatureReference feature="playback" />
| State | Type | Description |
|---|---|---|
| `paused` | `boolean` | Whether playback is paused |
| `ended` | `boolean` | Whether playback reached the end |
| `started` | `boolean` | Whether playback has started (played or seeked) |
| `waiting` | `boolean` | Whether playback is stalled waiting for data |
## Actions
| Action | Description |
|---|---|
| `play()` | Start playback (returns a `Promise`) |
| `pause()` | Pause playback |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectPlayback` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to playback state. Returns `undefined` if the playback feature is not configured.
@@ -59,5 +45,3 @@ class PlayButton extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectPlayback" />
@@ -3,26 +3,15 @@ title: Source
description: Media source state and actions for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Tracks the current media source and readiness.
## State
<FeatureReference feature="source" />
| State | Type | Description |
|---|---|---|
| `source` | `string \| null` | Current media source URL |
| `canPlay` | `boolean` | Whether enough data is loaded to begin playback |
## Actions
| Action | Description |
|---|---|
| `loadSource(src)` | Load a new media source |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectSource` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to source state. Returns `undefined` if the source feature is not configured.
@@ -57,5 +46,3 @@ class SourceInfo extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectSource" />
@@ -3,29 +3,15 @@ title: Text tracks
description: Subtitles, captions, and chapter track state for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Manages subtitles, captions, chapters, and thumbnail tracks.
## State
<FeatureReference feature="textTrack" />
| State | Type | Description |
|---|---|---|
| `textTrackList` | `MediaTextTrack[]` | All text tracks on the media element |
| `subtitlesShowing` | `boolean` | Whether captions/subtitles are enabled |
| `chaptersCues` | `MediaTextCue[]` | Cues from the first chapters track |
| `thumbnailCues` | `MediaTextCue[]` | Cues from the first thumbnails track |
| `thumbnailTrackSrc` | `string \| null` | Track `src` for resolving relative thumbnail URLs |
## Actions
| Action | Description |
|---|---|
| `toggleSubtitles(forceShow?)` | Toggle subtitle visibility. Pass `true`/`false` to force. |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectTextTracks` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to text track state. Returns `undefined` if the text tracks feature is not configured.
@@ -64,5 +50,3 @@ class CaptionsButton extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectTextTracks" />
@@ -3,27 +3,15 @@ title: Time
description: Playback position and duration state for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Tracks playback position and duration.
## State
<FeatureReference feature="time" />
| State | Type | Description |
|---|---|---|
| `currentTime` | `number` | Current playback position in seconds |
| `duration` | `number` | Total duration in seconds (0 if unknown) |
| `seeking` | `boolean` | Whether a seek operation is in progress |
## Actions
| Action | Description |
|---|---|
| `seek(time)` | Seek to a position in seconds (returns a `Promise`) |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectTime` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to time state. Returns `undefined` if the time feature is not configured.
@@ -62,5 +50,3 @@ class TimeDisplay extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectTime" />
@@ -3,28 +3,15 @@ title: Volume
description: Volume level and mute state for the player store
---
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
import FeatureReference from "@/components/docs/api-reference/FeatureReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Controls volume level and mute state.
## State
<FeatureReference feature="volume" />
| State | Type | Description |
|---|---|---|
| `volume` | `number` | Volume level from 0 (silent) to 1 (max) |
| `muted` | `boolean` | Whether audio is muted |
| `volumeAvailability` | `MediaFeatureAvailability` | Whether volume can be set on this platform |
## Actions
| Action | Description |
|---|---|
| `setVolume(volume)` | Set volume (clamped 01). Auto-unmutes when raising above zero. |
| `toggleMuted()` | Toggle mute. Restores volume to 0.25 when unmuting at zero. |
## Selector
### Selector
<FrameworkCase frameworks={["react"]}>
Pass `selectVolume` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe to volume state. Returns `undefined` if the volume feature is not configured.
@@ -68,5 +55,3 @@ class VolumeSlider extends MediaElement {
}
```
</FrameworkCase>
<UtilReference util="selectVolume" />
+24
View File
@@ -0,0 +1,24 @@
/**
* Zod schemas for feature API reference JSON files.
*
* FeatureStateDef and FeatureActionDef reuse StateDefSchema (identical shape)
* from component-reference.ts, following the same pattern as util-reference.ts.
*/
import { z } from 'astro/zod';
import { StateDefSchema } from './component-reference';
export const FeatureStateDefSchema = StateDefSchema;
export const FeatureActionDefSchema = StateDefSchema;
export const FeatureReferenceSchema = z.object({
name: z.string(),
slug: z.string(),
description: z.string().optional(),
state: z.record(z.string(), FeatureStateDefSchema),
actions: z.record(z.string(), FeatureActionDefSchema),
});
export type FeatureStateDef = z.infer<typeof FeatureStateDefSchema>;
export type FeatureActionDef = z.infer<typeof FeatureActionDefSchema>;
export type FeatureReference = z.infer<typeof FeatureReferenceSchema>;
+27
View File
@@ -0,0 +1,27 @@
/**
* Zod schemas for preset API reference JSON files.
*/
import { z } from 'astro/zod';
export const PresetSkinDefSchema = z.object({
name: z.string(),
tagName: z.string().optional(),
});
export const PresetReferenceSchema = z.object({
name: z.string(),
description: z.string().optional(),
featureBundle: z.string(),
features: z.array(z.string()),
html: z.object({
skins: z.array(PresetSkinDefSchema),
mediaElement: z.string().optional(),
}),
react: z.object({
skins: z.array(PresetSkinDefSchema),
mediaElement: z.string(),
}),
});
export type PresetSkinDef = z.infer<typeof PresetSkinDefSchema>;
export type PresetReference = z.infer<typeof PresetReferenceSchema>;
+59
View File
@@ -0,0 +1,59 @@
/**
* Centralized feature API reference subsection definitions.
*
* Mirrors componentReferenceModel.js for feature APIs. Produces heading/id data
* consumed by both FeatureReference.astro and remarkConditionalHeadings.
*
* Structure:
* ## API Reference (H2)
* ### State (H3) if feature has state properties
* ### Actions (H3) if feature has action methods
*/
function hasEntries(value) {
return Object.keys(value ?? {}).length > 0;
}
export function createFeatureReferenceModel(name, ref) {
if (!ref) return null;
const sections = [];
if (hasEntries(ref.state)) {
sections.push({ key: 'state', title: 'State', id: 'state', depth: 3 });
}
if (hasEntries(ref.actions)) {
sections.push({ key: 'actions', title: 'Actions', id: 'actions', depth: 3 });
}
return {
name,
description: ref.description,
heading: { id: 'api-reference', depth: 2, text: 'API Reference' },
sections,
data: ref,
};
}
export function buildFeatureReferenceTocHeadings(model) {
if (!model) return [];
const headings = [
{
depth: model.heading.depth,
text: model.heading.text,
slug: model.heading.id,
},
];
for (const section of model.sections) {
headings.push({
depth: section.depth,
text: section.title,
slug: section.id,
});
}
return headings;
}
@@ -4,10 +4,12 @@ import { fileURLToPath } from 'node:url';
import { kebabCase } from 'es-toolkit/string';
import GithubSlugger from 'github-slugger';
import { buildComponentReferenceTocHeadings, createComponentReferenceModel } from './componentReferenceModel';
import { buildFeatureReferenceTocHeadings, createFeatureReferenceModel } from './featureReferenceModel';
import { buildUtilReferenceTocHeadings, createUtilReferenceModel } from './utilReferenceModel';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const COMPONENT_REF_DIR = path.resolve(__dirname, '../content/generated-component-reference');
const FEATURE_REF_DIR = path.resolve(__dirname, '../content/generated-feature-reference');
const UTIL_REF_DIR = path.resolve(__dirname, '../content/generated-util-reference');
function readComponentRefJson(componentName) {
@@ -65,6 +67,8 @@ export default function remarkConditionalHeadings() {
return;
} else if (node.name === 'ComponentReference') {
injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
} else if (node.name === 'FeatureReference') {
injectFeatureReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
} else if (node.name === 'UtilReference') {
injectUtilReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
return;
@@ -148,6 +152,32 @@ function injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSl
}
}
function readFeatureRefJson(featureName) {
const filePath = path.join(FEATURE_REF_DIR, `${featureName}.json`);
try {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch {
return null;
}
}
function injectFeatureReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
const featureAttr = node.attributes?.find((a) => a.name === 'feature');
const featureName = typeof featureAttr?.value === 'string' ? featureAttr.value : null;
if (!featureName) return;
const json = readFeatureRefJson(featureName);
if (!json) return;
const featureModel = createFeatureReferenceModel(featureName, json);
const featureHeadings = buildFeatureReferenceTocHeadings(featureModel);
headingsWithMetadata.push(...featureHeadings);
for (const heading of featureHeadings) {
reservedSlugs.add(heading.slug);
}
}
function readUtilRefJson(slug) {
const filePath = path.join(UTIL_REF_DIR, `${slug}.json`);
try {