mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: eject examples (#149)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HTML Frosted Ejected</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family:
|
||||
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
margin: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
"
|
||||
>
|
||||
<media-provider style="display: block">
|
||||
<media-container
|
||||
style="
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2rem;
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
aspect-ratio: 16/9;
|
||||
"
|
||||
>
|
||||
<video
|
||||
src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4"
|
||||
playsinline
|
||||
poster="https://image.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/thumbnail.webp"
|
||||
></video>
|
||||
|
||||
<div class="overlay"></div>
|
||||
|
||||
<div class="control-bar surface">
|
||||
<media-tooltip delay="500" close-delay="0">
|
||||
<media-tooltip-trigger>
|
||||
<media-play-button class="button">
|
||||
<media-play-icon class="icon play-icon"></media-play-icon>
|
||||
<media-pause-icon class="icon pause-icon"></media-pause-icon>
|
||||
</media-play-button>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="12" collision-padding="12">
|
||||
<media-tooltip-popup class="surface popup-animation">
|
||||
<span class="tooltip play-tooltip">Play</span>
|
||||
<span class="tooltip pause-tooltip">Pause</span>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip>
|
||||
|
||||
<div class="time-controls">
|
||||
<!-- Use the show-remaining attribute to show count down/remaining time -->
|
||||
<media-current-time-display></media-current-time-display>
|
||||
|
||||
<media-tooltip track-cursor-axis="x">
|
||||
<media-tooltip-trigger>
|
||||
<media-time-slider class="slider">
|
||||
<media-time-slider-track class="slider-track">
|
||||
<media-time-slider-progress class="slider-progress"></media-time-slider-progress>
|
||||
<media-time-slider-pointer class="slider-pointer"></media-time-slider-pointer>
|
||||
</media-time-slider-track>
|
||||
<media-time-slider-thumb class="slider-thumb"></media-time-slider-thumb>
|
||||
</media-time-slider>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="18" collision-padding="12">
|
||||
<media-tooltip-popup class="surface popup-animation">
|
||||
<media-preview-time-display></media-preview-time-display>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip>
|
||||
|
||||
<media-duration-display></media-duration-display>
|
||||
</div>
|
||||
|
||||
<media-mute-button popovertarget="volume-slider-popover" class="button">
|
||||
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
|
||||
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
|
||||
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
|
||||
</media-mute-button>
|
||||
<media-popover
|
||||
id="volume-slider-popover"
|
||||
class="surface popup-animation"
|
||||
popover
|
||||
open-on-hover
|
||||
delay="200"
|
||||
close-delay="100"
|
||||
side="top"
|
||||
side-offset="12"
|
||||
collision-padding="12"
|
||||
>
|
||||
<media-volume-slider class="slider" orientation="vertical">
|
||||
<media-volume-slider-track class="slider-track">
|
||||
<media-volume-slider-progress class="slider-progress"></media-volume-slider-progress>
|
||||
</media-volume-slider-track>
|
||||
<media-volume-slider-thumb class="slider-thumb"></media-volume-slider-thumb>
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
|
||||
<media-tooltip delay="500" close-delay="0">
|
||||
<media-tooltip-trigger>
|
||||
<media-fullscreen-button class="button">
|
||||
<media-fullscreen-enter-icon class="icon fullscreen-enter-icon"></media-fullscreen-enter-icon>
|
||||
<media-fullscreen-exit-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-icon>
|
||||
</media-fullscreen-button>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="12" collision-padding="12">
|
||||
<media-tooltip-popup class="surface popup-animation">
|
||||
<span class="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
|
||||
<span class="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip>
|
||||
</div>
|
||||
</media-container>
|
||||
</media-provider>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "html-eject-frosted",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "HTML example for Video.js 10",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@videojs/html": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
media-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
media-container {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
media-container::before,
|
||||
media-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 10;
|
||||
}
|
||||
media-container::before {
|
||||
inset: 1px;
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
|
||||
}
|
||||
media-container::after {
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Fullscreen */
|
||||
media-container:fullscreen {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
media-container > ::slotted([slot='media']),
|
||||
media-container > video,
|
||||
media-container > audio {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Media Container UI Overlay Styling */
|
||||
media-container > .overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: start;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
|
||||
backdrop-filter: saturate(1.5) brightness(0.9);
|
||||
transition: opacity 0.15s ease-out;
|
||||
transition-delay: 500ms;
|
||||
opacity: 0;
|
||||
}
|
||||
media-container:hover > .overlay,
|
||||
media-container:has([data-paused]) > .overlay,
|
||||
media-container:has([aria-expanded='true']) > .overlay {
|
||||
opacity: 1;
|
||||
transition-duration: 100ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
/* Common Surface Styles - e.g. tooltips, popovers, controls */
|
||||
.surface {
|
||||
background-color: oklab(1 0 0 / 0.1);
|
||||
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklab(1 0 0 / 0.15),
|
||||
0 0 0 1px oklab(0 0 0 / 0.15),
|
||||
oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
|
||||
oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
|
||||
}
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.surface {
|
||||
background-color: oklab(0 0 0 / 0.7);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklab(0 0 0),
|
||||
0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
@media (prefers-contrast: more) {
|
||||
.surface {
|
||||
background-color: oklab(0 0 0 / 0.9);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklab(0 0 0),
|
||||
0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Media Control Bar UI/Styles */
|
||||
.control-bar {
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
inset-inline: 0.75rem;
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
will-change: scale, transform, filter, opacity;
|
||||
scale: 0.9;
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition-property: scale, transform, filter, opacity;
|
||||
transition-delay: 500ms;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease-out;
|
||||
transform-origin: bottom;
|
||||
color: oklab(1 0 0);
|
||||
}
|
||||
media-container:hover > .control-bar,
|
||||
media-container:has([data-paused]) > .control-bar,
|
||||
media-container:has([aria-expanded='true']) > .control-bar {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
filter: blur(0px);
|
||||
transition-delay: 0ms;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* Time Display Styling */
|
||||
.time-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 0.75rem;
|
||||
padding-inline: 0.375rem;
|
||||
}
|
||||
media-current-time-display,
|
||||
media-duration-display {
|
||||
text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Generic Media Button Styling */
|
||||
.button {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: calc(infinity * 1px);
|
||||
color: oklab(1 0 0 / 0.9);
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition-property: background-color, color, outline-offset;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.button:hover,
|
||||
.button:focus-visible,
|
||||
.button[aria-expanded='true'] {
|
||||
background-color: oklab(1 0 0 / 0.1);
|
||||
color: oklab(1 0 0);
|
||||
text-decoration: none;
|
||||
}
|
||||
.button:focus-visible {
|
||||
outline-color: oklch(62.3% 0.214 259.815);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.button[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.button .icon {
|
||||
grid-area: 1 / 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
|
||||
}
|
||||
|
||||
/* Media Play Button UI/Styles */
|
||||
media-play-button .icon {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
media-play-button:not([data-paused]) .pause-icon,
|
||||
media-play-button[data-paused] .play-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Media Fullscreen Button UI/Styles */
|
||||
media-fullscreen-button .icon {
|
||||
display: none;
|
||||
}
|
||||
media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
|
||||
media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* One way to define the "default visible" icon (CJP) */
|
||||
media-mute-button .icon {
|
||||
display: none;
|
||||
}
|
||||
media-mute-button:not([data-volume-level]) .volume-low-icon,
|
||||
media-mute-button[data-volume-level='high'] .volume-high-icon,
|
||||
media-mute-button[data-volume-level='low'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='medium'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* TimeSlider Component Styles */
|
||||
.slider {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-radius: calc(infinity * 1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Horizontal orientation styles */
|
||||
.slider[data-orientation='horizontal'] {
|
||||
min-width: 5rem;
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Vertical orientation styles */
|
||||
.slider[data-orientation='vertical'] {
|
||||
height: 5rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background-color: oklab(1 0 0 / 0.2);
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition: outline-offset 150ms ease-out;
|
||||
box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
/* Horizontal track styles */
|
||||
.slider-track[data-orientation='horizontal'] {
|
||||
width: 100%;
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
/* Vertical track styles */
|
||||
.slider-track[data-orientation='vertical'] {
|
||||
width: 0.25rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slider:focus-visible .slider-track {
|
||||
outline-color: oklch(62.3% 0.214 259.815);
|
||||
outline-offset: 6px;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: calc(infinity * 1px);
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
box-shadow:
|
||||
0 0 0 1px oklab(0 0 0 / 0.1),
|
||||
0 1px 3px 0 oklab(0 0 0 / 0.15),
|
||||
0 1px 2px -1px oklab(0 0 0 / 0.15);
|
||||
opacity: 0;
|
||||
transition-property: opacity, height, width;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.slider-thumb:active {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
.slider:hover .slider-thumb,
|
||||
.slider:focus-within .slider-thumb {
|
||||
opacity: 1;
|
||||
}
|
||||
.slider-track[data-orientation='horizontal'] .slider-thumb {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.slider-track[data-orientation='vertical'] .slider-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.slider-pointer {
|
||||
background-color: oklab(1 0 0 / 0.2);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.slider-progress {
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
media-preview-time-display {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.popup-animation {
|
||||
transition-property: transform, scale, opacity, filter;
|
||||
transition-duration: 200ms;
|
||||
transform: scale(1);
|
||||
transform-origin: bottom;
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
.popup-animation[data-starting-style],
|
||||
.popup-animation[data-ending-style] {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
}
|
||||
.popup-animation[data-instant] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
media-popover {
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
||||
/* Tooltip Component Styles */
|
||||
media-tooltip-popup {
|
||||
color: oklab(1 0 0);
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
media-tooltip-popup[data-paused] .play-tooltip,
|
||||
media-tooltip-popup:not([data-paused]) .pause-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
|
||||
media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import './frosted.css';
|
||||
import '@videojs/html/icons';
|
||||
import '@videojs/html/define/media-container';
|
||||
import '@videojs/html/define/media-provider';
|
||||
import '@videojs/html/define/media-play-button';
|
||||
import '@videojs/html/define/media-mute-button';
|
||||
import '@videojs/html/define/media-volume-slider';
|
||||
import '@videojs/html/define/media-time-slider';
|
||||
import '@videojs/html/define/media-fullscreen-button';
|
||||
import '@videojs/html/define/media-duration-display';
|
||||
import '@videojs/html/define/media-current-time-display';
|
||||
import '@videojs/html/define/media-preview-time-display';
|
||||
import '@videojs/html/define/media-popover';
|
||||
import '@videojs/html/define/media-tooltip';
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5174,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@vjs/html'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HTML Minimal Ejected</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family:
|
||||
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
margin: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
"
|
||||
>
|
||||
<media-provider style="display: block">
|
||||
<media-container
|
||||
style="
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2rem;
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
aspect-ratio: 16/9;
|
||||
"
|
||||
>
|
||||
<video
|
||||
src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4"
|
||||
playsinline
|
||||
poster="https://image.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/thumbnail.webp"
|
||||
></video>
|
||||
|
||||
<div class="overlay"></div>
|
||||
|
||||
<div class="control-bar">
|
||||
<!-- NOTE: We can decide if we further want to provide a further, "themed" media-play-button that comes with baked in default styles and icons. (CJP) -->
|
||||
<media-tooltip delay="500" close-delay="0">
|
||||
<media-tooltip-trigger>
|
||||
<media-play-button class="button">
|
||||
<media-play-icon class="icon play-icon"></media-play-icon>
|
||||
<media-pause-icon class="icon pause-icon"></media-pause-icon>
|
||||
</media-play-button>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="6" collision-padding="12">
|
||||
<media-tooltip-popup class="popup-animation">
|
||||
<span class="tooltip play-tooltip">Play</span>
|
||||
<span class="tooltip pause-tooltip">Pause</span>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip>
|
||||
|
||||
<div class="time-display-group">
|
||||
<!-- Use the show-remaining attribute to show count down/remaining time -->
|
||||
<media-current-time-display></media-current-time-display>
|
||||
|
||||
<span class="duration-display">
|
||||
/
|
||||
<media-duration-display></media-duration-display>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<media-tooltip track-cursor-axis="x">
|
||||
<media-tooltip-trigger>
|
||||
<media-time-slider class="slider">
|
||||
<media-time-slider-track class="slider-track">
|
||||
<media-time-slider-progress class="slider-progress"></media-time-slider-progress>
|
||||
<media-time-slider-pointer class="slider-pointer"></media-time-slider-pointer>
|
||||
</media-time-slider-track>
|
||||
<media-time-slider-thumb class="slider-thumb"></media-time-slider-thumb>
|
||||
</media-time-slider>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="12" collision-padding="12">
|
||||
<media-tooltip-popup class="popup-animation">
|
||||
<media-preview-time-display></media-preview-time-display>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip>
|
||||
|
||||
<div class="button-group">
|
||||
<media-mute-button popovertarget="volume-slider-popover" class="button">
|
||||
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
|
||||
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
|
||||
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
|
||||
</media-mute-button>
|
||||
<media-popover
|
||||
id="volume-slider-popover"
|
||||
class="popup-animation"
|
||||
popover
|
||||
open-on-hover
|
||||
delay="200"
|
||||
close-delay="100"
|
||||
side="top"
|
||||
side-offset="2"
|
||||
collision-padding="12"
|
||||
>
|
||||
<media-volume-slider class="slider" orientation="vertical">
|
||||
<media-volume-slider-track class="slider-track">
|
||||
<media-volume-slider-progress class="slider-progress"></media-volume-slider-progress>
|
||||
</media-volume-slider-track>
|
||||
<media-volume-slider-thumb class="slider-thumb"></media-volume-slider-thumb>
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
|
||||
<media-tooltip delay="500" close-delay="0">
|
||||
<media-tooltip-trigger>
|
||||
<media-fullscreen-button class="button">
|
||||
<media-fullscreen-enter-alt-icon
|
||||
class="icon fullscreen-enter-icon"
|
||||
></media-fullscreen-enter-alt-icon>
|
||||
<media-fullscreen-exit-alt-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-alt-icon>
|
||||
</media-fullscreen-button>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="6" collision-padding="12">
|
||||
<media-tooltip-popup class="popup-animation">
|
||||
<span class="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
|
||||
<span class="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</media-container>
|
||||
</media-provider>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "html-eject-minimal",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "HTML example for Video.js 10",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@videojs/html": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import './minimal.css';
|
||||
import '@videojs/html/icons';
|
||||
import '@videojs/html/define/media-container';
|
||||
import '@videojs/html/define/media-provider';
|
||||
import '@videojs/html/define/media-play-button';
|
||||
import '@videojs/html/define/media-mute-button';
|
||||
import '@videojs/html/define/media-volume-slider';
|
||||
import '@videojs/html/define/media-time-slider';
|
||||
import '@videojs/html/define/media-fullscreen-button';
|
||||
import '@videojs/html/define/media-duration-display';
|
||||
import '@videojs/html/define/media-current-time-display';
|
||||
import '@videojs/html/define/media-preview-time-display';
|
||||
import '@videojs/html/define/media-popover';
|
||||
import '@videojs/html/define/media-tooltip';
|
||||
@@ -0,0 +1,352 @@
|
||||
media-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
media-container {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
media-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 10;
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
media-container::after {
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fullscreen */
|
||||
media-container:fullscreen {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
media-container > ::slotted([slot='media']),
|
||||
media-container > video,
|
||||
media-container > audio {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Media Container UI Overlay Styling */
|
||||
media-container > .overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: start;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
|
||||
transform: translateY(100%);
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
transition-delay: 500ms;
|
||||
opacity: 0;
|
||||
}
|
||||
media-container:hover > .overlay,
|
||||
media-container:has([data-paused]) > .overlay,
|
||||
media-container:has([aria-expanded='true']) > .overlay {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition-duration: 100ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
/* Media Control Bar UI/Styles */
|
||||
.control-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
padding: 2.5rem 0.75rem 0.75rem 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
will-change: transform, filter, opacity;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition-property: transform, filter, opacity;
|
||||
transition-delay: 500ms;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease-out;
|
||||
color: oklab(1 0 0);
|
||||
}
|
||||
media-container:hover > .control-bar,
|
||||
media-container:has([data-paused]) > .control-bar,
|
||||
media-container:has([aria-expanded='true']) > .control-bar {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0px);
|
||||
transition-delay: 0ms;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* Time Display Styling */
|
||||
.time-display-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.duration-display {
|
||||
display: contents;
|
||||
color: oklab(1 0 0 / 0.5);
|
||||
}
|
||||
media-current-time-display,
|
||||
media-duration-display {
|
||||
text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Generic Media Button Styling */
|
||||
.button {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
padding: 0.625rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
color: oklab(1 0 0);
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition-property: background-color, color, outline-offset;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.button:hover,
|
||||
.button:focus-visible,
|
||||
.button[aria-expanded='true'] {
|
||||
color: oklab(1 0 0 / 0.8);
|
||||
text-decoration: none;
|
||||
}
|
||||
.button:focus-visible {
|
||||
outline-color: oklab(1 0 0);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.button[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.button .icon {
|
||||
grid-area: 1 / 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
|
||||
}
|
||||
|
||||
/* Media Play Button UI/Styles */
|
||||
media-play-button .icon {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
media-play-button:not([data-paused]) .pause-icon,
|
||||
media-play-button[data-paused] .play-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Media Fullscreen Button UI/Styles */
|
||||
media-fullscreen-button .icon {
|
||||
display: none;
|
||||
}
|
||||
media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
|
||||
media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* One way to define the "default visible" icon (CJP) */
|
||||
media-mute-button .icon {
|
||||
display: none;
|
||||
}
|
||||
media-mute-button:not([data-volume-level]) .volume-low-icon,
|
||||
media-mute-button[data-volume-level='high'] .volume-high-icon,
|
||||
media-mute-button[data-volume-level='low'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='medium'] .volume-low-icon,
|
||||
media-mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* TimeSlider Component Styles */
|
||||
.slider {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-radius: calc(infinity * 1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Horizontal orientation styles */
|
||||
.slider[data-orientation='horizontal'] {
|
||||
min-width: 5rem;
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Vertical orientation styles */
|
||||
.slider[data-orientation='vertical'] {
|
||||
height: 4.5rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background-color: oklab(1 0 0 / 0.2);
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition: outline-offset 150ms ease-out;
|
||||
box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
/* Horizontal track styles */
|
||||
.slider-track[data-orientation='horizontal'] {
|
||||
width: 100%;
|
||||
height: 0.1875rem;
|
||||
}
|
||||
|
||||
/* Vertical track styles */
|
||||
.slider-track[data-orientation='vertical'] {
|
||||
width: 0.1875rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slider:focus-visible .slider-track {
|
||||
outline-color: oklab(1 0 0);
|
||||
outline-offset: 6px;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: calc(infinity * 1px);
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
box-shadow:
|
||||
0 0 0 1px oklab(0 0 0 / 0.1),
|
||||
0 1px 3px 0 oklab(0 0 0 / 0.15),
|
||||
0 1px 2px -1px oklab(0 0 0 / 0.15);
|
||||
opacity: 0;
|
||||
scale: 0.7;
|
||||
transform-origin: center;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.slider:hover .slider-thumb,
|
||||
.slider:focus-within .slider-thumb {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
.slider-track[data-orientation='horizontal'] .slider-thumb {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.slider-track[data-orientation='vertical'] .slider-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.slider-pointer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-progress {
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
media-preview-time-display {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.popup-animation {
|
||||
transition-property: transform, scale, opacity, filter;
|
||||
transition-duration: 200ms;
|
||||
transform: scale(1);
|
||||
transform-origin: bottom;
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
.popup-animation[data-starting-style],
|
||||
.popup-animation[data-ending-style] {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
}
|
||||
.popup-animation[data-instant] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
media-popover {
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
||||
/* Tooltip Component Styles */
|
||||
media-tooltip-popup {
|
||||
color: oklab(1 0 0);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
background-color: oklab(1 0 0 / 0.1);
|
||||
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
|
||||
box-shadow:
|
||||
0 4px 6px -1px oklab(0 0 0 / 0.1),
|
||||
0 2px 4px -2px oklab(0 0 0 / 0.1);
|
||||
}
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
media-tooltip-popup {
|
||||
background-color: oklab(0 0 0 / 0.7);
|
||||
}
|
||||
}
|
||||
@media (prefers-contrast: more) {
|
||||
media-tooltip-popup {
|
||||
background-color: oklab(0 0 0 / 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
media-tooltip-popup[data-paused] .play-tooltip,
|
||||
media-tooltip-popup:not([data-paused]) .pause-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
|
||||
media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5174,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@vjs/html'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>React Frosted Ejected</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family:
|
||||
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
margin: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "react-eject-frosted",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "React example for Video.js 10",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@videojs/react": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"tailwindcss": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
|
||||
import {
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import './frosted.css';
|
||||
|
||||
type SkinProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element {
|
||||
return (
|
||||
<MediaContainer className={`vjs-frosted-skin ${className}`}>
|
||||
{children}
|
||||
|
||||
<div className="overlay" />
|
||||
|
||||
<div className="surface control-bar" data-testid="media-controls">
|
||||
<Tooltip.Root delay={500}>
|
||||
<Tooltip.Trigger>
|
||||
<PlayButton className="button play-button">
|
||||
<PlayIcon className="icon play-icon" />
|
||||
<PauseIcon className="icon pause-icon" />
|
||||
</PlayButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className="tooltip-popup surface popup-animation">
|
||||
<span className="tooltip play-tooltip">Play</span>
|
||||
<span className="tooltip pause-tooltip">Pause</span>
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
<div className="time-controls">
|
||||
<CurrentTimeDisplay
|
||||
// Use showRemaining to show count down/remaining time
|
||||
// showRemaining
|
||||
className="time-display"
|
||||
/>
|
||||
|
||||
<Tooltip.Root trackCursorAxis="x">
|
||||
<Tooltip.Trigger>
|
||||
<TimeSlider.Root className="slider">
|
||||
<TimeSlider.Track className="slider-track">
|
||||
<TimeSlider.Progress className="slider-progress" />
|
||||
<TimeSlider.Pointer className="slider-pointer" />
|
||||
</TimeSlider.Track>
|
||||
<TimeSlider.Thumb className="slider-thumb" />
|
||||
</TimeSlider.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner side="top" sideOffset={18} collisionPadding={12}>
|
||||
<Tooltip.Popup className="surface popup-animation tooltip-popup">
|
||||
<PreviewTimeDisplay className="time-display media-preview-time-display" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
<DurationDisplay className="time-display" />
|
||||
</div>
|
||||
|
||||
<Popover.Root openOnHover delay={200} closeDelay={100}>
|
||||
<Popover.Trigger>
|
||||
<MuteButton className="button mute-button">
|
||||
<VolumeHighIcon className="icon volume-high-icon" />
|
||||
<VolumeLowIcon className="icon volume-low-icon" />
|
||||
<VolumeOffIcon className="icon volume-off-icon" />
|
||||
</MuteButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Positioner side="top" sideOffset={12}>
|
||||
<Popover.Popup className="surface popup-animation popover-popup">
|
||||
<VolumeSlider.Root className="slider" orientation="vertical">
|
||||
<VolumeSlider.Track className="slider-track">
|
||||
<VolumeSlider.Progress className="slider-progress" />
|
||||
</VolumeSlider.Track>
|
||||
<VolumeSlider.Thumb className="slider-thumb" />
|
||||
</VolumeSlider.Root>
|
||||
</Popover.Popup>
|
||||
</Popover.Positioner>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
|
||||
<Tooltip.Root delay={500}>
|
||||
<Tooltip.Trigger>
|
||||
<FullscreenButton className="button fullscreen-button">
|
||||
<FullscreenEnterIcon className="icon fullscreen-enter-icon" />
|
||||
<FullscreenExitIcon className="icon fullscreen-exit-icon" />
|
||||
</FullscreenButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className="surface popup-animation tooltip-popup">
|
||||
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
|
||||
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</MediaContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
.vjs-frosted-skin * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
.vjs-frosted-skin::before,
|
||||
.vjs-frosted-skin::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 10;
|
||||
}
|
||||
.vjs-frosted-skin::before {
|
||||
inset: 1px;
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
|
||||
}
|
||||
.vjs-frosted-skin::after {
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Fullscreen */
|
||||
.vjs-frosted-skin:fullscreen {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin > ::slotted([slot='media']) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Media Container UI Overlay Styling */
|
||||
.vjs-frosted-skin > .overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: start;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
|
||||
backdrop-filter: saturate(1.5) brightness(0.9);
|
||||
transition: opacity 0.15s ease-out;
|
||||
transition-delay: 500ms;
|
||||
opacity: 0;
|
||||
}
|
||||
.vjs-frosted-skin:hover > .overlay,
|
||||
.vjs-frosted-skin:has([data-paused]) > .overlay,
|
||||
.vjs-frosted-skin:has([aria-expanded='true']) > .overlay {
|
||||
opacity: 1;
|
||||
transition-duration: 100ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
/* Common Surface Styles - e.g. tooltips, popovers, controls */
|
||||
.vjs-frosted-skin .surface {
|
||||
background-color: oklab(1 0 0 / 0.1);
|
||||
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklab(1 0 0 / 0.15),
|
||||
0 0 0 1px oklab(0 0 0 / 0.15),
|
||||
oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
|
||||
oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
|
||||
}
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.vjs-frosted-skin .surface {
|
||||
background-color: oklab(0 0 0 / 0.7);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklab(0 0 0),
|
||||
0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
@media (prefers-contrast: more) {
|
||||
.vjs-frosted-skin .surface {
|
||||
background-color: oklab(0 0 0 / 0.9);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px oklab(0 0 0),
|
||||
0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Media Control Bar UI/Styles */
|
||||
.vjs-frosted-skin .control-bar {
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
inset-inline: 0.75rem;
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
will-change: scale, transform, filter, opacity;
|
||||
scale: 0.9;
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition-property: scale, transform, filter, opacity;
|
||||
transition-delay: 500ms;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease-out;
|
||||
transform-origin: bottom;
|
||||
color: oklab(1 0 0);
|
||||
}
|
||||
.vjs-frosted-skin:hover > .control-bar,
|
||||
.vjs-frosted-skin:has([data-paused]) > .control-bar,
|
||||
.vjs-frosted-skin:has([aria-expanded='true']) > .control-bar {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
filter: blur(0px);
|
||||
transition-delay: 0ms;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* Time Display Styling */
|
||||
.vjs-frosted-skin .time-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 0.75rem;
|
||||
padding-inline: 0.375rem;
|
||||
}
|
||||
.vjs-frosted-skin .time-display {
|
||||
text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Generic Media Button Styling */
|
||||
.vjs-frosted-skin .button {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: calc(infinity * 1px);
|
||||
color: oklab(1 0 0 / 0.9);
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition-property: background-color, color, outline-offset;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.vjs-frosted-skin .button:hover,
|
||||
.vjs-frosted-skin .button:focus-visible,
|
||||
.vjs-frosted-skin .button[aria-expanded='true'] {
|
||||
background-color: oklab(1 0 0 / 0.1);
|
||||
color: oklab(1 0 0);
|
||||
text-decoration: none;
|
||||
}
|
||||
.vjs-frosted-skin .button:focus-visible {
|
||||
outline-color: oklch(62.3% 0.214 259.815);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.vjs-frosted-skin .button[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .button .icon {
|
||||
grid-area: 1 / 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
|
||||
}
|
||||
|
||||
/* Media Play Button UI/Styles */
|
||||
.vjs-frosted-skin .play-button .icon {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .play-button:not([data-paused]) .pause-icon,
|
||||
.vjs-frosted-skin .play-button[data-paused] .play-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Media Fullscreen Button UI/Styles */
|
||||
.vjs-frosted-skin .fullscreen-button .icon {
|
||||
display: none;
|
||||
}
|
||||
.vjs-frosted-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
|
||||
.vjs-frosted-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* One way to define the "default visible" icon (CJP) */
|
||||
.vjs-frosted-skin .mute-button .icon {
|
||||
display: none;
|
||||
}
|
||||
.vjs-frosted-skin .mute-button:not([data-volume-level]) .volume-low-icon,
|
||||
.vjs-frosted-skin .mute-button[data-volume-level='high'] .volume-high-icon,
|
||||
.vjs-frosted-skin .mute-button[data-volume-level='low'] .volume-low-icon,
|
||||
.vjs-frosted-skin .mute-button[data-volume-level='medium'] .volume-low-icon,
|
||||
.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* TimeSlider Component Styles */
|
||||
.vjs-frosted-skin .slider {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-radius: calc(infinity * 1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Horizontal orientation styles */
|
||||
.vjs-frosted-skin .slider[data-orientation='horizontal'] {
|
||||
min-width: 5rem;
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Vertical orientation styles */
|
||||
.vjs-frosted-skin .slider[data-orientation='vertical'] {
|
||||
height: 5rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .slider-track {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background-color: oklab(1 0 0 / 0.2);
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition: outline-offset 150ms ease-out;
|
||||
box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
/* Horizontal track styles */
|
||||
.vjs-frosted-skin .slider-track[data-orientation='horizontal'] {
|
||||
width: 100%;
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
/* Vertical track styles */
|
||||
.vjs-frosted-skin .slider-track[data-orientation='vertical'] {
|
||||
width: 0.25rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .slider:focus-visible .slider-track {
|
||||
outline-color: oklch(62.3% 0.214 259.815);
|
||||
outline-offset: 6px;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .slider-thumb {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: calc(infinity * 1px);
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
box-shadow:
|
||||
0 0 0 1px oklab(0 0 0 / 0.1),
|
||||
0 1px 3px 0 oklab(0 0 0 / 0.15),
|
||||
0 1px 2px -1px oklab(0 0 0 / 0.15);
|
||||
opacity: 0;
|
||||
transition-property: opacity, height, width;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.vjs-frosted-skin .slider-thumb:active {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
.vjs-frosted-skin .slider:hover .slider-thumb,
|
||||
.vjs-frosted-skin .slider:focus-within .slider-thumb {
|
||||
opacity: 1;
|
||||
}
|
||||
.vjs-frosted-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.vjs-frosted-skin .slider-track[data-orientation='vertical'] .slider-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .slider-pointer {
|
||||
background-color: oklab(1 0 0 / 0.2);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .slider-progress {
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .media-preview-time-display {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .popup-animation {
|
||||
transition-property: transform, scale, opacity, filter;
|
||||
transition-duration: 200ms;
|
||||
transform: scale(1);
|
||||
transform-origin: bottom;
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
.vjs-frosted-skin .popup-animation[data-starting-style],
|
||||
.vjs-frosted-skin .popup-animation[data-ending-style] {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
}
|
||||
.vjs-frosted-skin .popup-animation[data-instant] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .popover-popup {
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
||||
/* Tooltip Component Styles */
|
||||
.vjs-frosted-skin .tooltip-popup {
|
||||
color: oklab(1 0 0);
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .tooltip {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .tooltip-popup[data-paused] .play-tooltip,
|
||||
.vjs-frosted-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vjs-frosted-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
|
||||
.vjs-frosted-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MediaProvider, Video } from '@videojs/react';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import FrostedSkin from './FrostedSkin';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<MediaProvider>
|
||||
<FrostedSkin>
|
||||
<Video
|
||||
// @ts-expect-error -- types are incorrect
|
||||
src="https://stream.mux.com/UZMwOY6MgmhFNXLbSFXAuPKlRPss5XNA.m3u8"
|
||||
poster="https://image.mux.com/UZMwOY6MgmhFNXLbSFXAuPKlRPss5XNA/thumbnail.webp"
|
||||
playsInline
|
||||
/>
|
||||
</FrostedSkin>
|
||||
</MediaProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@videojs/react'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>React Minimal Ejected</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family:
|
||||
InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
margin: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "react-eject-minimal",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "React example for Video.js 10",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@videojs/react": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"tailwindcss": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
|
||||
import {
|
||||
FullscreenEnterAltIcon,
|
||||
FullscreenExitAltIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/react/icons';
|
||||
|
||||
import './minimal.css';
|
||||
|
||||
type SkinProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element {
|
||||
return (
|
||||
<MediaContainer className={`vjs-minimal-skin ${className}`}>
|
||||
{children}
|
||||
|
||||
<div className="overlay" />
|
||||
|
||||
<div className="control-bar">
|
||||
<Tooltip.Root delay={500} closeDelay={0}>
|
||||
<Tooltip.Trigger>
|
||||
<PlayButton className="button play-button">
|
||||
<PlayIcon className="icon play-icon" />
|
||||
<PauseIcon className="icon pause-icon" />
|
||||
</PlayButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner side="top-start" sideOffset={6} collisionPadding={12}>
|
||||
<Tooltip.Popup className="popup-animation tooltip-popup">
|
||||
<span className="tooltip play-tooltip">Play</span>
|
||||
<span className="tooltip pause-tooltip">Pause</span>
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
<div className="time-display-group">
|
||||
<CurrentTimeDisplay
|
||||
// Use showRemaining to show count down/remaining time
|
||||
// showRemaining
|
||||
className="time-display"
|
||||
/>
|
||||
|
||||
<span className="duration-display">
|
||||
/
|
||||
<DurationDisplay className="time-display" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Tooltip.Root trackCursorAxis="x">
|
||||
<Tooltip.Trigger>
|
||||
<TimeSlider.Root className="slider">
|
||||
<TimeSlider.Track className="slider-track">
|
||||
<TimeSlider.Progress className="slider-progress" />
|
||||
<TimeSlider.Pointer className="slider-pointer" />
|
||||
</TimeSlider.Track>
|
||||
<TimeSlider.Thumb className="slider-thumb" />
|
||||
</TimeSlider.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner side="top" sideOffset={12} collisionPadding={12}>
|
||||
<Tooltip.Popup className="popup-animation tooltip-popup">
|
||||
<PreviewTimeDisplay className="time-display media-preview-time-display" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
|
||||
<div className="button-group">
|
||||
<Popover.Root openOnHover delay={200} closeDelay={300}>
|
||||
<Popover.Trigger>
|
||||
<MuteButton className="button mute-button">
|
||||
<VolumeHighIcon className="icon volume-high-icon" />
|
||||
<VolumeLowIcon className="icon volume-low-icon" />
|
||||
<VolumeOffIcon className="icon volume-off-icon" />
|
||||
</MuteButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Positioner side="top" sideOffset={2}>
|
||||
<Popover.Popup className="popup-animation popover-popup">
|
||||
<VolumeSlider.Root className="slider" orientation="vertical">
|
||||
<VolumeSlider.Track className="slider-track">
|
||||
<VolumeSlider.Progress className="slider-progress" />
|
||||
</VolumeSlider.Track>
|
||||
<VolumeSlider.Thumb className="slider-thumb" />
|
||||
</VolumeSlider.Root>
|
||||
</Popover.Popup>
|
||||
</Popover.Positioner>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
|
||||
<Tooltip.Root delay={500} closeDelay={0}>
|
||||
<Tooltip.Trigger>
|
||||
<FullscreenButton className="button fullscreen-button">
|
||||
<FullscreenEnterAltIcon className="icon fullscreen-enter-icon" />
|
||||
<FullscreenExitAltIcon className="icon fullscreen-exit-icon" />
|
||||
</FullscreenButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner side="top-end" sideOffset={6} collisionPadding={12}>
|
||||
<Tooltip.Popup className="popup-animation tooltip-popup">
|
||||
<span className="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
|
||||
<span className="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
</MediaContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MediaProvider, Video } from '@videojs/react';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import MinimalSkin from './MinimalSkin';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<MediaProvider>
|
||||
<MinimalSkin>
|
||||
<Video
|
||||
// @ts-expect-error -- types are incorrect
|
||||
src="https://stream.mux.com/UZMwOY6MgmhFNXLbSFXAuPKlRPss5XNA.m3u8"
|
||||
poster="https://image.mux.com/UZMwOY6MgmhFNXLbSFXAuPKlRPss5XNA/thumbnail.webp"
|
||||
playsInline
|
||||
/>
|
||||
</MinimalSkin>
|
||||
</MediaProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,349 @@
|
||||
.vjs-minimal-skin * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
container: root / inline-size;
|
||||
overflow: clip;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
border-radius: inherit;
|
||||
background: oklab(0 0 0);
|
||||
}
|
||||
.vjs-minimal-skin::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 10;
|
||||
inset: 0;
|
||||
box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vjs-minimal-skin::after {
|
||||
box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fullscreen */
|
||||
.vjs-minimal-skin:fullscreen {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin > ::slotted([slot='media']) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Media Container UI Overlay Styling */
|
||||
.vjs-minimal-skin > .overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: start;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
|
||||
transform: translateY(100%);
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
transition-delay: 500ms;
|
||||
opacity: 0;
|
||||
}
|
||||
.vjs-minimal-skin:hover > .overlay,
|
||||
.vjs-minimal-skin:has([data-paused]) > .overlay,
|
||||
.vjs-minimal-skin:has([aria-expanded='true']) > .overlay {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition-duration: 100ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
/* Media Control Bar UI/Styles */
|
||||
.vjs-minimal-skin .control-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
padding: 2.5rem 0.75rem 0.75rem 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
will-change: transform, filter, opacity;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition-property: transform, filter, opacity;
|
||||
transition-delay: 500ms;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease-out;
|
||||
color: oklab(1 0 0);
|
||||
}
|
||||
.vjs-minimal-skin:hover > .control-bar,
|
||||
.vjs-minimal-skin:has([data-paused]) > .control-bar,
|
||||
.vjs-minimal-skin:has([aria-expanded='true']) > .control-bar {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0px);
|
||||
transition-delay: 0ms;
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* Time Display Styling */
|
||||
.vjs-minimal-skin .time-display-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.vjs-minimal-skin .duration-display {
|
||||
display: contents;
|
||||
color: oklab(1 0 0 / 0.5);
|
||||
}
|
||||
.vjs-minimal-skin .time-display {
|
||||
text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Generic Media Button Styling */
|
||||
.vjs-minimal-skin .button {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
padding: 0.625rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
color: oklab(1 0 0);
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition-property: background-color, color, outline-offset;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.vjs-minimal-skin .button:hover,
|
||||
.vjs-minimal-skin .button:focus-visible,
|
||||
.vjs-minimal-skin .button[aria-expanded='true'] {
|
||||
color: oklab(1 0 0 / 0.8);
|
||||
text-decoration: none;
|
||||
}
|
||||
.vjs-minimal-skin .button:focus-visible {
|
||||
outline-color: oklab(1 0 0);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.vjs-minimal-skin .button[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .button .icon {
|
||||
grid-area: 1 / 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
|
||||
}
|
||||
|
||||
/* Media Play Button UI/Styles */
|
||||
.vjs-minimal-skin .play-button .icon {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .play-button:not([data-paused]) .pause-icon,
|
||||
.vjs-minimal-skin .play-button[data-paused] .play-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Media Fullscreen Button UI/Styles */
|
||||
.vjs-minimal-skin .fullscreen-button .icon {
|
||||
display: none;
|
||||
}
|
||||
.vjs-minimal-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
|
||||
.vjs-minimal-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* One way to define the "default visible" icon (CJP) */
|
||||
.vjs-minimal-skin .mute-button .icon {
|
||||
display: none;
|
||||
}
|
||||
.vjs-minimal-skin .mute-button:not([data-volume-level]) .volume-low-icon,
|
||||
.vjs-minimal-skin .mute-button[data-volume-level='high'] .volume-high-icon,
|
||||
.vjs-minimal-skin .mute-button[data-volume-level='low'] .volume-low-icon,
|
||||
.vjs-minimal-skin .mute-button[data-volume-level='medium'] .volume-low-icon,
|
||||
.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* TimeSlider Component Styles */
|
||||
.vjs-minimal-skin .slider {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-radius: calc(infinity * 1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Horizontal orientation styles */
|
||||
.vjs-minimal-skin .slider[data-orientation='horizontal'] {
|
||||
min-width: 5rem;
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Vertical orientation styles */
|
||||
.vjs-minimal-skin .slider[data-orientation='vertical'] {
|
||||
height: 4.5rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .slider-track {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background-color: oklab(1 0 0 / 0.2);
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
transition: outline-offset 150ms ease-out;
|
||||
box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
/* Horizontal track styles */
|
||||
.vjs-minimal-skin .slider-track[data-orientation='horizontal'] {
|
||||
width: 100%;
|
||||
height: 0.1875rem;
|
||||
}
|
||||
|
||||
/* Vertical track styles */
|
||||
.vjs-minimal-skin .slider-track[data-orientation='vertical'] {
|
||||
width: 0.1875rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .slider:focus-visible .slider-track {
|
||||
outline-color: oklab(1 0 0);
|
||||
outline-offset: 6px;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .slider-thumb {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: calc(infinity * 1px);
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
box-shadow:
|
||||
0 0 0 1px oklab(0 0 0 / 0.1),
|
||||
0 1px 3px 0 oklab(0 0 0 / 0.15),
|
||||
0 1px 2px -1px oklab(0 0 0 / 0.15);
|
||||
opacity: 0;
|
||||
scale: 0.7;
|
||||
transform-origin: center;
|
||||
transition-property: opacity, scale;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
.vjs-minimal-skin .slider:hover .slider-thumb,
|
||||
.vjs-minimal-skin .slider:focus-within .slider-thumb {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
.vjs-minimal-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.vjs-minimal-skin .slider-track[data-orientation='vertical'] .slider-thumb {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .slider-pointer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .slider-progress {
|
||||
background-color: oklab(1 0 0);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .media-preview-time-display {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .popup-animation {
|
||||
transition-property: transform, scale, opacity, filter;
|
||||
transition-duration: 200ms;
|
||||
transform: scale(1);
|
||||
transform-origin: bottom;
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
.vjs-minimal-skin .popup-animation[data-starting-style],
|
||||
.vjs-minimal-skin .popup-animation[data-ending-style] {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
}
|
||||
.vjs-minimal-skin .popup-animation[data-instant] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .popover-popup {
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
||||
/* Tooltip Component Styles */
|
||||
.vjs-minimal-skin .tooltip-popup {
|
||||
color: oklab(1 0 0);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
background-color: oklab(1 0 0 / 0.1);
|
||||
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
|
||||
box-shadow:
|
||||
0 4px 6px -1px oklab(0 0 0 / 0.1),
|
||||
0 2px 4px -2px oklab(0 0 0 / 0.1);
|
||||
}
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.vjs-minimal-skin .tooltip-popup {
|
||||
background-color: oklab(0 0 0 / 0.7);
|
||||
}
|
||||
}
|
||||
@media (prefers-contrast: more) {
|
||||
.vjs-minimal-skin .tooltip-popup {
|
||||
background-color: oklab(0 0 0 / 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .tooltip {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .tooltip-popup[data-paused] .play-tooltip,
|
||||
.vjs-minimal-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vjs-minimal-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
|
||||
.vjs-minimal-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@videojs/react'],
|
||||
},
|
||||
});
|
||||
Generated
+106
@@ -94,6 +94,32 @@ importers:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
|
||||
examples/html-eject-frosted:
|
||||
dependencies:
|
||||
'@videojs/html':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/html
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
|
||||
examples/html-eject-minimal:
|
||||
dependencies:
|
||||
'@videojs/html':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/html
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
|
||||
examples/react-demo:
|
||||
dependencies:
|
||||
'@videojs/react':
|
||||
@@ -134,6 +160,86 @@ importers:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
|
||||
examples/react-eject-frosted:
|
||||
dependencies:
|
||||
'@videojs/react':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/react
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
react:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
screenfull:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
tailwindcss:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.14
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.0
|
||||
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))
|
||||
'@types/react':
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.24
|
||||
'@types/react-dom':
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.7(@types/react@18.3.24)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
|
||||
examples/react-eject-minimal:
|
||||
dependencies:
|
||||
'@videojs/react':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/react
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
react:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
screenfull:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
tailwindcss:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.14
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.0
|
||||
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))
|
||||
'@types/react':
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.24
|
||||
'@types/react-dom':
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.7(@types/react@18.3.24)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
'@videojs/utils':
|
||||
|
||||
@@ -1,71 +1,59 @@
|
||||
import type { Media, Skin } from '@/stores/homePageDemos';
|
||||
import type { Skin } from '@/stores/homePageDemos';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { TabsPanel, TabsRoot } from '@/components/Tabs';
|
||||
import { framework, media, skin } from '@/stores/homePageDemos';
|
||||
import { framework, skin } from '@/stores/homePageDemos';
|
||||
import ClientCode from '../Code/ClientCode';
|
||||
|
||||
function generateHTMLCode(skin: Skin, media: Media): string {
|
||||
function generateHTMLCode(skin: Skin): string {
|
||||
const skinTag = `${skin}-skin`;
|
||||
const mediaTag = media;
|
||||
const videoExtension = media === 'hls-video' ? 'm3u8' : 'mp4';
|
||||
|
||||
return `<video-provider>
|
||||
<${skinTag}>
|
||||
<${mediaTag} src="https://example.com/video.${videoExtension}"></${mediaTag}>
|
||||
<video src="https://example.com/video.mp4"></video>
|
||||
</${skinTag}>
|
||||
</video-provider>`;
|
||||
}
|
||||
|
||||
function generateReactCode(skin: Skin, media: Media): string {
|
||||
function generateReactCode(skin: Skin): string {
|
||||
const skinComponent = skin === 'frosted' ? 'FrostedSkin' : 'MinimalSkin';
|
||||
const mediaComponent = media === 'video' ? 'Video' : 'HLSVideo';
|
||||
const skinImport = skin === 'frosted' ? 'frosted' : 'minimal';
|
||||
const videoExtension = media === 'hls-video' ? 'm3u8' : 'mp4';
|
||||
|
||||
return `import { VideoProvider, ${mediaComponent} } from '@videojs/react';
|
||||
return `import { VideoProvider, Video } from '@videojs/react';
|
||||
import { ${skinComponent} } from '@videojs/react/skins/${skinImport}';
|
||||
|
||||
export const VideoPlayer = () => {
|
||||
return (
|
||||
<VideoProvider>
|
||||
<${skinComponent}>
|
||||
<${mediaComponent} src="https://example.com/video.${videoExtension}" />
|
||||
<Video src="https://example.com/video.mp4" />
|
||||
</${skinComponent}>
|
||||
</VideoProvider>
|
||||
);
|
||||
};`;
|
||||
}
|
||||
|
||||
function generateCSS(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
}
|
||||
|
||||
function generateJS(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
function generateJS(_skin: Skin): string {
|
||||
return `import '@videojs/html/skins/frosted';`;
|
||||
}
|
||||
|
||||
export default function BaseDemo({ className }: { className?: string }) {
|
||||
const $framework = useStore(framework);
|
||||
const $skin = useStore(skin);
|
||||
const $media = useStore(media);
|
||||
|
||||
if ($framework === 'html') {
|
||||
return (
|
||||
<TabsRoot
|
||||
id="base-html"
|
||||
aria-label="HTML implementation"
|
||||
titles={{ html: 'HTML', css: 'CSS', javascript: 'JavaScript' }}
|
||||
titles={{ html: 'HTML', javascript: 'JavaScript' }}
|
||||
className={className}
|
||||
maxWidth={false}
|
||||
>
|
||||
<TabsPanel tabsId="base-html" value="html">
|
||||
<ClientCode code={generateHTMLCode($skin, $media)} lang="html" />
|
||||
</TabsPanel>
|
||||
<TabsPanel tabsId="base-html" value="css">
|
||||
<ClientCode code={generateCSS($skin, $media)} lang="css" />
|
||||
<ClientCode code={generateHTMLCode($skin)} lang="html" />
|
||||
</TabsPanel>
|
||||
<TabsPanel tabsId="base-html" value="javascript">
|
||||
<ClientCode code={generateJS($skin, $media)} lang="javascript" />
|
||||
<ClientCode code={generateJS($skin)} lang="javascript" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
@@ -80,7 +68,7 @@ export default function BaseDemo({ className }: { className?: string }) {
|
||||
maxWidth={false}
|
||||
>
|
||||
<TabsPanel tabsId="base-react" value="react">
|
||||
<ClientCode code={generateReactCode($skin, $media)} lang="tsx" />
|
||||
<ClientCode code={generateReactCode($skin)} lang="tsx" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
import type { Media, Skin } from '@/stores/homePageDemos';
|
||||
import type { Skin } from '@/stores/homePageDemos';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { TabsPanel, TabsRoot } from '@/components/Tabs';
|
||||
import { framework, media, skin } from '@/stores/homePageDemos';
|
||||
import { framework, skin } from '@/stores/homePageDemos';
|
||||
import {
|
||||
generateHTMLCSS,
|
||||
generateHTMLJS,
|
||||
generateHTMLMarkup,
|
||||
generateReactComponent,
|
||||
generateReactCSS as genReactCSS,
|
||||
} from '@/utils/ejectCodeGenerator';
|
||||
import ClientCode from '../Code/ClientCode';
|
||||
|
||||
function generateHTMLCode(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
function generateReactCode(skin: Skin): string {
|
||||
return generateReactComponent(skin);
|
||||
}
|
||||
|
||||
function generateReactCode(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
function generateReactCSS(skin: Skin): string {
|
||||
return genReactCSS(skin);
|
||||
}
|
||||
|
||||
function generateCSSModuleCode(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
function generateHTMLCode(skin: Skin): string {
|
||||
return generateHTMLMarkup(skin);
|
||||
}
|
||||
|
||||
function generateCSS(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
function generateCSS(skin: Skin): string {
|
||||
return generateHTMLCSS(skin);
|
||||
}
|
||||
|
||||
function generateJS(_skin: Skin, _media: Media): string {
|
||||
return 'Coming soon';
|
||||
function generateJS(skin: Skin): string {
|
||||
return generateHTMLJS(skin);
|
||||
}
|
||||
|
||||
export default function EjectDemo({ className }: { className?: string }) {
|
||||
const $framework = useStore(framework);
|
||||
const $skin = useStore(skin);
|
||||
const $media = useStore(media);
|
||||
|
||||
if ($framework === 'html') {
|
||||
return (
|
||||
@@ -39,13 +45,13 @@ export default function EjectDemo({ className }: { className?: string }) {
|
||||
maxWidth={false}
|
||||
>
|
||||
<TabsPanel tabsId="eject-html" value="html">
|
||||
<ClientCode code={generateHTMLCode($skin, $media)} lang="html" />
|
||||
<ClientCode code={generateHTMLCode($skin)} lang="html" />
|
||||
</TabsPanel>
|
||||
<TabsPanel tabsId="eject-html" value="css">
|
||||
<ClientCode code={generateCSS($skin, $media)} lang="css" />
|
||||
<ClientCode code={generateCSS($skin)} lang="css" />
|
||||
</TabsPanel>
|
||||
<TabsPanel tabsId="eject-html" value="javascript">
|
||||
<ClientCode code={generateJS($skin, $media)} lang="javascript" />
|
||||
<ClientCode code={generateJS($skin)} lang="javascript" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
@@ -55,15 +61,15 @@ export default function EjectDemo({ className }: { className?: string }) {
|
||||
<TabsRoot
|
||||
id="eject-react"
|
||||
aria-label="React implementation"
|
||||
titles={{ react: 'React', css: 'CSS Module' }}
|
||||
titles={{ react: 'React', css: 'CSS' }}
|
||||
className={className}
|
||||
maxWidth={false}
|
||||
>
|
||||
<TabsPanel tabsId="eject-react" value="react">
|
||||
<ClientCode code={generateReactCode($skin, $media)} lang="tsx" />
|
||||
<ClientCode code={generateReactCode($skin)} lang="tsx" />
|
||||
</TabsPanel>
|
||||
<TabsPanel tabsId="eject-react" value="css">
|
||||
<ClientCode code={generateCSSModuleCode($skin, $media)} lang="css" />
|
||||
<ClientCode code={generateReactCSS($skin)} lang="css" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
|
||||
@@ -3,4 +3,96 @@ title: Customize Skins
|
||||
description: Learn how to customize Video.js v10 skins by copying and modifying them
|
||||
---
|
||||
|
||||
I'm going to put the eject examples here in another PR
|
||||
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
|
||||
import ServerCode from '@/components/Code/ServerCode.astro';
|
||||
import { TabsRoot, TabsPanel } from '@/components/Tabs.tsx';
|
||||
import {
|
||||
generateHTMLCSS,
|
||||
generateHTMLJS,
|
||||
generateHTMLMarkup,
|
||||
generateReactComponent,
|
||||
generateReactCSS,
|
||||
} from '@/utils/ejectCodeGenerator';
|
||||
|
||||
|
||||
Video.js v10 comes with pre-built skins like Frosted and Minimal, but you can fully customize them by "ejecting" the code and making it your own. While eventually we'll have a CLI that'll eject skins in your preferred framework and style, for now we invite you to try it out with these copy-paste-ready implementations.
|
||||
|
||||
> ⚠️ **Notice:** The HTML examples below aren't quite working, yet. Stay tuned for updates.
|
||||
|
||||
## Frosted Skin
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
|
||||
<TabsRoot
|
||||
aria-label="Frosted React implementation"
|
||||
titles={{ component: 'Component', css: 'CSS' }}
|
||||
client:load
|
||||
>
|
||||
<TabsPanel value="component" client:load>
|
||||
<ServerCode code={generateReactComponent('frosted')} lang="tsx" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="css" client:load>
|
||||
<ServerCode code={generateReactCSS('frosted')} lang="css" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
|
||||
<TabsRoot
|
||||
aria-label="Frosted HTML implementation"
|
||||
titles={{ markup: 'HTML', js: 'JS', css: 'CSS' }}
|
||||
client:load
|
||||
>
|
||||
<TabsPanel value="markup" client:load>
|
||||
<ServerCode code={generateHTMLMarkup('frosted')} lang="html" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="js" client:load>
|
||||
<ServerCode code={generateHTMLJS('frosted')} lang="typescript" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="css" client:load>
|
||||
<ServerCode code={generateHTMLCSS('frosted')} lang="css" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
|
||||
</FrameworkCase>
|
||||
|
||||
## Minimal Skin
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
|
||||
<TabsRoot
|
||||
aria-label="Minimal React implementation"
|
||||
titles={{ react: 'React', css: 'CSS' }}
|
||||
client:load
|
||||
>
|
||||
<TabsPanel value="react" client:load>
|
||||
<ServerCode code={generateReactComponent('minimal')} lang="tsx" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="css" client:load>
|
||||
<ServerCode code={generateReactCSS('minimal')} lang="css" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
|
||||
<TabsRoot
|
||||
aria-label="Minimal HTML implementation"
|
||||
titles={{ html: 'HTML', js: 'JS', css: 'CSS' }}
|
||||
client:load
|
||||
>
|
||||
<TabsPanel value="html" client:load>
|
||||
<ServerCode code={generateHTMLMarkup('minimal')} lang="html" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="js" client:load>
|
||||
<ServerCode code={generateHTMLJS('minimal')} lang="typescript" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="css" client:load>
|
||||
<ServerCode code={generateHTMLCSS('minimal')} lang="css" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
|
||||
</FrameworkCase>
|
||||
@@ -2,8 +2,6 @@ import { atom } from 'nanostores';
|
||||
|
||||
export type Framework = 'html' | 'react';
|
||||
export type Skin = 'frosted' | 'minimal';
|
||||
export type Media = 'video' | 'hls-video';
|
||||
|
||||
export const framework = atom<Framework>('react');
|
||||
export const skin = atom<Skin>('frosted');
|
||||
export const media = atom<Media>('video');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user