mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(skin): add error handling for audio players (#1048)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
559d5169b5
commit
df927f67fc
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user