feat(site): add util reference pipeline (#537)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-02-24 15:34:34 -06:00
committed by GitHub
co-authored by Claude Opus 4.6
parent c11395ece1
commit 78112fbefd
143 changed files with 7031 additions and 481 deletions
+30 -8
View File
@@ -26,27 +26,49 @@ function containsActivePath(item: Guide | Section): boolean {
}
const isActive = containsActivePath(item);
function getGroup(depth: number) {
if (depth === 0) return 'group';
if (depth === 1) return 'group/1';
if (depth === 2) return 'group/2';
return '';
}
function getGroupMargin(depth: number) {
if (depth === 1) return 'group-open/1:mb-1';
if (depth === 2) return 'group-open/2:mb-1';
return '';
}
function getGroupRotate(depth: number) {
if (depth === 0) return 'group-open:rotate-180';
if (depth === 1) return 'group-open/1:rotate-180';
if (depth === 2) return 'group-open/2:rotate-180';
return '';
}
---
{
isSection(item) ? (
<details
id={slugger.slug(item.sidebarLabel)}
open
class="group my-4 first:mt-0"
style={depth ? `padding-left: calc(var(--spacing) * ${depth * 4})` : ``}
open={isActive || item.defaultOpen !== false}
class:list={[
getGroup(depth),
depth === 0 && 'my-4 first:mt-0',
]}
>
<summary
class:list={[
'py-2 flex items-center gap-4 ',
'intent:text-dark-100 dark:intent:text-light-80 cursor-pointer',
depth === 0 && 'font-medium',
depth > 0 && 'border-l',
(depth === 0 || depth === 1) && 'font-medium',
depth > 0 && 'border-l pl-4',
isActive ? 'text-dark-100 dark:text-light-100 border-dark-100' : 'border-light-40 dark:border-dark-40',
getGroupMargin(depth)
]}
style={depth ? `margin-left: calc(var(--spacing) * ${(depth - 1) * 4})` : ``}
>
<span class="flex-1">{item.sidebarLabel}</span>
<ChevronDown size={12} className="group-open:rotate-180" />
<ChevronDown size={12} className={getGroupRotate(depth)} />
</summary>
<div>
{item.contents.map((contentItem) => (
@@ -61,10 +83,10 @@ const isActive = containsActivePath(item);
'py-2 flex items-center gap-4 ',
'intent:text-dark-100 dark:intent:text-light-100 cursor-pointer',
depth === 0 && 'font-medium',
depth > 0 && 'border-l',
depth > 0 && 'border-l pl-4',
isActive ? 'text-dark-100 dark:text-light-100 border-current ' : 'border-light-40 dark:border-dark-40',
]}
style={`padding-left: calc(var(--spacing) * ${depth * 4})`}
style={`margin-left: calc(var(--spacing) * ${(depth - 1) * 4})`}
href={`/docs/framework/${framework}/${item.slug}`}
>
{item.devOnly ? '[Dev only] ' : ''}
@@ -3,7 +3,7 @@ import debounce from 'just-debounce-it';
import throttle from 'just-throttle';
import type { RefObject } from 'react';
import { useEffect, useState } from 'react';
import { API_REFERENCE_SUBSECTION_TITLES } from '@/utils/apiReferenceModel';
import { API_REFERENCE_SUBSECTION_TITLES } from '@/utils/componentReferenceModel';
/**
* Find the first scrollable ancestor of an element
@@ -2,21 +2,20 @@
/**
* Renders the data attributes table for API reference.
*/
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 type { DataAttrDef } from '@/types/api-reference';
import InlineMarkdown from './InlineMarkdown.astro';
import type { DataAttrDef } from '@/types/component-reference';
import DataAttrRow from './DataAttrRow.astro';
interface Props {
dataAttributes: Record<string, DataAttrDef>;
componentName: string;
}
const { dataAttributes } = Astro.props;
const { dataAttributes, componentName } = Astro.props;
const attrs = Object.entries(dataAttributes);
---
@@ -25,18 +24,20 @@ const attrs = Object.entries(dataAttributes);
<Thead>
<Tr>
<Th>Attribute</Th>
<Th>Description</Th>
<Th>Type</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{
attrs.map(([name, def]) => (
<Tr>
<Td class="align-top">
<MarkdownCode>{name}</MarkdownCode>
</Td>
<Td><InlineMarkdown content={def.description} /></Td>
</Tr>
<DataAttrRow
name={name}
type={def.type}
detailedType={def.detailedType}
description={def.description}
componentName={componentName}
/>
))
}
</Tbody>
@@ -9,7 +9,7 @@ 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 { PropDef } from '@/types/api-reference';
import type { PropDef } from '@/types/component-reference';
import PropRow from './PropRow.astro';
interface Props {
@@ -35,7 +35,7 @@ const { props, componentName } = Astro.props;
<PropRow
name={name}
type={prop.type}
shortType={prop.shortType}
detailedType={prop.detailedType}
description={prop.description}
defaultValue={prop.default}
required={prop.required}
@@ -7,7 +7,7 @@ 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 { StateDef } from '@/types/api-reference';
import type { StateDef } from '@/types/component-reference';
import StateRow from './StateRow.astro';
interface Props {
@@ -34,7 +34,7 @@ const stateEntries = Object.entries(state);
<StateRow
name={name}
type={def.type}
shortType={def.shortType}
detailedType={def.detailedType}
description={def.description}
componentName={componentName}
/>
@@ -7,9 +7,9 @@ import H3 from '@/components/typography/H3Markdown.astro';
import H4 from '@/components/typography/H4Markdown.astro';
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import P from '@/components/typography/P.astro';
import type { ComponentApiReference } from '@/types/api-reference';
import type { ComponentReference } from '@/types/component-reference';
import { isValidFramework } from '@/types/docs';
import { createApiReferenceModel } from '@/utils/apiReferenceModel';
import { createComponentReferenceModel } from '@/utils/componentReferenceModel';
import FrameworkCase from '../FrameworkCase.astro';
import ApiDataAttrsTable from './ApiDataAttrsTable.astro';
import ApiPropsTable from './ApiPropsTable.astro';
@@ -26,11 +26,11 @@ if (!framework || !isValidFramework(framework)) {
throw new Error(`Invalid or missing framework param.`);
}
const entry = await getEntry('apiReference', kebabCase(component));
const apiRef: ComponentApiReference | null = entry?.data ?? null;
const entry = await getEntry('componentReference', kebabCase(component));
const apiRef: ComponentReference | null = entry?.data ?? null;
if (!apiRef) return;
const apiReferenceModel = createApiReferenceModel(component, apiRef);
const apiReferenceModel = createComponentReferenceModel(component, apiRef);
if (!apiReferenceModel) return;
const singlePropsSection = !apiReferenceModel.hasParts
@@ -92,7 +92,7 @@ const singleDataAttributesSection = !apiReferenceModel.hasParts
{partDataAttributesSection && (
<>
<H4 id={partDataAttributesSection.id}>{partDataAttributesSection.title}</H4>
<ApiDataAttrsTable dataAttributes={part.data.dataAttributes} />
<ApiDataAttrsTable dataAttributes={part.data.dataAttributes} componentName={part.componentName} />
</>
)}
</>
@@ -128,7 +128,7 @@ const singleDataAttributesSection = !apiReferenceModel.hasParts
{singleDataAttributesSection && (
<>
<H3 id={singleDataAttributesSection.id}>{singleDataAttributesSection.title}</H3>
<ApiDataAttrsTable dataAttributes={apiReferenceModel.data.dataAttributes} />
<ApiDataAttrsTable dataAttributes={apiReferenceModel.data.dataAttributes} componentName={component} />
</>
)}
</>
@@ -0,0 +1,26 @@
---
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import Td from '@/components/typography/Td.astro';
import DetailRow from './DetailRow.astro';
interface Props {
name: string;
type?: string;
detailedType?: string;
description?: string;
componentName: string;
}
const { name, type, detailedType, description, componentName } = Astro.props;
const id = `${componentName}-attr-${name}`;
---
<DetailRow id={id} name={name} type={type ?? ""} detailedType={detailedType} description={description} colspan={3}>
<Td class="align-top">
<MarkdownCode>{name}</MarkdownCode>
</Td>
<Td class="align-top">
{type ? <MarkdownCode>{type}</MarkdownCode> : null}
</Td>
</DetailRow>
@@ -19,14 +19,14 @@ interface Props {
id: string;
name: string;
type: string;
shortType?: string;
detailedType?: string;
description?: string;
colspan: number;
}
const { id, name, type, shortType, description, colspan } = Astro.props;
const { id, name, type, detailedType, description, colspan } = Astro.props;
const hasDetail = Boolean(shortType || description);
const hasDetail = Boolean(detailedType || description);
---
<Tr
@@ -38,25 +38,22 @@ const hasDetail = Boolean(shortType || description);
)}
>
<slot />
{
hasDetail && (
<Td class="align-top">
<button
aria-expanded="false"
aria-controls={`${id}-detail`}
aria-label={`Details for ${name}`}
data-detail-toggle
<Td class="align-top">
{
hasDetail && (<button
aria-expanded="false"
aria-controls={`${id}-detail`}
aria-label={`Details for ${name}`}
data-detail-toggle
>
<span
aria-hidden="true"
class="inline-block group-data-expanded:rotate-90"
>
<span
aria-hidden="true"
class="inline-block group-data-expanded:rotate-90"
>
</span>
</button>
</Td>
)
}
</span>
</button>)}
</Td>
</Tr>
{
@@ -74,14 +71,14 @@ const hasDetail = Boolean(shortType || description);
</>
)}
{(type || shortType) && (
{detailedType && (
<>
<dt class="text-dark-40 dark:text-light-40">
Type
</dt>
<dd>
<MarkdownCode>
{type || shortType}
{detailedType}
</MarkdownCode>
</dd>
</>
@@ -6,20 +6,19 @@ import DetailRow from './DetailRow.astro';
interface Props {
name: string;
type: string;
shortType?: string;
detailedType?: string;
description?: string;
defaultValue?: string;
required?: boolean;
componentName: string;
}
const { name, type, shortType, description, defaultValue, required, componentName } = Astro.props;
const { name, type, detailedType, description, defaultValue, required, componentName } = Astro.props;
const displayType = shortType ?? type;
const id = `${componentName}-${name}`;
---
<DetailRow id={id} name={name} type={type} shortType={shortType} description={description} colspan={4}>
<DetailRow id={id} name={name} type={type} detailedType={detailedType} description={description} colspan={4}>
<Td class="align-top">
<MarkdownCode>
{name}
@@ -27,7 +26,7 @@ const id = `${componentName}-${name}`;
</MarkdownCode>
</Td>
<Td class="align-top">
<MarkdownCode>{displayType}</MarkdownCode>
<MarkdownCode>{type}</MarkdownCode>
</Td>
<Td class="align-top">
<MarkdownCode>
@@ -6,22 +6,21 @@ import DetailRow from './DetailRow.astro';
interface Props {
name: string;
type: string;
shortType?: string;
detailedType?: string;
description?: string;
componentName: string;
}
const { name, type, shortType, description, componentName } = Astro.props;
const { name, type, detailedType, description, componentName } = Astro.props;
const displayType = shortType ?? type;
const id = `${componentName}-state-${name}`;
---
<DetailRow id={id} name={name} type={type} shortType={shortType} description={description} colspan={3}>
<DetailRow id={id} name={name} type={type} detailedType={detailedType} description={description} colspan={3}>
<Td class="align-top">
<MarkdownCode>{name}</MarkdownCode>
</Td>
<Td class="align-top">
<MarkdownCode>{displayType}</MarkdownCode>
<MarkdownCode>{type}</MarkdownCode>
</Td>
</DetailRow>
@@ -0,0 +1,46 @@
---
/**
* Renders the parameters table for util reference.
* Reuses PropRow.astro since ParamDef has the same shape as PropDef.
*/
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 { ParamDef } from '@/types/util-reference';
import PropRow from './PropRow.astro';
interface Props {
params: Record<string, ParamDef>;
utilName: string;
}
const { params, utilName } = Astro.props;
---
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Parameter</Th>
<Th>Type</Th>
<Th>Default</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{
Object.entries(params).map(([name, param]) => (
<PropRow
name={name}
type={param.type}
detailedType={param.detailedType}
description={param.description}
defaultValue={param.default}
required={param.required}
componentName={utilName}
/>
))
}
</Tbody>
</Table>
@@ -0,0 +1,80 @@
---
import { getEntry } from 'astro:content';
import { kebabCase } from 'es-toolkit/string';
import ContentWidth from '@/components/frames/ContentWidth.astro';
import H2 from '@/components/typography/H2Markdown.astro';
import H3 from '@/components/typography/H3Markdown.astro';
import H4 from '@/components/typography/H4Markdown.astro';
import P from '@/components/typography/P.astro';
import type { UtilReference } from '@/types/util-reference';
import { createUtilReferenceModel } from '@/utils/utilReferenceModel';
import InlineMarkdown from './InlineMarkdown.astro';
import UtilParamsTable from './UtilParamsTable.astro';
import UtilReturnTable from './UtilReturnTable.astro';
interface Props {
util: string;
slug?: string;
}
const { util, slug } = Astro.props;
const entry = await getEntry('utilReference', slug ?? kebabCase(util));
const ref: UtilReference | null = entry?.data ?? null;
if (!ref) return;
const model = createUtilReferenceModel(util, ref);
if (!model) return;
---
<ContentWidth>
<H2 id={model.heading.id}>{model.heading.text}</H2>
{model.isMultiOverload ? (
model.overloads.map((overload) => {
const paramsSection = overload.sections.find((s) => s.key === 'parameters');
const returnSection = overload.sections.find((s) => s.key === 'returnValue');
const hasParams = Object.keys(overload.data.parameters).length > 0;
return (
<>
<H3 id={overload.id}>{overload.label ?? `Overload ${overload.index}`}</H3>
{overload.description && (
<P><InlineMarkdown content={overload.description} /></P>
)}
{paramsSection && hasParams && (
<>
<H4 id={paramsSection.id}>{paramsSection.title}</H4>
<UtilParamsTable params={overload.data.parameters} utilName={`${util}-${overload.id}`} />
</>
)}
{returnSection && (
<>
<H4 id={returnSection.id}>{returnSection.title}</H4>
<UtilReturnTable returnValue={overload.data.returnValue} utilName={`${util}-${overload.id}`} />
</>
)}
</>
);
})
) : (
<>
{model.sections.find((s) => s.key === 'parameters') && (
<>
<H3 id="parameters">Parameters</H3>
<UtilParamsTable params={model.overload.parameters} utilName={util} />
</>
)}
{model.sections.find((s) => s.key === 'returnValue') && (
<>
<H3 id="return-value">Return Value</H3>
<UtilReturnTable returnValue={model.overload.returnValue} utilName={util} />
</>
)}
</>
)}
</ContentWidth>
@@ -0,0 +1,88 @@
---
/**
* Renders the return value section for util reference.
*
* Three rendering modes:
* - Mode 1: Object return with `fields` → table using StateRow
* - Mode 2: Simple return with detailedType/description → single-row disclosure table
* - Mode 3: Simple return without detail → inline type text
*/
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
import P from '@/components/typography/P.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 type { ReturnValue } from '@/types/util-reference';
import DetailRow from './DetailRow.astro';
import InlineMarkdown from './InlineMarkdown.astro';
import StateRow from './StateRow.astro';
interface Props {
returnValue: ReturnValue;
utilName: string;
}
const { returnValue, utilName } = Astro.props;
const hasFields = returnValue.fields && Object.keys(returnValue.fields).length > 0;
const hasDetail = !hasFields && Boolean(returnValue.detailedType || returnValue.description);
---
{hasFields ? (
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Property</Th>
<Th>Type</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{
Object.entries(returnValue.fields!).map(([name, field]) => (
<StateRow
name={name}
type={field.type}
detailedType={field.detailedType}
description={field.description}
componentName={`${utilName}-return`}
/>
))
}
</Tbody>
</Table>
) : hasDetail ? (
<Table maxWidth={false} outerClass="my-6">
<Thead>
<Tr>
<Th>Type</Th>
<Th />
</Tr>
</Thead>
<Tbody>
<DetailRow
id={`${utilName}-return-type`}
name="return type"
type={returnValue.type}
detailedType={returnValue.detailedType}
description={returnValue.description}
colspan={2}
>
<Td><MarkdownCode>{returnValue.type}</MarkdownCode></Td>
</DetailRow>
</Tbody>
</Table>
) : (
<P>
<MarkdownCode>{returnValue.type}</MarkdownCode>
{returnValue.description && (
<>
{" — "}
<InlineMarkdown content={returnValue.description} />
</>
)}
</P>
)}
@@ -0,0 +1,24 @@
.react-create-player-basic {
position: relative;
}
.react-create-player-basic video {
width: 100%;
}
.react-create-player-basic__controls {
position: absolute;
bottom: 10px;
left: 10px;
}
.react-create-player-basic__button {
padding-block: 8px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
padding-inline: 20px;
cursor: pointer;
}
@@ -0,0 +1,41 @@
import { createPlayer, features } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './BasicUsage.css';
const { Provider, Container, usePlayer } = createPlayer({
features: features.video,
});
function Controls() {
const store = usePlayer();
const paused = usePlayer((s) => s.paused);
return (
<div className="react-create-player-basic__controls">
<button
type="button"
className="react-create-player-basic__button"
onClick={() => (paused ? store.play() : store.pause())}
>
{paused ? 'Play' : 'Pause'}
</button>
</div>
);
}
export default function BasicUsage() {
return (
<Provider>
<Container className="react-create-player-basic">
<Video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoPlay
muted
playsInline
/>
<Controls />
</Container>
</Provider>
);
}
@@ -0,0 +1,10 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
import './BasicUsage.ts';
</script>
@@ -0,0 +1,37 @@
.html-create-player-basic {
position: relative;
}
.html-create-player-basic video {
width: 100%;
}
.html-create-player-basic__button {
padding-block: 8px;
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
padding-inline: 20px;
cursor: pointer;
}
.html-create-player-basic__button .show-when-paused {
display: none;
}
.html-create-player-basic__button .show-when-playing {
display: none;
}
.html-create-player-basic__button[data-paused] .show-when-paused {
display: inline;
}
.html-create-player-basic__button:not([data-paused]) .show-when-playing {
display: inline;
}
@@ -0,0 +1,12 @@
<demo-video-player class="html-create-player-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<demo-play-toggle class="html-create-player-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</demo-play-toggle>
</demo-video-player>
@@ -0,0 +1,57 @@
import {
applyElementProps,
applyStateDataAttrs,
createButton,
createPlayer,
features,
MediaElement,
selectPlayback,
} from '@videojs/html';
const { PlayerElement, PlayerController, context } = createPlayer({
features: [...features.video],
});
class VideoPlayer extends PlayerElement {
static readonly tagName = 'demo-video-player';
}
class PlayToggle extends MediaElement {
static readonly tagName = 'demo-play-toggle';
readonly #player = new PlayerController(this, context, selectPlayback);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => {
const state = this.#player.value;
if (!state) return;
state.paused ? state.play() : state.pause();
},
isDisabled: () => !this.#player.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override update(): void {
super.update();
const state = this.#player.value;
if (!state) return;
applyStateDataAttrs(this, state, { paused: 'data-paused', ended: 'data-ended' });
}
}
customElements.define(VideoPlayer.tagName, VideoPlayer);
customElements.define(PlayToggle.tagName, PlayToggle);
@@ -0,0 +1,10 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
import './BasicUsage.ts';
</script>
@@ -0,0 +1,36 @@
.html-player-controller-basic {
position: relative;
}
.html-player-controller-basic video {
width: 100%;
}
.html-player-controller-basic__panel {
display: flex;
gap: 16px;
padding: 12px;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
align-items: center;
}
.html-player-controller-basic__actions {
display: flex;
gap: 6px;
}
.html-player-controller-basic__actions button {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
font-size: 0.8125rem;
}
.html-player-controller-basic__state {
font-size: 0.8125rem;
color: #374151;
font-variant-numeric: tabular-nums;
}
@@ -0,0 +1,18 @@
<demo-ctrl-player class="html-player-controller-basic">
<video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoplay
muted
playsinline
></video>
<div class="html-player-controller-basic__panel">
<demo-ctrl-actions class="html-player-controller-basic__actions">
<button class="action-play">Play</button>
<button class="action-pause">Pause</button>
<button class="action-volume">50% Volume</button>
</demo-ctrl-actions>
<demo-ctrl-state class="html-player-controller-basic__state">
<span class="state-text">Paused: Yes | Time: 0.0s | Volume: 100%</span>
</demo-ctrl-state>
</div>
</demo-ctrl-player>
@@ -0,0 +1,67 @@
import { applyElementProps, createButton, createPlayer, features, MediaElement } from '@videojs/html';
const { PlayerElement, PlayerController, context } = createPlayer({
features: [...features.video],
});
class DemoPlayer extends PlayerElement {
static readonly tagName = 'demo-ctrl-player';
}
class PlayerActions extends MediaElement {
static readonly tagName = 'demo-ctrl-actions';
readonly #player = new PlayerController(this, context);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const signal = this.#disconnect.signal;
const playBtn = this.querySelector<HTMLButtonElement>('.action-play')!;
const pauseBtn = this.querySelector<HTMLButtonElement>('.action-pause')!;
const volumeBtn = this.querySelector<HTMLButtonElement>('.action-volume')!;
const bind = (el: HTMLElement, action: () => void) => {
const props = createButton({ onActivate: action, isDisabled: () => !this.#player.value });
applyElementProps(el, props, signal);
};
bind(playBtn, () => this.#player.value?.play());
bind(pauseBtn, () => this.#player.value?.pause());
bind(volumeBtn, () => this.#player.value?.changeVolume(0.5));
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
}
class PlayerState extends MediaElement {
static readonly tagName = 'demo-ctrl-state';
readonly #state = new PlayerController(this, context, (s) => ({
paused: s.paused,
currentTime: s.currentTime,
volume: s.volume,
}));
protected override update(): void {
super.update();
const state = this.#state.value;
if (!state) return;
const el = this.querySelector('.state-text');
if (el) {
el.textContent = `Paused: ${state.paused ? 'Yes' : 'No'} | Time: ${state.currentTime.toFixed(1)}s | Volume: ${Math.round(state.volume * 100)}%`;
}
}
}
customElements.define(DemoPlayer.tagName, DemoPlayer);
customElements.define(PlayerActions.tagName, PlayerActions);
customElements.define(PlayerState.tagName, PlayerState);
@@ -0,0 +1,36 @@
.react-render-element-basic {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
}
.react-render-element-basic__toggle {
align-self: flex-start;
padding: 6px 16px;
border-radius: 6px;
border: 1px solid #ccc;
background: #f5f5f5;
cursor: pointer;
}
.react-render-element-basic__tags {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.react-render-element-basic__tag {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 9999px;
background: #e5e7eb;
color: #374151;
transition: all 0.2s ease;
}
.react-render-element-basic__tag--active {
background: #3b82f6;
color: white;
}
@@ -0,0 +1,64 @@
import { renderElement } from '@videojs/react';
import { type ReactNode, useState } from 'react';
import './BasicUsage.css';
interface TagState {
active: boolean;
}
function Tag({
className,
style,
render,
active,
children,
}: renderElement.ComponentProps<TagState> & { active: boolean; children?: ReactNode }) {
const state: TagState = { active };
return renderElement(
'span',
{ className, style, render },
{
state,
props: { children },
stateAttrMap: { active: 'data-active' },
}
);
}
export default function BasicUsage() {
const [active, setActive] = useState(false);
const className = (state: TagState) =>
`react-render-element-basic__tag${state.active ? ' react-render-element-basic__tag--active' : ''}`;
const style = (state: TagState) => ({
fontSize: state.active ? '1.125rem' : '0.875rem',
});
return (
<div className="react-render-element-basic">
<button type="button" className="react-render-element-basic__toggle" onClick={() => setActive((prev) => !prev)}>
{active ? 'Deactivate' : 'Activate'}
</button>
<div className="react-render-element-basic__tags">
<Tag active={active} className={className} style={style}>
Default &lt;span&gt;
</Tag>
<Tag active={active} className={className} style={style} render={<strong />}>
Element &lt;strong&gt;
</Tag>
<Tag
active={active}
className={className}
style={style}
render={(props, state) => <em {...props}>{state.active ? 'Active!' : 'Inactive'}</em>}
/>
</div>
</div>
);
}
@@ -0,0 +1,30 @@
.react-use-button-basic {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
.react-use-button-basic__button {
align-self: flex-start;
padding: 8px 20px;
border-radius: 6px;
border: 1px solid #ccc;
background: #f5f5f5;
cursor: pointer;
font-variant-numeric: tabular-nums;
transition: opacity 0.2s;
}
.react-use-button-basic__button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.react-use-button-basic__label {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.875rem;
color: #6b7280;
}
@@ -0,0 +1,27 @@
import { useButton } from '@videojs/react';
import { useState } from 'react';
import './BasicUsage.css';
export default function BasicUsage() {
const [count, setCount] = useState(0);
const [disabled, setDisabled] = useState(false);
const { getButtonProps, buttonRef } = useButton({
displayName: 'ActivateButton',
onActivate: () => setCount((c) => c + 1),
isDisabled: () => disabled,
});
return (
<div className="react-use-button-basic">
<button ref={buttonRef} {...getButtonProps()} className="react-use-button-basic__button" disabled={disabled}>
Activated {count} times
</button>
<label className="react-use-button-basic__label">
<input type="checkbox" checked={disabled} onChange={(e) => setDisabled(e.target.checked)} />
Disabled
</label>
</div>
);
}
@@ -0,0 +1,36 @@
.react-use-media-basic {
position: relative;
}
.react-use-media-basic video {
width: 100%;
}
.react-use-media-basic__info {
display: flex;
flex-direction: column;
gap: 4px;
margin: 0;
padding: 12px;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
font-size: 0.8125rem;
}
.react-use-media-basic__info div {
display: flex;
gap: 8px;
}
.react-use-media-basic__info dt {
color: #6b7280;
min-width: 80px;
}
.react-use-media-basic__info dd {
margin: 0;
font-variant-numeric: tabular-nums;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -0,0 +1,52 @@
import { createPlayer, features, useMedia } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './BasicUsage.css';
const { Provider, Container } = createPlayer({
features: features.video,
});
function MediaInfo() {
const media = useMedia();
if (!media) return null;
return (
<dl className="react-use-media-basic__info">
<div>
<dt>tagName</dt>
<dd>{media.tagName.toLowerCase()}</dd>
</div>
<div>
<dt>src</dt>
<dd>{media.currentSrc || '—'}</dd>
</div>
<div>
<dt>videoWidth</dt>
<dd>{media.videoWidth}px</dd>
</div>
<div>
<dt>videoHeight</dt>
<dd>{media.videoHeight}px</dd>
</div>
</dl>
);
}
export default function BasicUsage() {
return (
<Provider>
<Container className="react-use-media-basic">
<Video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoPlay
muted
playsInline
loop
/>
<MediaInfo />
</Container>
</Provider>
);
}
@@ -0,0 +1,31 @@
.react-use-player-selector {
position: relative;
}
.react-use-player-selector video {
width: 100%;
}
.react-use-player-selector__state {
display: flex;
gap: 16px;
padding: 12px;
margin: 0;
font-size: 0.8125rem;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.react-use-player-selector__state div {
display: flex;
gap: 8px;
}
.react-use-player-selector__state dt {
color: #6b7280;
}
.react-use-player-selector__state dd {
margin: 0;
font-variant-numeric: tabular-nums;
}
@@ -0,0 +1,48 @@
import { createPlayer, features, usePlayer } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './Selector.css';
const { Provider, Container } = createPlayer({
features: features.video,
});
function StateDisplay() {
const state = usePlayer((s) => ({
paused: s.paused,
currentTime: s.currentTime,
duration: s.duration,
}));
return (
<dl className="react-use-player-selector__state">
<div>
<dt>Paused</dt>
<dd>{String(state.paused)}</dd>
</div>
<div>
<dt>Time</dt>
<dd>
{state.currentTime.toFixed(1)}s / {state.duration.toFixed(1)}s
</dd>
</div>
</dl>
);
}
export default function Selector() {
return (
<Provider>
<Container className="react-use-player-selector">
<Video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoPlay
muted
playsInline
loop
/>
<StateDisplay />
</Container>
</Provider>
);
}
@@ -0,0 +1,24 @@
.react-use-player-store {
position: relative;
}
.react-use-player-store video {
width: 100%;
}
.react-use-player-store__controls {
display: flex;
gap: 6px;
padding: 12px;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.react-use-player-store__controls button {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
font-size: 0.8125rem;
}
@@ -0,0 +1,40 @@
import { createPlayer, features, usePlayer } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './StoreAccess.css';
const { Provider, Container } = createPlayer({
features: features.video,
});
function Controls() {
const store = usePlayer();
return (
<div className="react-use-player-store__controls">
<button type="button" onClick={() => store.play()}>
Play
</button>
<button type="button" onClick={() => store.pause()}>
Pause
</button>
</div>
);
}
export default function StoreAccess() {
return (
<Provider>
<Container className="react-use-player-store">
<Video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoPlay
muted
playsInline
loop
/>
<Controls />
</Container>
</Provider>
);
}
@@ -0,0 +1,31 @@
.react-use-store-selector {
position: relative;
}
.react-use-store-selector video {
width: 100%;
}
.react-use-store-selector__state {
display: flex;
gap: 16px;
padding: 12px;
margin: 0;
font-size: 0.8125rem;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.react-use-store-selector__state div {
display: flex;
gap: 8px;
}
.react-use-store-selector__state dt {
color: #6b7280;
}
.react-use-store-selector__state dd {
margin: 0;
font-variant-numeric: tabular-nums;
}
@@ -0,0 +1,46 @@
import { createPlayer, features, usePlayer, useStore } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './Selector.css';
const { Provider, Container } = createPlayer({
features: features.video,
});
function DerivedState() {
const store = usePlayer();
const derived = useStore(store, (s) => ({
remaining: s.duration - s.currentTime,
progress: s.duration > 0 ? (s.currentTime / s.duration) * 100 : 0,
}));
return (
<dl className="react-use-store-selector__state">
<div>
<dt>Remaining</dt>
<dd>{derived.remaining.toFixed(1)}s</dd>
</div>
<div>
<dt>Progress</dt>
<dd>{derived.progress.toFixed(1)}%</dd>
</div>
</dl>
);
}
export default function Selector() {
return (
<Provider>
<Container className="react-use-store-selector">
<Video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoPlay
muted
playsInline
loop
/>
<DerivedState />
</Container>
</Provider>
);
}
@@ -0,0 +1,24 @@
.react-use-store-access {
position: relative;
}
.react-use-store-access video {
width: 100%;
}
.react-use-store-access__controls {
display: flex;
gap: 6px;
padding: 12px;
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.react-use-store-access__controls button {
padding: 4px 12px;
border-radius: 6px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
font-size: 0.8125rem;
}
@@ -0,0 +1,41 @@
import { createPlayer, features, usePlayer, useStore } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './StoreAccess.css';
const { Provider, Container } = createPlayer({
features: features.video,
});
function SeekControls() {
const store = usePlayer();
const s = useStore(store);
return (
<div className="react-use-store-access__controls">
<button type="button" onClick={() => s.seek(0)}>
Go to start
</button>
<button type="button" onClick={() => s.seek(s.state.duration / 2)}>
Go to middle
</button>
</div>
);
}
export default function StoreAccess() {
return (
<Provider>
<Container className="react-use-store-access">
<Video
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
autoPlay
muted
playsInline
loop
/>
<SeekControls />
</Container>
</Provider>
);
}