Files
v10/.claude/skills/api-reference/references/demo-patterns.md
T
2026-03-06 16:19:27 -06:00

278 lines
7.4 KiB
Markdown

# Demo Patterns
File structure and conventions for interactive component demos.
## Directory Structure
```
site/src/components/docs/demos/{component}/
├── html/css/
│ ├── BasicUsage.astro # Astro wrapper (renders HTML, imports CSS, bundles script)
│ ├── BasicUsage.html # Markup only (no <style> or <script>)
│ ├── BasicUsage.css # Styles
│ └── BasicUsage.ts # Side-effect imports for custom element registration
└── react/css/
├── BasicUsage.tsx # React component
└── BasicUsage.css # Styles
```
## BEM Naming
Block = `{framework}-{component}-{variant}`, element = `__{part}`:
```
html-play-button-basic /* HTML framework, block */
html-play-button-basic__button /* HTML framework, element */
react-play-button-basic /* React framework, block */
react-play-button-basic__button /* React framework, element */
```
The framework prefix (`html-` / `react-`) prevents CSS leaking between HTML and React demos on the same page (both render but one is hidden).
## HTML Demo Files
### .astro wrapper
```astro
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
import './BasicUsage.ts';
</script>
```
The `.astro` wrapper is required because only Astro `<script>` tags go through Vite's bundling pipeline.
### .html (markup only)
```html
<video-player class="html-mute-button-basic">
<video
src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
autoplay
muted
playsinline
loop
></video>
<media-mute-button class="html-mute-button-basic__button">
<span class="show-when-muted">Unmute</span>
<span class="show-when-unmuted">Mute</span>
</media-mute-button>
</video-player>
```
- No `<style>` or `<script>` tags
- Video attributes: `autoplay muted playsinline loop`
- State labels use CSS class names toggled by data attributes
### .css (styles)
```css
.html-mute-button-basic {
position: relative;
}
.html-mute-button-basic video {
width: 100%;
}
.html-mute-button-basic__button {
padding-block: 8px;
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
padding-inline: 20px;
cursor: pointer;
}
/* State-based visibility via data attributes */
.html-mute-button-basic__button .show-when-muted { display: none; }
.html-mute-button-basic__button .show-when-unmuted { display: none; }
.html-mute-button-basic__button[data-muted] .show-when-muted { display: inline; }
.html-mute-button-basic__button:not([data-muted]) .show-when-unmuted { display: inline; }
```
### .ts (registration imports)
```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/mute-button';
```
Import registration for:
- `@videojs/html/video/player` — always needed (registers `<video-player>`)
- `@videojs/html/ui/{component}` — registers the component's custom element
## React Demo Files
### .tsx (component)
```tsx
import { createPlayer, MuteButton } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const Player = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
<Player.Provider>
<Player.Container className="react-mute-button-basic">
<Video
src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
autoPlay
muted
playsInline
loop
/>
<MuteButton
className="react-mute-button-basic__button"
render={(props, state) => (
<button {...props}>{state.muted ? 'Unmute' : 'Mute'}</button>
)}
/>
</Player.Container>
</Player.Provider>
);
}
```
Key patterns:
- `createPlayer({ features: videoFeatures })` creates the player
- Video attributes: `autoPlay muted playsInline loop` (React camelCase)
- `render` prop for state-based rendering: `render={(props, state) => ...}`
- Spread `{...props}` on the rendered element for accessibility attributes
### .css (styles)
Same base styling as HTML but with `react-` BEM prefix:
```css
.react-mute-button-basic {
position: relative;
}
.react-mute-button-basic video {
width: 100%;
}
.react-mute-button-basic__button {
padding-block: 8px;
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
padding-inline: 20px;
cursor: pointer;
}
```
React CSS files typically don't need data-attribute selectors since the `render` prop handles state-based rendering. Include them only when CSS state reflection is used.
## State Reflection Patterns
### HTML: Data attribute selectors
Components expose state via `data-*` attributes. CSS toggles visibility:
```css
/* Hide all by default */
.html-play-button-basic__button .show-when-paused { display: none; }
.html-play-button-basic__button .show-when-playing { display: none; }
/* Show based on state */
.html-play-button-basic__button[data-paused] .show-when-paused { display: inline; }
.html-play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }
```
For multi-value attributes (e.g., `data-volume-level`):
```css
.html-mute-button-volume-levels__button .level-off,
.html-mute-button-volume-levels__button .level-low,
.html-mute-button-volume-levels__button .level-medium,
.html-mute-button-volume-levels__button .level-high {
display: none;
}
.html-mute-button-volume-levels__button[data-volume-level="off"] .level-off { display: inline; }
.html-mute-button-volume-levels__button[data-volume-level="low"] .level-low { display: inline; }
```
### React: Render prop
```tsx
<MuteButton
render={(props, state) => (
<button {...props}>
{state.volumeLevel === 'off'
? 'Off'
: state.volumeLevel === 'low'
? 'Low'
: state.volumeLevel === 'medium'
? 'Medium'
: 'High'}
</button>
)}
/>
```
### Three-state pattern (Play/Pause/Replay)
HTML uses `:not()` combinators to handle mutually exclusive states:
```css
.html-play-button-basic__button[data-paused]:not([data-ended]) .show-when-paused { display: inline; }
.html-play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }
.html-play-button-basic__button[data-ended] .show-when-ended { display: inline; }
```
React uses nested ternary in the render prop:
```tsx
render={(props, state) => (
<button {...props}>{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}</button>
)}
```
## Video Sources
- **Video**: `https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4`
- **Poster**: `https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg`
## Base Button Styles
All button demos share this base overlay style:
```css
.__button {
padding-block: 8px;
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
padding-inline: 20px;
cursor: pointer;
}
```