From 7a5ce94bca3e67d60d3213ab5f28e2146a36564e Mon Sep 17 00:00:00 2001 From: Darius Cepulis Date: Mon, 16 Mar 2026 14:05:33 -0500 Subject: [PATCH] docs(site): add Slider and Tooltip API reference pages (#862) Co-authored-by: Claude Opus 4.6 --- .../api-reference/references/mdx-structure.md | 14 +- internal/design/site/api-docs-builder.md | 35 ++- site/scripts/api-docs-builder/src/index.ts | 22 +- .../api-docs-builder/src/parts-handler.ts | 62 +++-- .../api-reference/ComponentReference.astro | 5 +- .../demos/slider/html/css/BasicUsage.astro | 10 + .../docs/demos/slider/html/css/BasicUsage.css | 59 +++++ .../demos/slider/html/css/BasicUsage.html | 8 + .../docs/demos/slider/html/css/BasicUsage.ts | 1 + .../demos/slider/html/css/WithPreview.astro | 10 + .../demos/slider/html/css/WithPreview.css | 81 +++++++ .../demos/slider/html/css/WithPreview.html | 11 + .../docs/demos/slider/html/css/WithPreview.ts | 1 + .../demos/slider/react/css/BasicUsage.css | 59 +++++ .../demos/slider/react/css/BasicUsage.tsx | 19 ++ .../demos/slider/react/css/WithPreview.css | 81 +++++++ .../demos/slider/react/css/WithPreview.tsx | 22 ++ .../demos/tooltip/html/css/BasicUsage.astro | 10 + .../demos/tooltip/html/css/BasicUsage.css | 28 +++ .../demos/tooltip/html/css/BasicUsage.html | 6 + .../docs/demos/tooltip/html/css/BasicUsage.ts | 1 + .../demos/tooltip/html/css/Grouping.astro | 10 + .../docs/demos/tooltip/html/css/Grouping.css | 27 +++ .../docs/demos/tooltip/html/css/Grouping.html | 10 + .../docs/demos/tooltip/html/css/Grouping.ts | 2 + .../demos/tooltip/react/css/BasicUsage.css | 34 +++ .../demos/tooltip/react/css/BasicUsage.tsx | 17 ++ .../docs/demos/tooltip/react/css/Grouping.css | 33 +++ .../docs/demos/tooltip/react/css/Grouping.tsx | 33 +++ site/src/content/docs/reference/controls.mdx | 2 +- .../docs/reference/fullscreen-button.mdx | 2 +- .../content/docs/reference/mute-button.mdx | 2 +- .../src/content/docs/reference/pip-button.mdx | 2 +- .../content/docs/reference/play-button.mdx | 2 +- site/src/content/docs/reference/popover.mdx | 2 +- site/src/content/docs/reference/poster.mdx | 2 +- site/src/content/docs/reference/slider.mdx | 198 ++++++++++++++++ site/src/content/docs/reference/thumbnail.mdx | 2 +- .../content/docs/reference/time-slider.mdx | 6 +- site/src/content/docs/reference/tooltip.mdx | 219 ++++++++++++++++++ .../content/docs/reference/volume-slider.mdx | 2 +- site/src/docs.config.ts | 2 + site/src/utils/componentReferenceModel.js | 15 +- site/src/utils/remarkConditionalHeadings.js | 5 +- 44 files changed, 1126 insertions(+), 48 deletions(-) create mode 100644 site/src/components/docs/demos/slider/html/css/BasicUsage.astro create mode 100644 site/src/components/docs/demos/slider/html/css/BasicUsage.css create mode 100644 site/src/components/docs/demos/slider/html/css/BasicUsage.html create mode 100644 site/src/components/docs/demos/slider/html/css/BasicUsage.ts create mode 100644 site/src/components/docs/demos/slider/html/css/WithPreview.astro create mode 100644 site/src/components/docs/demos/slider/html/css/WithPreview.css create mode 100644 site/src/components/docs/demos/slider/html/css/WithPreview.html create mode 100644 site/src/components/docs/demos/slider/html/css/WithPreview.ts create mode 100644 site/src/components/docs/demos/slider/react/css/BasicUsage.css create mode 100644 site/src/components/docs/demos/slider/react/css/BasicUsage.tsx create mode 100644 site/src/components/docs/demos/slider/react/css/WithPreview.css create mode 100644 site/src/components/docs/demos/slider/react/css/WithPreview.tsx create mode 100644 site/src/components/docs/demos/tooltip/html/css/BasicUsage.astro create mode 100644 site/src/components/docs/demos/tooltip/html/css/BasicUsage.css create mode 100644 site/src/components/docs/demos/tooltip/html/css/BasicUsage.html create mode 100644 site/src/components/docs/demos/tooltip/html/css/BasicUsage.ts create mode 100644 site/src/components/docs/demos/tooltip/html/css/Grouping.astro create mode 100644 site/src/components/docs/demos/tooltip/html/css/Grouping.css create mode 100644 site/src/components/docs/demos/tooltip/html/css/Grouping.html create mode 100644 site/src/components/docs/demos/tooltip/html/css/Grouping.ts create mode 100644 site/src/components/docs/demos/tooltip/react/css/BasicUsage.css create mode 100644 site/src/components/docs/demos/tooltip/react/css/BasicUsage.tsx create mode 100644 site/src/components/docs/demos/tooltip/react/css/Grouping.css create mode 100644 site/src/components/docs/demos/tooltip/react/css/Grouping.tsx create mode 100644 site/src/content/docs/reference/slider.mdx create mode 100644 site/src/content/docs/reference/tooltip.mdx diff --git a/.claude/skills/api-reference/references/mdx-structure.md b/.claude/skills/api-reference/references/mdx-structure.md index da74f2d6..348fddbd 100644 --- a/.claude/skills/api-reference/references/mdx-structure.md +++ b/.claude/skills/api-reference/references/mdx-structure.md @@ -121,7 +121,9 @@ Toggles mute on and off. Exposes a derived `volumeLevel` based on the current vo ### Styling -Show data attributes as a table, then CSS selector patterns: +**IMPORTANT:** All CSS code blocks in Styling sections MUST be wrapped in `` 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 @@ -133,9 +135,19 @@ Show data attributes as a table, then CSS selector patterns: Use `data-volume-level` for multi-level icon switching: + ```css media-mute-button[data-volume-level="off"] .icon-off { display: inline; } ``` + + + +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; } +``` + ``` ### Accessibility diff --git a/internal/design/site/api-docs-builder.md b/internal/design/site/api-docs-builder.md index 01593e41..c27bd907 100644 --- a/internal/design/site/api-docs-builder.md +++ b/internal/design/site/api-docs-builder.md @@ -325,9 +325,9 @@ always produce multi-part output since the re-exports are resolved rather than f 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 Core class -(matches `new \w+Core\(`). This captures the architectural relationship — the primary part -owns the Core — and is immune to import ordering and framework-divergent element structures. +**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. @@ -340,6 +340,10 @@ For each local named export in `index.parts.ts`: 3. If found → sub-part (gets its own tag name) 4. If not found AND `{component}-element.ts` exists → 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 @@ -351,9 +355,9 @@ 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 React-specific props (own members on the -`{LocalName}Props` interface, excluding inherited `UIComponentProps` members and `children`). -State and CSS custom properties remain empty. +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 @@ -910,6 +914,13 @@ HTML sees the tag name (e.g., "media-meter-track"). The TOC emits both variants 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 `` 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: @@ -938,7 +949,10 @@ H2 "API Reference" id="api-reference" The `remarkConditionalHeadings` remark plugin detects `` and `` 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. +same `id`/`slug` values as the rendered headings, so TOC links always match. For +``, 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. --- @@ -1146,6 +1160,13 @@ H3: Part name (framework-specific label) 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 `` component: + +```mdx + +``` + **State section preamble** (framework-specific): - **React:** "State is accessible via the `render`, `className`, and `style` props." diff --git a/site/scripts/api-docs-builder/src/index.ts b/site/scripts/api-docs-builder/src/index.ts index 38379321..b45696d3 100644 --- a/site/scripts/api-docs-builder/src/index.ts +++ b/site/scripts/api-docs-builder/src/index.ts @@ -30,6 +30,12 @@ const NAME_OVERRIDES: Record = { 'pip-button': 'PiPButton', }; +// Parts whose HTML element file doesn't follow the `{component}-{part}-element.ts` convention. +// Key: `{component}/{part-kebab}`, Value: element file basename (without `.ts`). +const PART_ELEMENT_OVERRIDES: Record = { + 'tooltip/provider': 'tooltip-group-element', +}; + function buildProps(coreData: CoreExtraction): Record { const props: Record = {}; for (const prop of coreData.props) { @@ -298,12 +304,14 @@ function buildSingleComponentReference(source: ComponentSource, program: ts.Prog } /** - * Check if a React source file instantiates a Core class (matches `new \w+Core\(`). + * Check if a React source file instantiates the component's own Core class + * (matches `new {ComponentName}Core(`). This prevents auxiliary classes like + * `TooltipGroupCore` from being mistaken for the primary Core. */ -function instantiatesCore(filePath: string): boolean { +function instantiatesCore(filePath: string, componentName: string): boolean { try { const content = fs.readFileSync(filePath, 'utf-8'); - return /new \w+Core\(/.test(content); + return new RegExp(`new ${componentName}Core\\b`).test(content); } catch { return false; } @@ -349,8 +357,10 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource for (const partExport of localExports) { const kebab = partKebabFromSource(partExport.source, componentKebab); - // Look for sub-part element file: {component}-{part}-element.ts - const subPartElementFile = path.join(htmlDir, `${componentKebab}-${kebab}-element.ts`); + // Look for sub-part element file: {component}-{part}-element.ts (or override) + const overrideKey = `${componentKebab}/${kebab}`; + const elementBasename = PART_ELEMENT_OVERRIDES[overrideKey] ?? `${componentKebab}-${kebab}-element`; + const subPartElementFile = path.join(htmlDir, `${elementBasename}.ts`); const hasSubPartElement = fs.existsSync(subPartElementFile); // Resolve React source path for JSDoc description extraction @@ -358,7 +368,7 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource const reactPath = fs.existsSync(reactFile) ? reactFile : undefined; // Primary detection: the part whose React source instantiates the Core class - const isPrimary = !!reactPath && instantiatesCore(reactPath); + const isPrimary = !!reactPath && instantiatesCore(reactPath, source.name); const subPartUsesDataAttrs = !isPrimary && !!reactPath && usesDataAttrs(reactPath); diff --git a/site/scripts/api-docs-builder/src/parts-handler.ts b/site/scripts/api-docs-builder/src/parts-handler.ts index a89ef6f5..d5ad3cee 100644 --- a/site/scripts/api-docs-builder/src/parts-handler.ts +++ b/site/scripts/api-docs-builder/src/parts-handler.ts @@ -77,8 +77,10 @@ export function extractPartDescription(filePath: string, program: ts.Program, pa /** * Extract custom React-specific props from a sub-part's Props interface. * - * Walks syntactic own members of `{localName}Props` (excluding inherited - * `UIComponentProps` members and `children`). + * Walks syntactic own members of `{localName}Props`, then also includes + * members from any extended interface declared within the project (i.e., not + * from `node_modules`). This picks up props from project types like + * `TooltipGroupProps` while excluding inherited React DOM attributes. */ export function extractSubPartProps(filePath: string, program: ts.Program, localName: string): Record { const sourceFile = program.getSourceFile(filePath); @@ -86,27 +88,53 @@ export function extractSubPartProps(filePath: string, program: ts.Program, local const checker = program.getTypeChecker(); const props: Record = {}; + const SKIP_PROPS = new Set(['children']); + + function collectFromMembers(members: ts.NodeArray) { + for (const member of members) { + if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue; + const name = member.name.text; + if (SKIP_PROPS.has(name) || !member.type) continue; + + let typeStr = checker.typeToString(checker.getTypeFromTypeNode(member.type)); + if (member.questionToken) typeStr = typeStr.replace(/ \| undefined$/, ''); + + const propDef: PropDef = { type: typeStr }; + + const symbol = checker.getSymbolAtLocation(member.name); + if (symbol) { + const docs = symbol.getDocumentationComment(checker); + const desc = docs.map((d) => d.text).join(''); + if (desc) propDef.description = desc; + } + + props[name] = propDef; + } + } + ts.forEachChild(sourceFile, function visit(node) { if (ts.isInterfaceDeclaration(node) && node.name.text === `${localName}Props`) { - for (const member of node.members) { - if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue; - const name = member.name.text; - if (name === 'children' || !member.type) continue; + // Collect own syntactic members. + collectFromMembers(node.members); - let typeStr = checker.typeToString(checker.getTypeFromTypeNode(member.type)); - // Only strips trailing ` | undefined`; other orderings (e.g., `undefined | string`) pass through. - if (member.questionToken) typeStr = typeStr.replace(/ \| undefined$/, ''); + // Walk extends clause and include members from project-local interfaces. + if (node.heritageClauses) { + for (const clause of node.heritageClauses) { + for (const expr of clause.types) { + const type = checker.getTypeAtLocation(expr); + const symbol = type.getSymbol(); + const decl = symbol?.declarations?.[0]; + if (!decl) continue; - const propDef: PropDef = { type: typeStr }; + // Only include if declared in project sources (not node_modules). + const declFile = decl.getSourceFile().fileName; + if (declFile.includes('node_modules')) continue; - const symbol = checker.getSymbolAtLocation(member.name); - if (symbol) { - const docs = symbol.getDocumentationComment(checker); - const desc = docs.map((d) => d.text).join(''); - if (desc) propDef.description = desc; + if (ts.isInterfaceDeclaration(decl)) { + collectFromMembers(decl.members); + } + } } - - props[name] = propDef; } } ts.forEachChild(node, visit); diff --git a/site/src/components/docs/api-reference/ComponentReference.astro b/site/src/components/docs/api-reference/ComponentReference.astro index b9f90aa1..e5fb03d8 100644 --- a/site/src/components/docs/api-reference/ComponentReference.astro +++ b/site/src/components/docs/api-reference/ComponentReference.astro @@ -19,9 +19,10 @@ import InlineMarkdown from './InlineMarkdown.astro'; interface Props { component: string; + partOrder?: string[]; } -const { component } = Astro.props; +const { component, partOrder } = Astro.props; const { framework } = Astro.params; if (!framework || !isValidFramework(framework)) { throw new Error(`Invalid or missing framework param.`); @@ -31,7 +32,7 @@ const entry = await getEntry('componentReference', kebabCase(component)); const apiRef: ComponentReference | null = entry?.data ?? null; if (!apiRef) return; -const apiReferenceModel = createComponentReferenceModel(component, apiRef); +const apiReferenceModel = createComponentReferenceModel(component, apiRef, partOrder); if (!apiReferenceModel) return; const showAttributeName = framework === 'html'; diff --git a/site/src/components/docs/demos/slider/html/css/BasicUsage.astro b/site/src/components/docs/demos/slider/html/css/BasicUsage.astro new file mode 100644 index 00000000..bf311233 --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/BasicUsage.astro @@ -0,0 +1,10 @@ +--- +import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro'; +import html from './BasicUsage.html?raw'; +import './BasicUsage.css'; +--- + + + diff --git a/site/src/components/docs/demos/slider/html/css/BasicUsage.css b/site/src/components/docs/demos/slider/html/css/BasicUsage.css new file mode 100644 index 00000000..54fcfe47 --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/BasicUsage.css @@ -0,0 +1,59 @@ +.html-slider-basic { + display: flex; + align-items: center; + padding: 24px; + background: #1a1a1a; +} + +.html-slider-basic__slider { + position: relative; + width: 100%; + display: flex; + align-items: center; + height: 20px; + cursor: pointer; +} + +.html-slider-basic__track { + position: absolute; + left: 0; + right: 0; + height: 4px; + background: rgba(255, 255, 255, 0.3); + border-radius: 9999px; + transition: height 150ms ease; +} + +.html-slider-basic__slider[data-interactive] .html-slider-basic__track { + height: 6px; +} + +.html-slider-basic__fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: var(--media-slider-fill); + background: white; + border-radius: 9999px; +} + +.html-slider-basic__thumb { + position: absolute; + left: var(--media-slider-fill); + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transform: translateX(-50%) scale(0); + transition: transform 150ms ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.html-slider-basic__slider[data-interactive] .html-slider-basic__thumb { + transform: translateX(-50%) scale(1); +} + +.html-slider-basic__slider[data-dragging] .html-slider-basic__thumb { + transform: translateX(-50%) scale(1.1); +} diff --git a/site/src/components/docs/demos/slider/html/css/BasicUsage.html b/site/src/components/docs/demos/slider/html/css/BasicUsage.html new file mode 100644 index 00000000..abcdd660 --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/BasicUsage.html @@ -0,0 +1,8 @@ +
+ + + + + + +
diff --git a/site/src/components/docs/demos/slider/html/css/BasicUsage.ts b/site/src/components/docs/demos/slider/html/css/BasicUsage.ts new file mode 100644 index 00000000..5bb4d98f --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/BasicUsage.ts @@ -0,0 +1 @@ +import '@videojs/html/ui/slider'; diff --git a/site/src/components/docs/demos/slider/html/css/WithPreview.astro b/site/src/components/docs/demos/slider/html/css/WithPreview.astro new file mode 100644 index 00000000..24bf136f --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/WithPreview.astro @@ -0,0 +1,10 @@ +--- +import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro'; +import html from './WithPreview.html?raw'; +import './WithPreview.css'; +--- + + + diff --git a/site/src/components/docs/demos/slider/html/css/WithPreview.css b/site/src/components/docs/demos/slider/html/css/WithPreview.css new file mode 100644 index 00000000..120582a2 --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/WithPreview.css @@ -0,0 +1,81 @@ +.html-slider-preview { + display: flex; + align-items: center; + padding: 40px 24px; + background: #1a1a1a; +} + +.html-slider-preview__slider { + position: relative; + width: 100%; + display: flex; + align-items: center; + height: 20px; + cursor: pointer; +} + +.html-slider-preview__track { + position: absolute; + left: 0; + right: 0; + height: 4px; + background: rgba(255, 255, 255, 0.3); + border-radius: 9999px; + transition: height 150ms ease; +} + +.html-slider-preview__slider[data-interactive] .html-slider-preview__track { + height: 6px; +} + +.html-slider-preview__fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: var(--media-slider-fill); + background: white; + border-radius: 9999px; +} + +.html-slider-preview__thumb { + position: absolute; + left: var(--media-slider-fill); + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transform: translateX(-50%) scale(0); + transition: transform 150ms ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.html-slider-preview__slider[data-interactive] .html-slider-preview__thumb { + transform: translateX(-50%) scale(1); +} + +.html-slider-preview__slider[data-dragging] .html-slider-preview__thumb { + transform: translateX(-50%) scale(1.1); +} + +.html-slider-preview__preview { + position: absolute; + bottom: 100%; + margin-bottom: 6px; + pointer-events: none; + opacity: 0; + transition: opacity 150ms ease; +} + +.html-slider-preview__slider[data-pointing] .html-slider-preview__preview { + opacity: 1; +} + +.html-slider-preview__value { + background: rgba(0, 0, 0, 0.8); + color: white; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; +} diff --git a/site/src/components/docs/demos/slider/html/css/WithPreview.html b/site/src/components/docs/demos/slider/html/css/WithPreview.html new file mode 100644 index 00000000..7560721a --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/WithPreview.html @@ -0,0 +1,11 @@ +
+ + + + + + + + + +
diff --git a/site/src/components/docs/demos/slider/html/css/WithPreview.ts b/site/src/components/docs/demos/slider/html/css/WithPreview.ts new file mode 100644 index 00000000..5bb4d98f --- /dev/null +++ b/site/src/components/docs/demos/slider/html/css/WithPreview.ts @@ -0,0 +1 @@ +import '@videojs/html/ui/slider'; diff --git a/site/src/components/docs/demos/slider/react/css/BasicUsage.css b/site/src/components/docs/demos/slider/react/css/BasicUsage.css new file mode 100644 index 00000000..34a8ebff --- /dev/null +++ b/site/src/components/docs/demos/slider/react/css/BasicUsage.css @@ -0,0 +1,59 @@ +.react-slider-basic { + display: flex; + align-items: center; + padding: 24px; + background: #1a1a1a; +} + +.react-slider-basic__slider { + position: relative; + width: 100%; + display: flex; + align-items: center; + height: 20px; + cursor: pointer; +} + +.react-slider-basic__track { + position: absolute; + left: 0; + right: 0; + height: 4px; + background: rgba(255, 255, 255, 0.3); + border-radius: 9999px; + transition: height 150ms ease; +} + +.react-slider-basic__slider[data-interactive] .react-slider-basic__track { + height: 6px; +} + +.react-slider-basic__fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: var(--media-slider-fill); + background: white; + border-radius: 9999px; +} + +.react-slider-basic__thumb { + position: absolute; + left: var(--media-slider-fill); + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transform: translateX(-50%) scale(0); + transition: transform 150ms ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.react-slider-basic__slider[data-interactive] .react-slider-basic__thumb { + transform: translateX(-50%) scale(1); +} + +.react-slider-basic__slider[data-dragging] .react-slider-basic__thumb { + transform: translateX(-50%) scale(1.1); +} diff --git a/site/src/components/docs/demos/slider/react/css/BasicUsage.tsx b/site/src/components/docs/demos/slider/react/css/BasicUsage.tsx new file mode 100644 index 00000000..957154a5 --- /dev/null +++ b/site/src/components/docs/demos/slider/react/css/BasicUsage.tsx @@ -0,0 +1,19 @@ +import { Slider } from '@videojs/react'; +import { useState } from 'react'; + +import './BasicUsage.css'; + +export default function BasicUsage() { + const [value, setValue] = useState(50); + + return ( +
+ + + + + + +
+ ); +} diff --git a/site/src/components/docs/demos/slider/react/css/WithPreview.css b/site/src/components/docs/demos/slider/react/css/WithPreview.css new file mode 100644 index 00000000..9a6871da --- /dev/null +++ b/site/src/components/docs/demos/slider/react/css/WithPreview.css @@ -0,0 +1,81 @@ +.react-slider-preview { + display: flex; + align-items: center; + padding: 40px 24px; + background: #1a1a1a; +} + +.react-slider-preview__slider { + position: relative; + width: 100%; + display: flex; + align-items: center; + height: 20px; + cursor: pointer; +} + +.react-slider-preview__track { + position: absolute; + left: 0; + right: 0; + height: 4px; + background: rgba(255, 255, 255, 0.3); + border-radius: 9999px; + transition: height 150ms ease; +} + +.react-slider-preview__slider[data-interactive] .react-slider-preview__track { + height: 6px; +} + +.react-slider-preview__fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: var(--media-slider-fill); + background: white; + border-radius: 9999px; +} + +.react-slider-preview__thumb { + position: absolute; + left: var(--media-slider-fill); + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transform: translateX(-50%) scale(0); + transition: transform 150ms ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.react-slider-preview__slider[data-interactive] .react-slider-preview__thumb { + transform: translateX(-50%) scale(1); +} + +.react-slider-preview__slider[data-dragging] .react-slider-preview__thumb { + transform: translateX(-50%) scale(1.1); +} + +.react-slider-preview__preview { + position: absolute; + bottom: 100%; + margin-bottom: 6px; + pointer-events: none; + opacity: 0; + transition: opacity 150ms ease; +} + +.react-slider-preview__slider[data-pointing] .react-slider-preview__preview { + opacity: 1; +} + +.react-slider-preview__value { + background: rgba(0, 0, 0, 0.8); + color: white; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; +} diff --git a/site/src/components/docs/demos/slider/react/css/WithPreview.tsx b/site/src/components/docs/demos/slider/react/css/WithPreview.tsx new file mode 100644 index 00000000..0bfa98f6 --- /dev/null +++ b/site/src/components/docs/demos/slider/react/css/WithPreview.tsx @@ -0,0 +1,22 @@ +import { Slider } from '@videojs/react'; +import { useState } from 'react'; + +import './WithPreview.css'; + +export default function WithPreview() { + const [value, setValue] = useState(50); + + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/site/src/components/docs/demos/tooltip/html/css/BasicUsage.astro b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.astro new file mode 100644 index 00000000..bf311233 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.astro @@ -0,0 +1,10 @@ +--- +import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro'; +import html from './BasicUsage.html?raw'; +import './BasicUsage.css'; +--- + + + diff --git a/site/src/components/docs/demos/tooltip/html/css/BasicUsage.css b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.css new file mode 100644 index 00000000..230c2d4b --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.css @@ -0,0 +1,28 @@ +.html-tooltip-basic { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} + +.html-tooltip-basic__trigger { + padding: 6px 16px; + 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; + cursor: pointer; +} + +.html-tooltip-basic__popup { + --media-tooltip-side-offset: 8px; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(10px); + color: white; + border-radius: 6px; + padding: 4px 10px; + font-size: 13px; + white-space: nowrap; + pointer-events: none; +} diff --git a/site/src/components/docs/demos/tooltip/html/css/BasicUsage.html b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.html new file mode 100644 index 00000000..09c2cb06 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.html @@ -0,0 +1,6 @@ +
+ + + Tooltip content + +
diff --git a/site/src/components/docs/demos/tooltip/html/css/BasicUsage.ts b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.ts new file mode 100644 index 00000000..82ad9736 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/BasicUsage.ts @@ -0,0 +1 @@ +import '@videojs/html/ui/tooltip'; diff --git a/site/src/components/docs/demos/tooltip/html/css/Grouping.astro b/site/src/components/docs/demos/tooltip/html/css/Grouping.astro new file mode 100644 index 00000000..b22e8696 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/Grouping.astro @@ -0,0 +1,10 @@ +--- +import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro'; +import html from './Grouping.html?raw'; +import './Grouping.css'; +--- + + + diff --git a/site/src/components/docs/demos/tooltip/html/css/Grouping.css b/site/src/components/docs/demos/tooltip/html/css/Grouping.css new file mode 100644 index 00000000..834c8cd8 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/Grouping.css @@ -0,0 +1,27 @@ +.html-tooltip-grouping { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} + +.html-tooltip-grouping__trigger { + padding: 6px 16px; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + color: black; + border: 1px solid rgba(255, 255, 255, 0.3); + cursor: pointer; +} + +.html-tooltip-grouping__popup { + --media-tooltip-side-offset: 8px; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(10px); + color: white; + border-radius: 6px; + padding: 4px 10px; + font-size: 13px; + white-space: nowrap; + pointer-events: none; +} diff --git a/site/src/components/docs/demos/tooltip/html/css/Grouping.html b/site/src/components/docs/demos/tooltip/html/css/Grouping.html new file mode 100644 index 00000000..341e98d2 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/Grouping.html @@ -0,0 +1,10 @@ + + + Play video + + + Mute audio + + + Enter fullscreen + diff --git a/site/src/components/docs/demos/tooltip/html/css/Grouping.ts b/site/src/components/docs/demos/tooltip/html/css/Grouping.ts new file mode 100644 index 00000000..385be318 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/html/css/Grouping.ts @@ -0,0 +1,2 @@ +import '@videojs/html/ui/tooltip'; +import '@videojs/html/ui/tooltip-group'; diff --git a/site/src/components/docs/demos/tooltip/react/css/BasicUsage.css b/site/src/components/docs/demos/tooltip/react/css/BasicUsage.css new file mode 100644 index 00000000..04c0e621 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/react/css/BasicUsage.css @@ -0,0 +1,34 @@ +.react-tooltip-basic { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} + +.react-tooltip-basic__trigger { + padding: 6px 16px; + 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; + cursor: pointer; +} + +.react-tooltip-basic__popup { + --media-tooltip-side-offset: 8px; + margin: 0; + border: 0; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(10px); + color: white; + border-radius: 6px; + padding: 4px 10px; + font-size: 13px; + white-space: nowrap; + pointer-events: none; +} + +.react-tooltip-basic__arrow { + fill: rgba(0, 0, 0, 0.85); +} diff --git a/site/src/components/docs/demos/tooltip/react/css/BasicUsage.tsx b/site/src/components/docs/demos/tooltip/react/css/BasicUsage.tsx new file mode 100644 index 00000000..76ad6483 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/react/css/BasicUsage.tsx @@ -0,0 +1,17 @@ +import { Tooltip } from '@videojs/react'; + +import './BasicUsage.css'; + +export default function BasicUsage() { + return ( +
+ + Hover me + + + Tooltip content + + +
+ ); +} diff --git a/site/src/components/docs/demos/tooltip/react/css/Grouping.css b/site/src/components/docs/demos/tooltip/react/css/Grouping.css new file mode 100644 index 00000000..9e72d5d0 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/react/css/Grouping.css @@ -0,0 +1,33 @@ +.react-tooltip-grouping { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} + +.react-tooltip-grouping__trigger { + padding: 6px 16px; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + color: black; + border: 1px solid rgba(255, 255, 255, 0.3); + cursor: pointer; +} + +.react-tooltip-grouping__popup { + --media-tooltip-side-offset: 8px; + margin: 0; + border: 0; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(10px); + color: white; + border-radius: 6px; + padding: 4px 10px; + font-size: 13px; + white-space: nowrap; + pointer-events: none; +} + +.react-tooltip-grouping__arrow { + fill: rgba(0, 0, 0, 0.85); +} diff --git a/site/src/components/docs/demos/tooltip/react/css/Grouping.tsx b/site/src/components/docs/demos/tooltip/react/css/Grouping.tsx new file mode 100644 index 00000000..e3361988 --- /dev/null +++ b/site/src/components/docs/demos/tooltip/react/css/Grouping.tsx @@ -0,0 +1,33 @@ +import { Tooltip } from '@videojs/react'; + +import './Grouping.css'; + +export default function Grouping() { + return ( +
+ + + Play + + + Play video + + + + Mute + + + Mute audio + + + + Fullscreen + + + Enter fullscreen + + + +
+ ); +} diff --git a/site/src/content/docs/reference/controls.mdx b/site/src/content/docs/reference/controls.mdx index b602c38a..827aedc7 100644 --- a/site/src/content/docs/reference/controls.mdx +++ b/site/src/content/docs/reference/controls.mdx @@ -73,7 +73,7 @@ media-controls:not([data-visible]) {
-React renders `
` elements with the same data attributes. Add a `className` and use it as the selector: +React renders `
` elements. Add a `className` to style them: ```css /* Click-through: clicks pass through controls to video beneath */ diff --git a/site/src/content/docs/reference/fullscreen-button.mdx b/site/src/content/docs/reference/fullscreen-button.mdx index ac63dd62..60b0f39c 100644 --- a/site/src/content/docs/reference/fullscreen-button.mdx +++ b/site/src/content/docs/reference/fullscreen-button.mdx @@ -53,7 +53,7 @@ media-fullscreen-button[data-fullscreen] { -React renders a ` + Label text + + ``` + + +## Behavior + +Displays a short label anchored to a trigger element. Opens after a configurable `delay` (default 600ms) on hover or immediately on focus. Closes when the pointer leaves or focus moves away, with an optional `closeDelay`. + +The `side` and `align` props control placement relative to the trigger. Positioning uses CSS Anchor Positioning where supported, with a JavaScript measurement fallback. + + + The component is composed from four parts: `Root` manages state and context, + `Trigger` renders a button that activates the tooltip, `Popup` contains the label content, + and `Arrow` renders a decorative pointer. Wrap multiple tooltips in a `Tooltip.Provider` + to coordinate open/close timing across a group — once a tooltip becomes visible, adjacent + tooltips open instantly within the `timeout` window, skipping the normal `delay`. + + + + The `` element is the popup itself. Link it to a trigger using + the `commandfor` attribute on any button, pointing to the tooltip's `id`. The element + discovers its trigger automatically and manages open/close state, ARIA attributes, and + positioning. Wrap tooltip trigger/popup pairs in `` to coordinate + timing — the group's `delay`, `close-delay`, and `timeout` attributes control shared + timing for all contained tooltips. + + +## Styling + +Use [CSS custom properties](#root-css-custom-properties) for positioning offsets: + + +```css +media-tooltip { + --media-tooltip-side-offset: 8px; + --media-tooltip-align-offset: 0px; +} +``` + + + +React renders standard DOM elements. Add a `className` to style them: + +```css +.tooltip-popup { + --media-tooltip-side-offset: 8px; + --media-tooltip-align-offset: 0px; +} +``` + + +Style based on open state and transition phases: + + +```css +media-tooltip[data-open] { + display: block; +} +media-tooltip[data-starting-style] { + opacity: 0; +} +media-tooltip[data-ending-style] { + opacity: 0; +} +media-tooltip[data-side="top"] { + transform-origin: bottom center; +} +media-tooltip[data-side="bottom"] { + transform-origin: top center; +} +``` + + + +```css +.tooltip-popup[data-open] { + display: block; +} +.tooltip-popup[data-starting-style] { + opacity: 0; +} +.tooltip-popup[data-ending-style] { + opacity: 0; +} +.tooltip-popup[data-side="top"] { + transform-origin: bottom center; +} +.tooltip-popup[data-side="bottom"] { + transform-origin: top center; +} +``` + + +## Accessibility + +The trigger receives `aria-describedby` pointing to the popup when open. The popup renders with `role="tooltip"` and `popover="manual"`. Tooltips open on focus and close when focus leaves, ensuring keyboard-only users can access the label. + +## Examples + +### Basic Usage + + + + + + + + + + + + + + + + + +### Grouping + + + Wrap multiple tooltips in a `Tooltip.Provider` to share a delay group. Once a tooltip + becomes visible, adjacent tooltips open instantly within the `timeout` window, skipping + the normal `delay`. + + + + + + + + + + Wrap tooltip trigger/popup pairs in `` to coordinate timing. The + group's `delay`, `close-delay`, and `timeout` attributes control shared timing for all + contained tooltips. + + + + + + + + + diff --git a/site/src/content/docs/reference/volume-slider.mdx b/site/src/content/docs/reference/volume-slider.mdx index 5c165696..13395bba 100644 --- a/site/src/content/docs/reference/volume-slider.mdx +++ b/site/src/content/docs/reference/volume-slider.mdx @@ -52,7 +52,7 @@ media-volume-slider::before { -React renders a `
` with the same data attributes and CSS custom properties. Add a `className` and use it as the selector: +React renders a `
` element. Add a `className` to style it: ```css .volume-slider::before { diff --git a/site/src/docs.config.ts b/site/src/docs.config.ts index f8406774..3a07244b 100644 --- a/site/src/docs.config.ts +++ b/site/src/docs.config.ts @@ -55,9 +55,11 @@ export const sidebar: Sidebar = [ { slug: 'reference/popover' }, { slug: 'reference/poster' }, { slug: 'reference/seek-button' }, + { slug: 'reference/slider' }, { slug: 'reference/thumbnail' }, { slug: 'reference/time' }, { slug: 'reference/time-slider' }, + { slug: 'reference/tooltip' }, { slug: 'reference/volume-slider' }, ], }, diff --git a/site/src/utils/componentReferenceModel.js b/site/src/utils/componentReferenceModel.js index 761d1907..c5dfe63d 100644 --- a/site/src/utils/componentReferenceModel.js +++ b/site/src/utils/componentReferenceModel.js @@ -82,7 +82,7 @@ function createSections(source, options) { * The shared model is what prevents anchor drift: ids are computed once and * reused verbatim by the renderer and the remark plugin. */ -export function createComponentReferenceModel(componentName, apiReference) { +export function createComponentReferenceModel(componentName, apiReference, partOrder) { if (!apiReference) { return null; } @@ -90,7 +90,18 @@ export function createComponentReferenceModel(componentName, apiReference) { const hasParts = Boolean(apiReference.parts && Object.keys(apiReference.parts).length > 0); if (hasParts) { - const parts = Object.entries(apiReference.parts).map(([partId, part]) => ({ + let partEntries = Object.entries(apiReference.parts); + + if (partOrder) { + const orderMap = new Map(partOrder.map((id, i) => [id, i])); + partEntries = partEntries.slice().sort((a, b) => { + const ai = orderMap.has(a[0]) ? orderMap.get(a[0]) : Number.MAX_SAFE_INTEGER; + const bi = orderMap.has(b[0]) ? orderMap.get(b[0]) : Number.MAX_SAFE_INTEGER; + return ai - bi; + }); + } + + const parts = partEntries.map(([partId, part]) => ({ id: partId, name: part.name, description: part.description, diff --git a/site/src/utils/remarkConditionalHeadings.js b/site/src/utils/remarkConditionalHeadings.js index 5d2a7e81..3f32f487 100644 --- a/site/src/utils/remarkConditionalHeadings.js +++ b/site/src/utils/remarkConditionalHeadings.js @@ -136,7 +136,10 @@ function injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSl const json = readComponentRefJson(componentName); if (!json) return; - const componentModel = createComponentReferenceModel(componentName, json); + const partOrderAttr = node.attributes?.find((a) => a.name === 'partOrder'); + const partOrder = extractArrayValue(partOrderAttr); + + const componentModel = createComponentReferenceModel(componentName, json, partOrder); const componentHeadings = buildComponentReferenceTocHeadings(componentModel); headingsWithMetadata.push(...componentHeadings);