From df927f67fcbd0aaa229b1a8e205ab3cb08f7a42d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 23 Mar 2026 09:33:55 +1100 Subject: [PATCH] feat(skin): add error handling for audio players (#1048) Co-authored-by: Claude Opus 4.6 --- cspell.json | 1 + packages/core/src/dom/media/types.ts | 1 + .../core/src/dom/store/features/presets.ts | 1 + .../src/define/audio/minimal-skin.tailwind.ts | 10 +-- .../html/src/define/audio/minimal-skin.ts | 10 +-- .../html/src/define/audio/skin.tailwind.ts | 10 +-- packages/html/src/define/audio/skin.ts | 10 +-- .../src/define/video/minimal-skin.tailwind.ts | 16 ++-- .../html/src/define/video/minimal-skin.ts | 16 ++-- .../html/src/define/video/skin.tailwind.ts | 16 ++-- packages/html/src/define/video/skin.ts | 16 ++-- .../src/player/tests/create-player.test-d.ts | 6 ++ .../presets/audio/minimal-skin.tailwind.tsx | 38 +++++----- .../react/src/presets/audio/minimal-skin.tsx | 26 ++++++- .../react/src/presets/audio/skin.tailwind.tsx | 38 +++++----- packages/react/src/presets/audio/skin.tsx | 22 +++++- .../src/presets/{video => }/error-dialog.tsx | 22 +++--- .../presets/video/minimal-skin.tailwind.tsx | 53 ++++++------- .../react/src/presets/video/minimal-skin.tsx | 27 ++++--- .../react/src/presets/video/skin.tailwind.tsx | 54 ++++++------- packages/react/src/presets/video/skin.tsx | 29 ++++--- packages/sandbox/app/shell/navbar.tsx | 2 +- packages/skins/src/default/css/audio.css | 52 +++++++++++-- .../src/default/css/components/buttons.css | 38 ++++++---- .../src/default/css/components/error.css | 56 ++------------ .../src/default/css/components/slider.css | 16 ++++ packages/skins/src/default/css/video.css | 71 +++++++++++++++++- .../src/default/tailwind/audio.tailwind.ts | 17 ++++- .../src/default/tailwind/components/button.ts | 20 ++--- .../src/default/tailwind/components/error.ts | 4 +- .../src/default/tailwind/components/seek.ts | 2 +- .../src/default/tailwind/video.tailwind.ts | 5 +- packages/skins/src/minimal/css/audio.css | 55 ++++++++++++-- .../src/minimal/css/components/buttons.css | 54 ++++++++----- .../src/minimal/css/components/error.css | 59 +-------------- .../skins/src/minimal/css/components/root.css | 2 + packages/skins/src/minimal/css/video.css | 75 ++++++++++++++++++- .../src/minimal/tailwind/audio.tailwind.ts | 27 ++++++- .../src/minimal/tailwind/components/button.ts | 24 +++--- .../src/minimal/tailwind/components/error.ts | 4 +- .../src/minimal/tailwind/components/root.ts | 1 + .../src/minimal/tailwind/components/seek.ts | 2 +- .../src/minimal/tailwind/video.tailwind.ts | 17 ++++- 43 files changed, 648 insertions(+), 377 deletions(-) rename packages/react/src/presets/{video => }/error-dialog.tsx (52%) diff --git a/cspell.json b/cspell.json index bb4511bf..16b5b73f 100644 --- a/cspell.json +++ b/cspell.json @@ -15,6 +15,7 @@ "rahim", "segoe", "shiki", + "squircle", "videojs", "vitesse", "wouter" diff --git a/packages/core/src/dom/media/types.ts b/packages/core/src/dom/media/types.ts index e3cbde0e..0946156d 100644 --- a/packages/core/src/dom/media/types.ts +++ b/packages/core/src/dom/media/types.ts @@ -66,6 +66,7 @@ export type AudioFeatures = [ PlayerFeature, PlayerFeature, PlayerFeature, + PlayerFeature, ]; // TODO: Define background video features (e.g., playback, source, buffer) diff --git a/packages/core/src/dom/store/features/presets.ts b/packages/core/src/dom/store/features/presets.ts index ef1e77d4..c1e110d2 100644 --- a/packages/core/src/dom/store/features/presets.ts +++ b/packages/core/src/dom/store/features/presets.ts @@ -32,6 +32,7 @@ export const audioFeatures: AudioFeatures = [ timeFeature, sourceFeature, bufferFeature, + errorFeature, ]; // TODO: Add background video features (e.g., playback, source, buffer) diff --git a/packages/html/src/define/audio/minimal-skin.tailwind.ts b/packages/html/src/define/audio/minimal-skin.tailwind.ts index 4e20d097..94fddbb0 100644 --- a/packages/html/src/define/audio/minimal-skin.tailwind.ts +++ b/packages/html/src/define/audio/minimal-skin.tailwind.ts @@ -45,7 +45,7 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} @@ -57,7 +57,7 @@ function getTemplateHTML() { - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} @@ -67,7 +67,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} @@ -95,13 +95,13 @@ function getTemplateHTML() {
- + Toggle playback rate - + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} ${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })} ${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })} diff --git a/packages/html/src/define/audio/minimal-skin.ts b/packages/html/src/define/audio/minimal-skin.ts index 75895a64..c2760e8a 100644 --- a/packages/html/src/define/audio/minimal-skin.ts +++ b/packages/html/src/define/audio/minimal-skin.ts @@ -28,7 +28,7 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} @@ -39,7 +39,7 @@ function getTemplateHTML() { Pause - + ${renderIcon('seek', { class: 'media-icon media-icon--seek media-icon--flipped' })} ${SEEK_TIME} @@ -49,7 +49,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: 'media-icon media-icon--seek' })} ${SEEK_TIME} @@ -77,13 +77,13 @@ function getTemplateHTML() {
- + Toggle playback rate - + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} ${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })} ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} diff --git a/packages/html/src/define/audio/skin.tailwind.ts b/packages/html/src/define/audio/skin.tailwind.ts index 26bc5654..69377ca7 100644 --- a/packages/html/src/define/audio/skin.tailwind.ts +++ b/packages/html/src/define/audio/skin.tailwind.ts @@ -43,7 +43,7 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} @@ -55,7 +55,7 @@ function getTemplateHTML() { - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} @@ -65,7 +65,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} @@ -87,12 +87,12 @@ function getTemplateHTML() { - + Toggle playback rate - + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} ${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })} ${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })} diff --git a/packages/html/src/define/audio/skin.ts b/packages/html/src/define/audio/skin.ts index 9d62d7cd..658d5c67 100644 --- a/packages/html/src/define/audio/skin.ts +++ b/packages/html/src/define/audio/skin.ts @@ -27,7 +27,7 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} @@ -38,7 +38,7 @@ function getTemplateHTML() { Pause - + ${renderIcon('seek', { class: 'media-icon media-icon--seek media-icon--flipped' })} ${SEEK_TIME} @@ -48,7 +48,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: 'media-icon media-icon--seek' })} ${SEEK_TIME} @@ -70,12 +70,12 @@ function getTemplateHTML() { - + Toggle playback rate - + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} ${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })} ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} diff --git a/packages/html/src/define/video/minimal-skin.tailwind.ts b/packages/html/src/define/video/minimal-skin.tailwind.ts index 50a38bca..1c8dc81c 100644 --- a/packages/html/src/define/video/minimal-skin.tailwind.ts +++ b/packages/html/src/define/video/minimal-skin.tailwind.ts @@ -63,7 +63,7 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} @@ -75,7 +75,7 @@ function getTemplateHTML() { - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} @@ -85,7 +85,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} @@ -121,13 +121,13 @@ function getTemplateHTML() {
- + Toggle playback rate - + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} ${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })} ${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })} @@ -143,7 +143,7 @@ function getTemplateHTML() { - + ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} @@ -154,7 +154,7 @@ function getTemplateHTML() { - + ${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })} ${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })} @@ -165,7 +165,7 @@ function getTemplateHTML() { - + ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} diff --git a/packages/html/src/define/video/minimal-skin.ts b/packages/html/src/define/video/minimal-skin.ts index 5de7b8a6..5bfd7a96 100644 --- a/packages/html/src/define/video/minimal-skin.ts +++ b/packages/html/src/define/video/minimal-skin.ts @@ -41,7 +41,7 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} @@ -52,7 +52,7 @@ function getTemplateHTML() { Pause - + ${renderIcon('seek', { class: 'media-icon media-icon--flipped' })} ${SEEK_TIME} @@ -62,7 +62,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: 'media-icon' })} ${SEEK_TIME} @@ -98,12 +98,12 @@ function getTemplateHTML() {
- + Toggle playback rate - + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} ${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })} ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} @@ -118,7 +118,7 @@ function getTemplateHTML() { - + ${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })} ${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })} @@ -126,7 +126,7 @@ function getTemplateHTML() { Toggle captions - + ${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })} ${renderIcon('pip-exit', { class: 'media-icon media-icon--pip-exit' })} @@ -135,7 +135,7 @@ function getTemplateHTML() { Exit picture-in-picture - + ${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })} ${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })} diff --git a/packages/html/src/define/video/skin.tailwind.ts b/packages/html/src/define/video/skin.tailwind.ts index 33941f35..074332ef 100644 --- a/packages/html/src/define/video/skin.tailwind.ts +++ b/packages/html/src/define/video/skin.tailwind.ts @@ -63,7 +63,7 @@ function getTemplateHTML() { - + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} @@ -75,7 +75,7 @@ function getTemplateHTML() { - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} @@ -85,7 +85,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} @@ -113,12 +113,12 @@ function getTemplateHTML() { - + Toggle playback rate - + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} ${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })} ${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })} @@ -134,7 +134,7 @@ function getTemplateHTML() { - + ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} @@ -145,7 +145,7 @@ function getTemplateHTML() { - + ${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })} ${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })} @@ -156,7 +156,7 @@ function getTemplateHTML() { - + ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} diff --git a/packages/html/src/define/video/skin.ts b/packages/html/src/define/video/skin.ts index 18e00283..380d4a46 100644 --- a/packages/html/src/define/video/skin.ts +++ b/packages/html/src/define/video/skin.ts @@ -43,7 +43,7 @@ function getTemplateHTML() { - + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} @@ -54,7 +54,7 @@ function getTemplateHTML() { Pause - + ${renderIcon('seek', { class: 'media-icon media-icon--flipped' })} ${SEEK_TIME} @@ -64,7 +64,7 @@ function getTemplateHTML() { Seek backward ${SEEK_TIME} seconds - + ${renderIcon('seek', { class: 'media-icon' })} ${SEEK_TIME} @@ -92,12 +92,12 @@ function getTemplateHTML() { - + Toggle playback rate - + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} ${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })} ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} @@ -112,7 +112,7 @@ function getTemplateHTML() { - + ${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })} ${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })} @@ -121,7 +121,7 @@ function getTemplateHTML() { Disable captions - + ${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })} ${renderIcon('pip-exit', { class: 'media-icon media-icon--pip-exit' })} @@ -130,7 +130,7 @@ function getTemplateHTML() { Exit picture-in-picture - + ${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })} ${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })} diff --git a/packages/html/src/player/tests/create-player.test-d.ts b/packages/html/src/player/tests/create-player.test-d.ts index 29c94a85..27818b5a 100644 --- a/packages/html/src/player/tests/create-player.test-d.ts +++ b/packages/html/src/player/tests/create-player.test-d.ts @@ -16,6 +16,12 @@ describe('createPlayer', () => { const result = createPlayer({ features: audioFeatures }); assertType>(result); + + const store = result.create(); + + assertType(store.error?.code); + assertType(store.error?.message); + assertType<() => void>(store.dismissError); }); it('resolves spread video features to VideoPlayerStore', () => { diff --git a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx index a74e1f19..198e4f9c 100644 --- a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx @@ -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 & { variant?: 'icon' }>(function Button( - { className, variant, ...props }, - ref -) { +const Button = forwardRef>(function Button({ className, ...props }, ref) { return ( -