diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad885af6..dcd8bbb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,6 +278,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.14 version: 4.1.14(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + '@videojs/html': + specifier: workspace:* + version: link:../packages/html '@videojs/react': specifier: workspace:* version: link:../packages/react diff --git a/website/README.md b/website/README.md index 99b54add..df50e367 100644 --- a/website/README.md +++ b/website/README.md @@ -94,7 +94,7 @@ See [content.config.ts](src/content.config.ts) for the blog collection definitio The most sophisticated part of the website is the documentation system, which adapts content based on: - **Framework** (HTML, React) -- **Styling approach** (CSS, Tailwind) +- **Styling approach** (CSS, more coming soon) ##### URL Structure @@ -102,14 +102,14 @@ The most sophisticated part of the website is the documentation system, which ad /docs/framework/{framework}/style/{style}/{slug}/ ``` -Example: `/docs/framework/react/style/tailwind/concepts/state-management/` +Example: `/docs/framework/react/style/css/concepts/state-management/` ##### Framework/Style Matrix | Framework | Available Styles | | --------- | ---------------- | -| HTML | css, tailwind | -| React | css, tailwind | +| HTML | css | +| React | css | ##### Content Filtering diff --git a/website/package.json b/website/package.json index 9bb816b9..7313c2ad 100644 --- a/website/package.json +++ b/website/package.json @@ -21,6 +21,7 @@ "@base-ui-components/react": "1.0.0-beta.4", "@nanostores/react": "^1.0.0", "@tailwindcss/vite": "^4.1.14", + "@videojs/html": "workspace:*", "@videojs/react": "workspace:*", "astro": "^5.14.4", "clsx": "^2.1.1", diff --git a/website/src/components/ServerCode.astro b/website/src/components/ServerCode.astro new file mode 100644 index 00000000..3048021c --- /dev/null +++ b/website/src/components/ServerCode.astro @@ -0,0 +1,28 @@ +--- +import type { ComponentProps } from 'astro/types'; +import { Code } from 'astro:components'; +import Pre from './typography/Pre.astro'; +import { twMerge } from 'tailwind-merge'; + +interface Props extends Omit, 'class'> { + maxWidth?: ComponentProps['maxWidth']; + wrapperClass?: ComponentProps['class']; + codeClass?: ComponentProps['class']; +} + +const { code, lang, maxWidth = true, themes, wrapperClass, codeClass, style, ...codeProps } = Astro.props; +--- + +
+  
+
diff --git a/website/src/components/docs/Container.astro b/website/src/components/docs/Container.astro new file mode 100644 index 00000000..badcf6a7 --- /dev/null +++ b/website/src/components/docs/Container.astro @@ -0,0 +1,7 @@ +--- +// Simple container component for constraining content width in documentation +--- + +
+ +
diff --git a/website/src/components/home/config.ts b/website/src/components/home/config.ts index 88d4716e..012268e8 100644 --- a/website/src/components/home/config.ts +++ b/website/src/components/home/config.ts @@ -1,5 +1,3 @@ // video.js 8 export const PLAYBACK_ID = 'UZMwOY6MgmhFNXLbSFXAuPKlRPss5XNA'; export const THUMB_TIME = 0; -// player.style -// export const PLAYBACK_ID = 'fXNzVtmtWuyz00xnSrJg4OJH6PyNo6D02UzmgeKGkP5YQ'; diff --git a/website/src/components/typography/Pre.astro b/website/src/components/typography/Pre.astro index 61567778..da0e7856 100644 --- a/website/src/components/typography/Pre.astro +++ b/website/src/components/typography/Pre.astro @@ -14,11 +14,10 @@ const { as: Tag = 'pre', maxWidth = true, class: className, style: _style, ...pr
- -
diff --git a/website/src/config/docs/sidebar.ts b/website/src/config/docs/sidebar.ts index ad6a16d5..f05a5b40 100644 --- a/website/src/config/docs/sidebar.ts +++ b/website/src/config/docs/sidebar.ts @@ -2,7 +2,17 @@ import type { Sidebar } from '@/types/docs'; export const sidebar: Sidebar = [ { sidebarLabel: 'Writing guides', slug: 'how-to/write-guides', devOnly: true }, - { slug: 'concepts/coming-soon' }, + { sidebarLabel: 'Getting started', contents: [{ slug: 'concepts/under-construction' }] }, + { + sidebarLabel: 'Components', + contents: [ + { slug: 'resources/play-button' }, + { slug: 'resources/mute-button' }, + { slug: 'resources/fullscreen-button' }, + { slug: 'resources/time-slider' }, + { slug: 'resources/volume-slider' }, + ], + }, // { // sidebarLabel: 'Getting started', // contents: [ diff --git a/website/src/content.config.ts b/website/src/content.config.ts index 60d6ea9c..c2c87456 100644 --- a/website/src/content.config.ts +++ b/website/src/content.config.ts @@ -1,5 +1,6 @@ import { file } from 'astro/loaders'; import { defineCollection, reference, z } from 'astro:content'; +import { SUPPORTED_FRAMEWORKS } from './types/docs'; import { defaultGitService } from './utils/gitService'; import { globWithParser } from './utils/globWithParser'; @@ -80,6 +81,7 @@ const docs = defineCollection({ title: z.string(), description: z.string(), updatedDate: z.coerce.date().optional(), + frameworkTitle: z.record(z.enum(SUPPORTED_FRAMEWORKS as [string, ...string[]]), z.string()).optional(), }), }); diff --git a/website/src/content/docs/concepts/coming-soon.mdx b/website/src/content/docs/concepts/coming-soon.mdx deleted file mode 100644 index 148ba135..00000000 --- a/website/src/content/docs/concepts/coming-soon.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Coming soon' -description: 'Video.js 10 docs are under construction!' ---- - -There's no content here... yet. \ No newline at end of file diff --git a/website/src/content/docs/concepts/under-construction.mdx b/website/src/content/docs/concepts/under-construction.mdx new file mode 100644 index 00000000..6b472f2a --- /dev/null +++ b/website/src/content/docs/concepts/under-construction.mdx @@ -0,0 +1,6 @@ +--- +title: 'Under Construction' +description: 'Video.js 10 docs are coming soon!' +--- + +Please pardon our dust as we build this all out. \ No newline at end of file diff --git a/website/src/content/docs/how-to/write-guides.mdx b/website/src/content/docs/how-to/write-guides.mdx index 188309cc..6b5ccab8 100644 --- a/website/src/content/docs/how-to/write-guides.mdx +++ b/website/src/content/docs/how-to/write-guides.mdx @@ -5,6 +5,8 @@ description: 'A guide on writing documentation for the Video.js project, and a t import FrameworkCase from '@/components/docs/FrameworkCase.astro'; import StyleCase from '@/components/docs/StyleCase.astro'; +import Container from '@/components/docs/Container.astro'; +import ServerCode from '@/components/ServerCode.astro'; ## What kind of guide are you writing? I haven't written this section yet, but when I do, it'll rehash [Diátaxis](https://diataxis.fr/). @@ -41,6 +43,39 @@ will render: Tailwind-only content +## Displaying Code from Files + +Use the `` component to display code imported from source files with syntax highlighting. Supports any language that [Shiki supports](https://shiki.style/languages). + +```mdx +import exampleCode from '@/examples/react/Example.tsx?raw'; +import ServerCode from '@/components/ServerCode.astro'; + + +``` + +will render: + + setCount(count + 1)}>{count}; +}`} lang="tsx" /> + +## Wrapping Live Demos + +Use the `` component to constrain live demos to a readable width. + +```mdx +import { MyDemo } from '@/examples/react/MyDemo'; +import Container from '@/components/docs/Container.astro'; + + + + +``` + ## Use Github-Flavored Markdown ### Headings diff --git a/website/src/content/docs/resources/fullscreen-button.mdx b/website/src/content/docs/resources/fullscreen-button.mdx new file mode 100644 index 00000000..c64c1b8b --- /dev/null +++ b/website/src/content/docs/resources/fullscreen-button.mdx @@ -0,0 +1,88 @@ +--- +title: FullscreenButton +frameworkTitle: + html: fullscreen-button +description: A button component for toggling fullscreen mode +--- + +import { FullscreenButtonDemo } from '@/examples/react/FullscreenButton/FullscreenButtonDemo'; +import componentModuleStr from '@/examples/react/FullscreenButton/BasicFullscreenButton.tsx?raw'; +import cssModuleStr from '@/examples/react/FullscreenButton/FullscreenButton.module.css?raw'; +import htmlStr from '@/examples/html/fullscreen-button/snippet.html?raw'; +import htmlCssStr from '@/examples/html/fullscreen-button/fullscreen-button.css?raw'; +import htmlJsStr from '@/examples/html/fullscreen-button/fullscreen-button.js?raw'; +import FrameworkCase from '@/components/docs/FrameworkCase.astro'; +import Container from '@/components/docs/Container.astro'; +import ServerCode from '@/components/ServerCode.astro'; + +## Features + +- Automatically switches icons based on fullscreen state +- Works with browser Fullscreen API +- Falls back gracefully when fullscreen not supported +- Accessible keyboard navigation + +## Live Example + + + + + +## Usage + + + +### Component + + + +### CSS Module + + + + + + +### HTML + + + +### CSS + + +### JavaScript + + + + + +## Data Attributes + +The FullscreenButton automatically sets data attributes based on fullscreen state: + +- `data-fullscreen` - Present when in fullscreen, absent when not + +Use these attributes for state-based styling in your CSS. + + + +## Props + +All standard button props are supported, plus: + +| Prop | Type | Description | +|------|------|-------------| +| `children` | `ReactNode` | Button content (typically icons) | +| `className` | `string` | CSS class name | + + + +## Accessibility + +- Automatically includes proper ARIA labels +- Keyboard accessible (Space/Enter) +- Announces fullscreen state changes to screen readers + +## Browser Support + +The FullscreenButton uses the standard Fullscreen API, which is supported in all modern browsers. diff --git a/website/src/content/docs/resources/mute-button.mdx b/website/src/content/docs/resources/mute-button.mdx new file mode 100644 index 00000000..d81efe01 --- /dev/null +++ b/website/src/content/docs/resources/mute-button.mdx @@ -0,0 +1,88 @@ +--- +title: MuteButton +frameworkTitle: + html: mute-button +description: A button component for toggling audio mute state +--- + +import { MuteButtonDemo } from '@/examples/react/MuteButton/MuteButtonDemo'; +import componentModuleStr from '@/examples/react/MuteButton/BasicMuteButton.tsx?raw'; +import cssModuleStr from '@/examples/react/MuteButton/MuteButton.module.css?raw'; +import htmlStr from '@/examples/html/mute-button/snippet.html?raw'; +import htmlCssStr from '@/examples/html/mute-button/mute-button.css?raw'; +import htmlJsStr from '@/examples/html/mute-button/mute-button.js?raw'; +import FrameworkCase from '@/components/docs/FrameworkCase.astro'; +import Container from '@/components/docs/Container.astro'; +import ServerCode from '@/components/ServerCode.astro'; + +## Features + +- Multi-state icon display (high, low, off) +- Automatically reflects volume level changes +- Toggles mute/unmute on click +- Accessible keyboard navigation + +## Live Example + + + + + +## Usage + + + +### Component + + + +### CSS Module + + + + + + +### HTML + + + +### CSS + + + +### JavaScript + + + + + +## Data Attributes + +The MuteButton automatically sets data attributes based on volume level: + +- `data-volume-level="high"` - Volume > 50% +- `data-volume-level="medium"` - Volume 25-50% +- `data-volume-level="low"` - Volume 1-24% +- `data-volume-level="off"` - Volume 0% (muted) + +Use these attributes for state-based styling in your CSS. + + + +## Props + +All standard button props are supported, plus: + +| Prop | Type | Description | +|------|------|-------------| +| `children` | `ReactNode` | Button content (typically icons) | +| `className` | `string` | CSS class name | + + + +## Accessibility + +- Automatically includes proper ARIA labels +- Keyboard accessible (Space/Enter) +- Announces volume state changes to screen readers diff --git a/website/src/content/docs/resources/play-button.mdx b/website/src/content/docs/resources/play-button.mdx new file mode 100644 index 00000000..a3a74fae --- /dev/null +++ b/website/src/content/docs/resources/play-button.mdx @@ -0,0 +1,86 @@ +--- +title: PlayButton +frameworkTitle: + html: play-button +description: A button component for playing and pausing media playback +--- + +import { PlayButtonDemo } from '@/examples/react/PlayButton/PlayButtonDemo'; +import componentModuleStr from '@/examples/react/PlayButton/BasicPlayButton.tsx?raw'; +import cssModuleStr from '@/examples/react/PlayButton/PlayButton.module.css?raw'; +import htmlStr from '@/examples/html/play-button/snippet.html?raw'; +import htmlCssStr from '@/examples/html/play-button/play-button.css?raw'; +import htmlJsStr from '@/examples/html/play-button/play-button.js?raw'; +import FrameworkCase from '@/components/docs/FrameworkCase.astro'; +import Container from '@/components/docs/Container.astro'; +import ServerCode from '@/components/ServerCode.astro'; + +## Features + +- Automatically switches icons based on playback state +- Uses data attributes for state-based styling +- Accessible keyboard navigation +- Works with any media element + +## Live Example + + + + + +## Usage + + + +### Component + + + +### CSS Module + + + + + + + +### HTML + + + +### CSS + + + +### JavaScript + + + + + +## Data Attributes + +The PlayButton automatically sets data attributes based on media state: + +- `data-paused` - Present when media is paused, absent when playing + +Use these attributes for state-based styling in your CSS. + + + +## Props + +All standard button props are supported, plus: + +| Prop | Type | Description | +|------|------|-------------| +| `children` | `ReactNode` | Button content (typically icons) | +| `className` | `string` | CSS class name | + + + +## Accessibility + +- Automatically includes proper ARIA labels +- Keyboard accessible (Space/Enter) +- Announces state changes to screen readers diff --git a/website/src/content/docs/resources/time-slider.mdx b/website/src/content/docs/resources/time-slider.mdx new file mode 100644 index 00000000..7ee60643 --- /dev/null +++ b/website/src/content/docs/resources/time-slider.mdx @@ -0,0 +1,111 @@ +--- +title: TimeSlider +frameworkTitle: + html: time-slider +description: A slider component for seeking through media content +--- + +import { TimeSliderDemo } from '@/examples/react/TimeSlider/TimeSliderDemo'; +import componentModuleStr from '@/examples/react/TimeSlider/BasicTimeSlider.tsx?raw'; +import cssModuleStr from '@/examples/react/TimeSlider/TimeSlider.module.css?raw'; +import htmlHorizontalStr from '@/examples/html/time-slider/snippet-horizontal.html?raw'; +import htmlVerticalStr from '@/examples/html/time-slider/snippet-vertical.html?raw'; +import htmlCssStr from '@/examples/html/time-slider/time-slider.css?raw'; +import FrameworkCase from '@/components/docs/FrameworkCase.astro'; +import Container from '@/components/docs/Container.astro'; +import ServerCode from '@/components/ServerCode.astro'; + +## Features + +- Supports both horizontal and vertical orientations +- Displays current playback position +- Shows preview position on hover +- Keyboard accessible (Arrow keys for seeking) +- Touch-friendly drag interaction + +## Live Example + + + + + +## Usage + + + +### Component + + + +### CSS Module + + + + + + + +### Horizontal Orientation + + + +### Vertical Orientation + + + +### CSS + + + + + + + +## Compound Components + +TimeSlider is composed of multiple sub-components: + +### TimeSlider.Root +The container component that manages state and interactions. + +**Props:** +- `orientation?: 'horizontal' | 'vertical'` - Slider orientation (default: 'horizontal') +- All standard div props + +### TimeSlider.Track +The background track element that contains progress and pointer indicators. + +### TimeSlider.Progress +Visual indicator showing how much of the media has been played. + +### TimeSlider.Pointer +Shows the hover/preview position when user moves cursor over the slider. + +### TimeSlider.Thumb +The draggable handle that indicates and controls the current playback position. + + + +## Data Attributes + +The TimeSlider automatically sets data attributes: + +- `data-orientation` - Current orientation ('horizontal' or 'vertical') +- `data-current-time` - Current playback time in seconds +- `data-duration` - Total media duration in seconds + +Use these attributes for state-based styling in your CSS. + +## CSS Variables + +The component exposes CSS variables for positioning: + +- `--slider-fill` - Percentage of progress (0-100%) +- `--slider-pointer` - Percentage of pointer position (0-100%) + +## Accessibility + +- Includes proper ARIA role (`slider`) +- Keyboard accessible (Arrow keys, Home, End) +- Screen reader announcements for time values +- Proper aria-valuemin, aria-valuemax, aria-valuenow attributes diff --git a/website/src/content/docs/resources/volume-slider.mdx b/website/src/content/docs/resources/volume-slider.mdx new file mode 100644 index 00000000..eb3267d7 --- /dev/null +++ b/website/src/content/docs/resources/volume-slider.mdx @@ -0,0 +1,108 @@ +--- +title: VolumeSlider +frameworkTitle: + html: volume-slider +description: A slider component for controlling media volume +--- + +import { VolumeSliderDemo } from '@/examples/react/VolumeSlider/VolumeSliderDemo'; +import componentModuleStr from '@/examples/react/VolumeSlider/BasicVolumeSlider.tsx?raw'; +import cssModuleStr from '@/examples/react/VolumeSlider/VolumeSlider.module.css?raw'; +import htmlHorizontalStr from '@/examples/html/volume-slider/snippet-horizontal.html?raw'; +import htmlVerticalStr from '@/examples/html/volume-slider/snippet-vertical.html?raw'; +import htmlCssStr from '@/examples/html/volume-slider/volume-slider.css?raw'; +import FrameworkCase from '@/components/docs/FrameworkCase.astro'; +import Container from '@/components/docs/Container.astro'; +import ServerCode from '@/components/ServerCode.astro'; + +## Features + +- Supports both horizontal and vertical orientations +- Displays current volume level +- Reflects muted state +- Keyboard accessible (Arrow keys for volume adjustment) +- Touch-friendly drag interaction + +## Live Example + + + + + +## Usage + + + +### Component + + + +### CSS Module + + + + + + + +### Horizontal Orientation + + + +### Vertical Orientation + + + +### CSS + + + + + + + +## Compound Components + +VolumeSlider is composed of multiple sub-components: + +### VolumeSlider.Root +The container component that manages state and interactions. + +**Props:** +- `orientation?: 'horizontal' | 'vertical'` - Slider orientation (default: 'horizontal') +- All standard div props + +### VolumeSlider.Track +The background track element that contains the progress indicator. + +### VolumeSlider.Progress +Visual indicator showing the current volume level. + +### VolumeSlider.Thumb +The draggable handle that indicates and controls the current volume level. + + + +## Data Attributes + +The VolumeSlider automatically sets data attributes: + +- `data-orientation` - Current orientation ('horizontal' or 'vertical') +- `data-muted` - Present when volume is muted +- `data-volume-level` - Volume level category: 'high' (>50%), 'medium' (25-50%), 'low' (1-24%), or 'off' (0%) + +Use these attributes for state-based styling in your CSS. + +## CSS Variables + +The component exposes CSS variables for positioning: + +- `--slider-fill` - Percentage of volume level (0-100%) +- `--slider-pointer` - Percentage of pointer position (0-100%) + +## Accessibility + +- Includes proper ARIA role (`slider`) +- Keyboard accessible (Arrow keys, Home, End) +- Screen reader announcements for volume values +- Proper aria-valuemin, aria-valuemax, aria-valuenow attributes diff --git a/website/src/examples/html/fullscreen-button/basic.html b/website/src/examples/html/fullscreen-button/basic.html new file mode 100644 index 00000000..e17ed6a9 --- /dev/null +++ b/website/src/examples/html/fullscreen-button/basic.html @@ -0,0 +1,31 @@ + + + + + + Basic Fullscreen Button - Video.js + + + + +
+ + + +
+ + + + + + + + +
+
+
+ + diff --git a/website/src/examples/html/fullscreen-button/fullscreen-button.css b/website/src/examples/html/fullscreen-button/fullscreen-button.css new file mode 100644 index 00000000..f85a8559 --- /dev/null +++ b/website/src/examples/html/fullscreen-button/fullscreen-button.css @@ -0,0 +1,85 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 2rem; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #0a0a0a; + color: white; +} + +.demo-container { + max-width: 800px; + margin: 0 auto; +} + +.media-container { + position: relative; + width: 100%; + max-width: 640px; +} + +video { + width: 100%; + height: auto; + display: block; +} + +.controls { + position: absolute; + bottom: 1rem; + right: 1rem; + z-index: 10; +} + +media-fullscreen-button { + position: relative; + display: grid; + padding: 0.625rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + border: none; + cursor: pointer; + color: white; + transition: background 150ms ease; +} + +media-fullscreen-button:hover { + background: rgba(255, 255, 255, 0.15); +} + +media-fullscreen-button:active { + background: rgba(255, 255, 255, 0.2); +} + +/* Icon positioning - both occupy same grid cell */ +media-fullscreen-enter-icon, +media-fullscreen-exit-icon { + grid-area: 1/1; + transition: opacity 200ms ease; + width: 18px; + height: 18px; +} + +/* Show/hide icons based on fullscreen state using data attributes */ +media-fullscreen-button:not([data-fullscreen]) media-fullscreen-enter-icon { + opacity: 1; +} + +media-fullscreen-button:not([data-fullscreen]) media-fullscreen-exit-icon { + opacity: 0; +} + +media-fullscreen-button[data-fullscreen] media-fullscreen-enter-icon { + opacity: 0; +} + +media-fullscreen-button[data-fullscreen] media-fullscreen-exit-icon { + opacity: 1; +} diff --git a/website/src/examples/html/fullscreen-button/fullscreen-button.js b/website/src/examples/html/fullscreen-button/fullscreen-button.js new file mode 100644 index 00000000..b46b1e27 --- /dev/null +++ b/website/src/examples/html/fullscreen-button/fullscreen-button.js @@ -0,0 +1,5 @@ +import '@videojs/html'; +import '@videojs/html/icons'; + +// The web components will automatically register themselves +// No additional setup needed diff --git a/website/src/examples/html/fullscreen-button/snippet.html b/website/src/examples/html/fullscreen-button/snippet.html new file mode 100644 index 00000000..9b857f41 --- /dev/null +++ b/website/src/examples/html/fullscreen-button/snippet.html @@ -0,0 +1,4 @@ + + + + diff --git a/website/src/examples/html/mute-button/basic.html b/website/src/examples/html/mute-button/basic.html new file mode 100644 index 00000000..26f2da46 --- /dev/null +++ b/website/src/examples/html/mute-button/basic.html @@ -0,0 +1,38 @@ + + + + + + Basic Mute Button - Video.js + + + + +
+ + + +
+ + + + + + + + + + + +
+
+
+ + diff --git a/website/src/examples/html/mute-button/mute-button.css b/website/src/examples/html/mute-button/mute-button.css new file mode 100644 index 00000000..d1ca4bf1 --- /dev/null +++ b/website/src/examples/html/mute-button/mute-button.css @@ -0,0 +1,87 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 2rem; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #0a0a0a; + color: white; +} + +.demo-container { + max-width: 800px; + margin: 0 auto; +} + +.media-container { + position: relative; + width: 100%; + max-width: 640px; +} + +video { + width: 100%; + height: auto; + display: block; +} + +.controls { + position: absolute; + bottom: 1rem; + left: 1rem; + z-index: 10; +} + +media-mute-button { + position: relative; + display: grid; + padding: 0.625rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + border: none; + cursor: pointer; + color: white; + transition: background 150ms ease; +} + +media-mute-button:hover { + background: rgba(255, 255, 255, 0.15); +} + +media-mute-button:active { + background: rgba(255, 255, 255, 0.2); +} + +/* Icon positioning - all icons occupy same grid cell */ +media-volume-high-icon, +media-volume-low-icon, +media-volume-off-icon { + grid-area: 1/1; + transition: opacity 200ms ease; + width: 18px; + height: 18px; + opacity: 0; +} + +/* Show appropriate icon based on volume level using data attributes */ +/* High volume (> 50%) */ +media-mute-button[data-volume-level='high'] media-volume-high-icon { + opacity: 1; +} + +/* Medium/Low volume (1-50%) */ +media-mute-button[data-volume-level='medium'] media-volume-low-icon, +media-mute-button[data-volume-level='low'] media-volume-low-icon { + opacity: 1; +} + +/* Muted/Off volume (0%) */ +media-mute-button[data-volume-level='off'] media-volume-off-icon { + opacity: 1; +} diff --git a/website/src/examples/html/mute-button/mute-button.js b/website/src/examples/html/mute-button/mute-button.js new file mode 100644 index 00000000..b46b1e27 --- /dev/null +++ b/website/src/examples/html/mute-button/mute-button.js @@ -0,0 +1,5 @@ +import '@videojs/html'; +import '@videojs/html/icons'; + +// The web components will automatically register themselves +// No additional setup needed diff --git a/website/src/examples/html/mute-button/snippet.html b/website/src/examples/html/mute-button/snippet.html new file mode 100644 index 00000000..b3c3fbb9 --- /dev/null +++ b/website/src/examples/html/mute-button/snippet.html @@ -0,0 +1,5 @@ + + + + + diff --git a/website/src/examples/html/play-button/basic.html b/website/src/examples/html/play-button/basic.html new file mode 100644 index 00000000..2462eb36 --- /dev/null +++ b/website/src/examples/html/play-button/basic.html @@ -0,0 +1,31 @@ + + + + + + Basic Play Button - Video.js + + + + +
+ + + +
+ + + + + + + + +
+
+
+ + diff --git a/website/src/examples/html/play-button/play-button.css b/website/src/examples/html/play-button/play-button.css new file mode 100644 index 00000000..cf9a4d61 --- /dev/null +++ b/website/src/examples/html/play-button/play-button.css @@ -0,0 +1,85 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 2rem; + font-family: + system-ui, + -apple-system, + sans-serif; + background: #0a0a0a; + color: white; +} + +.demo-container { + max-width: 800px; + margin: 0 auto; +} + +.media-container { + position: relative; + width: 100%; + max-width: 640px; +} + +video { + width: 100%; + height: auto; + display: block; +} + +.controls { + position: absolute; + bottom: 1rem; + left: 1rem; + z-index: 10; +} + +media-play-button { + position: relative; + display: grid; + padding: 0.625rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + border: none; + cursor: pointer; + color: white; + transition: background 150ms ease; +} + +media-play-button:hover { + background: rgba(255, 255, 255, 0.15); +} + +media-play-button:active { + background: rgba(255, 255, 255, 0.2); +} + +/* Icon positioning - both occupy same grid cell */ +media-play-icon, +media-pause-icon { + grid-area: 1/1; + transition: opacity 200ms ease; + width: 18px; + height: 18px; +} + +/* Show/hide icons based on paused state using data attributes */ +media-play-button[data-paused] media-play-icon { + opacity: 1; +} + +media-play-button[data-paused] media-pause-icon { + opacity: 0; +} + +media-play-button:not([data-paused]) media-play-icon { + opacity: 0; +} + +media-play-button:not([data-paused]) media-pause-icon { + opacity: 1; +} diff --git a/website/src/examples/html/play-button/play-button.js b/website/src/examples/html/play-button/play-button.js new file mode 100644 index 00000000..b46b1e27 --- /dev/null +++ b/website/src/examples/html/play-button/play-button.js @@ -0,0 +1,5 @@ +import '@videojs/html'; +import '@videojs/html/icons'; + +// The web components will automatically register themselves +// No additional setup needed diff --git a/website/src/examples/html/play-button/snippet.html b/website/src/examples/html/play-button/snippet.html new file mode 100644 index 00000000..a09c76fa --- /dev/null +++ b/website/src/examples/html/play-button/snippet.html @@ -0,0 +1,4 @@ + + + + diff --git a/website/src/examples/html/time-slider/snippet-horizontal.html b/website/src/examples/html/time-slider/snippet-horizontal.html new file mode 100644 index 00000000..f94f3267 --- /dev/null +++ b/website/src/examples/html/time-slider/snippet-horizontal.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/src/examples/html/time-slider/snippet-vertical.html b/website/src/examples/html/time-slider/snippet-vertical.html new file mode 100644 index 00000000..090338d6 --- /dev/null +++ b/website/src/examples/html/time-slider/snippet-vertical.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/src/examples/html/time-slider/time-slider.css b/website/src/examples/html/time-slider/time-slider.css new file mode 100644 index 00000000..8df318a1 --- /dev/null +++ b/website/src/examples/html/time-slider/time-slider.css @@ -0,0 +1,64 @@ +media-time-slider-root { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* Horizontal orientation */ +media-time-slider-root[data-orientation='horizontal'] { + width: 100%; + min-width: 100px; + height: 20px; +} + +/* Vertical orientation */ +media-time-slider-root[data-orientation='vertical'] { + width: 20px; + height: 100px; + flex-direction: column; +} + +media-time-slider-track { + position: relative; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.25rem; + overflow: hidden; +} + +/* Horizontal track */ +media-time-slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.375rem; +} + +/* Vertical track */ +media-time-slider-track[data-orientation='vertical'] { + width: 0.375rem; + height: 100%; +} + +media-time-slider-progress { + background-color: #007bff; + border-radius: inherit; + position: absolute; +} + +media-time-slider-pointer { + background-color: rgba(255, 255, 255, 0.5); + position: absolute; + pointer-events: none; +} + +media-time-slider-thumb { + width: 0.75rem; + height: 0.75rem; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 150ms ease; +} + +media-time-slider-root:hover media-time-slider-thumb { + transform: scale(1.2); +} diff --git a/website/src/examples/html/volume-slider/snippet-horizontal.html b/website/src/examples/html/volume-slider/snippet-horizontal.html new file mode 100644 index 00000000..b8fc054c --- /dev/null +++ b/website/src/examples/html/volume-slider/snippet-horizontal.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/src/examples/html/volume-slider/snippet-vertical.html b/website/src/examples/html/volume-slider/snippet-vertical.html new file mode 100644 index 00000000..2dfe75a4 --- /dev/null +++ b/website/src/examples/html/volume-slider/snippet-vertical.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/src/examples/html/volume-slider/volume-slider.css b/website/src/examples/html/volume-slider/volume-slider.css new file mode 100644 index 00000000..63fa227e --- /dev/null +++ b/website/src/examples/html/volume-slider/volume-slider.css @@ -0,0 +1,58 @@ +media-volume-slider-root { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* Horizontal orientation */ +media-volume-slider-root[data-orientation='horizontal'] { + width: 80px; + min-width: 80px; + height: 20px; +} + +/* Vertical orientation */ +media-volume-slider-root[data-orientation='vertical'] { + width: 20px; + height: 80px; + flex-direction: column; +} + +media-volume-slider-track { + position: relative; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.25rem; + overflow: hidden; +} + +/* Horizontal track */ +media-volume-slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.375rem; +} + +/* Vertical track */ +media-volume-slider-track[data-orientation='vertical'] { + width: 0.375rem; + height: 100%; +} + +media-volume-slider-progress { + background-color: #007bff; + border-radius: inherit; + position: absolute; +} + +media-volume-slider-thumb { + width: 0.75rem; + height: 0.75rem; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 150ms ease; +} + +media-volume-slider-root:hover media-volume-slider-thumb { + transform: scale(1.2); +} diff --git a/website/src/examples/react/FullscreenButton/BasicFullscreenButton.tsx b/website/src/examples/react/FullscreenButton/BasicFullscreenButton.tsx new file mode 100644 index 00000000..5a5b6807 --- /dev/null +++ b/website/src/examples/react/FullscreenButton/BasicFullscreenButton.tsx @@ -0,0 +1,21 @@ +import { FullscreenButton } from '@videojs/react'; +import { FullscreenEnterIcon, FullscreenExitIcon } from '@videojs/react/icons'; +import styles from './FullscreenButton.module.css'; + +/** + * Basic FullscreenButton example demonstrating: + * - Icon switching based on fullscreen state + * - Data attribute state selectors + * - Enter/exit fullscreen functionality + * + * Note: This component must be used within a MediaProvider context. + * See the usage example in the documentation. + */ +export function BasicFullscreenButton() { + return ( + + + + + ); +} diff --git a/website/src/examples/react/FullscreenButton/FullscreenButton.module.css b/website/src/examples/react/FullscreenButton/FullscreenButton.module.css new file mode 100644 index 00000000..07e0c724 --- /dev/null +++ b/website/src/examples/react/FullscreenButton/FullscreenButton.module.css @@ -0,0 +1,46 @@ +.button { + position: relative; + display: grid; + padding: 0.625rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + border: none; + cursor: pointer; + color: white; + transition: background 150ms ease; +} + +.button:hover { + background: rgba(255, 255, 255, 0.15); +} + +.button:active { + background: rgba(255, 255, 255, 0.2); +} + +/* Icon positioning - both occupy same grid cell */ +.fullscreenEnterIcon, +.fullscreenExitIcon { + grid-area: 1/1; + transition: opacity 200ms ease; + width: 18px; + height: 18px; +} + +/* Show/hide icons based on fullscreen state using data attributes */ +.button:not([data-fullscreen]) .fullscreenEnterIcon { + opacity: 1; +} + +.button:not([data-fullscreen]) .fullscreenExitIcon { + opacity: 0; +} + +.button[data-fullscreen] .fullscreenEnterIcon { + opacity: 0; +} + +.button[data-fullscreen] .fullscreenExitIcon { + opacity: 1; +} diff --git a/website/src/examples/react/FullscreenButton/FullscreenButtonDemo.tsx b/website/src/examples/react/FullscreenButton/FullscreenButtonDemo.tsx new file mode 100644 index 00000000..501ae24c --- /dev/null +++ b/website/src/examples/react/FullscreenButton/FullscreenButtonDemo.tsx @@ -0,0 +1,24 @@ +import { MediaContainer, MediaProvider, Video } from '@videojs/react'; +import { BasicFullscreenButton } from './BasicFullscreenButton'; + +/** + * Demo showing proper MediaProvider usage with FullscreenButton. + * The FullscreenButton automatically toggles fullscreen mode for + * the containing MediaContainer. + */ +export function FullscreenButtonDemo() { + return ( + + + + + ); +} diff --git a/website/src/examples/react/MuteButton/BasicMuteButton.tsx b/website/src/examples/react/MuteButton/BasicMuteButton.tsx new file mode 100644 index 00000000..014c342a --- /dev/null +++ b/website/src/examples/react/MuteButton/BasicMuteButton.tsx @@ -0,0 +1,22 @@ +import { MuteButton } from '@videojs/react'; +import { VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons'; +import styles from './MuteButton.module.css'; + +/** + * Basic MuteButton example demonstrating: + * - Multi-state icon switching (high/medium/low/off) + * - Volume level data attributes + * - Smooth icon transitions + * + * Note: This component must be used within a MediaProvider context. + * See the usage example in the documentation. + */ +export function BasicMuteButton() { + return ( + + + + + + ); +} diff --git a/website/src/examples/react/MuteButton/MuteButton.module.css b/website/src/examples/react/MuteButton/MuteButton.module.css new file mode 100644 index 00000000..aef039db --- /dev/null +++ b/website/src/examples/react/MuteButton/MuteButton.module.css @@ -0,0 +1,48 @@ +.button { + position: relative; + display: grid; + padding: 0.625rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + border: none; + cursor: pointer; + color: white; + transition: background 150ms ease; +} + +.button:hover { + background: rgba(255, 255, 255, 0.15); +} + +.button:active { + background: rgba(255, 255, 255, 0.2); +} + +/* Icon positioning - all icons occupy same grid cell */ +.volumeHighIcon, +.volumeLowIcon, +.volumeOffIcon { + grid-area: 1/1; + transition: opacity 200ms ease; + width: 18px; + height: 18px; + opacity: 0; +} + +/* Show appropriate icon based on volume level using data attributes */ +/* High volume (> 50%) */ +.button[data-volume-level='high'] .volumeHighIcon { + opacity: 1; +} + +/* Medium/Low volume (1-50%) */ +.button[data-volume-level='medium'] .volumeLowIcon, +.button[data-volume-level='low'] .volumeLowIcon { + opacity: 1; +} + +/* Muted/Off volume (0%) */ +.button[data-volume-level='off'] .volumeOffIcon { + opacity: 1; +} diff --git a/website/src/examples/react/MuteButton/MuteButtonDemo.tsx b/website/src/examples/react/MuteButton/MuteButtonDemo.tsx new file mode 100644 index 00000000..6be84474 --- /dev/null +++ b/website/src/examples/react/MuteButton/MuteButtonDemo.tsx @@ -0,0 +1,24 @@ +import { MediaContainer, MediaProvider, Video } from '@videojs/react'; +import { BasicMuteButton } from './BasicMuteButton'; + +/** + * Demo showing proper MediaProvider usage with MuteButton. + * The MuteButton automatically reflects the current volume state + * and toggles mute/unmute on click. + */ +export function MuteButtonDemo() { + return ( + + + + + ); +} diff --git a/website/src/examples/react/PlayButton/BasicPlayButton.tsx b/website/src/examples/react/PlayButton/BasicPlayButton.tsx new file mode 100644 index 00000000..95681240 --- /dev/null +++ b/website/src/examples/react/PlayButton/BasicPlayButton.tsx @@ -0,0 +1,21 @@ +import { PlayButton } from '@videojs/react'; +import { PauseIcon, PlayIcon } from '@videojs/react/icons'; +import styles from './PlayButton.module.css'; + +/** + * Basic PlayButton example demonstrating: + * - Icon switching based on paused state + * - CSS Modules for scoped styling + * - Data attribute selectors for state-based styling + * + * Note: This component must be used within a MediaProvider context. + * See the usage example in the documentation. + */ +export function BasicPlayButton() { + return ( + + + + + ); +} diff --git a/website/src/examples/react/PlayButton/PlayButton.module.css b/website/src/examples/react/PlayButton/PlayButton.module.css new file mode 100644 index 00000000..69742ba4 --- /dev/null +++ b/website/src/examples/react/PlayButton/PlayButton.module.css @@ -0,0 +1,46 @@ +.button { + position: relative; + display: grid; + padding: 0.625rem; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(12px); + border: none; + cursor: pointer; + color: white; + transition: background 150ms ease; +} + +.button:hover { + background: rgba(255, 255, 255, 0.15); +} + +.button:active { + background: rgba(255, 255, 255, 0.2); +} + +/* Icon positioning - both occupy same grid cell */ +.playIcon, +.pauseIcon { + grid-area: 1/1; + transition: opacity 200ms ease; + width: 18px; + height: 18px; +} + +/* Show/hide icons based on paused state using data attributes */ +.button[data-paused] .playIcon { + opacity: 1; +} + +.button[data-paused] .pauseIcon { + opacity: 0; +} + +.button:not([data-paused]) .playIcon { + opacity: 0; +} + +.button:not([data-paused]) .pauseIcon { + opacity: 1; +} diff --git a/website/src/examples/react/PlayButton/PlayButtonDemo.tsx b/website/src/examples/react/PlayButton/PlayButtonDemo.tsx new file mode 100644 index 00000000..2ce4e1d8 --- /dev/null +++ b/website/src/examples/react/PlayButton/PlayButtonDemo.tsx @@ -0,0 +1,24 @@ +import { MediaContainer, MediaProvider, Video } from '@videojs/react'; +import { BasicPlayButton } from './BasicPlayButton'; + +/** + * Demo showing proper MediaProvider usage with PlayButton. + * The MediaProvider wraps the entire media experience and provides + * the necessary context for all media components. + */ +export function PlayButtonDemo() { + return ( + + + + + ); +} diff --git a/website/src/examples/react/TimeSlider/BasicTimeSlider.tsx b/website/src/examples/react/TimeSlider/BasicTimeSlider.tsx new file mode 100644 index 00000000..e71d4c9f --- /dev/null +++ b/website/src/examples/react/TimeSlider/BasicTimeSlider.tsx @@ -0,0 +1,24 @@ +import { TimeSlider } from '@videojs/react'; +import styles from './TimeSlider.module.css'; + +/** + * Basic TimeSlider example demonstrating: + * - Progress and pointer visualization + * - Horizontal orientation + * - CSS Modules for scoped styling + * - Data attribute selectors for state-based styling + * + * Note: This component must be used within a MediaProvider context. + * See the usage example in the documentation. + */ +export function BasicTimeSlider() { + return ( + + + + + + + + ); +} diff --git a/website/src/examples/react/TimeSlider/TimeSlider.module.css b/website/src/examples/react/TimeSlider/TimeSlider.module.css new file mode 100644 index 00000000..ee481162 --- /dev/null +++ b/website/src/examples/react/TimeSlider/TimeSlider.module.css @@ -0,0 +1,64 @@ +.root { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* Horizontal orientation */ +.root[data-orientation='horizontal'] { + width: 100%; + min-width: 100px; + height: 20px; +} + +/* Vertical orientation */ +.root[data-orientation='vertical'] { + width: 20px; + height: 100px; + flex-direction: column; +} + +.track { + position: relative; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.25rem; + overflow: hidden; +} + +/* Horizontal track */ +.track[data-orientation='horizontal'] { + width: 100%; + height: 0.375rem; +} + +/* Vertical track */ +.track[data-orientation='vertical'] { + width: 0.375rem; + height: 100%; +} + +.progress { + background-color: #007bff; + border-radius: inherit; + position: absolute; +} + +.pointer { + background-color: rgba(255, 255, 255, 0.5); + position: absolute; + pointer-events: none; +} + +.thumb { + width: 0.75rem; + height: 0.75rem; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 150ms ease; +} + +.thumb:hover { + transform: scale(1.2); +} diff --git a/website/src/examples/react/TimeSlider/TimeSliderDemo.tsx b/website/src/examples/react/TimeSlider/TimeSliderDemo.tsx new file mode 100644 index 00000000..53cef782 --- /dev/null +++ b/website/src/examples/react/TimeSlider/TimeSliderDemo.tsx @@ -0,0 +1,24 @@ +import { MediaContainer, MediaProvider, Video } from '@videojs/react'; +import { BasicTimeSlider } from './BasicTimeSlider'; + +/** + * Demo showing proper MediaProvider usage with TimeSlider. + * The MediaProvider wraps the entire media experience and provides + * the necessary context for all media components. + */ +export function TimeSliderDemo() { + return ( + + + + + ); +} diff --git a/website/src/examples/react/VolumeSlider/BasicVolumeSlider.tsx b/website/src/examples/react/VolumeSlider/BasicVolumeSlider.tsx new file mode 100644 index 00000000..3f2d03a8 --- /dev/null +++ b/website/src/examples/react/VolumeSlider/BasicVolumeSlider.tsx @@ -0,0 +1,23 @@ +import { VolumeSlider } from '@videojs/react'; +import styles from './VolumeSlider.module.css'; + +/** + * Basic VolumeSlider example demonstrating: + * - Volume level visualization + * - Horizontal orientation + * - CSS Modules for scoped styling + * - Data attribute selectors for state-based styling + * + * Note: This component must be used within a MediaProvider context. + * See the usage example in the documentation. + */ +export function BasicVolumeSlider() { + return ( + + + + + + + ); +} diff --git a/website/src/examples/react/VolumeSlider/VolumeSlider.module.css b/website/src/examples/react/VolumeSlider/VolumeSlider.module.css new file mode 100644 index 00000000..03f231b3 --- /dev/null +++ b/website/src/examples/react/VolumeSlider/VolumeSlider.module.css @@ -0,0 +1,58 @@ +.root { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* Horizontal orientation */ +.root[data-orientation='horizontal'] { + width: 80px; + min-width: 80px; + height: 20px; +} + +/* Vertical orientation */ +.root[data-orientation='vertical'] { + width: 20px; + height: 80px; + flex-direction: column; +} + +.track { + position: relative; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.25rem; + overflow: hidden; +} + +/* Horizontal track */ +.track[data-orientation='horizontal'] { + width: 100%; + height: 0.375rem; +} + +/* Vertical track */ +.track[data-orientation='vertical'] { + width: 0.375rem; + height: 100%; +} + +.progress { + background-color: #007bff; + border-radius: inherit; + position: absolute; +} + +.thumb { + width: 0.75rem; + height: 0.75rem; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 150ms ease; +} + +.thumb:hover { + transform: scale(1.2); +} diff --git a/website/src/examples/react/VolumeSlider/VolumeSliderDemo.tsx b/website/src/examples/react/VolumeSlider/VolumeSliderDemo.tsx new file mode 100644 index 00000000..9c749053 --- /dev/null +++ b/website/src/examples/react/VolumeSlider/VolumeSliderDemo.tsx @@ -0,0 +1,24 @@ +import { MediaContainer, MediaProvider, Video } from '@videojs/react'; +import { BasicVolumeSlider } from './BasicVolumeSlider'; + +/** + * Demo showing proper MediaProvider usage with VolumeSlider. + * The MediaProvider wraps the entire media experience and provides + * the necessary context for all media components. + */ +export function VolumeSliderDemo() { + return ( + + + + + ); +} diff --git a/website/src/layouts/Docs.astro b/website/src/layouts/Docs.astro index 9fc37153..a031fda5 100644 --- a/website/src/layouts/Docs.astro +++ b/website/src/layouts/Docs.astro @@ -9,6 +9,7 @@ import Footer from '@/components/Footer.astro'; import Base from './Base.astro'; import { Selectors } from '@/components/docs/Selectors'; +import { getDocTitle } from '@/utils/docs/title'; type Props = { doc: CollectionEntry<'docs'>; @@ -19,12 +20,12 @@ type Props = { const { doc, framework, style, slug } = Astro.props; -// Fetch all docs to get their titles +// Fetch all docs to get their framework-specific titles const allDocs = await getCollection('docs'); -const docTitles = new Map(allDocs.map((d) => [d.id, d.data.title])); +const docTitles = new Map(allDocs.map((d) => [d.id, getDocTitle(d, framework)])); --- - +
[d.id, d.data.title])); +const docTitles = new Map(allDocs.map((d) => [d.id, getDocTitle(d, framework)])); // Build JSON-LD schema for TechArticle const pageUrl = new URL(Astro.url.pathname, Astro.site).toString(); const jsonLdSchema = createTechArticleSchema({ - title: doc.data.title, + title: getDocTitle(doc, framework), description: doc.data.description, url: pageUrl, updatedDate: doc.data.updatedDate, @@ -120,7 +121,7 @@ const jsonLdSchema = createTechArticleSchema({
-

