mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): add util reference pipeline (#537)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c11395ece1
commit
78112fbefd
@@ -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
-7
@@ -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 <span>
|
||||
</Tag>
|
||||
|
||||
<Tag active={active} className={className} style={style} render={<strong />}>
|
||||
Element <strong>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user