feat(skin): add error handling for audio players (#1048)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Potts
2026-03-23 09:33:55 +11:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 559d5169b5
commit df927f67fc
43 changed files with 648 additions and 377 deletions
@@ -12,6 +12,7 @@ import {
button,
buttonGroup,
controls,
error,
icon,
iconContainer,
iconFlipped,
@@ -34,23 +35,26 @@ import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { ErrorDialog } from '../error-dialog';
import type { MinimalAudioSkinProps } from './minimal-skin';
const SEEK_TIME = 10;
const ERROR_CLASSNAMES = {
root: error.root,
dialog: error.dialog,
content: error.content,
title: error.title,
description: error.description,
actions: error.actions,
close: cn(button.base, button.subtle),
};
/* --------------------------------------- Components ---------------------------------------- */
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { variant?: 'icon' }>(function Button(
{ className, variant, ...props },
ref
) {
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return (
<button
ref={ref}
type="button"
className={cn(button.base, variant === 'icon' ? button.icon : button.default, className)}
{...props}
/>
<button ref={ref} type="button" className={cn(button.base, button.subtle, button.icon, className)} {...props} />
);
});
@@ -106,7 +110,7 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<MuteButton className={iconState.mute.button} render={<Button />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
@@ -139,13 +143,15 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Container className={cn(root, className)} {...rest}>
{children}
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<div className={controls}>
<Tooltip.Provider>
<div className={buttonGroup}>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<PlayButton className={iconState.play.button} render={<Button />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
@@ -160,7 +166,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
@@ -174,7 +180,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
@@ -204,9 +210,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<div className={buttonGroup}>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Trigger render={<PlaybackRateButton className={playbackRate.button} render={<Button />} />} />
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -19,14 +19,32 @@ import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { ErrorDialog } from '../error-dialog';
import type { BaseSkinProps } from '../types';
const SEEK_TIME = 10;
export type MinimalAudioSkinProps = BaseSkinProps;
const SEEK_TIME = 10;
const ERROR_CLASSNAMES = {
root: 'media-error',
dialog: 'media-error__dialog',
content: 'media-error__content',
title: 'media-error__title',
description: 'media-error__description',
actions: 'media-error__actions',
close: 'media-button media-button--subtle',
};
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
return (
<button
ref={ref}
type="button"
className={cn('media-button media-button--subtle media-button--icon', className)}
{...props}
/>
);
});
function PlayLabel(): string {
@@ -71,6 +89,8 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<Container className={cn('media-minimal-skin media-minimal-skin--audio', className)} {...rest}>
{children}
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<div className="media-controls">
<Tooltip.Provider>
<div className="media-button-group">
@@ -10,6 +10,7 @@ import {
import {
button,
controls,
error,
icon,
iconContainer,
iconFlipped,
@@ -33,23 +34,26 @@ import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { ErrorDialog } from '../error-dialog';
import type { AudioSkinProps } from './skin';
const SEEK_TIME = 10;
const ERROR_CLASSNAMES = {
root: error.root,
dialog: error.dialog,
content: error.content,
title: error.title,
description: error.description,
actions: error.actions,
close: cn(button.base, button.subtle),
};
/* --------------------------------------- Components ---------------------------------------- */
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { variant?: 'icon' }>(function Button(
{ className, variant, ...props },
ref
) {
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return (
<button
ref={ref}
type="button"
className={cn(button.base, variant === 'icon' ? button.icon : button.default, className)}
{...props}
/>
<button ref={ref} type="button" className={cn(button.base, button.subtle, button.icon, className)} {...props} />
);
});
@@ -105,7 +109,7 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<MuteButton className={iconState.mute.button} render={<Button />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
@@ -138,12 +142,14 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Container className={cn(root, className)} {...rest}>
{children}
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<div className={controls}>
<Tooltip.Provider>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<PlayButton className={iconState.play.button} render={<Button />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
@@ -158,7 +164,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
@@ -172,7 +178,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
@@ -196,9 +202,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
</Time.Group>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Trigger render={<PlaybackRateButton className={playbackRate.button} render={<Button />} />} />
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
+21 -1
View File
@@ -19,14 +19,32 @@ import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { ErrorDialog } from '../error-dialog';
import type { BaseSkinProps } from '../types';
const SEEK_TIME = 10;
const ERROR_CLASSNAMES = {
root: 'media-error',
dialog: 'media-error__dialog',
content: 'media-error__content',
title: 'media-error__title',
description: 'media-error__description',
actions: 'media-error__actions',
close: 'media-button media-button--subtle',
};
export type AudioSkinProps = BaseSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
return (
<button
ref={ref}
type="button"
className={cn('media-button media-button--subtle media-button--icon', className)}
{...props}
/>
);
});
function PlayLabel(): string {
@@ -71,6 +89,8 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<Container className={cn('media-default-skin media-default-skin--audio', className)} {...rest}>
{children}
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<div className="media-surface media-controls">
<Tooltip.Provider>
<Tooltip.Root side="top">
@@ -5,7 +5,7 @@ import { type ReactNode, useRef } from 'react';
import { usePlayer } from '@/player/context';
import { AlertDialog } from '@/ui/alert-dialog';
export interface ErrorDialogClasses {
export interface ErrorDialogClassNames {
root?: string;
dialog?: string;
content?: string;
@@ -15,7 +15,7 @@ export interface ErrorDialogClasses {
close?: string;
}
export function ErrorDialog({ classes }: { classes?: ErrorDialogClasses }): ReactNode {
export function ErrorDialog({ classNames }: { classNames?: ErrorDialogClassNames }): ReactNode {
const errorState = usePlayer(selectError);
const lastError = useRef(errorState?.error);
@@ -25,21 +25,21 @@ export function ErrorDialog({ classes }: { classes?: ErrorDialogClasses }): Reac
return (
<AlertDialog.Root
open={!!errorState.error}
open={Boolean(errorState.error)}
onOpenChange={(open) => {
if (!open) errorState.dismissError();
}}
>
<AlertDialog.Popup className={classes?.root}>
<div className={classes?.dialog}>
<div className={classes?.content}>
<AlertDialog.Title className={classes?.title}>Something went wrong.</AlertDialog.Title>
<AlertDialog.Description className={classes?.description}>
{lastError.current?.message ?? 'An error occurred while trying to play the video. Please try again.'}
<AlertDialog.Popup className={classNames?.root}>
<div className={classNames?.dialog}>
<div className={classNames?.content}>
<AlertDialog.Title className={classNames?.title}>Something went wrong.</AlertDialog.Title>
<AlertDialog.Description className={classNames?.description}>
{lastError.current?.message ?? 'An error occurred. Please try again.'}
</AlertDialog.Description>
</div>
<div className={classes?.actions}>
<AlertDialog.Close className={classes?.close}>OK</AlertDialog.Close>
<div className={classNames?.actions}>
<AlertDialog.Close className={classNames?.close}>OK</AlertDialog.Close>
</div>
</div>
</AlertDialog.Popup>
@@ -55,24 +55,26 @@ import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { isRenderProp } from '@/utils/use-render';
import { ErrorDialog } from './error-dialog';
import { ErrorDialog } from '../error-dialog';
import type { MinimalVideoSkinProps } from './minimal-skin';
const SEEK_TIME = 10;
const ERROR_CLASSNAMES = {
root: error.root,
dialog: error.dialog,
content: error.content,
title: error.title,
description: error.description,
actions: error.actions,
close: cn(button.base, button.primary),
};
/* --------------------------------------- Components ---------------------------------------- */
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { variant?: 'icon' }>(function Button(
{ className, variant, ...props },
ref
) {
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return (
<button
ref={ref}
type="button"
className={cn(button.base, variant === 'icon' ? button.icon : button.default, className)}
{...props}
/>
<button ref={ref} type="button" className={cn(button.base, button.subtle, button.icon, className)} {...props} />
);
});
@@ -117,15 +119,6 @@ const SliderThumb = forwardRef<HTMLDivElement, ComponentProps<'div'> & { persist
);
});
const errorClasses = {
root: error.root,
dialog: error.dialog,
content: error.content,
title: error.title,
actions: error.actions,
close: cn(button.base, button.default),
};
function PlayLabel(): string {
const paused = usePlayer((s) => Boolean(s.paused));
const ended = usePlayer((s) => Boolean(s.ended));
@@ -152,7 +145,7 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<MuteButton className={iconState.mute.button} render={<Button />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
@@ -201,7 +194,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
)}
/>
<ErrorDialog classes={errorClasses} />
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<Controls.Root
data-controls="" // Used as a hook for Tailwind has-[] styles
@@ -212,7 +205,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<PlayButton className={iconState.play.button} render={<Button />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
@@ -227,7 +220,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
@@ -241,7 +234,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
@@ -278,9 +271,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<div className={buttonGroup}>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Trigger render={<PlaybackRateButton className={playbackRate.button} render={<Button />} />} />
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -289,7 +280,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton className={iconState.captions.button} render={<Button variant="icon" />}>
<CaptionsButton className={iconState.captions.button} render={<Button />}>
<CaptionsOffIcon className={cn(icon, iconState.captions.off)} />
<CaptionsOnIcon className={cn(icon, iconState.captions.on)} />
</CaptionsButton>
@@ -303,7 +294,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton className={iconState.pip.button} render={<Button variant="icon" />}>
<PiPButton className={iconState.pip.button} render={<Button />}>
<PipEnterIcon className={cn(icon, iconState.pip.off)} />
<PipExitIcon className={cn(icon, iconState.pip.on)} />
</PiPButton>
@@ -317,7 +308,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton className={iconState.fullscreen.button} render={<Button variant="icon" />}>
<FullscreenButton className={iconState.fullscreen.button} render={<Button />}>
<FullscreenEnterIcon className={cn(icon, iconState.fullscreen.enter)} />
<FullscreenExitIcon className={cn(icon, iconState.fullscreen.exit)} />
</FullscreenButton>
@@ -35,27 +35,34 @@ import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { isRenderProp } from '@/utils/use-render';
import { ErrorDialog } from '../error-dialog';
import type { BaseVideoSkinProps } from '../types';
import { ErrorDialog } from './error-dialog';
const SEEK_TIME = 10;
export type MinimalVideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
});
const errorClasses = {
const ERROR_CLASSNAMES = {
root: 'media-error',
dialog: 'media-error__dialog',
content: 'media-error__content',
title: 'media-error__title',
description: 'media-error__description',
actions: 'media-error__actions',
close: 'media-button',
close: 'media-button media-button--subtle',
};
export type MinimalVideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return (
<button
ref={ref}
type="button"
className={cn('media-button media-button--subtle media-button--icon', className)}
{...props}
/>
);
});
function PlayLabel(): string {
const paused = usePlayer((s) => Boolean(s.paused));
const ended = usePlayer((s) => Boolean(s.ended));
@@ -125,7 +132,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
)}
/>
<ErrorDialog classes={errorClasses} />
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<Controls.Root className="media-controls">
<Tooltip.Provider>
@@ -54,24 +54,26 @@ import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { isRenderProp } from '@/utils/use-render';
import { ErrorDialog } from './error-dialog';
import { ErrorDialog } from '../error-dialog';
import type { VideoSkinProps } from './skin';
const SEEK_TIME = 10;
const ERROR_CLASSNAMES = {
root: error.root,
dialog: error.dialog,
content: error.content,
title: error.title,
description: error.description,
actions: error.actions,
close: cn(button.base, button.primary),
};
/* --------------------------------------- Components ---------------------------------------- */
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { variant?: 'icon' }>(function Button(
{ className, variant, ...props },
ref
) {
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return (
<button
ref={ref}
type="button"
className={cn(button.base, variant === 'icon' ? button.icon : button.default, className)}
{...props}
/>
<button ref={ref} type="button" className={cn(button.base, button.subtle, button.icon, className)} {...props} />
);
});
@@ -116,16 +118,6 @@ const SliderThumb = forwardRef<HTMLDivElement, ComponentProps<'div'> & { persist
);
});
const errorClasses = {
root: error.root,
dialog: error.dialog,
content: error.content,
title: error.title,
description: error.description,
actions: error.actions,
close: cn(button.base, button.default),
};
function PlayLabel(): string {
const paused = usePlayer((s) => Boolean(s.paused));
const ended = usePlayer((s) => Boolean(s.ended));
@@ -152,7 +144,7 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<MuteButton className={iconState.mute.button} render={<Button />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
@@ -203,7 +195,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
)}
/>
<ErrorDialog classes={errorClasses} />
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<Controls.Root
data-controls="" // Used as a hook for Tailwind has-[] styles
@@ -213,7 +205,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<PlayButton className={iconState.play.button} render={<Button />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
@@ -228,7 +220,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
@@ -242,7 +234,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
@@ -271,9 +263,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
</Time.Group>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Trigger render={<PlaybackRateButton className={playbackRate.button} render={<Button />} />} />
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -282,7 +272,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton className={iconState.captions.button} render={<Button variant="icon" />}>
<CaptionsButton className={iconState.captions.button} render={<Button />}>
<CaptionsOffIcon className={cn(icon, iconState.captions.off)} />
<CaptionsOnIcon className={cn(icon, iconState.captions.on)} />
</CaptionsButton>
@@ -296,7 +286,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton className={iconState.pip.button} render={<Button variant="icon" />}>
<PiPButton className={iconState.pip.button} render={<Button />}>
<PipEnterIcon className={cn(icon, iconState.pip.off)} />
<PipExitIcon className={cn(icon, iconState.pip.on)} />
</PiPButton>
@@ -310,7 +300,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton className={iconState.fullscreen.button} render={<Button variant="icon" />}>
<FullscreenButton className={iconState.fullscreen.button} render={<Button />}>
<FullscreenEnterIcon className={cn(icon, iconState.fullscreen.enter)} />
<FullscreenExitIcon className={cn(icon, iconState.fullscreen.exit)} />
</FullscreenButton>
+18 -11
View File
@@ -35,27 +35,34 @@ import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { isRenderProp } from '@/utils/use-render';
import { ErrorDialog } from '../error-dialog';
import type { BaseVideoSkinProps } from '../types';
import { ErrorDialog } from './error-dialog';
const SEEK_TIME = 10;
export type VideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
});
const errorClasses = {
const ERROR_CLASSNAMES = {
root: 'media-error',
dialog: 'media-error__dialog media-surface',
dialog: 'media-error__dialog',
content: 'media-error__content',
title: 'media-error__title',
description: 'media-error__description',
actions: 'media-error__actions',
close: 'media-button',
close: 'media-button media-button--subtle',
};
export type VideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return (
<button
ref={ref}
type="button"
className={cn('media-button media-button--subtle media-button--icon', className)}
{...props}
/>
);
});
function PlayLabel(): string {
const paused = usePlayer((s) => Boolean(s.paused));
const ended = usePlayer((s) => Boolean(s.ended));
@@ -127,7 +134,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
)}
/>
<ErrorDialog classes={errorClasses} />
<ErrorDialog classNames={ERROR_CLASSNAMES} />
<Controls.Root className="media-surface media-controls">
<Tooltip.Provider>