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">