feat(skins): add html port of minimal skin (#140)

This commit is contained in:
Sam Potts
2025-10-26 23:17:14 +11:00
committed by GitHub
parent 67d1007d0c
commit 47ac22e9b8
19 changed files with 567 additions and 81 deletions
+1
View File
@@ -23,6 +23,7 @@ export default {
'react',
'root',
'site',
'skins',
'test',
'utils',
]],
+4
View File
@@ -11,6 +11,10 @@
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>
+12 -5
View File
@@ -1,15 +1,22 @@
import { defineVjsPlayer } from '@videojs/html';
import '@videojs/html/skins/frosted';
import '@videojs/html/skins/minimal';
defineVjsPlayer();
document.body.innerHTML = /* html */ `
<media-provider>
<div style="display: grid; place-items: center; min-height: 100vh;">
<div style="display: flex; flex-direction: column; justify-content: center; min-height: 100vh; gap: 2rem; padding: 1rem; ">
<media-provider>
<media-skin-frosted style="border-radius: 2rem; width: 100%; max-width: 960px; margin: 2rem auto; aspect-ratio: 16/9">
<video slot="media" src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4" muted playsinline poster="https://image.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/thumbnail.webp"></video>
<video slot="media" src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4" playsinline poster="https://image.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/thumbnail.webp"></video>
</media-skin-frosted>
</div>
</media-provider>
</media-provider>
<media-provider>
<media-skin-minimal style="border-radius: 0.75rem; width: 100%; max-width: 960px; margin: 2rem auto; aspect-ratio: 16/9">
<video slot="media" src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4" playsinline poster="https://image.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/thumbnail.webp"></video>
</media-skin-minimal>
</media-provider>
</div>
`;
+1 -1
View File
@@ -185,7 +185,7 @@ export default function App(): JSX.Element {
<MediaProvider key={key}>
<Skin className={skinClassName}>
{/* @ts-expect-error -- types are incorrect */}
<Video src={mediaSource} poster={poster} playsinline />
<Video src={mediaSource} poster={poster} playsInline />
</Skin>
</MediaProvider>
</div>
-8
View File
@@ -6,13 +6,5 @@
'Noto Color Emoji';
}
.body {
@apply antialiased font-[510] font-sans text-[0.8125rem] @7xl/root:text-[0.9375rem] leading-normal tracking-[-0.0125em];
}
:root {
font-feature-settings: 'cv01', 'ss01', 'ss03';
}
/* Make reduced-transparency variant */
@custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce);
@@ -186,11 +186,11 @@ export class TimeSliderThumbBase extends HTMLElement {
if (props['data-orientation'] === 'horizontal') {
this.style.left = 'var(--slider-fill, 0%)';
this.style.top = '50%';
this.style.transform = 'translate(-50%, -50%)';
this.style.translate = '-50% -50%';
} else {
this.style.bottom = 'var(--slider-fill, 0%)';
this.style.left = '50%';
this.style.transform = 'translate(-50%, 50%)';
this.style.translate = '-50% 50%';
}
}
}
@@ -173,11 +173,11 @@ export class VolumeSliderThumbBase extends HTMLElement {
if (props['data-orientation'] === 'horizontal') {
this.style.left = 'var(--slider-fill, 0%)';
this.style.top = '50%';
this.style.transform = 'translate(-50%, -50%)';
this.style.translate = '-50% -50%';
} else {
this.style.bottom = 'var(--slider-fill, 0%)';
this.style.left = '50%';
this.style.transform = 'translate(-50%, 50%)';
this.style.translate = '-50% 50%';
}
}
}
+2
View File
@@ -1,4 +1,6 @@
export * as MediaFullscreenEnterAltIcon from './media-fullscreen-enter-alt-icon';
export * as MediaFullscreenEnterIcon from './media-fullscreen-enter-icon';
export * as MediaFullscreenExitAltIcon from './media-fullscreen-exit-alt-icon';
export * as MediaFullscreenExitIcon from './media-fullscreen-exit-icon';
export * as MediaPauseIcon from './media-pause-icon';
export * as MediaPlayIcon from './media-play-icon';
@@ -0,0 +1,21 @@
import { SVG_ICONS } from '@videojs/icons';
import { MediaChromeIcon } from './media-chrome-icon';
export function getTemplateHTML() {
return /* html */ `
${MediaChromeIcon.getTemplateHTML()}
<style>
:host {
display: var(--media-fullscreen-enter-icon-display, inline-flex);
}
</style>
${SVG_ICONS.fullscreenEnterAlt}
`;
}
export class MediaFullscreenEnterAltIcon extends MediaChromeIcon {
static getTemplateHTML: () => string = getTemplateHTML;
}
customElements.define('media-fullscreen-enter-alt-icon', MediaFullscreenEnterAltIcon);
@@ -0,0 +1,21 @@
import { SVG_ICONS } from '@videojs/icons';
import { MediaChromeIcon } from './media-chrome-icon';
export function getTemplateHTML() {
return /* html */ `
${MediaChromeIcon.getTemplateHTML()}
<style>
:host {
display: var(--media-fullscreen-exit-icon-display, inline-flex);
}
</style>
${SVG_ICONS.fullscreenExitAlt}
`;
}
export class MediaFullscreenExitAltIcon extends MediaChromeIcon {
static getTemplateHTML: () => string = getTemplateHTML;
}
customElements.define('media-fullscreen-exit-alt-icon', MediaFullscreenExitAltIcon);
+3 -2
View File
@@ -22,11 +22,12 @@ export function getTemplateHTML() {
<media-container>
<slot name="media" slot="media"></slot>
<div class="overlay"></div>
<div class="control-bar surface">
<!-- 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-root delay="600" close-delay="0">
<media-tooltip-root delay="500" close-delay="0">
<media-tooltip-trigger>
<media-play-button class="button">
<media-play-icon class="icon play-icon"></media-play-icon>
@@ -91,7 +92,7 @@ export function getTemplateHTML() {
</media-popover-portal>
</media-popover-root>
<media-tooltip-root delay="600" close-delay="0">
<media-tooltip-root 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>
+8 -10
View File
@@ -33,10 +33,6 @@ media-container::after {
media-container:fullscreen {
border-radius: 0;
}
media-container:fullscreen video {
height: 100%;
width: 100%;
}
media-container > ::slotted([slot='media']) {
display: block;
@@ -52,6 +48,7 @@ media-container > .overlay {
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;
@@ -62,12 +59,13 @@ 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: rgba(255, 255, 255, 0.1);
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),
@@ -223,15 +221,15 @@ media-mute-button[data-volume-level='off'] .volume-off-icon {
/* Horizontal orientation styles */
.slider-root[data-orientation='horizontal'] {
min-width: 80px;
min-width: 5rem;
width: 100%;
height: 20px;
height: 1.25rem;
}
/* Vertical orientation styles */
.slider-root[data-orientation='vertical'] {
height: 80px;
width: 20px;
height: 5rem;
width: 1.25rem;
}
.slider-track {
@@ -278,7 +276,7 @@ media-mute-button[data-volume-level='off'] .volume-off-icon {
opacity: 0;
transition-property: opacity, height, width;
transition-duration: 150ms;
transition-timing-function: ease-in-out;
transition-timing-function: ease-out;
}
.slider-thumb:active {
width: 0.75rem;
+99
View File
@@ -1,4 +1,5 @@
import { MediaSkin } from '@/media/media-skin';
import styles from './styles.css';
import '@/media/media-container';
import '@/media/media-provider';
import '@/components/media-play-button';
@@ -11,11 +12,109 @@ import '@/components/media-current-time-display';
import '@/components/media-preview-time-display';
import '@/components/media-popover';
import '@/components/media-tooltip';
import '@/icons';
export function getTemplateHTML() {
return /* html */`
${MediaSkin.getTemplateHTML()}
<style>${styles}</style>
<media-container>
<slot name="media" slot="media"></slot>
<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-root 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-root>
<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-root track-cursor-axis="x">
<media-tooltip-trigger>
<media-time-slider-root class="slider-root">
<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-root>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="12" collision-padding="12">
<media-tooltip-popup class="popup-animation">
<preview-time-display></preview-time-display>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
<div class="button-group">
<media-popover-root open-on-hover delay="200" close-delay="100">
<media-popover-trigger>
<media-mute-button 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-trigger>
<media-popover-portal>
<media-popover-positioner side="top" side-offset="2" collision-padding="12">
<media-popover-popup class="popup-animation">
<media-volume-slider-root class="slider-root" 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-root>
</media-popover-popup>
</media-popover-positioner>
</media-popover-portal>
</media-popover-root>
<media-tooltip-root 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-root>
</div>
</div>
</media-container>
`;
}
+346
View File
@@ -0,0 +1,346 @@
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']) {
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-root {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: calc(infinity * 1px);
outline: none;
}
/* Horizontal orientation styles */
.slider-root[data-orientation='horizontal'] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
/* Vertical orientation styles */
.slider-root[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-root: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-root:hover .slider-thumb,
.slider-root: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;
}
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-popup {
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;
}
@@ -1,5 +1,16 @@
import type { PropsWithChildren } from 'react';
import { CurrentTimeDisplay } from '@/components/CurrentTimeDisplay';
import { DurationDisplay } from '@/components/DurationDisplay';
import { FullscreenButton } from '@/components/FullscreenButton';
import { MediaContainer } from '@/components/MediaContainer';
import MuteButton from '@/components/MuteButton';
import PlayButton from '@/components/PlayButton';
import { Popover } from '@/components/Popover';
import { PreviewTimeDisplay } from '@/components/PreviewTimeDisplay';
import { TimeSlider } from '@/components/TimeSlider';
import { Tooltip } from '@/components/Tooltip';
import { VolumeSlider } from '@/components/VolumeSlider';
import {
FullscreenEnterIcon,
FullscreenExitIcon,
@@ -9,18 +20,6 @@ import {
VolumeLowIcon,
VolumeOffIcon,
} from '@/icons';
import { CurrentTimeDisplay } from '../../components/CurrentTimeDisplay';
import { DurationDisplay } from '../../components/DurationDisplay';
import { FullscreenButton } from '../../components/FullscreenButton';
import { MediaContainer } from '../../components/MediaContainer';
import MuteButton from '../../components/MuteButton';
import PlayButton from '../../components/PlayButton';
import { Popover } from '../../components/Popover';
import { PreviewTimeDisplay } from '../../components/PreviewTimeDisplay';
import { TimeSlider } from '../../components/TimeSlider';
import { Tooltip } from '../../components/Tooltip';
import { VolumeSlider } from '../../components/VolumeSlider';
import styles from './styles';
type SkinProps = PropsWithChildren<{
@@ -32,8 +31,7 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
{children}
{/* Background gradient to help with controls contrast. */}
<div className={styles.Overlay} aria-hidden="true" />
<div className={styles.Overlay} />
<div className={`${styles.Surface} ${styles.Controls}`} data-testid="media-controls">
<Tooltip.Root delay={500}>
+10 -13
View File
@@ -15,21 +15,21 @@ const styles: FrostedSkinStyles = {
'vjs:relative vjs:isolate vjs:@container/root vjs:group/root vjs:overflow-clip vjs:bg-black',
// Base typography
'vjs:text-[0.8125rem]', // 13px
// Prevent rounded corners in fullscreen.
'vjs:[&:fullscreen]:rounded-none vjs:[&:fullscreen]:[&_video]:h-full vjs:[&:fullscreen]:[&_video]:w-full',
// Fancy borders.
'vjs:after:absolute vjs:after:inset-0 vjs:after:ring-black/10 vjs:after:ring-1 vjs:dark:after:ring-white/10 vjs:after:ring-inset vjs:after:z-10 vjs:after:pointer-events-none vjs:after:rounded-[inherit]',
'vjs:before:absolute vjs:before:inset-px vjs:before:rounded-[inherit] vjs:before:ring-white/15 vjs:before:ring-1 vjs:before:ring-inset vjs:before:z-10 vjs:before:pointer-events-none vjs:dark:before:ring-0',
// Prevent rounded corners in fullscreen.
'vjs:[&:fullscreen]:rounded-none',
// Ensure the nested video inherits the radius.
'vjs:[&_video]:w-full vjs:[&_video]:h-full',
),
Overlay: cn(
'vjs:opacity-0 vjs:delay-500 vjs:rounded-[inherit] vjs:absolute vjs:inset-0 vjs:pointer-events-none vjs:bg-gradient-to-t vjs:from-black/50 vjs:via-black/20 vjs:to-transparent vjs:transition-opacity vjs:backdrop-saturate-150 vjs:backdrop-brightness-90',
'vjs:opacity-0 vjs:delay-500 vjs:duration-300 vjs:rounded-[inherit] vjs:absolute vjs:inset-0 vjs:pointer-events-none vjs:bg-gradient-to-t vjs:from-black/50 vjs:via-black/20 vjs:to-transparent vjs:transition-opacity vjs:backdrop-saturate-150 vjs:backdrop-brightness-90',
// Hide when playing (for now).
// FIXME: This is crude temporary logic, we'll improve it later I guess with a [data-show-controls] attribute or something?
'vjs:has-[+.controls_[data-paused]]:opacity-100 vjs:has-[+.controls_[data-paused]]:delay-0',
'vjs:has-[+.controls_[aria-expanded="true"]]:opacity-100 vjs:has-[+.controls_[aria-expanded="true"]]:delay-0',
'vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0',
'vjs:has-[+.controls_[data-paused]]:opacity-100 vjs:has-[+.controls_[data-paused]]:delay-0 vjs:has-[+.controls_[data-paused]]:duration-100',
'vjs:has-[+.controls_[aria-expanded="true"]]:opacity-100 vjs:has-[+.controls_[aria-expanded="true"]]:delay-0 vjs:has-[+.controls_[aria-expanded="true"]]:duration-100',
'vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0 vjs:group-hover/root:duration-100',
),
Surface: cn(
'vjs:bg-white/10 vjs:backdrop-blur-3xl vjs:backdrop-saturate-150 vjs:backdrop-brightness-90',
@@ -123,9 +123,7 @@ const styles: FrostedSkinStyles = {
TimeControls: cn('vjs:flex-1 vjs:flex vjs:items-center vjs:gap-3 vjs:px-1.5'),
TimeDisplay: cn('vjs:tabular-nums vjs:text-shadow-2xs/25'),
SliderRoot: cn(
'vjs:group/slider-root',
'vjs:outline-0',
'vjs:flex vjs:items-center vjs:justify-center vjs:flex-1 vjs:group/slider vjs:relative vjs:rounded-full',
'vjs:group/slider-root vjs:outline-0 vjs:flex vjs:items-center vjs:justify-center vjs:flex-1 vjs:relative vjs:rounded-full',
'vjs:[&[data-orientation="horizontal"]]:h-5 vjs:[&[data-orientation="horizontal"]]:min-w-20',
'vjs:[&[data-orientation="vertical"]]:w-5 vjs:[&[data-orientation="vertical"]]:h-20',
),
@@ -139,10 +137,9 @@ const styles: FrostedSkinStyles = {
// TODO: Work out what we want to do here.
SliderPointer: cn('vjs:bg-white/20 vjs:rounded-[inherit]'),
SliderThumb: cn(
'vjs:bg-white vjs:z-10 vjs:select-none vjs:ring vjs:ring-black/10 vjs:rounded-full vjs:shadow-sm vjs:shadow-black/15',
'vjs:opacity-0 vjs:transition-[opacity,height,width] vjs:ease-in-out',
'vjs:group-hover/slider:opacity-100 vjs:group-focus-within/slider:opacity-100',
'vjs:size-2.5 vjs:active:size-3 vjs:group-active/slider:size-3',
'vjs:bg-white vjs:z-10 vjs:select-none vjs:ring vjs:ring-black/10 vjs:rounded-full vjs:shadow-sm vjs:shadow-black/15 vjs:opacity-0 vjs:transition-[opacity,height,width] vjs:ease-out',
'vjs:group-hover/slider-root:opacity-100 vjs:group-focus-within/slider-root:opacity-100',
'vjs:size-2.5 vjs:active:size-3 vjs:group-active/slider-root:size-3',
'vjs:[&[data-orientation="horizontal"]]:hover:cursor-ew-resize',
'vjs:[&[data-orientation="vertical"]]:hover:cursor-ns-resize',
),
@@ -1,9 +1,14 @@
import type { PropsWithChildren } from 'react';
import { CurrentTimeDisplay } from '@/components/CurrentTimeDisplay';
import { DurationDisplay } from '@/components/DurationDisplay';
import { FullscreenButton } from '@/components/FullscreenButton';
import { MediaContainer } from '@/components/MediaContainer';
import MuteButton from '@/components/MuteButton';
import PlayButton from '@/components/PlayButton';
import Popover from '@/components/Popover';
import PreviewTimeDisplay from '@/components/PreviewTimeDisplay';
import { TimeSlider } from '@/components/TimeSlider';
import Tooltip from '@/components/Tooltip';
import VolumeSlider from '@/components/VolumeSlider';
import {
@@ -15,13 +20,6 @@ import {
VolumeLowIcon,
VolumeOffIcon,
} from '@/icons';
import { CurrentTimeDisplay } from '../../components/CurrentTimeDisplay';
import { DurationDisplay } from '../../components/DurationDisplay';
import { FullscreenButton } from '../../components/FullscreenButton';
import { MediaContainer } from '../../components/MediaContainer';
import MuteButton from '../../components/MuteButton';
import PlayButton from '../../components/PlayButton';
import { TimeSlider } from '../../components/TimeSlider';
import styles from './styles';
type SkinProps = PropsWithChildren<{
@@ -33,7 +31,7 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
{children}
<div className={styles.Overlay} aria-hidden="true" />
<div className={styles.Overlay} />
<div className={styles.Controls}>
<Tooltip.Root delay={500} closeDelay={0}>
@@ -53,7 +51,7 @@ export default function MinimalSkin({ children, className = '' }: SkinProps): JS
</Tooltip.Portal>
</Tooltip.Root>
<div className="flex items-center gap-1">
<div className={styles.TimeDisplayGroup}>
<CurrentTimeDisplay
// Use showRemaining to show count down/remaining time
// showRemaining
+13 -13
View File
@@ -15,16 +15,15 @@ const styles: MinimalSkinStyles = {
'vjs:relative vjs:isolate vjs:@container/root vjs:group/root vjs:overflow-clip vjs:bg-black',
// Base typography
'vjs:text-[0.8125rem]', // 13px
// 'ring-1 ring-inset ring-black/10 dark:ring-white/10',
'vjs:after:absolute vjs:after:inset-0 vjs:after:ring-black/10 vjs:after:ring-1 vjs:dark:after:ring-white/10 vjs:after:ring-inset vjs:after:z-10 vjs:after:pointer-events-none vjs:after:rounded-[inherit]',
'vjs:after:absolute vjs:after:inset-0 vjs:after:ring-black/20 vjs:after:ring-1 vjs:dark:after:ring-white/20 vjs:after:ring-inset vjs:after:z-10 vjs:after:pointer-events-none vjs:after:rounded-[inherit]',
// Prevent rounded corners in fullscreen.
'vjs:[&:fullscreen]:rounded-none vjs:[&:fullscreen]:[&_video]:h-full vjs:[&:fullscreen]:[&_video]:w-full',
'vjs:[&:fullscreen]:rounded-none',
// Ensure the nested video inherits the radius.
'vjs:[&_video]:w-full vjs:[&_video]:h-full',
),
Overlay: cn(
'vjs:absolute vjs:inset-0 vjs:rounded-[inherit] vjs:pointer-events-none',
'vjs:bg-gradient-to-t vjs:from-black/70 vjs:via-black/50 vjs:via-[120px] vjs:to-transparent',
'vjs:bg-gradient-to-t vjs:from-black/70 vjs:via-black/50 vjs:via-[7.5rem] vjs:to-transparent',
'vjs:opacity-0 vjs:delay-500 vjs:duration-300',
// FIXME: Temporary hide/show logic
'vjs:has-[+.controls_[data-paused]]:opacity-100 vjs:has-[+.controls_[data-paused]]:delay-0 vjs:has-[+.controls_[data-paused]]:duration-75',
@@ -37,14 +36,15 @@ const styles: MinimalSkinStyles = {
'controls', // FIXME: Temporary className hook for above logic in the overlay. Can be removed once have a proper way to handle controls visibility.
'vjs:@container/controls vjs:absolute vjs:inset-x-0 vjs:bottom-0 vjs:flex vjs:items-center vjs:gap-3.5 vjs:z-20 vjs:px-3 vjs:pb-3 vjs:pt-10 vjs:text-white',
// Animation
'vjs:transition vjs:ease-in-out',
'vjs:transition vjs:will-change-[transform,filter,opacity] vjs:ease-out',
// FIXME: Temporary hide/show logic
'vjs:translate-y-full vjs:opacity-0 vjs:delay-500 vjs:duration-300',
'vjs:has-[[data-paused]]:translate-y-0 vjs:has-[[data-paused]]:opacity-100 vjs:has-[[data-paused]]:delay-0 vjs:has-[[data-paused]]:duration-75',
'vjs:has-[[aria-expanded="true"]]:translate-y-0 vjs:has-[[aria-expanded="true"]]:opacity-100 vjs:has-[[aria-expanded="true"]]:delay-0 vjs:has-[[aria-expanded="true"]]:duration-75',
'vjs:group-hover/root:translate-y-0 vjs:group-hover/root:opacity-100 vjs:group-hover/root:delay-0 vjs:group-hover/root:duration-75',
'vjs:translate-y-full vjs:opacity-0 vjs:blur-sm vjs:delay-500 vjs:duration-300',
'vjs:has-[[data-paused]]:translate-y-0 vjs:has-[[data-paused]]:opacity-100 vjs:has-[[data-paused]]:blur-none vjs:has-[[data-paused]]:delay-0 vjs:has-[[data-paused]]:duration-75',
'vjs:has-[[aria-expanded="true"]]:translate-y-0 vjs:has-[[aria-expanded="true"]]:opacity-100 vjs:has-[[aria-expanded="true"]]:blur-none vjs:has-[[aria-expanded="true"]]:delay-0 vjs:has-[[aria-expanded="true"]]:duration-75',
'vjs:group-hover/root:translate-y-0 vjs:group-hover/root:opacity-100 vjs:group-hover/root:blur-none vjs:group-hover/root:delay-0 vjs:group-hover/root:duration-75',
),
Icon: cn('icon'),
TimeDisplayGroup: cn('flex items-center gap-1'),
Button: cn(
'vjs:group/button vjs:cursor-pointer vjs:relative vjs:shrink-0 vjs:transition-[color,background,outline-offset] vjs:select-none vjs:p-2.5 vjs:rounded-md',
// Background/foreground
@@ -114,7 +114,7 @@ const styles: MinimalSkinStyles = {
TimeDisplay: cn('vjs:tabular-nums vjs:text-shadow-2xs/20'),
DurationDisplay: cn('vjs:text-white/50 vjs:contents'),
SliderRoot: cn(
'vjs:flex vjs:items-center vjs:justify-center vjs:flex-1 vjs:group/slider vjs:relative vjs:rounded-full',
'vjs:group/slider-root vjs:outline-0 vjs:flex vjs:items-center vjs:justify-center vjs:flex-1 vjs:group/slider vjs:relative vjs:rounded-full',
'vjs:[&[data-orientation="horizontal"]]:h-5 vjs:[&[data-orientation="horizontal"]]:min-w-20',
'vjs:[&[data-orientation="vertical"]]:w-5 vjs:[&[data-orientation="vertical"]]:h-18',
),
@@ -122,13 +122,13 @@ const styles: MinimalSkinStyles = {
'vjs:relative vjs:select-none vjs:rounded-[inherit] vjs:bg-white/10',
'vjs:[&[data-orientation="horizontal"]]:w-full vjs:[&[data-orientation="horizontal"]]:h-[0.1875rem]',
'vjs:[&[data-orientation="vertical"]]:w-[0.1875rem]',
'vjs:-outline-offset-2 vjs:group-focus-visible/slider-root:outline-2 vjs:group-focus-visible/slider-root:outline-offset-2 vjs:group-focus-visible/slider-root:outline-white',
'vjs:-outline-offset-2 vjs:group-focus-visible/slider-root:outline-2 vjs:group-focus-visible/slider-root:outline-offset-6 vjs:group-focus-visible/slider-root:outline-white',
),
SliderProgress: cn('vjs:bg-white vjs:rounded-[inherit]'),
SliderPointer: cn('vjs:hidden'),
SliderThumb: cn(
'vjs:opacity-0 vjs:scale-70 vjs:group-hover/slider:opacity-100 vjs:group-hover/slider:scale-100 vjs:focus-visible:opacity-100 vjs:focus-visible:scale-100',
'vjs:bg-white vjs:z-10 vjs:size-3 vjs:select-none vjs:ring vjs:ring-black/10 vjs:rounded-full vjs:shadow-sm vjs:shadow-black/15 vjs:transition-[opacity,scale] vjs:ease-out',
'vjs:bg-white vjs:z-10 vjs:size-3 vjs:select-none vjs:ring vjs:ring-black/10 vjs:rounded-full vjs:shadow-sm vjs:shadow-black/15 vjs:transition-[opacity,scale] vjs:ease-out vjs:opacity-0 vjs:scale-70 vjs:origin-center',
'vjs:group-hover/slider-root:opacity-100 vjs:group-hover/slider-root:scale-100',
'vjs:[&[data-orientation="horizontal"]]:hover:cursor-ew-resize',
'vjs:[&[data-orientation="vertical"]]:hover:cursor-ns-resize',
),
@@ -23,6 +23,7 @@ export interface MinimalSkinStyles {
readonly FullscreenEnterTooltip: string;
readonly FullscreenExitTooltip: string;
readonly TimeSliderRoot: string;
readonly TimeDisplayGroup: string;
readonly TimeDisplay: string;
readonly DurationDisplay: string;
readonly SliderRoot: string;