{doc.data.title}

+

{getDocTitle(doc, framework)}

{doc.data.description}

diff --git a/website/src/types/docs.ts b/website/src/types/docs.ts index 7d28c88e..13c46a09 100644 --- a/website/src/types/docs.ts +++ b/website/src/types/docs.ts @@ -1,6 +1,6 @@ export const FRAMEWORK_STYLES = { - react: ['css', 'tailwind'], - html: ['css', 'tailwind'], + react: ['css'], + html: ['css'], } as const; export type SupportedFramework = keyof typeof FRAMEWORK_STYLES; diff --git a/website/src/utils/docs/__tests__/routing.test.ts b/website/src/utils/docs/__tests__/routing.test.ts index 9151c8ff..0ced206d 100644 --- a/website/src/utils/docs/__tests__/routing.test.ts +++ b/website/src/utils/docs/__tests__/routing.test.ts @@ -1,5 +1,5 @@ import type { Guide, Sidebar } from '../../../types/docs'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { resolveDocsLinkUrl, resolveFrameworkChange, @@ -7,6 +7,61 @@ import { resolveStyleChange, } from '../routing'; +// Mock the validation functions from @/types/docs to use mock framework/style configuration +// Note: This mock is hoisted, so we define MOCK_FRAMEWORK_STYLES inside the factory +vi.mock('@/types/docs', async () => { + const actual = await vi.importActual('@/types/docs'); + + // Mock framework/style configuration for tests + // This allows tests to remain stable when the actual FRAMEWORK_STYLES changes + const MOCK_FRAMEWORK_STYLES = { + html: ['css', 'tailwind'], + react: ['css', 'tailwind'], + } as const; + + type MockFramework = keyof typeof MOCK_FRAMEWORK_STYLES; + type MockStyle = typeof MOCK_FRAMEWORK_STYLES[MockFramework][number]; + + return { + ...actual, + // Mock FRAMEWORK_STYLES to match our test config + FRAMEWORK_STYLES: MOCK_FRAMEWORK_STYLES, + + // Mock DEFAULT_FRAMEWORK + DEFAULT_FRAMEWORK: 'react' as const, + + // Mock isValidFramework to check against mock frameworks + isValidFramework: (value: string | undefined | null): value is MockFramework => { + if (!value) return false; + return value === 'html' || value === 'react'; + }, + + // Mock isValidStyleForFramework to check against mock styles + isValidStyleForFramework: ( + framework: MockFramework, + style: string | undefined | null, + ): style is MockStyle => { + if (!style) return false; + return MOCK_FRAMEWORK_STYLES[framework]?.includes(style as MockStyle) ?? false; + }, + + // Mock getDefaultStyle to return first style from mock config + getDefaultStyle: (framework: F): MockStyle => { + return MOCK_FRAMEWORK_STYLES[framework][0]; + }, + }; +}); + +// Re-export the mock types for use in tests +// Prefixed with _ to indicate it's only used for type derivation +const _MOCK_FRAMEWORK_STYLES = { + html: ['css', 'tailwind'], + react: ['css', 'tailwind'], +} as const; + +type MockFramework = keyof typeof _MOCK_FRAMEWORK_STYLES; +type MockStyle = typeof _MOCK_FRAMEWORK_STYLES[MockFramework][number]; + describe('routing utilities', () => { // Test fixtures - comprehensive mock sidebar for testing const guideForEveryone: Guide = { @@ -16,24 +71,24 @@ describe('routing utilities', () => { const guideReactOnly: Guide = { slug: 'concepts/react-only', - frameworks: ['react'], + frameworks: ['react'] satisfies MockFramework[], }; const guideTailwindOnly: Guide = { slug: 'concepts/tailwind-only', - styles: ['tailwind'], + styles: ['tailwind'] satisfies MockStyle[], }; const guideHtmlCssOnly: Guide = { slug: 'how-to/html-css-only', - frameworks: ['html'], - styles: ['css'], + frameworks: ['html'] satisfies MockFramework[], + styles: ['css'] satisfies MockStyle[], }; const guideReactTailwind: Guide = { slug: 'how-to/react-tailwind', - frameworks: ['react'], - styles: ['tailwind'], + frameworks: ['react'] satisfies MockFramework[], + styles: ['tailwind'] satisfies MockStyle[], }; const mockSidebar: Sidebar = [ @@ -210,9 +265,8 @@ describe('routing utilities', () => { }); it('should change style to default when current style invalid for new framework', () => { - // Create a temporary guide with a framework that only supports one style - // Since both html and react support css and tailwind, we'll test with an edge case - // where we'd need to demonstrate style adjustment. For now, this tests the logic path. + // Since both html and react support css and tailwind in our mock, we'll test + // the logic by verifying that valid styles are kept const result = resolveFrameworkChange({ currentFramework: 'react', currentStyle: 'tailwind', @@ -221,7 +275,7 @@ describe('routing utilities', () => { }, mockSidebar); expect(result.selectedFramework).toBe('html'); - // tailwind is also valid for html, so it will be kept + // tailwind is valid for both frameworks, so it will be kept expect(result.selectedStyle).toBe('tailwind'); expect(result.reason).toContain('kept style'); }); @@ -439,10 +493,6 @@ describe('routing utilities', () => { describe('priority 4: change both framework and style', () => { it('should use guide\'s first valid framework and style as fallback', () => { - // Use concepts/react-only with HTML + styled-components - // - html doesn't support react-only (framework restriction) - // - react doesn't support styled-components... wait, it does! - // Let me use a different test case const result = resolveDocsLinkUrl({ targetSlug: 'concepts/react-only', contextFramework: 'html', // doesn't support react-only diff --git a/website/src/utils/docs/__tests__/sidebar.test.ts b/website/src/utils/docs/__tests__/sidebar.test.ts index 3f484f8e..046d5ef4 100644 --- a/website/src/utils/docs/__tests__/sidebar.test.ts +++ b/website/src/utils/docs/__tests__/sidebar.test.ts @@ -1,5 +1,5 @@ import type { Guide, Section, Sidebar } from '../../../types/docs'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { filterSidebar, findFirstGuide, @@ -8,18 +8,59 @@ import { getValidStylesForGuide, } from '../sidebar'; +// Mock FRAMEWORK_STYLES from @/types/docs to use our test config +// Note: This mock is hoisted, so we define MOCK_FRAMEWORK_STYLES inside the factory +vi.mock('@/types/docs', async () => { + const actual = await vi.importActual('@/types/docs'); + + // Mock framework/style configuration for tests + // This allows tests to remain stable when the actual FRAMEWORK_STYLES changes + const MOCK_FRAMEWORK_STYLES = { + html: ['css', 'tailwind'], + react: ['css', 'tailwind'], + } as const; + + type MockFramework = keyof typeof MOCK_FRAMEWORK_STYLES; + type MockStyle = typeof MOCK_FRAMEWORK_STYLES[MockFramework][number]; + + return { + ...actual, + // Mock FRAMEWORK_STYLES to match our test config + FRAMEWORK_STYLES: MOCK_FRAMEWORK_STYLES, + + // Mock isValidStyleForFramework to check against mock styles + isValidStyleForFramework: ( + framework: MockFramework, + style: string | undefined | null, + ): style is MockStyle => { + if (!style) return false; + return MOCK_FRAMEWORK_STYLES[framework]?.includes(style as MockStyle) ?? false; + }, + }; +}); + +// Re-export the mock types for use in tests +// Prefixed with _ to indicate it's only used for type derivation +const _MOCK_FRAMEWORK_STYLES = { + html: ['css', 'tailwind'], + react: ['css', 'tailwind'], +} as const; + +type MockFramework = keyof typeof _MOCK_FRAMEWORK_STYLES; +type MockStyle = typeof _MOCK_FRAMEWORK_STYLES[MockFramework][number]; + describe('sidebar utilities', () => { - // Test fixtures + // Test fixtures using mock framework/style values const mockGuide1: Guide = { slug: 'guide-1', - frameworks: ['html', 'react'], - styles: ['css', 'tailwind'], + frameworks: ['html', 'react'] satisfies MockFramework[], + styles: ['css', 'tailwind'] satisfies MockStyle[], }; const mockGuide2: Guide = { slug: 'guide-2', - frameworks: ['react'], - styles: ['tailwind'], + frameworks: ['react'] satisfies MockFramework[], + styles: ['tailwind'] satisfies MockStyle[], }; const mockGuide3: Guide = { @@ -29,7 +70,7 @@ describe('sidebar utilities', () => { const mockSection: Section = { sidebarLabel: 'Section 1', - frameworks: ['html', 'react'], + frameworks: ['html', 'react'] satisfies MockFramework[], contents: [mockGuide1, mockGuide2], }; @@ -217,25 +258,31 @@ describe('sidebar utilities', () => { it('should return framework styles that guide supports', () => { const result = getValidStylesForGuide(mockGuide1, 'html'); + // Should return the intersection of mockGuide1 styles and html framework styles + // With our mock, both have ['css', 'tailwind'], so intersection is both expect(result).toEqual(['css', 'tailwind']); }); it('should return only styles that both framework and guide support', () => { const result = getValidStylesForGuide(mockGuide1, 'react'); + // Should return the intersection of mockGuide1 styles and react framework styles + // With our mock, both have ['css', 'tailwind'], so intersection is both expect(result).toEqual(['css', 'tailwind']); }); it('should return all framework styles if guide has no restrictions', () => { const result = getValidStylesForGuide(mockGuide3, 'react'); + // Since mockGuide3 has no style restrictions, it should return all framework styles + // With our mock, react supports ['css', 'tailwind'] expect(result).toEqual(['css', 'tailwind']); }); it('should handle guide with limited style support', () => { const cssOnlyGuide: Guide = { slug: 'css-only', - styles: ['css'], + styles: ['css'] satisfies MockStyle[], }; const result = getValidStylesForGuide(cssOnlyGuide, 'react'); @@ -246,8 +293,8 @@ describe('sidebar utilities', () => { // Guide that only supports html framework, but we check with react const cssOnlyGuide: Guide = { slug: 'css-only-test', - styles: ['css'], - frameworks: ['html'], + styles: ['css'] satisfies MockStyle[], + frameworks: ['html'] satisfies MockFramework[], }; const result = getValidStylesForGuide(cssOnlyGuide, 'react'); @@ -258,13 +305,14 @@ describe('sidebar utilities', () => { it('should return all guide styles when guide has style restrictions', () => { const result = getValidStylesForGuide(mockGuide1); - expect(result).toEqual(['css', 'tailwind']); + expect(result).toEqual(['css', 'tailwind'] satisfies MockStyle[]); }); it('should return all possible styles when guide has no restrictions', () => { const result = getValidStylesForGuide(mockGuide3); - // Should include all styles from all frameworks (css and tailwind for both react and html) + // Should include all styles from MOCK_FRAMEWORK_STYLES (deduplicated) + // Both frameworks support ['css', 'tailwind'], so we get both expect(result).toEqual(expect.arrayContaining(['css', 'tailwind'])); expect(result).toHaveLength(2); }); @@ -272,7 +320,7 @@ describe('sidebar utilities', () => { it('should return guide-specific styles for limited support guides', () => { const tailwindOnlyGuide: Guide = { slug: 'tailwind-only', - styles: ['tailwind'], + styles: ['tailwind'] satisfies MockStyle[], }; const result = getValidStylesForGuide(tailwindOnlyGuide); diff --git a/website/src/utils/docs/__tests__/title.test.ts b/website/src/utils/docs/__tests__/title.test.ts new file mode 100644 index 00000000..eddce745 --- /dev/null +++ b/website/src/utils/docs/__tests__/title.test.ts @@ -0,0 +1,130 @@ +import type { CollectionEntry } from 'astro:content'; +import { describe, expect, it } from 'vitest'; +import { getDocTitle } from '../title'; + +describe('getDocTitle', () => { + // Mock fixtures + const mockDocWithFrameworkTitle: CollectionEntry<'docs'> = { + id: 'resources/play-button', + collection: 'docs', + data: { + title: 'PlayButton', + description: 'A button component for playing and pausing media playback', + frameworkTitle: { + react: 'PlayButton', + html: 'play-button', + }, + }, + // Mock required Astro fields + body: '', + slug: 'resources/play-button', + } as CollectionEntry<'docs'>; + + const mockDocWithoutFrameworkTitle: CollectionEntry<'docs'> = { + id: 'concepts/basic', + collection: 'docs', + data: { + title: 'Basic Concepts', + description: 'Introduction to basic concepts', + }, + body: '', + slug: 'concepts/basic', + } as CollectionEntry<'docs'>; + + const mockDocWithPartialFrameworkTitle: CollectionEntry<'docs'> = { + id: 'resources/mute-button', + collection: 'docs', + data: { + title: 'MuteButton', + description: 'A button for muting audio', + frameworkTitle: { + react: 'MuteButton', + // html framework title not defined + }, + }, + body: '', + slug: 'resources/mute-button', + } as CollectionEntry<'docs'>; + + describe('with frameworkTitle defined', () => { + it('should return framework-specific title when available', () => { + const reactTitle = getDocTitle(mockDocWithFrameworkTitle, 'react'); + const htmlTitle = getDocTitle(mockDocWithFrameworkTitle, 'html'); + + expect(reactTitle).toBe('PlayButton'); + expect(htmlTitle).toBe('play-button'); + }); + + it('should return react title for react framework', () => { + const result = getDocTitle(mockDocWithFrameworkTitle, 'react'); + + expect(result).toBe('PlayButton'); + }); + + it('should return html title for html framework', () => { + const result = getDocTitle(mockDocWithFrameworkTitle, 'html'); + + expect(result).toBe('play-button'); + }); + + it('should handle different titles for different frameworks', () => { + const reactTitle = getDocTitle(mockDocWithFrameworkTitle, 'react'); + const htmlTitle = getDocTitle(mockDocWithFrameworkTitle, 'html'); + + expect(reactTitle).not.toBe(htmlTitle); + expect(reactTitle).toBe('PlayButton'); + expect(htmlTitle).toBe('play-button'); + }); + }); + + describe('without frameworkTitle', () => { + it('should fall back to default title when frameworkTitle is undefined', () => { + const reactTitle = getDocTitle(mockDocWithoutFrameworkTitle, 'react'); + const htmlTitle = getDocTitle(mockDocWithoutFrameworkTitle, 'html'); + + expect(reactTitle).toBe('Basic Concepts'); + expect(htmlTitle).toBe('Basic Concepts'); + }); + + it('should return same title for both frameworks when no frameworkTitle', () => { + const reactTitle = getDocTitle(mockDocWithoutFrameworkTitle, 'react'); + const htmlTitle = getDocTitle(mockDocWithoutFrameworkTitle, 'html'); + + expect(reactTitle).toBe(htmlTitle); + }); + }); + + describe('partial frameworkTitle', () => { + it('should return frameworkTitle for defined framework', () => { + const reactTitle = getDocTitle(mockDocWithPartialFrameworkTitle, 'react'); + + expect(reactTitle).toBe('MuteButton'); + }); + + it('should fall back to default title when framework not in frameworkTitle', () => { + const htmlTitle = getDocTitle(mockDocWithPartialFrameworkTitle, 'html'); + + expect(htmlTitle).toBe('MuteButton'); // Falls back to default title + }); + }); + + describe('edge cases', () => { + it('should handle empty frameworkTitle object', () => { + const docWithEmptyFrameworkTitle: CollectionEntry<'docs'> = { + id: 'edge-case/empty', + collection: 'docs', + data: { + title: 'Default Title', + description: 'Test', + frameworkTitle: {}, + }, + body: '', + slug: 'edge-case/empty', + } as CollectionEntry<'docs'>; + + const result = getDocTitle(docWithEmptyFrameworkTitle, 'react'); + + expect(result).toBe('Default Title'); + }); + }); +}); diff --git a/website/src/utils/docs/title.ts b/website/src/utils/docs/title.ts new file mode 100644 index 00000000..5fecb114 --- /dev/null +++ b/website/src/utils/docs/title.ts @@ -0,0 +1,17 @@ +import type { CollectionEntry } from 'astro:content'; +import type { SupportedFramework } from '@/types/docs'; + +/** + * Get the title for a document, using framework-specific title if available, + * otherwise falling back to the default title. + * + * @param doc - The document from the docs collection + * @param framework - The framework context (react or html) + * @returns The framework-specific title or default title + */ +export function getDocTitle( + doc: CollectionEntry<'docs'>, + framework: SupportedFramework, +): string { + return doc.data.frameworkTitle?.[framework] ?? doc.data.title; +}