mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore(root): streamline agent guidance and skills
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# Builder Conventions
|
||||
|
||||
Naming and file placement conventions required by the api-docs-builder at `site/scripts/api-docs-builder/`.
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Path | Purpose |
|
||||
|------|------|---------|
|
||||
| Core | `packages/core/src/core/ui/{name}/{name}-core.ts` | Props, State, defaultProps |
|
||||
| Data attrs | `packages/core/src/core/ui/{name}/{name}-data-attrs.ts` | Data attribute definitions |
|
||||
| CSS vars | `packages/core/src/core/ui/{name}/{name}-css-vars.ts` | CSS custom property definitions (optional) |
|
||||
| HTML element | `packages/html/src/ui/{name}/{name}-element.ts` | Custom element with `static tagName` |
|
||||
| React parts | `packages/react/src/ui/{name}/index.parts.ts` | Multi-part detection (optional) |
|
||||
|
||||
## Naming Requirements
|
||||
|
||||
The builder derives PascalCase from kebab-case using `kebabCase` from es-toolkit. All interfaces and exports must follow this pattern:
|
||||
|
||||
| Convention | Example (play-button) |
|
||||
|-----------|----------------------|
|
||||
| Props interface | `PlayButtonProps` |
|
||||
| State interface | `PlayButtonState` |
|
||||
| Core class | `PlayButtonCore` |
|
||||
| Data attrs export | `PlayButtonDataAttrs` |
|
||||
| CSS vars export | `PlayButtonCSSVars` |
|
||||
| HTML element class | `PlayButtonElement` |
|
||||
| HTML tag name | `static tagName = 'media-play-button'` |
|
||||
|
||||
## NAME_OVERRIDES
|
||||
|
||||
When kebab-to-pascal conversion doesn't produce the correct name, add an override in `site/src/utils/api-reference-overrides.ts` (the shared map the builder imports and the reference pages invert for slug lookup):
|
||||
|
||||
```ts
|
||||
export const NAME_OVERRIDES: Record<string, string> = {
|
||||
'pip-button': 'PiPButton',
|
||||
'airplay-button': 'AirPlayButton',
|
||||
};
|
||||
```
|
||||
|
||||
Use overrides only when the standard conversion fails (e.g., acronyms like PiP). Prefer aligning component naming with the standard conversion when possible.
|
||||
|
||||
The same map covers media elements whose PascalCase name doesn't kebab-case to their element tag name (e.g. `'hlsjs-video': 'HlsJsVideo'`). It is keyed by the generated-reference file slug regardless of component vs. media.
|
||||
|
||||
## Multi-Part Components
|
||||
|
||||
**Detection**: Presence of `packages/react/src/ui/{name}/index.parts.ts`.
|
||||
|
||||
**Non-local re-export filtering**: Only exports with source paths starting with `./` are treated as parts. Re-exports from other directories (e.g., `../slider/index.parts`) are filtered out. This prevents domain variant components (TimeSlider, VolumeSlider) from inheriting base component parts.
|
||||
|
||||
**Single-part fallback**: When filtering leaves only one part (typically Root), the component uses single-part mode — the remaining part's props/state/data-attrs are promoted to the top level, not nested under `parts`.
|
||||
|
||||
**Primary part identification**: The part whose React source file instantiates the component's Core class (matches `new \w+Core\(`). The primary part receives the shared core props/state/data-attrs/css-vars.
|
||||
|
||||
**Non-primary parts**: Each gets its own element file at `{name}-{part}-element.ts`. Element class must be `{Name}{Part}Element` (e.g., `TimeGroupElement`).
|
||||
|
||||
**Framework-divergent parts**: All parts get `platforms.react`. Parts with a matching HTML element file also get `platforms.html`. The renderer filters parts by framework — React-only parts are hidden in HTML docs.
|
||||
|
||||
**Part descriptions**: Extracted from JSDoc on the React component export:
|
||||
```tsx
|
||||
/** Displays a formatted time value. */
|
||||
export const Value = ...;
|
||||
```
|
||||
|
||||
## JSDoc Extraction
|
||||
|
||||
- **Data attribute descriptions**: From JSDoc comments on each property in the data-attrs export object
|
||||
- **Part descriptions**: From JSDoc on React component exports in their `.tsx` files
|
||||
- **Prop/state descriptions**: From JSDoc on interface properties in the core file
|
||||
|
||||
### Util JSDoc
|
||||
|
||||
Util exports (hooks, controllers, factories, selectors) have their own JSDoc conventions for `@param`, `@label`, and `@public` tags. See `references/util-conventions.md` → "JSDoc Conventions".
|
||||
|
||||
## Common Failures
|
||||
|
||||
The builder fails silently for many issues — data just won't appear in the JSON:
|
||||
|
||||
| Symptom | Cause |
|
||||
|---------|-------|
|
||||
| No JSON generated | Core file missing or Props interface not found |
|
||||
| Empty props | Interface not named `{PascalCase}Props` |
|
||||
| Empty state | Interface not named `{PascalCase}State` |
|
||||
| No data attributes | File missing or export not named `{PascalCase}DataAttrs` |
|
||||
| No CSS vars | File missing or export not named `{PascalCase}CSSVars` |
|
||||
| No HTML tag | Element file missing or no `static tagName` |
|
||||
| No part descriptions | Missing JSDoc on React component exports |
|
||||
| Wrong PascalCase | Need a `NAME_OVERRIDES` entry |
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
# Generate JSON
|
||||
pnpm -F site api-docs
|
||||
|
||||
# Check component output
|
||||
cat site/src/content/generated-component-reference/{name}.json
|
||||
|
||||
# Check util output
|
||||
cat site/src/content/generated-util-reference/{slug}.json
|
||||
|
||||
# Verify schema
|
||||
# The builder validates against ComponentReferenceSchema / UtilReferenceSchema before writing.
|
||||
# Schema errors are logged as errors and cause exit code 1.
|
||||
```
|
||||
@@ -0,0 +1,277 @@
|
||||
# Demo Patterns
|
||||
|
||||
File structure and conventions for interactive component demos.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
site/src/components/docs/demos/{component}/
|
||||
├── html/css/
|
||||
│ ├── BasicUsage.astro # Astro wrapper (renders HTML, imports CSS, bundles script)
|
||||
│ ├── BasicUsage.html # Markup only (no <style> or <script>)
|
||||
│ ├── BasicUsage.css # Styles
|
||||
│ └── BasicUsage.ts # Side-effect imports for custom element registration
|
||||
└── react/css/
|
||||
├── BasicUsage.tsx # React component
|
||||
└── BasicUsage.css # Styles
|
||||
```
|
||||
|
||||
## BEM Naming
|
||||
|
||||
Block = `{framework}-{component}-{variant}`, element = `__{part}`:
|
||||
|
||||
```
|
||||
html-play-button-basic /* HTML framework, block */
|
||||
html-play-button-basic__button /* HTML framework, element */
|
||||
react-play-button-basic /* React framework, block */
|
||||
react-play-button-basic__button /* React framework, element */
|
||||
```
|
||||
|
||||
The framework prefix (`html-` / `react-`) prevents CSS leaking between HTML and React demos on the same page (both render but one is hidden).
|
||||
|
||||
## HTML Demo Files
|
||||
|
||||
### .astro wrapper
|
||||
|
||||
```astro
|
||||
---
|
||||
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>
|
||||
```
|
||||
|
||||
The `.astro` wrapper is required because only Astro `<script>` tags go through Vite's bundling pipeline.
|
||||
|
||||
### .html (markup only)
|
||||
|
||||
```html
|
||||
<video-player class="html-mute-button-basic">
|
||||
<video
|
||||
src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
loop
|
||||
></video>
|
||||
<media-mute-button class="html-mute-button-basic__button">
|
||||
<span class="show-when-muted">Unmute</span>
|
||||
<span class="show-when-unmuted">Mute</span>
|
||||
</media-mute-button>
|
||||
</video-player>
|
||||
```
|
||||
|
||||
- No `<style>` or `<script>` tags
|
||||
- Video attributes: `autoplay muted playsinline loop`
|
||||
- State labels use CSS class names toggled by data attributes
|
||||
|
||||
### .css (styles)
|
||||
|
||||
```css
|
||||
.html-mute-button-basic {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.html-mute-button-basic video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.html-mute-button-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;
|
||||
}
|
||||
|
||||
/* State-based visibility via data attributes */
|
||||
.html-mute-button-basic__button .show-when-muted { display: none; }
|
||||
.html-mute-button-basic__button .show-when-unmuted { display: none; }
|
||||
.html-mute-button-basic__button[data-muted] .show-when-muted { display: inline; }
|
||||
.html-mute-button-basic__button:not([data-muted]) .show-when-unmuted { display: inline; }
|
||||
```
|
||||
|
||||
### .ts (registration imports)
|
||||
|
||||
```ts
|
||||
import '@videojs/html/video/player';
|
||||
import '@videojs/html/ui/mute-button';
|
||||
```
|
||||
|
||||
Import registration for:
|
||||
|
||||
- `@videojs/html/video/player` — always needed (registers `<video-player>`)
|
||||
- `@videojs/html/ui/{component}` — registers the component's custom element
|
||||
|
||||
## React Demo Files
|
||||
|
||||
### .tsx (component)
|
||||
|
||||
```tsx
|
||||
import { createPlayer, MuteButton } from '@videojs/react';
|
||||
import { Video, videoFeatures } from '@videojs/react/video';
|
||||
|
||||
import './BasicUsage.css';
|
||||
|
||||
const Player = createPlayer({ features: videoFeatures });
|
||||
|
||||
export default function BasicUsage() {
|
||||
return (
|
||||
<Player.Provider>
|
||||
<Player.Container className="react-mute-button-basic">
|
||||
<Video
|
||||
src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
loop
|
||||
/>
|
||||
<MuteButton
|
||||
className="react-mute-button-basic__button"
|
||||
render={(props, state) => (
|
||||
<button {...props}>{state.muted ? 'Unmute' : 'Mute'}</button>
|
||||
)}
|
||||
/>
|
||||
</Player.Container>
|
||||
</Player.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Key patterns:
|
||||
|
||||
- `createPlayer({ features: videoFeatures })` creates the player
|
||||
- Video attributes: `autoPlay muted playsInline loop` (React camelCase)
|
||||
- `render` prop for state-based rendering: `render={(props, state) => ...}`
|
||||
- Spread `{...props}` on the rendered element for accessibility attributes
|
||||
|
||||
### .css (styles)
|
||||
|
||||
Same base styling as HTML but with `react-` BEM prefix:
|
||||
|
||||
```css
|
||||
.react-mute-button-basic {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.react-mute-button-basic video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-mute-button-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;
|
||||
}
|
||||
```
|
||||
|
||||
React CSS files typically don't need data-attribute selectors since the `render` prop handles state-based rendering. Include them only when CSS state reflection is used.
|
||||
|
||||
## State Reflection Patterns
|
||||
|
||||
### HTML: Data attribute selectors
|
||||
|
||||
Components expose state via `data-*` attributes. CSS toggles visibility:
|
||||
|
||||
```css
|
||||
/* Hide all by default */
|
||||
.html-play-button-basic__button .show-when-paused { display: none; }
|
||||
.html-play-button-basic__button .show-when-playing { display: none; }
|
||||
|
||||
/* Show based on state */
|
||||
.html-play-button-basic__button[data-paused] .show-when-paused { display: inline; }
|
||||
.html-play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }
|
||||
```
|
||||
|
||||
For multi-value attributes (e.g., `data-volume-level`):
|
||||
|
||||
```css
|
||||
.html-mute-button-volume-levels__button .level-off,
|
||||
.html-mute-button-volume-levels__button .level-low,
|
||||
.html-mute-button-volume-levels__button .level-medium,
|
||||
.html-mute-button-volume-levels__button .level-high {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.html-mute-button-volume-levels__button[data-volume-level="off"] .level-off { display: inline; }
|
||||
.html-mute-button-volume-levels__button[data-volume-level="low"] .level-low { display: inline; }
|
||||
```
|
||||
|
||||
### React: Render prop
|
||||
|
||||
```tsx
|
||||
<MuteButton
|
||||
render={(props, state) => (
|
||||
<button {...props}>
|
||||
{state.volumeLevel === 'off'
|
||||
? 'Off'
|
||||
: state.volumeLevel === 'low'
|
||||
? 'Low'
|
||||
: state.volumeLevel === 'medium'
|
||||
? 'Medium'
|
||||
: 'High'}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Three-state pattern (Play/Pause/Replay)
|
||||
|
||||
HTML uses `:not()` combinators to handle mutually exclusive states:
|
||||
|
||||
```css
|
||||
.html-play-button-basic__button[data-paused]:not([data-ended]) .show-when-paused { display: inline; }
|
||||
.html-play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }
|
||||
.html-play-button-basic__button[data-ended] .show-when-ended { display: inline; }
|
||||
```
|
||||
|
||||
React uses nested ternary in the render prop:
|
||||
|
||||
```tsx
|
||||
render={(props, state) => (
|
||||
<button {...props}>{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}</button>
|
||||
)}
|
||||
```
|
||||
|
||||
## Video Sources
|
||||
|
||||
- **Video**: `https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4`
|
||||
- **Poster**: `https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg`
|
||||
|
||||
## Base Button Styles
|
||||
|
||||
All button demos share this base overlay style:
|
||||
|
||||
```css
|
||||
.__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;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,350 @@
|
||||
# MDX Structure
|
||||
|
||||
Structure and conventions for API reference MDX pages at `site/src/content/docs/reference/`.
|
||||
|
||||
## Component Pages
|
||||
|
||||
### Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: MuteButton # PascalCase component name
|
||||
frameworkTitle:
|
||||
html: media-mute-button # HTML custom element tag name
|
||||
description: A button component for muting and unmuting audio playback
|
||||
---
|
||||
```
|
||||
|
||||
- `title`: PascalCase React component name
|
||||
- `frameworkTitle.html`: The `static tagName` from the HTML element file
|
||||
- `description`: One-line description of the component
|
||||
|
||||
### Page Structure
|
||||
|
||||
```
|
||||
frontmatter
|
||||
imports (React demos, HTML demos)
|
||||
## Anatomy
|
||||
## Behavior (if applicable)
|
||||
## Styling (if applicable)
|
||||
## Accessibility (if applicable)
|
||||
## Examples
|
||||
### BasicUsage
|
||||
### [Additional demos]
|
||||
<ComponentReference component="{PascalCase}" />
|
||||
```
|
||||
|
||||
## Imports Section
|
||||
|
||||
### React demo imports
|
||||
|
||||
```mdx
|
||||
{/* React demos */}
|
||||
import BasicUsageDemoReact from "@/components/docs/demos/{component}/react/css/BasicUsage";
|
||||
import basicUsageReactTsx from "@/components/docs/demos/{component}/react/css/BasicUsage.tsx?raw";
|
||||
import basicUsageReactCss from "@/components/docs/demos/{component}/react/css/BasicUsage.css?raw";
|
||||
```
|
||||
|
||||
- Component import: default export from `.tsx` (no extension needed)
|
||||
- Source imports: `?raw` suffix for displaying source code in tabs
|
||||
|
||||
### HTML demo imports
|
||||
|
||||
```mdx
|
||||
{/* HTML demos */}
|
||||
import BasicUsageDemoHtml from "@/components/docs/demos/{component}/html/css/BasicUsage.astro";
|
||||
import basicUsageHtml from "@/components/docs/demos/{component}/html/css/BasicUsage.html?raw";
|
||||
import basicUsageHtmlCss from "@/components/docs/demos/{component}/html/css/BasicUsage.css?raw";
|
||||
import basicUsageHtmlTs from "@/components/docs/demos/{component}/html/css/BasicUsage.ts?raw";
|
||||
```
|
||||
|
||||
- `.astro` wrapper: renders live demo
|
||||
- `.html`, `.css`, `.ts`: `?raw` imports for source tabs
|
||||
|
||||
### Import naming convention
|
||||
|
||||
| Type | Pattern | Example |
|
||||
|------|---------|---------|
|
||||
| React component | `{DemoName}DemoReact` | `BasicUsageDemoReact` |
|
||||
| React source | `{demoName}React{Ext}` | `basicUsageReactTsx` |
|
||||
| HTML component | `{DemoName}DemoHtml` | `BasicUsageDemoHtml` |
|
||||
| HTML source | `{demoName}Html` / `{demoName}Html{Ext}` | `basicUsageHtml`, `basicUsageHtmlCss` |
|
||||
|
||||
## Anatomy Section
|
||||
|
||||
Anatomy shows part nesting with self-closing placeholders, following the Base UI anatomy convention. No hooks, state, handlers, or option mapping — working code belongs in Examples. This holds even when a component has no React component form (e.g. the radio groups, whose React API is a hook feeding `Menu.RadioGroup`): show the part skeleton and let the Behavior prose link to the hook for wiring.
|
||||
|
||||
```mdx
|
||||
## Anatomy
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
```tsx
|
||||
<MuteButton />
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```html
|
||||
<media-mute-button></media-mute-button>
|
||||
```
|
||||
</FrameworkCase>
|
||||
```
|
||||
|
||||
For multi-part components, show composed usage:
|
||||
|
||||
```mdx
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
```tsx
|
||||
<Time.Group>
|
||||
<Time.Value type="current" />
|
||||
<Time.Separator />
|
||||
<Time.Value type="duration" />
|
||||
</Time.Group>
|
||||
```
|
||||
</FrameworkCase>
|
||||
```
|
||||
|
||||
## Prose Sections
|
||||
|
||||
### Behavior
|
||||
|
||||
Explain state transitions, timing, and interaction logic. Use tables for enumerated states:
|
||||
|
||||
```mdx
|
||||
## Behavior
|
||||
|
||||
Toggles mute on and off. Exposes a derived `volumeLevel` based on the current volume and mute state:
|
||||
|
||||
| Level | Condition |
|
||||
|-------|-----------|
|
||||
| `off` | Muted or volume is 0 |
|
||||
| `low` | Volume < 0.5 |
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
**IMPORTANT:** All CSS code blocks in Styling sections MUST be wrapped in `<FrameworkCase>` blocks. HTML examples use custom element selectors (`media-mute-button`), React examples use className-based selectors (`.mute-button`). Never show bare CSS without a framework wrapper — React users should not see HTML element selectors and vice versa.
|
||||
|
||||
Show data attributes as a table, then framework-specific CSS selector patterns:
|
||||
|
||||
```mdx
|
||||
## Styling
|
||||
|
||||
| Attribute | Values | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `data-muted` | Present / absent | Present when audio is muted |
|
||||
| `data-volume-level` | `"off"` \| `"low"` \| `"medium"` \| `"high"` | Current volume level |
|
||||
|
||||
Use `data-volume-level` for multi-level icon switching:
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-mute-button[data-volume-level="off"] .icon-off { display: inline; }
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders standard DOM elements with the same data attributes. Add a `className` and use it as the selector:
|
||||
|
||||
```css
|
||||
.mute-button[data-volume-level="off"] .icon-off { display: inline; }
|
||||
```
|
||||
</FrameworkCase>
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
Describe ARIA attributes, keyboard interactions, and label overrides:
|
||||
|
||||
```mdx
|
||||
## Accessibility
|
||||
|
||||
Renders a `<button>` with an automatic `aria-label`: "Unmute" when muted, "Mute" when unmuted. Override with the `label` prop. Keyboard activation: <kbd>Enter</kbd> / <kbd>Space</kbd>.
|
||||
```
|
||||
|
||||
## Examples Section
|
||||
|
||||
### Nesting pattern
|
||||
|
||||
```mdx
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo files={[
|
||||
{ title: "App.tsx", code: basicUsageReactTsx, lang: "tsx" },
|
||||
{ title: "App.css", code: basicUsageReactCss, lang: "css" },
|
||||
]}>
|
||||
<BasicUsageDemoReact client:idle />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo files={[
|
||||
{ title: "index.html", code: basicUsageHtml, lang: "html" },
|
||||
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
|
||||
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
|
||||
]}>
|
||||
<BasicUsageDemoHtml />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
```
|
||||
|
||||
Key details:
|
||||
- React demos use `client:idle` for hydration
|
||||
- HTML demos render server-side (no `client:*` directive)
|
||||
- React source tabs: `App.tsx`, `App.css`
|
||||
- HTML source tabs: `index.html`, `index.css`, `index.ts`
|
||||
|
||||
### ComponentReference Component
|
||||
|
||||
Always the last element in the file:
|
||||
|
||||
```mdx
|
||||
<ComponentReference component="MuteButton" />
|
||||
```
|
||||
|
||||
The component auto-renders Props, State, Data Attributes, and CSS Custom Properties for single-part and all Parts for multi-part. For multi-part components, React-only parts are hidden in HTML docs via framework filtering.
|
||||
|
||||
### Required Astro Component Imports
|
||||
|
||||
Every component reference MDX needs these at the top of the imports:
|
||||
|
||||
```mdx
|
||||
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
|
||||
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
|
||||
import StyleCase from "@/components/docs/StyleCase.astro";
|
||||
import Demo from "@/components/docs/demos/Demo.astro";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-linking
|
||||
|
||||
Link generously between related reference pages.
|
||||
|
||||
Same-framework or cross-framework link:
|
||||
|
||||
```mdx
|
||||
Within a player provider, <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> is usually simpler.
|
||||
```
|
||||
|
||||
Selector page linking to framework-specific utils:
|
||||
|
||||
```mdx
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
Pass `selectPlayback` to <DocsLink slug="reference/use-player">`usePlayer`</DocsLink> to subscribe.
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
Pass `selectPlayback` to <DocsLink slug="reference/player-controller">`PlayerController`</DocsLink> to subscribe.
|
||||
</FrameworkCase>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Util Pages
|
||||
|
||||
Util pages document React hooks/utilities and HTML controllers/mixins. They are simpler than component pages — no demos, no anatomy.
|
||||
|
||||
### Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: usePlayer
|
||||
description: Hook to access the player store from within a Player Provider
|
||||
---
|
||||
```
|
||||
|
||||
- `title`: The exported function/class name (e.g., `usePlayer`, `PlayerController`)
|
||||
- No `frameworkTitle` — util pages are framework-specific
|
||||
- `description`: One-line description
|
||||
|
||||
### Page Structure
|
||||
|
||||
```
|
||||
frontmatter
|
||||
import UtilReference
|
||||
## Import
|
||||
## Usage
|
||||
<UtilReference util="{Name}" />
|
||||
```
|
||||
|
||||
### Import Section
|
||||
|
||||
Show the import statement for the util:
|
||||
|
||||
```mdx
|
||||
## Import
|
||||
|
||||
\`\`\`tsx
|
||||
import { usePlayer } from '@videojs/react';
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Usage Section
|
||||
|
||||
Explain usage patterns with code examples. For multi-overload utils, document each overload:
|
||||
|
||||
```mdx
|
||||
## Usage
|
||||
|
||||
`usePlayer` has two overloads:
|
||||
|
||||
**Store access (no subscription)** -- returns the store instance.
|
||||
|
||||
\`\`\`tsx
|
||||
const store = usePlayer();
|
||||
\`\`\`
|
||||
|
||||
**Selector-based subscription** -- returns selected state.
|
||||
|
||||
\`\`\`tsx
|
||||
const paused = usePlayer((s) => s.paused);
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### UtilReference Component
|
||||
|
||||
Always the last element in the file:
|
||||
|
||||
```mdx
|
||||
<UtilReference util="usePlayer" />
|
||||
```
|
||||
|
||||
The component auto-renders Parameters and Return Value tables. For multi-overload utils, it renders each overload with its own sections.
|
||||
|
||||
### Required Import
|
||||
|
||||
```mdx
|
||||
import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sidebar Entry
|
||||
|
||||
Add to `site/src/docs.config.ts` in the appropriate section, alphabetically:
|
||||
|
||||
- **Components** — UI component reference pages
|
||||
- **Hooks & Utilities** (`frameworks: ['react']`) — React hooks and utilities
|
||||
- **Controllers & Mixins** (`frameworks: ['html']`) — HTML controllers and mixins
|
||||
|
||||
```ts
|
||||
{
|
||||
sidebarLabel: 'Components',
|
||||
contents: [
|
||||
// sorted alphabetically
|
||||
{ slug: 'reference/buffering-indicator' },
|
||||
{ slug: 'reference/controls' },
|
||||
// ...
|
||||
{ slug: 'reference/{name}' }, // <-- insert alphabetically
|
||||
// ...
|
||||
],
|
||||
},
|
||||
```
|
||||
@@ -0,0 +1,228 @@
|
||||
# Util Reference Conventions
|
||||
|
||||
Conventions for the util reference system that documents React hooks/utilities and HTML controllers/mixins.
|
||||
|
||||
## Architecture
|
||||
|
||||
Util references use **convention-based auto-discovery** from package index files. The builder scans entry points, resolves local module paths, and includes exports matching naming conventions or annotated with `@public`.
|
||||
|
||||
## Auto-Discovery Pipeline
|
||||
|
||||
The builder (`site/scripts/api-docs-builder/src/util-handler.ts`) scans entry points:
|
||||
|
||||
```ts
|
||||
packages/react/src/index.ts → framework: 'react'
|
||||
packages/store/src/react/hooks/index.ts → framework: 'react'
|
||||
packages/html/src/index.ts → framework: 'html'
|
||||
packages/store/src/html/controllers/index.ts → framework: 'html'
|
||||
packages/core/src/dom/store/selectors.ts → framework: null (agnostic)
|
||||
packages/store/src/core/selector.ts → framework: null (agnostic)
|
||||
```
|
||||
|
||||
Framework-agnostic entries (`framework: null`) produce JSON without a `frameworks` field, meaning they apply to all frameworks. Framework-specific entries get `frameworks: ['react']` or `frameworks: ['html']` in the JSON.
|
||||
|
||||
**Phase 1 — Resolve local modules.** Raw TS AST reads export declarations from each index file, keeping only local paths (`./...`), skipping external packages (`@videojs/...`).
|
||||
|
||||
**Phase 2 — Filter by convention.** Each local module is parsed with TAE (typescript-api-extractor) or raw TS AST. Exports are included if they match naming conventions or have `@public`.
|
||||
|
||||
### Adding a New Util
|
||||
|
||||
1. Export it from the appropriate package index file
|
||||
2. Add JSDoc with a description
|
||||
3. If it doesn't match a naming convention (see below), add `@public` to the JSDoc
|
||||
4. Run `pnpm api-docs` to generate its JSON
|
||||
4. Create an MDX page with `<UtilReference util="{Name}" />`
|
||||
5. Add to the sidebar in `docs.config.ts`
|
||||
|
||||
No code changes needed in the builder itself — convention over configuration.
|
||||
|
||||
## JSDoc Conventions
|
||||
|
||||
The builder extracts JSDoc from source exports to populate reference pages. These rules are the detailed form of the API-reference exception in the root `AGENTS.md`.
|
||||
|
||||
### Summary description (required)
|
||||
|
||||
Every util export needs a JSDoc summary. This becomes the description in the generated JSON:
|
||||
|
||||
```ts
|
||||
/** Subscribe to the player's volume state. */
|
||||
export function useVolume(...): VolumeResult;
|
||||
```
|
||||
|
||||
### `@param` descriptions (required for non-obvious params)
|
||||
|
||||
Unlike internal code, API reference exports need `@param` tags so the builder can populate parameter tables. Describe intent and defaults, not types:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Subscribe to derived state with customizable equality check.
|
||||
*
|
||||
* @param subscribe - Subscribe function that returns an unsubscribe callback.
|
||||
* @param selector - Derives a value from the snapshot.
|
||||
* @param isEqual - Custom equality function. Defaults to `shallowEqual`.
|
||||
*/
|
||||
export function useSelector<S, R>(...): R;
|
||||
```
|
||||
|
||||
Format: `@param name - description` (dash after name).
|
||||
|
||||
### No `@returns`
|
||||
|
||||
Return types are inferred from the TypeScript signature. Don't add `@returns`.
|
||||
|
||||
### `@label` for multi-overload functions
|
||||
|
||||
When a function has multiple overload signatures with different return types, each overload gets its own JSDoc block with an `@label` tag. The label becomes a heading in the docs:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Create a player instance with typed store, Provider, and hooks.
|
||||
*
|
||||
* @label Video
|
||||
* @param config - Player configuration with features.
|
||||
*/
|
||||
export function createPlayer(config: CreatePlayerConfig<VideoFeatures>): CreatePlayerResult<VideoPlayerStore>;
|
||||
|
||||
/**
|
||||
* Create a player for audio media.
|
||||
*
|
||||
* @label Audio
|
||||
* @param config - Player configuration with features.
|
||||
*/
|
||||
export function createPlayer(config: CreatePlayerConfig<AudioFeatures>): CreatePlayerResult<AudioPlayerStore>;
|
||||
```
|
||||
|
||||
Without `@label`, overloads render as "Overload 1", "Overload 2", etc.
|
||||
|
||||
### `@label` for constructor overloads
|
||||
|
||||
Same pattern applies to controller constructors:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* @label Without Selector
|
||||
* @param host - The host element that owns this controller.
|
||||
* @param state - The State container to subscribe to.
|
||||
*/
|
||||
constructor(host: ReactiveControllerHost, state: State<T>);
|
||||
|
||||
/**
|
||||
* @label With Selector
|
||||
* @param host - The host element that owns this controller.
|
||||
* @param state - The State container to subscribe to.
|
||||
* @param selector - Derives a value from the state.
|
||||
*/
|
||||
constructor(host: ReactiveControllerHost, state: State<T>, selector: Selector<T, R>);
|
||||
```
|
||||
|
||||
### `@public` for non-convention exports
|
||||
|
||||
Exports that don't match a naming convention (`use*`, `*Controller`, `create*`, `select*`) need `@public` to be discovered:
|
||||
|
||||
```ts
|
||||
/** @public The default player context for consuming the player store. */
|
||||
export const playerContext = createContext<PlayerContextValue>(...);
|
||||
```
|
||||
|
||||
## Inclusion Conventions
|
||||
|
||||
Exports are auto-included when they match these patterns:
|
||||
|
||||
| Pattern | Match Rule | Examples |
|
||||
|---------|-----------|----------|
|
||||
| Hooks | Name starts with `use`, is a function | `usePlayer`, `useStore` |
|
||||
| Controllers | Name ends with `Controller` | `PlayerController`, `StoreController` |
|
||||
| Factories | Name starts with `create`, is a function | `createPlayer` |
|
||||
| Mixin factories | Name starts with `create` + contains `Mixin` | `createProviderMixin` |
|
||||
| `@public` | Has `@public` JSDoc tag | `playerContext`, `mergeProps`, `renderElement` |
|
||||
|
||||
Exports that don't match any convention are excluded (UI components, types, internal helpers).
|
||||
|
||||
## Slug Conventions
|
||||
|
||||
- Slugs are kebab-case: `use-player`, `player-controller`, `merge-props`
|
||||
- All slugs must be unique across both frameworks
|
||||
- When the same name exists in both React and HTML (e.g., `createPlayer`), prefix the HTML slug: `html-create-player`
|
||||
|
||||
## Frameworks
|
||||
|
||||
| Framework | Sidebar Section | Source Packages |
|
||||
|-----------|----------------|-----------------|
|
||||
| `react` | Hooks & Utilities | `@videojs/react`, `@videojs/store/react` |
|
||||
| `html` | Controllers & Mixins | `@videojs/html`, `@videojs/store/html` |
|
||||
| `null` (agnostic) | Selectors | `@videojs/core/dom`, `@videojs/store` |
|
||||
|
||||
Agnostic utils omit the `frameworks` field in JSON, meaning they're available to all frameworks.
|
||||
|
||||
## Overloads
|
||||
|
||||
Use multiple overloads when the return type genuinely differs between signatures:
|
||||
|
||||
```ts
|
||||
// Two overloads — different return types
|
||||
usePlayer() → PlayerStore
|
||||
usePlayer(selector) → T (selected value)
|
||||
```
|
||||
|
||||
For simple param-count differences with the same return type, use a single overload with optional params instead.
|
||||
|
||||
## Generated JSON
|
||||
|
||||
Output: `site/src/content/generated-util-reference/{slug}.json`
|
||||
|
||||
Schema: `UtilReferenceSchema` from `site/src/types/util-reference.ts`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "usePlayer",
|
||||
"description": "...",
|
||||
"overloads": [
|
||||
{
|
||||
"description": "...",
|
||||
"parameters": { "selector": { "type": "...", "required": true } },
|
||||
"returnValue": { "type": "...", "description": "..." }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Content Collection
|
||||
|
||||
The `utilReference` collection in `site/src/content.config.ts` loads from `generated-util-reference/` and validates against `UtilReferenceSchema`.
|
||||
|
||||
## Astro Components
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `UtilReference.astro` | Main component — loads JSON, renders sections |
|
||||
| `UtilParamsTable.astro` | Parameter table (reuses `PropRow.astro`) |
|
||||
| `UtilReturnTable.astro` | Return value table (reuses `StateRow.astro` for object returns) |
|
||||
|
||||
## Common Failures
|
||||
|
||||
| Symptom | Cause |
|
||||
|---------|-------|
|
||||
| No JSON generated | Export not in a scanned index file, or doesn't match convention / lack `@public` |
|
||||
| Zod validation error | Schema mismatch — check field names and types |
|
||||
| Missing from sidebar | Not added to `docs.config.ts` in correct section |
|
||||
| Page 404 | MDX file missing or slug mismatch |
|
||||
| Wrong framework section | Export in wrong entry point — check which index file re-exports it |
|
||||
| TAE crash on index file | Known issue with `UniqueESSymbol` types — raw TS AST fallback handles this |
|
||||
|
||||
## Discovery vs MDX Pages
|
||||
|
||||
Auto-discovery generates JSON files in `site/src/content/generated-util-reference/`. A JSON file without a corresponding MDX page is harmless — it sits unused.
|
||||
|
||||
Only utils with **both** generated JSON **and** a manually-created MDX page appear in the docs. This is intentional: discovery casts a wide net using naming conventions, while MDX pages are curated to document the public API surface.
|
||||
|
||||
For example, `SubscriptionController` is discovered (matches `*Controller`) but has no MDX page because it's an internal building block not intended for direct consumer use.
|
||||
|
||||
When adding a new util to the docs:
|
||||
1. Ensure the builder discovers it (check with `pnpm api-docs`)
|
||||
2. Create the MDX page at `site/src/content/docs/reference/{slug}.mdx`
|
||||
3. Add to the sidebar in `docs.config.ts`
|
||||
|
||||
## Tests
|
||||
|
||||
- `site/scripts/api-docs-builder/src/tests/util-handler.test.ts` — fixture-based tests for discovery, slug uniqueness, frameworks, overloads
|
||||
- `site/src/utils/tests/utilReferenceModel.test.ts` — validates model structure and TOC headings
|
||||
Reference in New Issue
Block a user