48 KiB
API Docs Builder Spec
Ground-truth specification for the API docs builder pipeline: from TypeScript source code to rendered documentation tables. When implementation diverges from this spec, this spec wins.
Inspired by Base UI's API reference system. Base UI generates one JSON file per component part, and each part gets its own props and data-attributes tables. Our system aspires to the same philosophy but has a known architectural limitation: a single Props/State interface per component at the core level means only one part (the "primary") can own core-level props and state. Non-primary parts get shared data attributes, custom React-specific props, and a description.
Principles:
- Convention over configuration. The builder infers structure from file naming and placement. No config files, no explicit annotations for standard cases. Follow the conventions and things just work.
- Spec wins. When implementation diverges from this spec, the spec is the source of truth.
Scope: This spec covers the builder (extraction + JSON generation), the reference model layer (JSON → heading/section structure), TOC integration, and rendered output (tables + disclosure panels). It does not cover CSS styling, demo scaffolding, or MDX page authoring.
1. Sample Inputs
These fictional examples are the source-of-truth for expected behavior throughout this spec.
1a. Single-part component: ToggleButton
Core file — packages/core/src/core/ui/toggle-button/toggle-button-core.ts:
interface ToggleButtonProps {
/** Whether the button is disabled. */
disabled: boolean;
/** Custom label for the button. */
label: string | ((state: ToggleButtonState) => string);
}
interface ToggleButtonState {
/** Whether the toggle is pressed. */
pressed: boolean;
/** Whether the button is disabled. */
disabled: boolean;
}
class ToggleButtonCore {
static readonly defaultProps = {
disabled: false,
label: '',
} as const;
}
Data attributes file — packages/core/src/core/ui/toggle-button/toggle-button-data-attrs.ts:
import type { StateAttrMap } from '../types';
export const ToggleButtonDataAttrs = {
/** Present when the toggle is pressed. */
pressed: 'data-pressed',
/** Present when the button is disabled. */
disabled: 'data-disabled',
} as const satisfies StateAttrMap<ToggleButtonState>;
HTML element file — packages/html/src/ui/toggle-button/toggle-button-element.ts:
export class ToggleButtonElement extends ... {
static readonly tagName = 'media-toggle-button';
}
1b. Multi-part component: Meter
Core file — packages/core/src/core/ui/meter/meter-core.ts:
interface MeterProps {
/** Minimum value. */
min: number;
/** Maximum value. */
max: number;
/** Custom label for accessibility. */
label: string | ((state: MeterState) => string);
}
interface MeterState {
/** Current value as a percentage (0–1). */
percentage: number;
/** The fill level. */
fillState: 'empty' | 'partial' | 'full';
}
class MeterCore {
static readonly defaultProps = {
min: 0,
max: 100,
label: '',
} as const;
}
Data attributes file — packages/core/src/core/ui/meter/meter-data-attrs.ts:
import type { StateAttrMap } from '../types';
export const MeterDataAttrs = {
/** Current percentage as a string. */
percentage: 'data-percentage',
/** The fill level. */
fillState: 'data-fill-state',
} as const satisfies StateAttrMap<MeterState>;
HTML element files:
packages/html/src/ui/meter/meter-element.ts→static tagName = 'media-meter'packages/html/src/ui/meter/meter-track-element.ts→static tagName = 'media-meter-track'packages/html/src/ui/meter/meter-fill-element.ts→static tagName = 'media-meter-fill'
React parts index — packages/react/src/ui/meter/index.parts.ts:
export { default as Track } from './Track';
export { default as Fill } from './Fill';
export { default as Indicator } from './Indicator';
React component JSDoc:
// Track.tsx
/** The track area of the meter. Renders a `<div>` element. */
export default function Track(...) { ... }
// Fill.tsx
/** The filled portion of the meter. Renders a `<div>` element. */
export default function Fill(...) { ... }
// Indicator.tsx — no matching HTML element file
/** A visual indicator for the current value. Renders a `<span>` element. */
export default function Indicator(...) { ... }
1c. Single-overload util: useVolume
/**
* Subscribe to the player's volume state.
*/
export function useVolume(options?: { muted?: boolean }): {
/** Current volume level (0–1). */
volume: number;
/** Whether audio is muted. */
muted: boolean;
/** Set the volume level. */
setVolume: (level: number) => void;
};
Exported from packages/react/src/index.ts → framework: react.
1d. Multi-overload util: createPlayer
/**
* Create a player instance with typed store, Provider component, Container, and hooks.
*/
/** @label Video */
export function createPlayer(config: CreatePlayerConfig<VideoFeatures>): CreatePlayerResult<VideoPlayerStore>;
/** @label Audio */
export function createPlayer(config: CreatePlayerConfig<AudioFeatures>): CreatePlayerResult<AudioPlayerStore>;
Exported from packages/react/src/index.ts → framework: react.
Return types differ (VideoPlayerStore vs AudioPlayerStore). Each overload uses the optional
@label JSDoc tag to give it a descriptive heading in the docs (see §2d).
1e. Context: playerContext
Source — packages/html/src/player/context.ts:
/** @public The default player context instance for consuming the player store in controllers. */
export const playerContext = createContext<PlayerContextValue, typeof PLAYER_CONTEXT_KEY>(PLAYER_CONTEXT_KEY);
Exported from packages/html/src/index.ts → framework: html. Discovered via @public JSDoc.
This is a non-function, non-controller export — a context value.
2. Builder Pipeline
2a. Component discovery
The builder scans packages/core/src/core/ui/ for directories. For each directory with
kebab-name {name}:
| File | Location | Required |
|---|---|---|
| Core | packages/core/src/core/ui/{name}/{name}-core.ts |
Yes |
| Data attrs | packages/core/src/core/ui/{name}/{name}-data-attrs.ts |
No |
| CSS vars | packages/core/src/core/ui/{name}/{name}-css-vars.ts |
No |
| HTML element | packages/html/src/ui/{name}/{name}-element.ts |
No |
| React parts index | packages/react/src/ui/{name}/index.parts.ts |
No |
Kebab-to-PascalCase conversion: toggle-button → ToggleButton. For cases where standard
conversion fails (e.g., pip-button → PiPButton), a NAME_OVERRIDES map provides the correct
PascalCase name.
Multi-part detection: A component is multi-part if and only if index.parts.ts exists.
Domain variant components: Components like TimeSlider and VolumeSlider that share base
logic (e.g., from slider/) must still have their own directories under core/ui/. The
builder discovers components by directory — files nested inside a shared directory (like
slider/time-slider-core.ts) won't be found.
2b. Component extraction
Single-part components
Extract from the three source files and merge into one reference object.
| Source | Extracts |
|---|---|
| Core file | Props interface members (if present), State interface members (if present), defaultProps values (if present) |
| Data attrs file | Data attribute names, JSDoc descriptions, and inferred types (if file exists) |
| CSS vars file | CSS custom property names and JSDoc descriptions (if file exists) |
| HTML element file | static tagName value (if file exists) |
Naming conventions the builder depends on:
| Symbol | Expected name |
|---|---|
| Props interface | {PascalCase}Props |
| State interface | {PascalCase}State |
| Core class | {PascalCase}Core |
| Data attrs export | {PascalCase}DataAttrs |
| CSS vars export | {PascalCase}CSSVars |
| HTML element class | {PascalCase}Element |
All symbols are optional. Only the Core class is required. If a component has no Props
interface, props is {}. If it has no State interface, state is {}. If the Core class
has no defaultProps, no defaults are populated. If both Props and State are missing, the
component is skipped with a warning.
Extraction conventions (apply to both Props and State members):
- Members named
refare auto-skipped (React internal). - Members with
@ignoreJSDoc tag are skipped.
Missing-symbol behavior:
| Missing symbol | Behavior | Output |
|---|---|---|
| Props interface only | Silent | props: {} |
| State interface only | Silent | state: {} |
| Both Props and State | Warn, skip component | Component omitted |
defaultProps static |
Silent | Props have no default field |
| Data-attrs file/export | Silent | dataAttributes: {} |
| CSS-vars file/export | Silent | cssCustomProperties: {} |
| JSDoc on a data attribute | Silent | description: "" (empty string) |
| HTML element file | Silent | No platforms.html section |
This applies uniformly to single-part and multi-part component extraction.
Data attribute type inference:
The builder infers data attribute types from the StateAttrMap<State> constraint on the
data-attrs export. Every data-attrs file follows the pattern:
export const FooDataAttrs = { ... } as const satisfies StateAttrMap<FooState>;
The builder uses the TypeScript type checker to:
- Extract the
Statetype argument from thesatisfies StateAttrMap<State>expression - Resolve each property key's type on the state interface
- Format the resolved type as a display string
| State property type | Inferred display type | Example |
|---|---|---|
boolean |
(omitted) | data-paused — presence/absence, no type shown |
| String literal union | The union | 'off' | 'low' | 'medium' | 'high' |
| Named type alias (resolves to union) | Expanded literals | VolumeLevel → 'off' | 'low' | 'medium' | 'high' |
string |
string |
Rare — freeform string value |
number |
number |
Rare — numeric attribute |
Boolean attributes are presence/absence by convention (the runtime uses setAttribute/
removeAttribute), so their type is omitted from the output to avoid noise. Non-boolean
types are included to show the enumerated values the attribute can take.
Fallback: If the satisfies expression is absent or the type checker cannot resolve the
state type, the builder falls back to the existing @type JSDoc tag extraction. Manual
@type tags are no longer needed when the satisfies StateAttrMap<State> pattern is used.
Multi-part components
The builder discovers parts from index.parts.ts and matches them to HTML element files.
Re-exported parts: When index.parts.ts re-exports parts from another component (source
path doesn't start with ./), the builder resolves the re-export back to its origin. It
parses the origin's index.parts.ts, matches each re-exported name to the original local
export, then derives the kebab segment and HTML element file from the origin component —
not the current one. Re-exported parts are never primary. For example, TimeSlider re-exports
Buffer, Fill, Thumb, Track, and Value from ../slider/index.parts; each resolves to the
Slider component's HTML element files (slider-buffer-element.ts, etc.).
Single-part fallback: When all exports are local and filtering leaves only one part, the
component uses single-part mode. The remaining part (typically Root) becomes the top-level
component — its props/state/data-attrs/CSS-vars are promoted to the component level, not
nested under parts. Components with re-exported parts (like TimeSlider and VolumeSlider)
always produce multi-part output since the re-exports are resolved rather than filtered.
Primary vs. sub-part convention:
Every multi-part component has one primary part and one or more sub-parts.
Primary part: The part whose React source file instantiates the component's own Core class
(matches new {ComponentName}Core\b). This captures the architectural relationship — the primary
part owns the Core — and is immune to import ordering and framework-divergent element structures.
Sub-part element files use the naming convention {component}-{part}-element.ts (e.g.,
time-group-element.ts) for HTML tag resolution.
Part-to-element matching:
For each local named export in index.parts.ts:
- Derive kebab segment from the export's source path (e.g.,
./time-value→value) - Look for
{component}-{part}-element.tsin the HTML directory - If found → sub-part (gets its own tag name)
- If not found AND
{component}-element.tsexists → check via Core-instantiation for primary
For cases where the element file doesn't follow standard naming (e.g., Tooltip's Provider maps
to tooltip-group-element.ts, not tooltip-provider-element.ts), a PART_ELEMENT_OVERRIDES
map provides the correct filename.
For re-exported parts: use the origin component's kebab and HTML directory for element file
lookup. The element class name is derived from the filename convention (kebabToPascal of the
basename, e.g., slider-buffer-element.ts → SliderBufferElement), not from the current
component's PascalCase name. This same convention-based derivation is also used for local
non-primary parts.
What the primary part gets: The shared core Props, State, data attributes, CSS custom properties, and the root element's tag name.
What sub-parts get: Their own tag name, a description (from React JSDoc), shared data
attributes from the component's *-data-attrs.ts file (when the sub-part's React source
references stateAttrMap), and custom props from the {LocalName}Props interface — own
members plus members inherited from project-local interfaces, excluding children and
React DOM attributes. State and CSS custom properties remain empty.
For re-exported sub-parts, data attributes come from the origin component's data-attrs file (e.g., TimeSlider.Fill uses Slider's data-attrs, not TimeSlider's), because the builder can't resolve spread entries and the origin file has the complete set that sub-parts inherit.
What the top-level component gets: Empty props, state, dataAttributes, cssCustomProperties,
and empty platforms. All meaningful data lives in the parts record.
Framework-divergent parts: Parts discovered from index.parts.ts always get
platforms.react. Parts with a matching HTML element file also get platforms.html. The
renderer filters parts by framework — only parts with the current framework's platform
entry are shown. This handles cases like Popover where Arrow, Popup, and Trigger are
React-only compound parts with no HTML element counterparts.
Known limitation: Our architecture has a single Props/State interface per component at the core level, so only the primary part can own core-level props and state. Sub-parts can declare custom React-specific props (e.g.,
SliderValueProps.type), which the builder extracts from the React source. In Base UI, each part has its own props independently.
2c. Util discovery
The builder scans a fixed set of entry points:
| Entry point | Framework |
|---|---|
packages/react/src/index.ts |
react |
packages/store/src/react/hooks/index.ts |
react |
packages/html/src/index.ts |
html |
packages/store/src/html/controllers/index.ts |
html |
packages/core/src/dom/store/selectors.ts |
(none — framework-agnostic) |
packages/store/src/core/selector.ts |
(none — framework-agnostic) |
Re-export scoping: For each entry point, the builder resolves local re-exports (relative
import paths starting with ./) and scans the resolved modules. External package re-exports
(e.g., export * from '@videojs/core/dom') are not followed — cross-package exports are
discovered via their own dedicated entry points instead. This prevents duplication and
incorrect framework tagging.
Inclusion rules — An export is included if it matches any naming convention OR has
@public JSDoc:
| Pattern | Rule | Category |
|---|---|---|
use* (capital 4th char) + function |
Hook | react |
select* (capital 7th char) + function |
Selector | varies |
*Controller (non-function) |
Controller | html |
create* + function |
Factory | varies |
@public JSDoc tag |
Explicit inclusion | varies |
Note: The raw TS AST fallback (used when TAE fails on a module) relaxes the function
check for select* — it includes by name alone, since type information isn't reliably
available in that path.
Display name: The export name is used as-is, except create*Mixin factories strip the
create prefix (e.g., createProviderMixin → ProviderMixin).
Leaf module scanning: When an entry point has no relative re-exports (i.e., resolveLocalModules
returns an empty list), the file itself is scanned directly.
Slug generation: kebab-case of the display name. If two frameworks produce the same slug,
React keeps the bare slug and HTML gets prefixed with html- (e.g., create-player vs
html-create-player). React entries must come before HTML entries in UTIL_ENTRY_POINTS
to ensure this ordering.
2d. Util extraction
Functions (hooks, factories): Extract call signatures via TypeScript API. Each signature becomes an overload with parameters and return value.
Controllers: Extract constructor signatures. Each constructor becomes an overload. The
return value type uses ClassName<TypeParams> when the class has type parameters, or just
ClassName when it has none. The return value describes the controller's public interface
(e.g., value property).
Contexts (non-function, non-controller): These are non-function, non-controller exports
included via @public JSDoc. They produce a single overload with empty parameters and a
returnValue containing the type string.
All overloads are preserved. When a function or constructor has multiple overload
signatures, each becomes a separate entry in the overloads array. This applies uniformly
to function call signatures and class constructor signatures.
Overload labels (@label): If an overload signature has a @label JSDoc tag, the
builder extracts its value as the overload's label. This is optional — overloads without
@label get no label and fall back to "Overload {N}" headings in the rendered output.
Degraded type repair: After TAE extraction, the builder cross-references param and return
types against the raw TS AST. When a formatted type contains any (word boundary) or __type
— indicators that TAE couldn't resolve the type — the builder replaces it with the source
annotation text from the raw AST declaration. This handles types TAE can't resolve (e.g.,
cross-package interfaces like Media, ESSymbol-based types like PlayerContext).
2e. Type formatting
Every type string goes through two stages:
Stage 1: Format — Convert the TypeScript type node to a human-readable string.
| TS construct | Formatted as | Example |
|---|---|---|
| Primitives | Lowercase name | boolean, string, number |
| String literals | Single-quoted | 'current' |
| Unions | Pipe-separated | 'current' | 'duration' | 'remaining' |
| Objects | Inline notation | { volume: number; muted: boolean } |
| Arrays | Bracket suffix | string[] |
| Functions | Arrow notation | ((level: number) => void) |
| Tuples | Bracket notation | [string, number] |
| Empty object type | object |
object (not {}) |
| Type parameter (small constraint) | Constraint expansion | string for T extends string |
| Type parameter (large constraint, >5 union members) | Parameter name | TagName for TagName extends keyof JSX.IntrinsicElements |
Stage 2: Abbreviate — Shorten complex types for display. The abbreviated form goes in type;
the full form goes in detailedType (shown in the disclosure panel).
Abbreviation checks rules in the following order. The first match wins:
| # | Rule | Condition | Abbreviated to | detailedType |
|---|---|---|---|---|
| 1 | Pure function | Type contains =>, and either no | or is a single function type (paren-depth matching detects (params) => return | union vs (fn) | undefined) |
function |
Full signature |
| 2 | on* / get* callback |
Name matches, type contains => |
function |
Full signature |
| 3 | className |
Name matches, type contains => |
string | function |
Full union |
| 4 | style |
Name matches, type contains => |
CSSProperties | function |
Full union |
| 5 | render |
Name matches, type contains => |
ReactElement | function |
Full union |
| 6 | Simple primitive | boolean, string, number |
(no abbreviation) | (omitted) |
| 7 | Object literal (> 40 chars) | Starts with { , length > 40 |
object |
Full inline notation |
| 8 | Short union (< 3 members AND < 40 chars, no fn) | — | (no abbreviation) | (omitted) |
| 9 | Union with a function member | Type contains => and | |
Non-fn members | function |
Full union |
| 10 | Long type (> 40 chars) | — | (as-is, truncated) | Full type |
| 11 | Fallback | Everything else | (no abbreviation) | (omitted) |
detailedType is only present when abbreviation occurred. If the type is short enough to
display as-is, detailedType is omitted.
Union member ordering: null, undefined, and any sort to the end.
Default values: Stored as string representations of the literal value. Examples: 'false',
"''" (empty string), '0', 'null', '[]'.
3. JSON Schemas
These are the intermediate format between builder and front-end. Defined as Zod schemas in
site/src/types/.
3a. Component reference
Output: site/src/content/generated-component-reference/{kebab-name}.json
ComponentReference
├── name: string — PascalCase (e.g., "ToggleButton")
├── description?: string — JSDoc description of the component
├── props: Record<string, PropDef> — Empty {} for multi-part top-level
├── state: Record<string, StateDef> — Empty {} for multi-part top-level
├── dataAttributes: Record<string, DataAttrDef>
├── cssCustomProperties: Record<string, CSSVarDef>
├── platforms
│ ├── html?
│ │ └── tagName: string — e.g., "media-toggle-button"
│ └── react? — Present for React-discovered parts (object, no fields)
└── parts?: Record<string, PartReference> — Only for multi-part components
└── [partId]
├── name: string — PascalCase part name (e.g., "Track")
├── description?: string — From React component JSDoc
├── props: Record<string, PropDef>
├── state: Record<string, StateDef>
├── dataAttributes: Record<string, DataAttrDef>
├── cssCustomProperties: Record<string, CSSVarDef>
└── platforms
├── html?
│ └── tagName: string
└── react? — Always present (parts come from index.parts.ts)
CSSVarDef:
└── description: string — JSDoc description of the CSS custom property
PropDef:
├── type: string — Abbreviated type for display
├── detailedType?: string — Full type (only if abbreviated)
├── description?: string — JSDoc description
├── default?: string — String representation of default value
└── required?: boolean — Only present when true
StateDef:
├── type: string
├── detailedType?: string
└── description?: string
DataAttrDef:
├── description: string
├── type?: string — Inferred from state type. Omitted for boolean (presence/absence).
│ Shows enumerated values for non-boolean types (e.g., "'empty' | 'partial' | 'full'").
│ Abbreviated via the same rules as PropDef/StateDef (§2e Stage 2).
└── detailedType?: string — Full type when abbreviation occurred. Same semantics as PropDef.detailedType.
3b. Util reference
Output: site/src/content/generated-util-reference/{slug}.json
UtilReference
├── name: string — Display name (e.g., "useVolume")
├── description?: string — JSDoc description
├── frameworks?: string[] — e.g., ["react"] or ["html"]; omitted if agnostic
└── overloads: UtilOverload[] — At least one
└── [n]
├── label?: string — From @label JSDoc tag (e.g., "Video")
├── description?: string — Overload-specific description
├── parameters: Record<string, ParamDef>
└── returnValue: ReturnValue
ParamDef — Same shape as PropDef:
├── type: string
├── detailedType?: string
├── description?: string
├── default?: string — Default parameter value (e.g., "0", "'muted'")
└── required?: boolean
ReturnValue:
├── type: string — e.g., "object", "void", "boolean"
├── detailedType?: string
├── description?: string — Used when return is a simple type (no fields)
└── fields?: Record<string, ReturnFieldDef> — Used when return is an object
└── [fieldName]
├── type: string
├── detailedType?: string
└── description?: string
Cleanup rules: Optional fields are omitted from JSON when undefined. required: false is
omitted (absence means not required). This keeps JSON files small.
4. Expected JSON Output for Sample Inputs
4a. ToggleButton (single-part component)
{
"name": "ToggleButton",
"props": {
"disabled": {
"type": "boolean",
"description": "Whether the button is disabled.",
"default": "false"
},
"label": {
"type": "string | function",
"detailedType": "string | ((state: ToggleButtonState) => string)",
"description": "Custom label for the button.",
"default": "''"
}
},
"state": {
"pressed": {
"type": "boolean",
"description": "Whether the toggle is pressed."
},
"disabled": {
"type": "boolean",
"description": "Whether the button is disabled."
}
},
"dataAttributes": {
"data-pressed": {
"description": "Present when the toggle is pressed."
},
"data-disabled": {
"description": "Present when the button is disabled."
}
},
"cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-toggle-button"
}
}
}
4b. Meter (multi-part component)
{
"name": "Meter",
"props": {},
"state": {},
"dataAttributes": {},
"cssCustomProperties": {},
"platforms": {},
"parts": {
"indicator": {
"name": "Indicator",
"description": "A visual indicator for the current value. Renders a `<span>` element.",
"props": {
"min": {
"type": "number",
"description": "Minimum value.",
"default": "0"
},
"max": {
"type": "number",
"description": "Maximum value.",
"default": "100"
},
"label": {
"type": "string | function",
"detailedType": "string | ((state: MeterState) => string)",
"description": "Custom label for accessibility.",
"default": "''"
}
},
"state": {
"percentage": {
"type": "number",
"description": "Current value as a percentage (0–1)."
}
},
"dataAttributes": {
"data-percentage": {
"description": "Current percentage as a string.",
"type": "number"
},
"data-fill-state": {
"description": "The fill level.",
"type": "'empty' | 'partial' | 'full'"
}
},
"cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-meter"
},
"react": {}
}
},
"track": {
"name": "Track",
"description": "The track area of the meter. Renders a `<div>` element.",
"props": {},
"state": {},
"dataAttributes": {},
"cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-meter-track"
},
"react": {}
}
},
"fill": {
"name": "Fill",
"description": "The filled portion of the meter. Renders a `<div>` element.",
"props": {},
"state": {},
"dataAttributes": {},
"cssCustomProperties": {},
"platforms": {
"html": {
"tagName": "media-meter-fill"
},
"react": {}
}
}
}
}
4c. useVolume (single-overload hook)
{
"name": "useVolume",
"description": "Subscribe to the player's volume state.",
"overloads": [
{
"parameters": {
"options": {
"type": "object",
"detailedType": "{ muted?: boolean }",
"default": "{}"
}
},
"returnValue": {
"type": "object",
"detailedType": "{ volume: number; muted: boolean; setVolume: (level: number) => void }",
"fields": {
"volume": {
"type": "number",
"description": "Current volume level (0–1)."
},
"muted": {
"type": "boolean",
"description": "Whether audio is muted."
},
"setVolume": {
"type": "function",
"detailedType": "((level: number) => void)",
"description": "Set the volume level."
}
}
}
}
],
"frameworks": ["react"]
}
4d. createPlayer (multi-overload)
Both overloads are preserved. Return types differ (VideoPlayerStore vs AudioPlayerStore).
{
"name": "createPlayer",
"description": "Create a player instance with typed store, Provider component, Container, and hooks.",
"overloads": [
{
"label": "Video",
"description": "Create a player instance with typed store, Provider component, Container, and hooks.",
"parameters": {
"config": {
"type": "CreatePlayerConfig<VideoFeatures>",
"required": true
}
},
"returnValue": {
"type": "CreatePlayerResult<VideoPlayerStore>",
"fields": {
"Provider": {
"type": "React.FC<ProviderProps>"
},
"Container": {
"type": "function",
"detailedType": "React.ForwardRefExoticComponent<ContainerProps & RefAttributes<HTMLDivElement>>"
},
"usePlayer": {
"type": "UsePlayerHook<VideoPlayerStore>"
}
}
}
},
{
"label": "Audio",
"parameters": {
"config": {
"type": "CreatePlayerConfig<AudioFeatures>",
"required": true
}
},
"returnValue": {
"type": "CreatePlayerResult<AudioPlayerStore>",
"fields": {
"Provider": {
"type": "React.FC<ProviderProps>"
},
"Container": {
"type": "function",
"detailedType": "React.ForwardRefExoticComponent<ContainerProps & RefAttributes<HTMLDivElement>>"
},
"usePlayer": {
"type": "UsePlayerHook<AudioPlayerStore>"
}
}
}
}
],
"frameworks": ["react"]
}
4e. playerContext (context)
{
"name": "playerContext",
"description": "The default player context instance for consuming the player store in controllers.",
"overloads": [
{
"parameters": {},
"returnValue": {
"type": "Context<typeof PLAYER_CONTEXT_KEY, PlayerContextValue>"
}
}
],
"frameworks": ["html"]
}
5. Reference Model Layer
The reference model transforms flat JSON into a structured heading/section model. This model is consumed by two places:
- Astro components — to render headings and tables
- remarkConditionalHeadings — to inject heading entries into the table of contents
Both consume the same model, which prevents anchor drift (TOC links matching rendered heading IDs).
5a. Component reference model
Single-part heading structure:
H2 "API Reference" id="api-reference"
├─ H3 "Props" id="props" (if props non-empty)
├─ H3 "State" id="state" (if state non-empty)
├─ H3 "Data attributes" id="data-attributes" (if dataAttributes non-empty)
└─ H3 "CSS custom properties" id="css-custom-properties" (if cssCustomProperties non-empty)
Multi-part heading structure:
H2 "API Reference" id="api-reference"
├─ H3 "{Part.name}" (React) id="{partId}"
│ or "{part.tagName}" (HTML)
│ ├─ H4 "Props" id="{partId}-props" (if props non-empty)
│ ├─ H4 "State" id="{partId}-state" (if state non-empty)
│ ├─ H4 "Data attributes" id="{partId}-data-attributes" (if dataAttributes non-empty)
│ └─ H4 "CSS custom properties" id="{partId}-css-custom-properties" (if cssCustomProperties non-empty)
├─ H3 next part...
└─ ...
Multi-part H3 headings are framework-aware: React sees the PascalCase part name (e.g., "Track"),
HTML sees the tag name (e.g., "media-meter-track"). The TOC emits both variants with
frameworks metadata so the correct one displays per framework.
Framework filtering: The TOC and rendered output only emit headings for parts the current
framework supports (derived from platforms keys). React-only parts (those with
platforms.react but no platforms.html) are hidden when viewing HTML docs.
Part ordering: By default, parts render in JSON key order (primary first, then discovery
order). The <ComponentReference> component accepts an optional partOrder prop — an array
of part IDs (e.g., ["provider", "root", "trigger", "popup", "arrow"]) that overrides
the default order. This lets MDX authors match the anatomy. The reordering is applied inside
createComponentReferenceModel, so both the rendered output and the TOC consume the same
order. Parts not listed in partOrder appear after the listed ones in their original order.
5b. Util reference model
Single-overload heading structure:
H2 "API Reference" id="api-reference"
├─ H3 "Parameters" id="parameters" (if params non-empty)
└─ H3 "Return Value" id="return-value"
Multi-overload heading structure:
H2 "API Reference" id="api-reference"
├─ H3 "{label}" or "Overload 1" id="{slug}" or "overload-1"
│ ├─ H4 "Parameters" id="{slug}-parameters" (if params non-empty)
│ └─ H4 "Return Value" id="{slug}-return-value"
├─ H3 "{label}" or "Overload 2" id="{slug}" or "overload-2"
│ ├─ H4 "Parameters" id="{slug}-parameters"
│ └─ H4 "Return Value" id="{slug}-return-value"
└─ ...
5c. TOC integration
The remarkConditionalHeadings remark plugin detects <ComponentReference> and
<UtilReference> components in MDX, loads the generated JSON, builds the reference model, and
injects synthetic heading entries into frontmatter.conditionalHeadings. These entries carry the
same id/slug values as the rendered headings, so TOC links always match. For
<ComponentReference>, the plugin also reads the optional partOrder attribute and forwards
it to createComponentReferenceModel, ensuring the TOC reflects the same part ordering as
the rendered page.
6. Rendered Output
6a. Props table (components)
Rendered by ApiPropsTable → PropRow → DetailRow.
Columns:
| Prop | Type | Default |
|---|
- Prop — Property name in monospace. Required props have an orange
*suffix. - Type — Abbreviated type in monospace.
- Default — Default value in monospace, or
—if none. - (toggle) — Disclosure triangle. Only present if the row has a description or detailedType.
Disclosure panel (when expanded):
Contains a description list (<dl>):
- Description — Markdown-rendered description. Only shown if
descriptionis present. - Type — Full
detailedTypein monospace. Only shown ifdetailedTypeis present (i.e., the type was abbreviated).
Sort order: Required props first, then alphabetical. (Defined at the builder level via
sortProps.)
ToggleButton example:
| Prop | Type | Default | |
|---|---|---|---|
disabled |
boolean |
false |
|
label |
string | function |
'' |
▸ |
Expanding label:
Description: Custom label for the button. Type:
string | ((state: ToggleButtonState) => string)
6b. State table (components)
Rendered by ApiStateTable → StateRow → DetailRow.
Columns:
| Property | Type |
|---|
- Property — State property name in monospace.
- Type — Type in monospace (abbreviated if needed).
- (toggle) — Disclosure triangle, same rules as props.
Disclosure panel: Same as props (description + detailedType).
State preamble (framework-specific, shown above the table):
- React: "State is accessible via the
render,className, andstyleprops." - HTML: "State is reflected as data attributes for CSS styling."
ToggleButton example:
| Property | Type | |
|---|---|---|
pressed |
boolean |
▸ |
disabled |
boolean |
▸ |
Expanding pressed:
Description: Whether the toggle is pressed.
6c. Data attributes table (components)
Rendered by ApiDataAttrsTable → DataAttrRow → DetailRow. Uses the same disclosure
pattern as props and state tables.
Columns:
| Attribute | Type |
|---|
- Attribute — Data attribute name in monospace (e.g.,
data-pressed). - Type — Inferred from
StateAttrMap<State>. Shows enumerated values in monospace for non-boolean types. Empty cell for boolean (present/absent) attributes. Abbreviated via §2e Stage 2 when the type is long. - (toggle) — Disclosure triangle. Only present if the row has a description or detailedType.
Disclosure panel (when expanded):
Contains a description list (<dl>):
- Description — Markdown-rendered description. Only shown if
descriptionis present. - Type — Full
detailedTypein monospace. Only shown ifdetailedTypeis present (i.e., the type was abbreviated).
ToggleButton example (boolean attributes — type column empty):
| Attribute | Type | |
|---|---|---|
data-pressed |
▸ | |
data-disabled |
▸ |
Expanding data-pressed:
Description: Present when the toggle is pressed.
Meter example (mix of boolean and non-boolean — types inferred from state):
| Attribute | Type | |
|---|---|---|
data-percentage |
number |
▸ |
data-fill-state |
'empty' | 'partial' | 'full' |
▸ |
Expanding data-percentage:
Description: Current percentage as a string.
6d. Parameters table (utils)
Rendered by UtilParamsTable → PropRow → DetailRow. Reuses the same row component as
component props.
Columns:
| Parameter | Type | Default |
|---|
- Parameter — Parameter name in monospace. Required params have an orange
*suffix. - Type — Abbreviated type in monospace.
- Default — Default value in monospace, or
—if none. - (toggle) — Disclosure triangle.
Disclosure panel: Same as props (description + detailedType).
useVolume example:
| Parameter | Type | Default | |
|---|---|---|---|
options |
object |
{} |
▸ |
Expanding options:
Type:
{ muted?: boolean }
6e. Return value (utils)
Rendered by UtilReturnTable. Two rendering modes:
Mode 1: Object return with fields — Renders a table using StateRow:
| Property | Type |
|---|
Same columns and disclosure behavior as the state table.
useVolume example:
| Property | Type | |
|---|---|---|
volume |
number |
▸ |
muted |
boolean |
▸ |
setVolume |
function |
▸ |
Expanding setVolume:
Description: Set the volume level. Type:
((level: number) => void)
Mode 2: Simple return with detail — When no fields but detailedType or description is
present, renders a single-row disclosure table using DetailRow:
| Type | |
|---|---|
function |
▸ |
Expanding the row reveals the detailed type and/or description, same as prop/state disclosure.
Mode 3: Simple return (no detail) — When no fields, no detailedType, and no description,
renders inline:
ReturnType— Description text here.
6f. CSS custom properties table (components)
Rendered by ApiCSSVarsTable → CSSVarRow → DetailRow. Uses the same disclosure
pattern as data attributes tables but with no type column.
Columns:
| Variable |
|---|
- Variable — CSS custom property name in monospace (e.g.,
--media-slider-fill). - (toggle) — Disclosure triangle. Present if the row has a description.
Disclosure panel (when expanded):
Contains a description list (<dl>):
- Description — Markdown-rendered description. Only shown if
descriptionis present.
6g. Multi-part component rendering
For multi-part components, the top-level has no tables (all empty). Each part renders as:
H3: Part name (framework-specific label)
Part description (if present)
H4: Props (if non-empty) → Props table
H4: State (if non-empty) → State table
H4: Data attributes (if non-empty) → Data attributes table
H4: CSS custom properties (if non-empty) → CSS custom properties table
Part ordering: Parts render in JSON key order by default (primary part first). To match
the component anatomy, pass partOrder on the <ComponentReference> component:
<ComponentReference component="Tooltip" partOrder={["provider", "root", "trigger", "popup", "arrow"]} />
State section preamble (framework-specific):
- React: "State is accessible via the
render,className, andstyleprops." - HTML: "State is reflected as data attributes for CSS styling."
6h. Multi-overload util rendering
Each overload renders as:
H3: "{label}" or "Overload {N}"
Overload description (if present)
H4: Parameters (if non-empty) → Parameters table
H4: Return Value → Return value table or inline
Heading text: If the overload has a label (from @label JSDoc), use it as the H3
heading text. Otherwise fall back to "Overload {N}".
Heading ID: Labeled overloads use the kebab-case slug of the label (e.g., "Video" →
id="video"). Unlabeled overloads use id="overload-{n}".
6i. Disclosure panel interaction
The DetailRow component implements an expandable disclosure pattern:
- Toggle button renders a disclosure triangle (
▸) that rotates 90° when expanded. - Clicking anywhere on the summary row toggles the detail panel (unless the click target is a link or button).
- Uses
aria-expandedandaria-controlsfor accessibility. - The detail panel is initially
hiddenand toggled via JavaScript.
7. Full Rendered Example: ToggleButton
Given the ToggleButton source code from Section 1a, the user sees:
## API Reference
### Props
| Prop | Type | Default | |
|------------|---------------------|---------|-|
| disabled | boolean | false | |
| label | string | function | '' | ▸ |
└─ [expanded] Description: Custom label for the button.
Type: string | ((state: ToggleButtonState) => string)
### State
State is accessible via the render, className, and style props. ← React
State is reflected as data attributes for CSS styling. ← HTML
| Property | Type | |
|------------|---------|--|
| pressed | boolean | ▸ |
| disabled | boolean | ▸ |
└─ [expanded] Description: Whether the toggle is pressed.
└─ [expanded] Description: Whether the button is disabled.
### Data attributes
| Attribute | Type | |
|-----------------|------|-|
| data-pressed | | ▸ |
| data-disabled | | ▸ |
└─ [expanded] Description: Present when the toggle is pressed.
└─ [expanded] Description: Present when the button is disabled.
8. Full Rendered Example: Meter (multi-part)
Given the Meter source code from Section 1b, the user sees:
## API Reference
### Indicator ← React framework
### media-meter ← HTML framework
A visual indicator for the current value. Renders a `<span>` element.
#### Props
| Prop | Type | Default | |
|-------|---------------------|---------|-|
| label | string | function | '' | ▸ |
| max | number | 100 | |
| min | number | 0 | |
#### State
State is accessible via the render, className, and style props. ← React
State is reflected as data attributes for CSS styling. ← HTML
| Property | Type | |
|------------|--------|-|
| percentage | number | ▸ |
#### Data attributes
| Attribute | Type | |
|------------------|-------------------------------|-|
| data-percentage | number | ▸ |
| data-fill-state | 'empty' | 'partial' | 'full' | ▸ |
└─ [expanded] Description: Current percentage as a string.
└─ [expanded] Description: The fill level.
### Track ← React framework
### media-meter-track ← HTML framework
The track area of the meter. Renders a `<div>` element.
(no Props, State, or Data attributes sections — all empty)
### Fill ← React framework
### media-meter-fill ← HTML framework
The filled portion of the meter. Renders a `<div>` element.
(no Props, State, or Data attributes sections — all empty)
9. Full Rendered Example: useVolume (single-overload)
## API Reference
### Parameters
| Parameter | Type | Default | |
|-----------|--------|---------|-|
| options | object | {} | ▸ |
└─ [expanded] Type: { muted?: boolean }
### Return Value
| Property | Type | |
|-----------|----------|-|
| volume | number | ▸ |
| muted | boolean | ▸ |
| setVolume | function | ▸ |
└─ [expanded] Description: Set the volume level.
Type: ((level: number) => void)
10. Full Rendered Example: createPlayer (multi-overload, labeled)
## API Reference
### Video ← from @label "Video"
Create a player instance with typed store, Provider component, Container, and hooks.
#### Parameters
| Parameter | Type | Default | |
|-----------|-----------------------------------|---------|-|
| config* | CreatePlayerConfig<VideoFeatures> | — | |
#### Return Value
| Property | Type | |
|-----------|---------------------------------|-|
| Provider | React.FC<ProviderProps> | |
| Container | function | ▸ |
| usePlayer | UsePlayerHook<VideoPlayerStore> | |
└─ [expanded] Type: React.ForwardRefExoticComponent<ContainerProps & RefAttributes<HTMLDivElement>>
### Audio ← from @label "Audio"
#### Parameters
| Parameter | Type | Default | |
|-----------|-----------------------------------|---------|-|
| config* | CreatePlayerConfig<AudioFeatures> | — | |
#### Return Value
| Property | Type | |
|-----------|---------------------------------|-|
| Provider | React.FC<ProviderProps> | |
| Container | function | ▸ |
| usePlayer | UsePlayerHook<AudioPlayerStore> | |
└─ [expanded] Type: React.ForwardRefExoticComponent<ContainerProps & RefAttributes<HTMLDivElement>>