feat(skin): add captions button to video skins (#612)

This commit is contained in:
Sam Potts
2026-02-26 11:09:22 +11:00
committed by GitHub
parent cb09079758
commit fbf888fda3
13 changed files with 195 additions and 14 deletions
@@ -103,7 +103,7 @@
inset: 0;
border-radius: inherit;
background-image: linear-gradient(to top, oklch(0 0 0 / 0.7), oklch(0 0 0 / 0.5) 7.5rem, oklch(0 0 0 / 0));
backdrop-filter: blur(0) saturate(1.5) brightness(0.9);
backdrop-filter: blur(0) saturate(1.2) brightness(0.9);
opacity: 0;
transition-property: opacity, backdrop-filter;
transition-duration: 500ms;
@@ -123,7 +123,7 @@
transition-delay: 0ms;
}
.media-minimal-skin .media-error[data-visible] ~ .media-overlay {
backdrop-filter: blur(8px) saturate(1.5) brightness(0.9);
backdrop-filter: blur(8px) saturate(1.2) brightness(0.9);
}
/* ==========================================================================
@@ -1,5 +1,7 @@
import type { FullscreenButtonState, MuteButtonState, PlayButtonState } from '@videojs/core';
import {
CaptionsOffIcon,
CaptionsOnIcon,
FullscreenEnterIcon,
FullscreenExitIcon,
PauseIcon,
@@ -16,6 +18,7 @@ import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container } from '@/player/context';
import { BufferingIndicator } from '@/ui/buffering-indicator';
import { CaptionsButton, type CaptionsButtonState } from '@/ui/captions-button';
import { Controls } from '@/ui/controls';
import { ErrorDialog } from '@/ui/error-dialog';
import { FullscreenButton } from '@/ui/fullscreen-button';
@@ -98,6 +101,16 @@ function MuteButtonIcon({ state, className, ...rest }: { state: MuteButtonState
);
}
function CaptionsButtonIcon({ state, className, ...rest }: { state: CaptionsButtonState } & ComponentProps<'svg'>) {
const { active } = state;
return (
<>
<CaptionsOffIcon {...rest} className={cn(className, { [iconHidden]: active })} />
<CaptionsOnIcon {...rest} className={cn(className, { [iconHidden]: !active })} />
</>
);
}
function FullscreenButtonIcon({ state, className, ...rest }: { state: FullscreenButtonState } & ComponentProps<'svg'>) {
const { fullscreen } = state;
return (
@@ -278,6 +291,14 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
)}
/>
<CaptionsButton
render={(props, state) => (
<Button variant="icon" {...props}>
<CaptionsButtonIcon state={state} className={icon} />
</Button>
)}
/>
<PiPButton
render={(props) => (
<Button variant="icon" {...props}>
@@ -330,7 +351,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
// Default: hidden
'opacity-0',
'bg-gradient-to-t from-black/70 via-black/50 via-[7.5rem] to-transparent',
'backdrop-blur-[0px] backdrop-saturate-150 backdrop-brightness-90',
'backdrop-blur-[0px] backdrop-saturate-120 backdrop-brightness-90',
// Transitions
'transition-[opacity,backdrop-filter] ease-out',
'duration-500 delay-500',
@@ -1,5 +1,7 @@
import type { FullscreenButtonState, MuteButtonState, PlayButtonState } from '@videojs/core';
import {
CaptionsOffIcon,
CaptionsOnIcon,
FullscreenEnterIcon,
FullscreenExitIcon,
PauseIcon,
@@ -16,6 +18,7 @@ import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container } from '@/player/context';
import { BufferingIndicator } from '@/ui/buffering-indicator';
import { CaptionsButton, type CaptionsButtonState } from '@/ui/captions-button';
import { Controls } from '@/ui/controls';
import { ErrorDialog } from '@/ui/error-dialog';
import { FullscreenButton } from '@/ui/fullscreen-button';
@@ -56,6 +59,16 @@ function MuteButtonIcon({ state, className, ...rest }: { state: MuteButtonState
);
}
function CaptionsButtonIcon({ state, className, ...rest }: { state: CaptionsButtonState } & ComponentProps<'svg'>) {
const { active } = state;
return (
<>
<CaptionsOffIcon {...rest} className={cn(className, { 'media-icon--hidden': active })} />
<CaptionsOnIcon {...rest} className={cn(className, { 'media-icon--hidden': !active })} />
</>
);
}
function FullscreenButtonIcon({ state, className, ...rest }: { state: FullscreenButtonState } & ComponentProps<'svg'>) {
const { fullscreen } = state;
return (
@@ -160,6 +173,14 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
)}
/>
<CaptionsButton
render={(props, state) => (
<Button {...props} className="media-button--icon">
<CaptionsButtonIcon state={state} className="media-icon" />
</Button>
)}
/>
<PiPButton
render={(props) => (
<Button {...props} className="media-button--icon">
+2 -2
View File
@@ -106,7 +106,7 @@
inset: 0;
border-radius: inherit;
background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
backdrop-filter: blur(0) saturate(1.5) brightness(0.9);
backdrop-filter: blur(0) saturate(1.2) brightness(0.9);
opacity: 0;
transition-property: opacity, backdrop-filter;
transition-duration: 300ms;
@@ -126,7 +126,7 @@
transition-delay: 0ms;
}
.media-default-skin .media-error[data-visible] ~ .media-overlay {
backdrop-filter: blur(8px) saturate(1.5) brightness(0.9);
backdrop-filter: blur(8px) saturate(1.2) brightness(0.9);
}
/* ==========================================================================
@@ -1,5 +1,7 @@
import type { FullscreenButtonState, MuteButtonState, PlayButtonState } from '@videojs/core';
import {
CaptionsOffIcon,
CaptionsOnIcon,
FullscreenEnterIcon,
FullscreenExitIcon,
PauseIcon,
@@ -16,6 +18,7 @@ import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container } from '@/player/context';
import { BufferingIndicator } from '@/ui/buffering-indicator';
import { CaptionsButton, type CaptionsButtonState } from '@/ui/captions-button';
import { Controls } from '@/ui/controls';
import { ErrorDialog } from '@/ui/error-dialog';
import { FullscreenButton } from '@/ui/fullscreen-button';
@@ -112,6 +115,16 @@ function MuteButtonIcon({ state, className, ...rest }: { state: MuteButtonState
);
}
function CaptionsButtonIcon({ state, className, ...rest }: { state: CaptionsButtonState } & ComponentProps<'svg'>) {
const { active } = state;
return (
<>
<CaptionsOffIcon {...rest} className={cn(className, { [iconHidden]: active })} />
<CaptionsOnIcon {...rest} className={cn(className, { [iconHidden]: !active })} />
</>
);
}
function FullscreenButtonIcon({ state, className, ...rest }: { state: FullscreenButtonState } & ComponentProps<'svg'>) {
const { fullscreen } = state;
return (
@@ -277,6 +290,14 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Time.Value type="duration" className="text-shadow-2xs text-shadow-black/25 tabular-nums" />
</Time.Group>
<CaptionsButton
render={(props, state) => (
<Button variant="icon" {...props}>
<CaptionsButtonIcon state={state} className={icon} />
</Button>
)}
/>
<MuteButton
render={(props, state) => (
<Button variant="icon" {...props}>
@@ -336,7 +357,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
// Default: hidden
'opacity-0',
'bg-gradient-to-t from-black/50 via-black/30 to-transparent',
'backdrop-blur-[0px] backdrop-saturate-150 backdrop-brightness-90',
'backdrop-blur-[0px] backdrop-saturate-120 backdrop-brightness-90',
// Transitions
'transition-[opacity,backdrop-filter] ease-out',
'duration-300 delay-500',
+21
View File
@@ -1,5 +1,7 @@
import type { FullscreenButtonState, MuteButtonState, PlayButtonState } from '@videojs/core';
import {
CaptionsOffIcon,
CaptionsOnIcon,
FullscreenEnterIcon,
FullscreenExitIcon,
PauseIcon,
@@ -16,6 +18,7 @@ import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container } from '@/player/context';
import { BufferingIndicator } from '@/ui/buffering-indicator';
import { CaptionsButton, type CaptionsButtonState } from '@/ui/captions-button';
import { Controls } from '@/ui/controls';
import { ErrorDialog } from '@/ui/error-dialog';
import { FullscreenButton } from '@/ui/fullscreen-button';
@@ -56,6 +59,16 @@ function MuteButtonIcon({ state, className, ...rest }: { state: MuteButtonState
);
}
function CaptionsButtonIcon({ state, className, ...rest }: { state: CaptionsButtonState } & ComponentProps<'svg'>) {
const { active } = state;
return (
<>
<CaptionsOffIcon {...rest} className={cn(className, { 'media-icon--hidden': active })} />
<CaptionsOnIcon {...rest} className={cn(className, { 'media-icon--hidden': !active })} />
</>
);
}
function FullscreenButtonIcon({ state, className, ...rest }: { state: FullscreenButtonState } & ComponentProps<'svg'>) {
const { fullscreen } = state;
return (
@@ -155,6 +168,14 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
)}
/>
<CaptionsButton
render={(props, state) => (
<Button {...props} className="media-button--icon">
<CaptionsButtonIcon state={state} className="media-icon" />
</Button>
)}
/>
<PiPButton
render={(props) => (
<Button {...props} className="media-button--icon">